Something went wrong. Try again.
A lexicon-driven AppView for ATProto.
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353mod common;
use axum::body::Body;use axum::http::{Method, Request, StatusCode};use http_body_util::BodyExt;use serde_json::{Value, json};use serial_test::serial;use tower::ServiceExt;
use common::app::TestApp;
// ---------------------------------------------------------------------------// Helpers// ---------------------------------------------------------------------------
async fn json_body(resp: axum::response::Response) -> Value { let body = resp.into_body().collect().await.unwrap().to_bytes(); serde_json::from_slice(&body).unwrap()}
fn admin_get( uri: &str, cookie: (axum::http::HeaderName, axum::http::HeaderValue),) -> Request<Body> { Request::builder() .uri(uri) .header(cookie.0, cookie.1) .body(Body::empty()) .unwrap()}
fn admin_post( uri: &str, cookie: (axum::http::HeaderName, axum::http::HeaderValue), body: &Value,) -> Request<Body> { Request::builder() .method(Method::POST) .uri(uri) .header(cookie.0, cookie.1) .header("content-type", "application/json") .body(Body::from(serde_json::to_vec(body).unwrap())) .unwrap()}
fn admin_delete( uri: &str, cookie: (axum::http::HeaderName, axum::http::HeaderValue),) -> Request<Body> { Request::builder() .method(Method::DELETE) .uri(uri) .header(cookie.0, cookie.1) .body(Body::empty()) .unwrap()}
fn get_with_host(uri: &str, host: &str) -> Request<Body> { Request::builder() .uri(uri) .header("host", host) .body(Body::empty()) .unwrap()}
async fn seed_domain(app: &TestApp, id: &str, url: &str, is_primary: bool) { let now = happyview::db::now_rfc3339(); let sql = happyview::db::adapt_sql( "INSERT INTO domains (id, url, is_primary, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", app.state.db_backend, ); sqlx::query(&sql) .bind(id) .bind(url) .bind(if is_primary { 1i32 } else { 0i32 }) .bind(&now) .bind(&now) .execute(&app.state.db) .await .unwrap(); app.state .domain_cache .insert(happyview::domain::Domain { id: id.into(), url: url.into(), is_primary, created_at: now.clone(), updated_at: now, }) .await;}
// ---------------------------------------------------------------------------// Domains tests// ---------------------------------------------------------------------------
#[tokio::test]#[serial]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;
let resp = app .router .clone() .oneshot(admin_get("/admin/domains", app.admin_cookie())) .await .unwrap();
assert_eq!(resp.status(), StatusCode::OK); let json = json_body(resp).await; let domains = json.as_array().expect("expected array"); assert_eq!(domains.len(), 1, "expected 1 domain, got {}", domains.len()); assert_eq!(domains[0]["url"], "http://127.0.0.1:0"); assert_eq!(domains[0]["is_primary"], true);}
#[tokio::test]#[serial]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;
// Create a new domain let resp = app .router .clone() .oneshot(admin_post( "/admin/domains", app.admin_cookie(), &json!({ "url": "http://127.0.0.1:9999" }), )) .await .unwrap();
assert_eq!( resp.status(), StatusCode::CREATED, "expected 201 on create, got {}", resp.status() ); let json = json_body(resp).await; assert!( json["id"].is_string(), "expected id in response, got {:?}", json ); assert_eq!(json["url"], "http://127.0.0.1:9999"); assert_eq!(json["is_primary"], false);
let new_id = json["id"].as_str().unwrap().to_string();
// Delete the newly created domain let resp = app .router .clone() .oneshot(admin_delete( &format!("/admin/domains/{new_id}"), app.admin_cookie(), )) .await .unwrap();
assert_eq!( resp.status(), StatusCode::NO_CONTENT, "expected 204 on delete, got {}", resp.status() );}
#[tokio::test]#[serial]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;
// Attempt to create a domain with the same URL let resp = app .router .clone() .oneshot(admin_post( "/admin/domains", app.admin_cookie(), &json!({ "url": "http://127.0.0.1:0" }), )) .await .unwrap();
assert_eq!( resp.status(), StatusCode::BAD_REQUEST, "expected 400 on duplicate URL, got {}", resp.status() );}
#[tokio::test]#[serial]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;
let resp = app .router .clone() .oneshot(admin_delete( "/admin/domains/primary-id", app.admin_cookie(), )) .await .unwrap();
assert_eq!( resp.status(), StatusCode::BAD_REQUEST, "expected 400 when deleting primary domain, got {}", resp.status() );}
#[tokio::test]#[serial]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; seed_domain(&app, "id-b", "http://127.0.0.1:9999", false).await;
// Set domain b as primary let resp = app .router .clone() .oneshot(admin_post( "/admin/domains/id-b/primary", app.admin_cookie(), &json!({}), )) .await .unwrap();
assert_eq!( resp.status(), StatusCode::NO_CONTENT, "expected 204 on set primary, got {}", resp.status() );
// Verify domain b is now primary let resp = app .router .clone() .oneshot(admin_get("/admin/domains", app.admin_cookie())) .await .unwrap();
assert_eq!(resp.status(), StatusCode::OK); let json = json_body(resp).await; let domains = json.as_array().expect("expected array"); let domain_b = domains .iter() .find(|d| d["id"] == "id-b") .expect("domain b not found"); assert_eq!( domain_b["is_primary"], true, "expected domain b to be primary" );}
#[tokio::test]#[serial]async fn unknown_host_returns_421_on_domain_scoped_routes() { common::require_db!(); let app = TestApp::new().await;
// No domains seeded — cache is empty let resp = app .router .clone() .oneshot(get_with_host("/config", "unknown.example.com")) .await .unwrap();
assert_eq!( resp.status(), StatusCode::MISDIRECTED_REQUEST, "expected 421 for unknown host, got {}", resp.status() );}
#[tokio::test]#[serial]async fn health_check_bypasses_domain_resolution() { common::require_db!(); let app = TestApp::new().await;
// No domains seeded — cache is empty let resp = app .router .clone() .oneshot(get_with_host("/health", "unknown.example.com")) .await .unwrap();
assert_eq!( resp.status(), StatusCode::OK, "expected 200 on /health regardless of host, got {}", resp.status() );}
#[tokio::test]#[serial]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" seed_domain(&app, "local-id", "http://localhost:3000", true).await;
let resp = app .router .clone() .oneshot(get_with_host("/config", "localhost:3000")) .await .unwrap();
assert_eq!( resp.status(), StatusCode::OK, "expected 200 on /config with known host, got {}", resp.status() );
let json = json_body(resp).await; assert_eq!( json["public_url"], "http://localhost:3000", "expected public_url to match domain URL, got {:?}", json["public_url"] );}