use std::sync::{LazyLock, Once}; use axum::{Router, body::Body, body::to_bytes, http::StatusCode}; use diesel::{Connection, PgConnection, pg::Pg, prelude::*}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; use henka_api::{build_router, model::user::NewUser}; use rstest::*; use serde_json::Value; use tower::ServiceExt; use henka_api::model::user::User; static DB_URL: LazyLock = LazyLock::new(|| std::env::var("TEST_DATABASE_URL").expect("TEST_DATABASE_URL not set")); pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); static INIT: Once = Once::new(); pub fn setup_db(conn: &mut C) where C: MigrationHarness, { INIT.call_once(|| { conn.run_pending_migrations(MIGRATIONS) .expect("should never fail, but its just tests"); }); } #[fixture] fn conn() -> PgConnection { PgConnection::establish(&DB_URL).expect("postgres unreachable") } #[fixture] fn db(mut conn: PgConnection) { setup_db(&mut conn); } #[fixture] fn user(mut conn: PgConnection, _db: ()) -> User { diesel::insert_into(henka_api::diesel_schema::users::table) .values(&NewUser { nick_name: "testuser", password_hash: "", }) .returning(User::as_returning()) .get_result(&mut conn) .expect("Error creating testuser") } #[rstest] #[tokio::test] async fn test_add_without_auth() { let (status, body) = query(build_router(&*DB_URL), "{ add(a: 1, b: 2) }", None).await; assert_eq!(status, StatusCode::OK); assert_eq!(body["data"]["add"], 3); } #[rstest] #[tokio::test] async fn test_mul_without_auth_returns_error(_db: ()) { let (status, body) = query(build_router(&*DB_URL), "{ mul(a: 3, b: 4) }", None).await; assert_eq!(status, StatusCode::OK); assert!( body.get("errors").is_some(), "expected GraphQL error for unauthorized access" ); assert!(body["data"]["mul"].is_null()); } #[rstest] #[tokio::test] async fn test_mul_with_wrong_token_returns_error(_db: (), user: User) { let token = uuid::Uuid::new_v4(); assert_ne!( user.id, token, "Flaky test fuse, UUID is the same by accident!" ); let (status, body) = query( build_router(&*DB_URL), "{ mul(a: 3, b: 4) }", Some(&token.to_string()), ) .await; assert_eq!(status, StatusCode::OK); assert!( body.get("errors").is_some(), "expected GraphQL error for wrong token" ); } #[rstest] #[tokio::test] async fn test_mul_with_valid_token(_db: (), user: User) { let (status, body) = query( build_router(&*DB_URL), "{ mul(a: 3, b: 4) }", Some(&user.id.to_string()), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!(body["data"]["mul"], 12); } #[expect(clippy::unwrap_used, reason = "unwrap is fine here, test only")] async fn query(app: Router, q: &str, token: Option<&str>) -> (StatusCode, Value) { let mut req_builder = axum::http::Request::builder() .method("POST") .uri("/graphql") .header("content-type", "application/json"); if let Some(token) = token { let bearer = format!("Bearer {}", token); req_builder = req_builder.header("authorization", bearer); } let req = req_builder .body(Body::from(serde_json::json!({"query": q}).to_string())) .unwrap(); let res = app.oneshot(req).await.unwrap(); let status = res.status(); let bytes = to_bytes(res.into_body(), usize::MAX).await.unwrap(); let body: Value = serde_json::from_slice(&bytes).unwrap(); (status, body) } // ---- me query integration tests ---- #[rstest] #[tokio::test] async fn test_me_without_auth_returns_error() { let (status, body) = query(build_router(&*DB_URL), "{ me { id name } }", None).await; assert_eq!(status, StatusCode::OK); assert!( body.get("errors").is_some(), "expected GraphQL error for unauthenticated me query" ); assert!(body["data"]["me"].is_null()); } #[rstest] #[tokio::test] async fn test_me_with_valid_token(_db: (), user: User) { let (status, body) = query( build_router(&*DB_URL), "{ me { id name } }", Some(&user.id.to_string()), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!(body["data"]["me"]["name"], "testuser"); assert_eq!(body["data"]["me"]["id"], user.id.to_string()); } #[rstest] #[tokio::test] async fn test_me_with_wrong_token_returns_error(_db: ()) { let wrong_token = uuid::Uuid::new_v4(); let (status, body) = query( build_router(&*DB_URL), "{ me { id name } }", Some(&wrong_token.to_string()), ) .await; assert_eq!(status, StatusCode::OK); assert!( body.get("errors").is_some(), "expected GraphQL error for wrong token on me query" ); assert!(body["data"]["me"].is_null()); } // ---- registerUser mutation integration tests ---- const REGISTER_MUTATION_SUCCESS: &str = r#"mutation { registerUser(name: "newuser", email: "user@example.com", emailConfirm: "user@example.com", password: "pass", passwordConfirm: "pass") { id name } }"#; #[rstest] #[tokio::test] async fn test_register_user_succeeds() { let (status, body) = query(build_router(&*DB_URL), REGISTER_MUTATION_SUCCESS, None).await; assert_eq!(status, StatusCode::OK); assert_eq!(body["data"]["registerUser"]["name"], "newuser"); assert!(body["data"]["registerUser"]["id"].as_str().is_some()); } #[rstest] #[case::email_mismatch( r#"mutation { registerUser(name: "newuser", email: "user@example.com", emailConfirm: "different@example.com", password: "pass", passwordConfirm: "pass") { id name } }"#, "email confirmation does not match" )] #[case::password_mismatch( r#"mutation { registerUser(name: "newuser", email: "user@example.com", emailConfirm: "user@example.com", password: "pass", passwordConfirm: "different") { id name } }"#, "password confirmation does not match" )] #[case::both_mismatch( r#"mutation { registerUser(name: "newuser", email: "user@example.com", emailConfirm: "other@example.com", password: "pass", passwordConfirm: "different") { id name } }"#, "email and password confirmations do not match" )] #[tokio::test] async fn test_register_user_validation_fails( #[case] mutation: &str, #[case] expected_message: &str, ) { let (status, body) = query(build_router(&*DB_URL), mutation, None).await; assert_eq!(status, StatusCode::OK); assert!(body.get("errors").is_some()); assert_eq!(body["errors"][0]["message"], expected_message); } #[rstest] #[tokio::test] async fn test_register_user_forbidden_when_logged_in(_db: (), user: User) { let (status, body) = query( build_router(&*DB_URL), REGISTER_MUTATION_SUCCESS, Some(&user.id.to_string()), ) .await; assert_eq!(status, StatusCode::OK); assert!(body.get("errors").is_some()); assert_eq!( body["errors"][0]["message"], "account creation forbidden when logged in" ); } // ---- Non-GraphQL endpoint tests ---- #[expect(clippy::unwrap_used, reason = "unwrap is fine here, test only")] async fn get_body(app: Router, path: &str) -> (StatusCode, String) { let req = axum::http::Request::builder() .method("GET") .uri(path) .body(Body::empty()) .unwrap(); let res = app.oneshot(req).await.unwrap(); let status = res.status(); let bytes = to_bytes(res.into_body(), usize::MAX).await.unwrap(); let text = String::from_utf8(bytes.to_vec()).unwrap(); (status, text) } #[rstest] #[case::homepage("/", "juniper_axum/simple example")] #[case::graphiql("/graphiql", "GraphiQL")] #[case::playground("/playground", "GraphQL Playground")] #[tokio::test] async fn test_non_graphql_endpoints(#[case] path: &str, #[case] expected_content: &str) { let (status, body) = get_body(build_router(&*DB_URL), path).await; assert_eq!(status, StatusCode::OK); assert!( body.contains(expected_content), "expected body to contain '{expected_content}'" ); }