diff --git a/tests/admin_plugins_official.rs b/tests/admin_plugins_official.rs --- a/tests/admin_plugins_official.rs +++ b/tests/admin_plugins_official.rs @@ -41,8 +41,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn official_plugins_endpoint_returns_cached_list() { + common::require_db!(); let app = TestApp::new().await; let gh = MockServer::start().await; @@ -111,8 +111,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn plugins_list_populates_update_available_when_behind() { + common::require_db!(); let app = TestApp::new().await; let gh = MockServer::start().await; @@ -187,8 +187,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn check_update_endpoint_refreshes_cache_on_demand() { + common::require_db!(); // Start the mock server BEFORE building the app so we can wire its URL // into the registry config. let gh = MockServer::start().await; diff --git a/tests/common/app.rs b/tests/common/app.rs --- a/tests/common/app.rs +++ b/tests/common/app.rs @@ -118,12 +118,23 @@ }, }) .expect("Failed to create test OAuth client"); + let domain_cache = happyview::domain::DomainCache::new(); + domain_cache + .insert(happyview::domain::Domain { + id: uuid::Uuid::new_v4().to_string(), + url: "http://127.0.0.1:0".to_string(), + is_primary: true, + created_at: now_rfc3339(), + updated_at: now_rfc3339(), + }) + .await; + let state = AppState { config, http: reqwest::Client::new(), db: pool.clone(), db_backend: backend, - domain_cache: happyview::domain::DomainCache::new(), + domain_cache, lexicons, collections_tx, labeler_subscriptions_tx, @@ -158,7 +169,15 @@ happyview::proxy_config::ProxyConfig::default(), ))), }; - let router = server::router(state.clone()); + let router = server::router(state.clone()).layer(axum::middleware::from_fn( + |mut req: axum::extract::Request, next: axum::middleware::Next| async move { + if !req.headers().contains_key("host") { + req.headers_mut() + .insert("host", axum::http::HeaderValue::from_static("127.0.0.1")); + } + next.run(req).await + }, + )); Self { router, @@ -178,20 +197,7 @@ } pub async fn new_with_encryption() -> Self { let mut app = Self::new().await; - // Set a test encryption key (32 bytes) app.state.config.token_encryption_key = Some([0x42u8; 32]); - // Seed a domain so the domain middleware doesn't reject requests with 421 - app.state - .domain_cache - .insert(happyview::domain::Domain { - id: uuid::Uuid::new_v4().to_string(), - url: "http://127.0.0.1:0".to_string(), - is_primary: true, - created_at: now_rfc3339(), - updated_at: now_rfc3339(), - }) - .await; - // Rebuild the router with the updated state app.router = server::router(app.state.clone()); app } diff --git a/tests/common/mod.rs b/tests/common/mod.rs --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -6,3 +6,14 @@ #[allow(dead_code, unused_imports)] pub mod db; #[allow(dead_code, unused_imports)] pub mod fixtures; + +macro_rules! require_db { + () => { + if std::env::var("TEST_DATABASE_URL").is_err() { + eprintln!("skipped (TEST_DATABASE_URL not set)"); + return; + } + }; +} + +pub(crate) use require_db; diff --git a/tests/dev_happyview.rs b/tests/dev_happyview.rs --- a/tests/dev_happyview.rs +++ b/tests/dev_happyview.rs @@ -88,6 +88,7 @@ /// Unauthenticated request (no Authorization header) should be rejected. #[tokio::test] #[serial] async fn list_api_clients_unauthenticated_returns_non_200() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let req = Request::builder() @@ -111,6 +112,7 @@ /// DPoP-authenticated request returns 200 with a `clients` array. #[tokio::test] #[serial] async fn list_api_clients_authenticated_returns_200_with_clients_array() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let user_did = "did:plc:testowner"; let (client_key, dpop_key, access_token) = setup_dpop_session(&app, user_did).await; @@ -147,6 +149,7 @@ /// Authenticated request for a nonexistent client ID returns 404. #[tokio::test] #[serial] async fn get_api_client_not_found() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let user_did = "did:plc:testowner404"; let (client_key, dpop_key, access_token) = setup_dpop_session(&app, user_did).await; @@ -178,6 +181,7 @@ /// clientKey (hvc_) and clientSecret (hvs_) in the response. #[tokio::test] #[serial] async fn create_api_client_via_xrpc() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let user_did = "did:plc:testcreator"; let (client_key, dpop_key, access_token) = setup_dpop_session(&app, user_did).await; @@ -230,6 +234,7 @@ /// Creating a public client returns no clientSecret in the response. #[tokio::test] #[serial] async fn create_api_client_public_no_secret() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let user_did = "did:plc:testcreatorpublic"; let (client_key, dpop_key, access_token) = setup_dpop_session(&app, user_did).await; @@ -286,6 +291,7 @@ /// Create a client, delete it, then verify a GET returns 404. #[tokio::test] #[serial] async fn delete_api_client_success() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let user_did = "did:plc:testownerdelete"; let (client_key, dpop_key, access_token) = setup_dpop_session(&app, user_did).await; @@ -367,6 +373,7 @@ /// Attempting to delete a nonexistent client returns 404. #[tokio::test] #[serial] async fn delete_api_client_not_found() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let user_did = "did:plc:testownerdel404"; let (client_key, dpop_key, access_token) = setup_dpop_session(&app, user_did).await; @@ -397,6 +404,7 @@ /// Authenticated request returns 200 with the matching client. #[tokio::test] #[serial] async fn get_api_client_returns_client() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let user_did = "did:plc:testownerget"; let (client_key, dpop_key, access_token) = setup_dpop_session(&app, user_did).await; diff --git a/tests/dpop_auth.rs b/tests/dpop_auth.rs --- a/tests/dpop_auth.rs +++ b/tests/dpop_auth.rs @@ -47,6 +47,7 @@ #[tokio::test] #[serial] async fn test_provision_dpop_key_confidential_client() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let (client_key, client_secret, _id) = app.create_api_client("confidential", None).await; @@ -72,6 +73,7 @@ #[tokio::test] #[serial] async fn test_provision_dpop_key_public_client_requires_pkce() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let (client_key, _secret, _id) = app .create_api_client("public", Some(vec!["http://localhost:3000".to_string()])) @@ -94,6 +96,7 @@ #[tokio::test] #[serial] async fn test_provision_dpop_key_public_client_with_pkce() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let (client_key, _secret, _id) = app .create_api_client("public", Some(vec!["http://localhost:3000".to_string()])) @@ -127,6 +130,7 @@ #[tokio::test] #[serial] async fn test_register_session_validates_scopes() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let (client_key, client_secret, _id) = app.create_api_client("confidential", None).await; @@ -165,6 +169,7 @@ #[tokio::test] #[serial] async fn test_register_session_requires_atproto_scope() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let (client_key, client_secret, _id) = app.create_api_client("confidential", None).await; @@ -201,6 +206,7 @@ #[tokio::test] #[serial] async fn test_full_flow_provision_register_delete() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let (client_key, client_secret, _id) = app.create_api_client("confidential", None).await; @@ -265,6 +271,7 @@ #[tokio::test] #[serial] async fn test_xrpc_rejects_bearer_auth() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; // Bearer auth should be explicitly rejected on XRPC routes @@ -290,6 +297,7 @@ #[tokio::test] #[serial] async fn test_xrpc_allows_anonymous_queries() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; // Anonymous access (no auth header) should pass through to lexicon lookup. @@ -314,6 +322,7 @@ #[tokio::test] #[serial] async fn test_xrpc_procedure_requires_dpop_auth() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; // POST to an XRPC procedure without DPoP auth should be rejected @@ -339,6 +348,7 @@ #[tokio::test] #[serial] async fn test_xrpc_dpop_auth_accepted() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let (client_key, client_secret, _id) = app.create_api_client("confidential", None).await; diff --git a/tests/e2e_admin.rs b/tests/e2e_admin.rs --- a/tests/e2e_admin.rs +++ b/tests/e2e_admin.rs @@ -63,8 +63,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn admin_no_auth_returns_401() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -84,8 +84,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn admin_wrong_token_returns_401() { + common::require_db!(); let app = TestApp::new().await; // No valid session cookie — the request will be rejected. @@ -107,8 +107,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn admin_valid_token_returns_200() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -123,8 +123,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn admin_non_admin_did_returns_403() { + common::require_db!(); let app = TestApp::new().await; // Use a DID that is NOT in the admins table. @@ -143,8 +143,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn admin_auto_bootstrap_first_user() { + common::require_db!(); let app = TestApp::new().await; let backend = app.state.db_backend; @@ -186,8 +186,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn lexicon_create_returns_201() { + common::require_db!(); let app = TestApp::new().await; let body = json!({ "lexicon_json": fixtures::game_record_lexicon(), @@ -209,8 +209,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn lexicon_upsert_returns_200_with_incremented_revision() { + common::require_db!(); let app = TestApp::new().await; let body = json!({ "lexicon_json": fixtures::game_record_lexicon(), @@ -241,8 +241,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn lexicon_invalid_version_returns_400() { + common::require_db!(); let app = TestApp::new().await; let body = json!({ "lexicon_json": { "lexicon": 99, "id": "test.bad" }, @@ -260,8 +260,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn lexicon_missing_id_returns_400() { + common::require_db!(); let app = TestApp::new().await; let body = json!({ "lexicon_json": { "lexicon": 1 }, @@ -279,8 +279,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn lexicon_list_all() { + common::require_db!(); let app = TestApp::new().await; // Seed a lexicon @@ -311,8 +311,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn lexicon_get_by_id() { + common::require_db!(); let app = TestApp::new().await; app.router @@ -343,8 +343,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn lexicon_get_not_found() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -362,8 +362,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn lexicon_delete() { + common::require_db!(); let app = TestApp::new().await; app.router @@ -392,8 +392,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn lexicon_delete_not_found() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -415,8 +415,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn stats_empty_db() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -434,8 +434,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn stats_with_seeded_records() { + common::require_db!(); let app = TestApp::new().await; let backend = app.state.db_backend; @@ -496,8 +496,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn backfill_create_job() { + common::require_db!(); let app = TestApp::new().await; // Register a record-type lexicon first (required by backfill validation). @@ -527,14 +527,14 @@ .unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); let json = json_body(resp).await; - assert_eq!(json["status"], "completed"); + assert_eq!(json["status"], "running"); assert!(json.get("id").is_some()); } #[tokio::test] #[serial] -#[ignore] async fn backfill_list_jobs() { + common::require_db!(); let app = TestApp::new().await; // Create a job first @@ -567,8 +567,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn admin_create_returns_did() { + common::require_db!(); let app = TestApp::new().await; let body = json!({ "did": "did:plc:newadmin" }); @@ -587,8 +587,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn admin_created_did_authenticates() { + common::require_db!(); let app = TestApp::new().await; let new_did = "did:plc:newadmin2"; @@ -618,8 +618,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn admin_list_returns_dids() { + common::require_db!(); let app = TestApp::new().await; let cookie = app.admin_cookie(); @@ -643,8 +643,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn admin_delete_returns_204() { + common::require_db!(); let app = TestApp::new().await; // Create an admin to delete @@ -675,8 +675,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn admin_delete_not_found() { + common::require_db!(); let app = TestApp::new().await; let cookie = app.admin_cookie(); diff --git a/tests/e2e_api_clients.rs b/tests/e2e_api_clients.rs --- a/tests/e2e_api_clients.rs +++ b/tests/e2e_api_clients.rs @@ -3,7 +3,6 @@ use axum::body::Body; use axum::http::{Request, StatusCode}; use happyview::db::{adapt_sql, now_rfc3339}; -use happyview::oauth::pds_write::generate_dpop_proof; use http_body_util::BodyExt; use serde_json::{Value, json}; use serial_test::serial; @@ -88,8 +87,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn create_api_client_returns_201() { + common::require_db!(); let app = TestApp::new().await; let body = sample_api_client_body(); @@ -115,8 +114,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn create_api_client_duplicate_client_id_url_fails() { + common::require_db!(); let app = TestApp::new().await; let body = sample_api_client_body(); @@ -141,8 +140,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn create_api_client_registers_in_oauth_registry() { + common::require_db!(); let app = TestApp::new().await; let body = sample_api_client_body(); @@ -168,8 +167,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn list_api_clients_empty() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -186,8 +185,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn list_api_clients_returns_created_clients() { + common::require_db!(); let app = TestApp::new().await; // Create two clients @@ -246,8 +245,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn get_api_client_returns_details() { + common::require_db!(); let app = TestApp::new().await; let body = sample_api_client_body(); @@ -286,8 +285,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn get_api_client_not_found() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -309,8 +308,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn update_api_client_changes_fields() { + common::require_db!(); let app = TestApp::new().await; // Create @@ -363,8 +362,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn update_api_client_not_found() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -383,8 +382,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn update_api_client_deactivate_removes_from_registry() { + common::require_db!(); let app = TestApp::new().await; let create_resp = app @@ -429,8 +428,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn delete_api_client_returns_204() { + common::require_db!(); let app = TestApp::new().await; let create_resp = app @@ -471,8 +470,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn delete_api_client_removes_from_oauth_registry() { + common::require_db!(); let app = TestApp::new().await; let create_resp = app @@ -508,8 +507,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn delete_api_client_not_found() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -531,8 +530,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn api_clients_no_auth_returns_401() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -552,8 +551,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn api_clients_non_admin_returns_403() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -575,8 +574,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn oauth_registry_get_or_default_returns_default_for_unknown() { + common::require_db!(); let app = TestApp::new().await; let client = app @@ -591,8 +590,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn oauth_registry_get_or_default_returns_default_for_none() { + common::require_db!(); let app = TestApp::new().await; let client = app.state.oauth.get_or_default(None); @@ -607,8 +606,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn create_api_client_with_rate_limit_overrides() { + common::require_db!(); let app = TestApp::new().await; let body = json!({ "name": "Rate Limited App", @@ -645,492 +644,22 @@ assert_eq!(json["rate_limit_capacity"], 50); assert_eq!(json["rate_limit_refill_rate"], 1.5); } -// --------------------------------------------------------------------------- -// Self-service API client creation (POST /oauth/api-clients) -// --------------------------------------------------------------------------- - -/// Helper to make a POST request with JSON body and extra headers. -fn post_json_with_headers( - uri: &str, - body: &serde_json::Value, - headers: Vec<(&str, &str)>, -) -> Request { - let mut builder = Request::builder() - .method("POST") - .uri(uri) - .header("content-type", "application/json") - .header("host", "127.0.0.1:0"); - for (name, value) in headers { - builder = builder.header(name, value); - } - builder - .body(Body::from(serde_json::to_vec(body).unwrap())) - .unwrap() -} - -/// Parse a response body as JSON, returning `null` on empty/invalid bodies. -async fn response_json(resp: axum::response::Response) -> Value { - let body = resp.into_body().collect().await.unwrap().to_bytes(); - serde_json::from_slice(&body).unwrap_or(json!(null)) -} - -/// Runs the full DPoP provisioning flow and returns the values needed to call -/// the self-service endpoint: -/// `(client_key, dpop_key_json, access_token)` -/// -/// `user_did` is the DID that will be associated with the DPoP session. -async fn setup_dpop_session(app: &TestApp, user_did: &str) -> (String, Value, String) { - let (client_key, client_secret, _id) = app.create_api_client("confidential", None).await; - - // 1. Provision DPoP key - let key_req = post_json_with_headers( - "/oauth/dpop-keys", - &json!({}), - vec![ - ("x-client-key", &client_key), - ("x-client-secret", &client_secret), - ], - ); - let key_resp = app.router.clone().oneshot(key_req).await.unwrap(); - assert_eq!( - key_resp.status(), - StatusCode::CREATED, - "dpop key provisioning failed" - ); - let key_body = response_json(key_resp).await; - let provision_id = key_body["provision_id"].as_str().unwrap().to_string(); - let dpop_key = key_body["dpop_key"].clone(); - - // 2. Register session - let access_token = format!("test-access-{}", uuid::Uuid::new_v4()); - let session_req = post_json_with_headers( - "/oauth/sessions", - &json!({ - "provision_id": provision_id, - "did": user_did, - "access_token": &access_token, - "scopes": "atproto", - "pds_url": "https://pds.example.com", - }), - vec![ - ("x-client-key", &client_key), - ("x-client-secret", &client_secret), - ], - ); - let session_resp = app.router.clone().oneshot(session_req).await.unwrap(); - assert_eq!( - session_resp.status(), - StatusCode::CREATED, - "session registration failed" - ); - - (client_key, dpop_key, access_token) -} - -/// Build a self-service POST /oauth/api-clients request with full DPoP auth. -fn self_service_request( - client_key: &str, - access_token: &str, - dpop_proof: &str, - body: &Value, -) -> Request { - Request::builder() - .method("POST") - .uri("/oauth/api-clients") - .header("host", "127.0.0.1:0") - .header("content-type", "application/json") - .header("x-client-key", client_key) - .header("authorization", format!("DPoP {}", access_token)) - .header("dpop", dpop_proof) - .body(Body::from(serde_json::to_vec(body).unwrap())) - .unwrap() -} +// Self-service client creation tests removed — the /oauth/api-clients route +// no longer exists. The XRPC equivalent (dev.happyview.createApiClient) is +// tested in tests/dev_happyview.rs. // --------------------------------------------------------------------------- -// Happy path +// Cascade: deactivate / delete parent cascades to children // --------------------------------------------------------------------------- -#[tokio::test] -#[serial] -#[ignore] -async fn test_self_service_create_confidential_child_client() { - let app = TestApp::new_with_encryption().await; - let (client_key, dpop_key, access_token) = setup_dpop_session(&app, "did:plc:testadmin").await; - - let request_url = "http://127.0.0.1:0/oauth/api-clients"; - let proof = generate_dpop_proof(&dpop_key, "POST", request_url, &access_token, None) - .expect("failed to generate DPoP proof"); - - let body = json!({ - "name": "My Confidential Child", - "client_id_url": "https://child-confidential.example.com/oauth-client-metadata.json", - "client_uri": "https://child-confidential.example.com", - "redirect_uris": ["https://child-confidential.example.com/callback"], - "scopes": "atproto", - "client_type": "confidential" - }); - - let req = self_service_request(&client_key, &access_token, &proof, &body); - let resp = app.router.clone().oneshot(req).await.unwrap(); - - assert_eq!(resp.status(), StatusCode::CREATED); - let json = response_json(resp).await; - - // Verify all expected fields - assert!(json["id"].as_str().is_some(), "response should have id"); - let key = json["client_key"].as_str().unwrap(); - assert!(key.starts_with("hvc_"), "client_key should start with hvc_"); - let secret = json["client_secret"].as_str().unwrap(); - assert!( - secret.starts_with("hvs_"), - "client_secret should start with hvs_" - ); - assert_eq!(json["name"], "My Confidential Child"); - assert_eq!( - json["client_id_url"], - "https://child-confidential.example.com/oauth-client-metadata.json" - ); - assert_eq!(json["client_type"], "confidential"); -} - -#[tokio::test] -#[serial] -#[ignore] -async fn test_self_service_create_public_child_client() { - let app = TestApp::new_with_encryption().await; - let (client_key, dpop_key, access_token) = setup_dpop_session(&app, "did:plc:testadmin").await; - - let request_url = "http://127.0.0.1:0/oauth/api-clients"; - let proof = generate_dpop_proof(&dpop_key, "POST", request_url, &access_token, None) - .expect("failed to generate DPoP proof"); - - let body = json!({ - "name": "My Public Child", - "client_id_url": "https://child-public.example.com/oauth-client-metadata.json", - "client_uri": "https://child-public.example.com", - "redirect_uris": ["https://child-public.example.com/callback"], - "scopes": "atproto", - "client_type": "public" - }); - - let req = self_service_request(&client_key, &access_token, &proof, &body); - let resp = app.router.clone().oneshot(req).await.unwrap(); - - assert_eq!(resp.status(), StatusCode::CREATED); - let json = response_json(resp).await; - - assert!(json["id"].as_str().is_some(), "response should have id"); - let key = json["client_key"].as_str().unwrap(); - assert!(key.starts_with("hvc_"), "client_key should start with hvc_"); - assert_eq!(json["name"], "My Public Child"); - assert_eq!( - json["client_id_url"], - "https://child-public.example.com/oauth-client-metadata.json" - ); - assert_eq!(json["client_type"], "public"); - // Public clients should NOT have a client_secret - assert!( - json["client_secret"].is_null(), - "public client should not have a client_secret" - ); -} - -// --------------------------------------------------------------------------- -// Error cases -// --------------------------------------------------------------------------- - -#[tokio::test] -#[serial] -#[ignore] -async fn test_self_service_child_cannot_create_children() { - let app = TestApp::new_with_encryption().await; - - // Create a parent client via the helper (top-level, no parent_client_id). - let (_parent_key, _parent_secret, parent_id) = - app.create_api_client("confidential", None).await; - - // Insert a child client directly in the DB with parent_client_id set. - let child_key = format!("hvc_{}", hex::encode([0xAAu8; 16])); - let child_secret = format!("hvs_{}", hex::encode([0xBBu8; 32])); - let child_secret_hash = hex::encode(sha2::Sha256::digest(child_secret.as_bytes())); - let child_id = uuid::Uuid::new_v4().to_string(); - let now = now_rfc3339(); - - let sql = adapt_sql( - "INSERT INTO api_clients (id, client_key, client_secret_hash, name, client_id_url, client_uri, redirect_uris, scopes, client_type, is_active, created_by, created_at, updated_at, parent_client_id, owner_did) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)", - app.state.db_backend, - ); - sqlx::query(&sql) - .bind(&child_id) - .bind(&child_key) - .bind(&child_secret_hash) - .bind("child-client") - .bind("https://child-no-nest.example.com/oauth-client-metadata.json") - .bind("https://child-no-nest.example.com") - .bind("[]") - .bind("atproto") - .bind("confidential") - .bind("did:plc:testadmin") - .bind(&now) - .bind(&now) - .bind(&parent_id) - .bind("did:plc:testadmin") - .execute(&app.state.db) - .await - .expect("failed to insert child client"); - - // The endpoint checks parent_client_id IS NULL at step 6, BEFORE DPoP - // validation. So we just need the DPoP headers to exist — they do not - // need to be cryptographically valid. - let body = json!({ - "name": "Grandchild", - "client_id_url": "https://grandchild.example.com/oauth-client-metadata.json", - "client_uri": "https://grandchild.example.com", - "redirect_uris": ["https://grandchild.example.com/callback"], - "scopes": "atproto", - "client_type": "confidential" - }); - - let req = Request::builder() - .method("POST") - .uri("/oauth/api-clients") - .header("host", "127.0.0.1:0") - .header("content-type", "application/json") - .header("x-client-key", &child_key) - .header("authorization", "DPoP fake-token") - .header("dpop", "fake-proof") - .body(Body::from(serde_json::to_vec(&body).unwrap())) - .unwrap(); - - let resp = app.router.clone().oneshot(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::FORBIDDEN); -} - -#[tokio::test] -#[serial] -#[ignore] -async fn test_self_service_duplicate_client_id_url() { - let app = TestApp::new_with_encryption().await; - let (client_key, dpop_key, access_token) = setup_dpop_session(&app, "did:plc:testadmin").await; - - let shared_url = "https://dup-test.example.com/oauth-client-metadata.json"; - - // First creation should succeed. - let request_url = "http://127.0.0.1:0/oauth/api-clients"; - let proof1 = - generate_dpop_proof(&dpop_key, "POST", request_url, &access_token, None).expect("proof 1"); - - let body = json!({ - "name": "First Child", - "client_id_url": shared_url, - "client_uri": "https://dup-test.example.com", - "redirect_uris": ["https://dup-test.example.com/callback"], - "scopes": "atproto", - "client_type": "confidential" - }); - - let req1 = self_service_request(&client_key, &access_token, &proof1, &body); - let resp1 = app.router.clone().oneshot(req1).await.unwrap(); - assert_eq!(resp1.status(), StatusCode::CREATED); - - // Second creation with the same client_id_url should fail with 409. - let proof2 = - generate_dpop_proof(&dpop_key, "POST", request_url, &access_token, None).expect("proof 2"); - - let body2 = json!({ - "name": "Second Child", - "client_id_url": shared_url, - "client_uri": "https://dup-test2.example.com", - "redirect_uris": ["https://dup-test2.example.com/callback"], - "scopes": "atproto", - "client_type": "confidential" - }); - - let req2 = self_service_request(&client_key, &access_token, &proof2, &body2); - let resp2 = app.router.clone().oneshot(req2).await.unwrap(); - assert_eq!(resp2.status(), StatusCode::CONFLICT); -} - -#[tokio::test] -#[serial] -#[ignore] -async fn test_self_service_missing_client_key() { - let app = TestApp::new_with_encryption().await; - - let body = json!({ - "name": "No Key Client", - "client_id_url": "https://nokey.example.com/oauth-client-metadata.json", - "client_uri": "https://nokey.example.com", - "redirect_uris": ["https://nokey.example.com/callback"], - "scopes": "atproto", - "client_type": "confidential" - }); - - // No x-client-key header at all. - let req = Request::builder() - .method("POST") - .uri("/oauth/api-clients") - .header("host", "127.0.0.1:0") - .header("content-type", "application/json") - .header("authorization", "DPoP fake-token") - .header("dpop", "fake-proof") - .body(Body::from(serde_json::to_vec(&body).unwrap())) - .unwrap(); - - let resp = app.router.clone().oneshot(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); -} - -#[tokio::test] -#[serial] -#[ignore] -async fn test_self_service_invalid_client_type() { - let app = TestApp::new_with_encryption().await; - - // Step 3 (client_type validation) happens before step 5 (client resolution). - // The request just needs the required headers to exist. - let body = json!({ - "name": "Invalid Type Client", - "client_id_url": "https://badtype.example.com/oauth-client-metadata.json", - "client_uri": "https://badtype.example.com", - "redirect_uris": ["https://badtype.example.com/callback"], - "scopes": "atproto", - "client_type": "invalid" - }); - - let req = Request::builder() - .method("POST") - .uri("/oauth/api-clients") - .header("host", "127.0.0.1:0") - .header("content-type", "application/json") - .header("x-client-key", "hvc_doesnotmatter") - .header("authorization", "DPoP fake-token") - .header("dpop", "fake-proof") - .body(Body::from(serde_json::to_vec(&body).unwrap())) - .unwrap(); - - let resp = app.router.clone().oneshot(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); -} - -#[tokio::test] -#[serial] -#[ignore] -async fn test_self_service_parent_owner_not_in_users() { - let app = TestApp::new_with_encryption().await; - - // Create a parent API client whose created_by DID is NOT in the users table. - let orphan_did = "did:plc:orphan"; - let (client_key, client_secret, _id) = { - use happyview::db::{adapt_sql, now_rfc3339}; - use rand::RngCore; - use sha2::{Digest, Sha256}; - - let mut key_bytes = [0u8; 16]; - rand::rng().fill_bytes(&mut key_bytes); - let client_key = format!("hvc_{}", hex::encode(key_bytes)); - - let mut secret_bytes = [0u8; 32]; - rand::rng().fill_bytes(&mut secret_bytes); - let client_secret = format!("hvs_{}", hex::encode(secret_bytes)); - let secret_hash = hex::encode(Sha256::digest(client_secret.as_bytes())); - - let id = uuid::Uuid::new_v4().to_string(); - let now = now_rfc3339(); - - let sql = adapt_sql( - "INSERT INTO api_clients (id, client_key, client_secret_hash, name, client_id_url, client_uri, redirect_uris, scopes, client_type, allowed_origins, is_active, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)", - app.state.db_backend, - ); - - sqlx::query(&sql) - .bind(&id) - .bind(&client_key) - .bind(&secret_hash) - .bind("orphan-client") - .bind(format!("https://orphan.example.com/oauth/{}", &id[..8])) - .bind("https://orphan.example.com") - .bind("[]") - .bind("atproto") - .bind("confidential") - .bind(None::) - .bind(orphan_did) - .bind(&now) - .bind(&now) - .execute(&app.state.db) - .await - .expect("failed to create orphan API client"); - - app.state.rate_limiter.register_client_identity( - client_key.clone(), - happyview::rate_limit::ClientIdentity { - secret_hash, - client_uri: "https://orphan.example.com".to_string(), - }, - ); - - (client_key, client_secret, id) - }; - - // Provision a DPoP key and session using this orphan parent client. - let key_req = post_json_with_headers( - "/oauth/dpop-keys", - &json!({}), - vec![ - ("x-client-key", &client_key), - ("x-client-secret", &client_secret), - ], - ); - let key_resp = app.router.clone().oneshot(key_req).await.unwrap(); - assert_eq!(key_resp.status(), StatusCode::CREATED); - let key_body = response_json(key_resp).await; - let provision_id = key_body["provision_id"].as_str().unwrap().to_string(); - let dpop_key = key_body["dpop_key"].clone(); - - let access_token = format!("test-access-{}", uuid::Uuid::new_v4()); - let session_req = post_json_with_headers( - "/oauth/sessions", - &json!({ - "provision_id": provision_id, - "did": "did:plc:sessionuser", - "access_token": &access_token, - "scopes": "atproto", - "pds_url": "https://pds.example.com", - }), - vec![ - ("x-client-key", &client_key), - ("x-client-secret", &client_secret), - ], - ); - let session_resp = app.router.clone().oneshot(session_req).await.unwrap(); - assert_eq!(session_resp.status(), StatusCode::CREATED); - - let request_url = "http://127.0.0.1:0/oauth/api-clients"; - let proof = generate_dpop_proof(&dpop_key, "POST", request_url, &access_token, None) - .expect("failed to generate DPoP proof"); - - let body = json!({ - "name": "Orphan Owner Child", - "client_id_url": "https://orphan-child.example.com/oauth-client-metadata.json", - "client_uri": "https://orphan-child.example.com", - "redirect_uris": ["https://orphan-child.example.com/callback"], - "scopes": "atproto", - "client_type": "confidential" - }); - - let req = self_service_request(&client_key, &access_token, &proof, &body); - let resp = app.router.clone().oneshot(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::FORBIDDEN); -} - // --------------------------------------------------------------------------- // Cascade: deactivate / delete parent cascades to children // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn test_deactivate_parent_cascades_to_children() { + common::require_db!(); let app = TestApp::new().await; // Create parent via admin API @@ -1234,8 +763,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn test_delete_parent_cascades_to_children() { + common::require_db!(); let app = TestApp::new().await; // Create parent via admin API @@ -1332,8 +861,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn test_list_api_clients_filter_by_parent() { + common::require_db!(); let app = TestApp::new().await; // Create two parents via admin API @@ -1482,8 +1011,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn test_list_api_clients_includes_parent_and_owner_fields() { + common::require_db!(); let app = TestApp::new().await; // Create a top-level parent via admin API diff --git a/tests/e2e_base_path.rs b/tests/e2e_base_path.rs --- a/tests/e2e_base_path.rs +++ b/tests/e2e_base_path.rs @@ -8,8 +8,8 @@ use tower::ServiceExt; #[tokio::test] #[serial] -#[ignore] async fn health_at_root_when_base_path_set() { + common::require_db!(); let app = common::app::TestApp::new_with_base_path("/hv").await; let resp = app @@ -31,8 +31,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn health_not_nested_under_base_path() { + common::require_db!(); let app = common::app::TestApp::new_with_base_path("/hv").await; let resp = app @@ -52,8 +52,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn config_accessible_under_base_path() { + common::require_db!(); let app = common::app::TestApp::new_with_base_path("/hv").await; let resp = app @@ -74,8 +74,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn config_not_at_root_when_base_path_set() { + common::require_db!(); let app = common::app::TestApp::new_with_base_path("/hv").await; let resp = app @@ -96,8 +96,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn redirect_includes_base_path_prefix() { + common::require_db!(); let app = common::app::TestApp::new_with_base_path("/hv").await; let resp = app diff --git a/tests/e2e_delegation.rs b/tests/e2e_delegation.rs --- a/tests/e2e_delegation.rs +++ b/tests/e2e_delegation.rs @@ -296,6 +296,7 @@ #[tokio::test] #[serial] async fn link_account_success() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:owner1"; let target_did = "did:plc:studio1"; @@ -325,6 +326,7 @@ #[tokio::test] #[serial] async fn link_account_already_linked() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:owner2"; let target_did = "did:plc:studio2"; @@ -348,6 +350,7 @@ #[tokio::test] #[serial] async fn link_account_self_link_rejected() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let user_did = "did:plc:selflinker"; @@ -368,6 +371,7 @@ #[tokio::test] #[serial] async fn link_account_no_session_for_target() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:owner3"; let target_did = "did:plc:nosession"; @@ -394,6 +398,7 @@ #[tokio::test] #[serial] async fn unlink_account_success() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:unlink_owner"; let target_did = "did:plc:unlink_studio"; @@ -430,6 +435,7 @@ #[tokio::test] #[serial] async fn unlink_account_non_owner_rejected() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:unlink_owner2"; let admin_did = "did:plc:unlink_admin2"; @@ -472,6 +478,7 @@ #[tokio::test] #[serial] async fn add_delegate_success() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:add_owner"; let member_did = "did:plc:add_member"; @@ -518,6 +525,7 @@ #[tokio::test] #[serial] async fn add_delegate_owner_role_rejected() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:add_owner2"; let target_did = "did:plc:add_studio2"; @@ -540,6 +548,7 @@ #[tokio::test] #[serial] async fn add_delegate_already_exists() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:add_owner3"; let member_did = "did:plc:add_member3"; @@ -576,6 +585,7 @@ #[tokio::test] #[serial] async fn add_delegate_member_cannot_add() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:add_owner4"; let member_did = "did:plc:add_member4"; @@ -618,6 +628,7 @@ #[tokio::test] #[serial] async fn remove_delegate_success() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:rm_owner"; let member_did = "did:plc:rm_member"; @@ -669,6 +680,7 @@ #[tokio::test] #[serial] async fn remove_delegate_cannot_remove_owner() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:rm_owner2"; let target_did = "did:plc:rm_studio2"; @@ -691,6 +703,7 @@ #[tokio::test] #[serial] async fn remove_delegate_admin_cannot_remove_admin() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:rm_owner3"; let admin1_did = "did:plc:rm_admin3a"; @@ -742,6 +755,7 @@ #[tokio::test] #[serial] async fn list_accounts_returns_linked_accounts() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:list_owner"; let studio1 = "did:plc:list_studio1"; @@ -781,6 +795,7 @@ #[tokio::test] #[serial] async fn list_accounts_empty() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let user_did = "did:plc:no_accounts"; @@ -807,6 +822,7 @@ #[tokio::test] #[serial] async fn get_account_not_a_delegate() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:ga_owner"; let target_did = "did:plc:ga_studio"; @@ -839,6 +855,7 @@ #[tokio::test] #[serial] async fn list_delegates_member_cannot_list() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:ld_owner"; let member_did = "did:plc:ld_member"; @@ -969,6 +986,7 @@ #[tokio::test] #[serial] async fn delegated_write_non_delegate_rejected() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; seed_procedure_lexicon(&app).await; @@ -995,6 +1013,7 @@ #[tokio::test] #[serial] async fn delegated_write_member_rejected() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; seed_procedure_lexicon(&app).await; @@ -1039,6 +1058,7 @@ #[tokio::test] #[serial] async fn delegated_write_owner_success() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; seed_procedure_lexicon(&app).await; @@ -1086,6 +1106,7 @@ #[tokio::test] #[serial] async fn delegated_write_admin_success() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; seed_procedure_lexicon(&app).await; @@ -1148,6 +1169,7 @@ #[tokio::test] #[serial] async fn admin_can_add_delegate() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:acd_owner"; @@ -1211,6 +1233,7 @@ #[tokio::test] #[serial] async fn admin_can_remove_member() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:arm_owner"; @@ -1276,6 +1299,7 @@ #[tokio::test] #[serial] async fn admin_can_list_delegates() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:ald_owner"; @@ -1319,6 +1343,7 @@ #[tokio::test] #[serial] async fn member_can_view_account() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:mva_owner"; @@ -1362,6 +1387,7 @@ #[tokio::test] #[serial] async fn owner_can_remove_admin() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:ora_owner"; @@ -1419,6 +1445,7 @@ #[tokio::test] #[serial] async fn cross_client_get_account_rejected() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:xc_ga_owner"; let target_did = "did:plc:xc_ga_studio"; @@ -1444,6 +1471,7 @@ #[tokio::test] #[serial] async fn cross_client_add_delegate_rejected() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:xc_ad_owner"; let target_did = "did:plc:xc_ad_studio"; @@ -1467,6 +1495,7 @@ #[tokio::test] #[serial] async fn cross_client_delegated_write_rejected() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; seed_procedure_lexicon(&app).await; @@ -1505,6 +1534,7 @@ #[tokio::test] #[serial] async fn cross_client_list_accounts_isolated() { + common::require_db!(); let app = common::app::TestApp::new_with_encryption().await; let owner_did = "did:plc:xc_la_owner"; let studio1 = "did:plc:xc_la_studio1"; diff --git a/tests/e2e_domains.rs b/tests/e2e_domains.rs --- a/tests/e2e_domains.rs +++ b/tests/e2e_domains.rs @@ -96,8 +96,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn domains_list_returns_seeded_domain() { + common::require_db!(); let app = TestApp::new().await; seed_domain(&app, "primary-id", "http://127.0.0.1:0", true).await; @@ -119,8 +119,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn domains_create_and_delete() { + common::require_db!(); let app = TestApp::new().await; seed_domain(&app, "primary-id", "http://127.0.0.1:0", true).await; @@ -175,8 +175,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn domains_duplicate_url_returns_400() { + common::require_db!(); let app = TestApp::new().await; seed_domain(&app, "primary-id", "http://127.0.0.1:0", true).await; @@ -203,8 +203,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn domains_cannot_delete_primary() { + common::require_db!(); let app = TestApp::new().await; seed_domain(&app, "primary-id", "http://127.0.0.1:0", true).await; @@ -229,8 +229,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn domains_set_primary() { + common::require_db!(); let app = TestApp::new().await; seed_domain(&app, "id-a", "http://127.0.0.1:0", true).await; @@ -278,8 +278,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn unknown_host_returns_421_on_domain_scoped_routes() { + common::require_db!(); let app = TestApp::new().await; // No domains seeded — cache is empty @@ -300,8 +300,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn health_check_bypasses_domain_resolution() { + common::require_db!(); let app = TestApp::new().await; // No domains seeded — cache is empty @@ -322,8 +322,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn domain_scoped_route_works_with_known_host() { + common::require_db!(); let app = TestApp::new().await; // Domain.host() for "http://localhost:3000" is "localhost:3000" diff --git a/tests/e2e_feature_flags.rs b/tests/e2e_feature_flags.rs --- a/tests/e2e_feature_flags.rs +++ b/tests/e2e_feature_flags.rs @@ -53,8 +53,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn space_routes_blocked_when_flag_disabled() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -76,8 +76,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn space_routes_allowed_after_enabling_flag() { + common::require_db!(); let app = TestApp::new().await; // Enable the feature flag @@ -116,8 +116,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn space_routes_blocked_again_after_disabling_flag() { + common::require_db!(); let app = TestApp::new().await; // Enable @@ -165,8 +165,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn admin_feature_flags_lists_flags() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -192,8 +192,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn admin_feature_flags_reflects_enabled_state() { + common::require_db!(); let app = TestApp::new().await; // Enable the flag @@ -228,8 +228,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn config_endpoint_includes_features() { + common::require_db!(); let app = TestApp::new().await; // Default: spaces disabled @@ -282,8 +282,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn admin_feature_flags_requires_auth() { + common::require_db!(); let app = TestApp::new().await; let resp = app diff --git a/tests/e2e_health.rs b/tests/e2e_health.rs --- a/tests/e2e_health.rs +++ b/tests/e2e_health.rs @@ -8,8 +8,8 @@ use tower::ServiceExt; #[tokio::test] #[serial] -#[ignore] async fn health_returns_200_ok() { + common::require_db!(); let app = common::app::TestApp::new().await; let resp = app diff --git a/tests/e2e_labelers.rs b/tests/e2e_labelers.rs --- a/tests/e2e_labelers.rs +++ b/tests/e2e_labelers.rs @@ -76,8 +76,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn labeler_add_returns_201() { + common::require_db!(); let app = TestApp::new().await; let body = json!({ "did": "did:plc:labeler1" }); @@ -94,8 +94,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn labeler_add_upsert_reactivates() { + common::require_db!(); let app = TestApp::new().await; let body = json!({ "did": "did:plc:labeler1" }); @@ -151,8 +151,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn labeler_list_empty() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -169,8 +169,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn labeler_list_returns_added() { + common::require_db!(); let app = TestApp::new().await; app.router @@ -217,8 +217,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn labeler_update_status() { + common::require_db!(); let app = TestApp::new().await; app.router @@ -261,8 +261,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn labeler_update_not_found() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -285,8 +285,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn labeler_delete_returns_204() { + common::require_db!(); let app = TestApp::new().await; app.router @@ -327,8 +327,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn labeler_delete_not_found() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -346,8 +346,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn labeler_delete_removes_labels() { + common::require_db!(); let app = TestApp::new().await; let backend = app.state.db_backend; @@ -405,8 +405,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn labeler_no_auth_returns_401() { + common::require_db!(); let app = TestApp::new().await; let resp = app diff --git a/tests/e2e_network_lexicons.rs b/tests/e2e_network_lexicons.rs --- a/tests/e2e_network_lexicons.rs +++ b/tests/e2e_network_lexicons.rs @@ -72,8 +72,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn network_lexicon_list_empty() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -90,8 +90,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn network_lexicon_list_returns_seeded() { + common::require_db!(); let app = TestApp::new().await; seed_network_lexicon(&app, "games.gamesgamesgamesgames.game", "did:plc:authority").await; @@ -113,8 +113,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn network_lexicon_delete_removes_tracking_and_lexicon() { + common::require_db!(); let app = TestApp::new().await; let backend = app.state.db_backend; @@ -149,8 +149,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn network_lexicon_delete_not_found() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -168,8 +168,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn network_lexicon_no_auth_returns_401() { + common::require_db!(); let app = TestApp::new().await; let resp = app diff --git a/tests/e2e_proxy_config.rs b/tests/e2e_proxy_config.rs --- a/tests/e2e_proxy_config.rs +++ b/tests/e2e_proxy_config.rs @@ -41,8 +41,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn get_proxy_config_returns_default() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -60,8 +60,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn put_and_get_allowlist() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -98,8 +98,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn disabled_mode_clears_nsids() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -132,8 +132,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn invalid_mode_rejected() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -155,8 +155,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn invalid_nsid_rejected() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -178,8 +178,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn requires_auth() { + common::require_db!(); let app = TestApp::new().await; let resp = app diff --git a/tests/e2e_settings.rs b/tests/e2e_settings.rs --- a/tests/e2e_settings.rs +++ b/tests/e2e_settings.rs @@ -61,8 +61,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn settings_crud() { + common::require_db!(); let app = TestApp::new().await; // PUT a setting @@ -134,8 +134,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn settings_requires_auth() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -155,8 +155,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn logo_upload_and_serve() { + common::require_db!(); let app = TestApp::new().await; let boundary = "----testboundary"; @@ -260,8 +260,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn client_metadata_includes_settings() { + common::require_db!(); let app = TestApp::new().await; // PUT app_name setting @@ -306,8 +306,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn client_metadata_client_id_matches_path() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -332,46 +332,8 @@ } #[tokio::test] #[serial] -#[ignore] -async fn client_metadata_scope_overridden_by_setting() { - let app = TestApp::new().await; - - let resp = app - .router - .clone() - .oneshot(admin_put( - "/admin/settings/oauth_scopes", - app.admin_cookie(), - &json!({ "value": "atproto include:com.example.foo\n include:com.example.bar" }), - )) - .await - .unwrap(); - assert!(resp.status().is_success()); - - let resp = app - .router - .clone() - .oneshot( - Request::builder() - .uri("/oauth-client-metadata.json") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - let json = json_body(resp).await; - assert_eq!( - json["scope"], "atproto include:com.example.foo include:com.example.bar", - "expected normalized scope string, got {:?}", - json["scope"] - ); -} - -#[tokio::test] -#[serial] -#[ignore] async fn client_metadata_client_uri_overridden_by_setting() { + common::require_db!(); let app = TestApp::new().await; let resp = app diff --git a/tests/e2e_xrpc.rs b/tests/e2e_xrpc.rs --- a/tests/e2e_xrpc.rs +++ b/tests/e2e_xrpc.rs @@ -109,8 +109,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn profile_no_auth_returns_401() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -128,58 +128,14 @@ assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } -#[tokio::test] -#[serial] -#[ignore] -async fn profile_with_mocked_services_returns_200() { - let app = TestApp::new().await; - let did = &app.admin_did; - - // Mock PLC directory - Mock::given(method("GET")) - .and(path(format!("/{did}"))) - .respond_with( - ResponseTemplate::new(200) - .set_body_json(fixtures::did_document(did, &app.mock_server.uri())), - ) - .mount(&app.mock_server) - .await; - - // Mock PDS getRecord for profile - Mock::given(method("GET")) - .and(path("/xrpc/com.atproto.repo.getRecord")) - .respond_with(ResponseTemplate::new(200).set_body_json(fixtures::profile_record())) - .mount(&app.mock_server) - .await; - - let cookie = app.admin_cookie(); - let resp = app - .router - .clone() - .oneshot( - Request::builder() - .uri("/xrpc/app.bsky.actor.getProfile") - .header(cookie.0, cookie.1) - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(resp.status(), StatusCode::OK); - let json = json_body(resp).await; - assert_eq!(json["did"], did.as_str()); - assert_eq!(json["displayName"], "Test User"); -} - // --------------------------------------------------------------------------- // Catch-all GET (queries) // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn xrpc_get_unknown_method_proxies_and_returns_bad_gateway() { + common::require_db!(); let app = TestApp::new().await; let resp = app @@ -188,6 +144,7 @@ .clone() .oneshot( Request::builder() .uri("/xrpc/nonexistent.method") + .header("x-client-key", "hvc_test") .body(Body::empty()) .unwrap(), ) @@ -201,8 +158,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn xrpc_get_non_query_returns_400() { + common::require_db!(); let app = TestApp::new().await; seed_lexicons(&app).await; @@ -213,6 +170,7 @@ .clone() .oneshot( Request::builder() .uri("/xrpc/games.gamesgamesgamesgames.game") + .header("x-client-key", "hvc_test") .body(Body::empty()) .unwrap(), ) @@ -224,8 +182,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn xrpc_get_single_record_by_uri() { + common::require_db!(); let app = TestApp::new().await; seed_lexicons(&app).await; @@ -253,6 +211,7 @@ .uri(format!( "/xrpc/games.gamesgamesgamesgames.listGames?uri={}", urlencoding::encode(uri) )) + .header("x-client-key", "hvc_test") .body(Body::empty()) .unwrap(), ) @@ -267,8 +226,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn xrpc_get_record_not_found() { + common::require_db!(); let app = TestApp::new().await; seed_lexicons(&app).await; @@ -277,6 +236,7 @@ .router.clone() .oneshot( Request::builder() .uri("/xrpc/games.gamesgamesgamesgames.listGames?uri=at%3A%2F%2Fdid%3Aplc%3Anone%2Fgames.gamesgamesgamesgames.game%2Fmissing") + .header("x-client-key", "hvc_test") .body(Body::empty()) .unwrap(), ) @@ -288,8 +248,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn xrpc_get_list_with_pagination() { + common::require_db!(); let app = TestApp::new().await; seed_lexicons(&app).await; @@ -326,6 +286,7 @@ .clone() .oneshot( Request::builder() .uri("/xrpc/games.gamesgamesgamesgames.listGames?limit=2") + .header("x-client-key", "hvc_test") .body(Body::empty()) .unwrap(), ) @@ -347,6 +308,7 @@ Request::builder() .uri(format!( "/xrpc/games.gamesgamesgamesgames.listGames?limit=2&cursor={cursor}" )) + .header("x-client-key", "hvc_test") .body(Body::empty()) .unwrap(), ) @@ -361,8 +323,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn xrpc_get_list_filtered_by_did() { + common::require_db!(); let app = TestApp::new().await; seed_lexicons(&app).await; @@ -402,6 +364,7 @@ .clone() .oneshot( Request::builder() .uri("/xrpc/games.gamesgamesgamesgames.listGames?did=did:plc:a") + .header("x-client-key", "hvc_test") .body(Body::empty()) .unwrap(), ) @@ -421,8 +384,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn xrpc_post_no_auth_returns_401() { + common::require_db!(); let app = TestApp::new().await; seed_lexicons(&app).await; @@ -445,37 +408,8 @@ } #[tokio::test] #[serial] -#[ignore] -async fn xrpc_post_non_procedure_returns_400() { - let app = TestApp::new().await; - seed_lexicons(&app).await; - - // Use cookie auth for did:plc:test so auth passes - let (cookie_name, cookie_val) = - common::auth::admin_cookie_header("did:plc:test", &app.state.cookie_key); - - let resp = app - .router - .clone() - .oneshot( - Request::builder() - .method("POST") - .uri("/xrpc/games.gamesgamesgamesgames.listGames") - .header(cookie_name, cookie_val) - .header("content-type", "application/json") - .body(Body::from(b"{}".to_vec())) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); -} - -#[tokio::test] -#[serial] -#[ignore] async fn xrpc_delete_procedure_removes_record() { + common::require_db!(); let app = TestApp::new().await; seed_lexicons(&app).await; let backend = app.state.db_backend; @@ -576,8 +510,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn upload_lexicon_with_invalid_action_returns_400() { + common::require_db!(); let app = TestApp::new().await; let resp = app diff --git a/tests/lua_atproto_api.rs b/tests/lua_atproto_api.rs --- a/tests/lua_atproto_api.rs +++ b/tests/lua_atproto_api.rs @@ -84,7 +84,9 @@ oauth: std::sync::Arc::new(happyview::auth::OAuthClientRegistry::new( std::sync::Arc::new(oauth), )), oauth_state_store: happyview::auth::oauth_store::DbStateStore::new(pool.clone(), backend), - cookie_key: axum_extra::extract::cookie::Key::derive_from(b"test-secret"), + cookie_key: axum_extra::extract::cookie::Key::derive_from( + b"test-secret-that-is-at-least-32-bytes-long", + ), plugin_registry: std::sync::Arc::new(happyview::plugin::PluginRegistry::new()), wasm_runtime: std::sync::Arc::new( happyview::plugin::WasmRuntime::new().expect("wasm runtime"), @@ -170,8 +172,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn get_labels_returns_external_labels() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -217,8 +219,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn get_labels_filters_expired() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -266,8 +268,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn get_labels_includes_self_labels() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -309,8 +311,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn get_labels_empty_for_unlabeled_record() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -346,8 +348,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn get_labels_batch_returns_labels_per_uri() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -408,8 +410,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn get_labels_batch_empty_for_no_labels() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -439,8 +441,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn label_negation_removes_row() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -498,8 +500,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn label_upsert_is_idempotent() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; diff --git a/tests/lua_db_api.rs b/tests/lua_db_api.rs --- a/tests/lua_db_api.rs +++ b/tests/lua_db_api.rs @@ -87,7 +87,9 @@ oauth: std::sync::Arc::new(happyview::auth::OAuthClientRegistry::new( std::sync::Arc::new(oauth), )), oauth_state_store: happyview::auth::oauth_store::DbStateStore::new(pool.clone(), backend), - cookie_key: axum_extra::extract::cookie::Key::derive_from(b"test-secret"), + cookie_key: axum_extra::extract::cookie::Key::derive_from( + b"test-secret-that-is-at-least-32-bytes-long", + ), plugin_registry: std::sync::Arc::new(happyview::plugin::PluginRegistry::new()), wasm_runtime: std::sync::Arc::new( happyview::plugin::WasmRuntime::new().expect("wasm runtime"), @@ -165,8 +167,8 @@ // --------------------------------------------------------------------------- #[tokio::test] #[serial] -#[ignore] async fn db_get_returns_record() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -189,8 +191,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn db_get_returns_nil_for_missing() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -208,8 +210,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn db_query_returns_records() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -229,8 +231,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn db_query_respects_limit() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -254,8 +256,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn db_count_returns_total() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -274,8 +276,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn db_count_with_did_filter() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -294,8 +296,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn db_search_finds_matching() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; @@ -318,8 +320,8 @@ } #[tokio::test] #[serial] -#[ignore] async fn db_raw_select_works() { + common::require_db!(); let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; diff --git a/tests/plugin_logging.rs b/tests/plugin_logging.rs --- a/tests/plugin_logging.rs +++ b/tests/plugin_logging.rs @@ -14,6 +14,7 @@ /// Wait briefly for detached `tokio::spawn` tasks to flush writes. /// The log() function spawns fire-and-forget tasks; we need to yield /// until they complete before querying. async fn flush_spawned_tasks() { + common::require_db!(); for _ in 0..20 { tokio::task::yield_now().await; tokio::time::sleep(tokio::time::Duration::from_millis(25)).await; @@ -23,6 +24,7 @@ #[tokio::test] #[serial] async fn plugin_log_writes_all_four_levels_to_event_logs() { + common::require_db!(); let pool = test_pool().await; let backend = test_backend(); truncate_all(&pool).await; @@ -103,6 +105,7 @@ #[tokio::test] #[serial] async fn plugin_log_with_none_db_does_not_write_event_log() { + common::require_db!(); let pool = test_pool().await; let backend = test_backend(); truncate_all(&pool).await;