Something went wrong. Try again.
A lexicon-driven AppView for ATProto.
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426mod common;
use axum::body::Body;use axum::http::{Request, StatusCode};use happyview::db::{adapt_sql, now_rfc3339};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("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_patch( uri: &str, cookie: (axum::http::HeaderName, axum::http::HeaderValue), body: &Value,) -> Request<Body> { Request::builder() .method("PATCH") .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("DELETE") .uri(uri) .header(cookie.0, cookie.1) .body(Body::empty()) .unwrap()}
// ---------------------------------------------------------------------------// POST /admin/labelers// ---------------------------------------------------------------------------
#[tokio::test]#[serial]async fn labeler_add_returns_201() { common::require_db!(); let app = TestApp::new().await;
let body = json!({ "did": "did:plc:labeler1" });
let resp = app .router .clone() .oneshot(admin_post("/admin/labelers", app.admin_cookie(), &body)) .await .unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);}
#[tokio::test]#[serial]async fn labeler_add_upsert_reactivates() { common::require_db!(); let app = TestApp::new().await;
let body = json!({ "did": "did:plc:labeler1" });
// First add app.router .clone() .clone() .oneshot(admin_post("/admin/labelers", app.admin_cookie(), &body)) .await .unwrap();
// Pause it app.router .clone() .clone() .oneshot(admin_patch( "/admin/labelers/did:plc:labeler1", app.admin_cookie(), &json!({ "status": "paused" }), )) .await .unwrap();
// Re-add (upsert should reactivate) let resp = app .router .clone() .clone() .oneshot(admin_post("/admin/labelers", app.admin_cookie(), &body)) .await .unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
// Verify it's active again let resp = app .router .clone() .oneshot(admin_get("/admin/labelers", app.admin_cookie())) .await .unwrap();
let json = json_body(resp).await; let labelers = json.as_array().unwrap(); assert_eq!(labelers.len(), 1); assert_eq!(labelers[0]["status"], "active");}
// ---------------------------------------------------------------------------// GET /admin/labelers// ---------------------------------------------------------------------------
#[tokio::test]#[serial]async fn labeler_list_empty() { common::require_db!(); let app = TestApp::new().await;
let resp = app .router .clone() .oneshot(admin_get("/admin/labelers", app.admin_cookie())) .await .unwrap();
assert_eq!(resp.status(), StatusCode::OK); let json = json_body(resp).await; assert!(json.as_array().unwrap().is_empty());}
#[tokio::test]#[serial]async fn labeler_list_returns_added() { common::require_db!(); let app = TestApp::new().await;
app.router .clone() .clone() .oneshot(admin_post( "/admin/labelers", app.admin_cookie(), &json!({ "did": "did:plc:lab1" }), )) .await .unwrap();
app.router .clone() .clone() .oneshot(admin_post( "/admin/labelers", app.admin_cookie(), &json!({ "did": "did:plc:lab2" }), )) .await .unwrap();
let resp = app .router .clone() .oneshot(admin_get("/admin/labelers", app.admin_cookie())) .await .unwrap();
assert_eq!(resp.status(), StatusCode::OK); let json = json_body(resp).await; let labelers = json.as_array().unwrap(); assert_eq!(labelers.len(), 2); assert_eq!(labelers[0]["did"], "did:plc:lab1"); assert_eq!(labelers[0]["status"], "active"); assert_eq!(labelers[1]["did"], "did:plc:lab2");}
// ---------------------------------------------------------------------------// PATCH /admin/labelers/{did}// ---------------------------------------------------------------------------
#[tokio::test]#[serial]async fn labeler_update_status() { common::require_db!(); let app = TestApp::new().await;
app.router .clone() .clone() .oneshot(admin_post( "/admin/labelers", app.admin_cookie(), &json!({ "did": "did:plc:lab1" }), )) .await .unwrap();
let resp = app .router .clone() .clone() .oneshot(admin_patch( "/admin/labelers/did:plc:lab1", app.admin_cookie(), &json!({ "status": "paused" }), )) .await .unwrap();
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
// Verify status changed let resp = app .router .clone() .oneshot(admin_get("/admin/labelers", app.admin_cookie())) .await .unwrap();
let json = json_body(resp).await; let labelers = json.as_array().unwrap(); assert_eq!(labelers[0]["status"], "paused");}
#[tokio::test]#[serial]async fn labeler_update_not_found() { common::require_db!(); let app = TestApp::new().await;
let resp = app .router .clone() .oneshot(admin_patch( "/admin/labelers/did:plc:nonexistent", app.admin_cookie(), &json!({ "status": "paused" }), )) .await .unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);}
// ---------------------------------------------------------------------------// DELETE /admin/labelers/{did}// ---------------------------------------------------------------------------
#[tokio::test]#[serial]async fn labeler_delete_returns_204() { common::require_db!(); let app = TestApp::new().await;
app.router .clone() .clone() .oneshot(admin_post( "/admin/labelers", app.admin_cookie(), &json!({ "did": "did:plc:lab1" }), )) .await .unwrap();
let resp = app .router .clone() .clone() .oneshot(admin_delete( "/admin/labelers/did:plc:lab1", app.admin_cookie(), )) .await .unwrap();
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
// Verify it's gone let resp = app .router .clone() .oneshot(admin_get("/admin/labelers", app.admin_cookie())) .await .unwrap();
let json = json_body(resp).await; assert!(json.as_array().unwrap().is_empty());}
#[tokio::test]#[serial]async fn labeler_delete_not_found() { common::require_db!(); let app = TestApp::new().await;
let resp = app .router .clone() .oneshot(admin_delete( "/admin/labelers/did:plc:nonexistent", app.admin_cookie(), )) .await .unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);}
#[tokio::test]#[serial]async fn labeler_delete_removes_labels() { common::require_db!(); let app = TestApp::new().await; let backend = app.state.db_backend;
// Add a labeler app.router .clone() .clone() .oneshot(admin_post( "/admin/labelers", app.admin_cookie(), &json!({ "did": "did:plc:lab1" }), )) .await .unwrap();
// Seed some labels from that labeler let sql = adapt_sql( "INSERT INTO labels (src, uri, val, cts) VALUES (?, ?, ?, ?)", backend, ); sqlx::query(&sql) .bind("did:plc:lab1") .bind("at://did:plc:user/test.collection/rkey1") .bind("adult-content") .bind(now_rfc3339()) .execute(&app.state.db) .await .unwrap();
// Delete the labeler app.router .clone() .clone() .oneshot(admin_delete( "/admin/labelers/did:plc:lab1", app.admin_cookie(), )) .await .unwrap();
// Verify labels were also removed let sql = adapt_sql("SELECT COUNT(*) FROM labels WHERE src = ?", backend); let count: (i64,) = sqlx::query_as(&sql) .bind("did:plc:lab1") .fetch_one(&app.state.db) .await .unwrap();
assert_eq!(count.0, 0);}
// ---------------------------------------------------------------------------// Auth// ---------------------------------------------------------------------------
#[tokio::test]#[serial]async fn labeler_no_auth_returns_401() { common::require_db!(); let app = TestApp::new().await;
let resp = app .router .clone() .oneshot( Request::builder() .uri("/admin/labelers") .body(Body::empty()) .unwrap(), ) .await .unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);}