Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356//! Tests for the appview read layer.//!//! The read helpers are exercised against an in-memory SQLite database with the//! migrations applied and representative rows seeded directly (the projection is//! tested separately in `indexing::projection`). The bsky profile fetch is never//! hit here: the polymodel union variant comes from seeded SQLite, and the bsky//! variant's union discrimination is covered by a serde round-trip.
#![cfg(test)]
pub(crate) use axum::body::Body;pub(crate) use axum::http::{Request, StatusCode};pub(crate) use dioxus::prelude::dioxus_fullstack::FullstackContext;use jacquard::oauth::authstore::ClientAuthStore;pub(crate) use jacquard::oauth::client::OAuthSession;use jacquard::oauth::keyset::Keyset;use jacquard::oauth::scopes::Scopes;use jacquard::oauth::session::{ClientSessionData, DpopClientData};use jacquard::oauth::types::{OAuthTokenType, TokenSet};use jacquard_common::deps::fluent_uri::Uri;use jacquard_common::deps::smol_str::SmolStr;pub(crate) use jacquard_common::session::SessionKey;pub(crate) use jacquard_common::types::datetime::Datetime;pub(crate) use jacquard_common::types::string::Did;pub(crate) use polymodel_api::space_polymodel::actor::ProfileViewUnion;pub(crate) use polymodel_api::space_polymodel::actor::get_session::{ GetSessionOutput, SessionView,};pub(crate) use serde_json::json;pub(crate) use sqlx::SqlitePool;use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};pub(crate) use std::collections::HashSet;use std::str::FromStr;pub(crate) use tower::ServiceExt;
pub(crate) use super::error::AppError;pub(crate) use super::state::AppState;
pub(crate) const DID_A: &str = "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa";pub(crate) const DID_B: &str = "did:plc:bbbbbbbbbbbbbbbbbbbbbbbb";
pub(crate) fn did(s: &str) -> Did { Did::new_owned(s).unwrap()}
pub(crate) fn client_session(account_did: &str, session_id: &str) -> ClientSessionData { let dpop_key = Keyset::generate_es256("test") .unwrap() .public_jwks() .keys .into_iter() .next() .unwrap() .key; ClientSessionData { account_did: did(account_did), session_id: SmolStr::new(session_id), host_url: Uri::parse("https://pds.example.com").unwrap().to_owned(), authserver_url: SmolStr::new("https://issuer.example.com"), authserver_token_endpoint: SmolStr::new("https://issuer.example.com/token"), authserver_revocation_endpoint: None, scopes: Scopes::empty(), dpop_data: DpopClientData { dpop_key, dpop_authserver_nonce: SmolStr::default(), dpop_host_nonce: SmolStr::default(), }, token_set: TokenSet { iss: SmolStr::new("https://issuer.example.com"), sub: did(account_did), aud: SmolStr::new("https://pds.example.com"), scope: None, refresh_token: None, access_token: SmolStr::new("token"), token_type: OAuthTokenType::DPoP, expires_at: Some(Datetime::raw_str("2099-01-01T00:00:00.000Z")), }, resolved_scopes: None, }}
pub(crate) async fn seed_oauth_session( state: &AppState, account_did: &str, session_id: &str,) -> SessionKey { let data = client_session(account_did, session_id); let key = SessionKey::new(data.account_did.clone(), data.session_id.clone()); state .oauth .registry .store .upsert_session(data) .await .unwrap(); key}
pub(crate) async fn pool() -> SqlitePool { let options = SqliteConnectOptions::from_str("sqlite::memory:").unwrap(); let pool = SqlitePoolOptions::new() .max_connections(1) .connect_with(options) .await .unwrap(); sqlx::migrate!("./migrations").run(&pool).await.unwrap(); pool}
pub(crate) async fn state() -> AppState { let pool = pool().await; let bootstrap = crate::oauth::bootstrap_oauth(pool.clone(), Some("http://localhost")) .expect("ephemeral OAuth bootstrap for tests"); AppState::new(pool, bootstrap)}
// Seeding uses runtime queries so the committed .sqlx cache stays focused on production SQL.
pub(crate) fn blob(cid: &str) -> serde_json::Value { json!({ "$type": "blob", "ref": { "$link": cid }, "mimeType": "image/jpeg", "size": 1234 })}
pub(crate) fn image(cid: &str, alt: &str) -> serde_json::Value { json!({ "alt": alt, "aspectRatio": { "width": 4, "height": 3 }, "image": blob(cid) })}
pub(crate) fn strong_ref(uri: &str) -> serde_json::Value { json!({ "uri": uri, "cid": "bafyreicid" })}
pub(crate) async fn seed_thing( pool: &SqlitePool, did: &str, rkey: &str, name: &str, models: &[&str], like_count: i64,) -> String { let uri = format!("at://{did}/space.polymodel.library.thing/{rkey}"); let now = chrono::Utc::now() .timestamp_nanos_opt() .expect("current timestamp must fit in i64 nanoseconds"); let created = now - 86_400_000_000_000; // 1 day ago by default sqlx::query( r#"INSERT INTO things (did, rkey, uri, cid, name, summary, license, tags_json, tags_text, instructions_text, cover_json, derived_from_uri, created_at, indexed_at, record_json) VALUES (?, ?, ?, ?, ?, NULL, 'CC-BY-4.0', NULL, NULL, NULL, NULL, NULL, ?, ?, ?)"#, ) .bind(did) .bind(rkey) .bind(&uri) .bind("cid") .bind(name) .bind(created) .bind(now) .bind(json!({ "name": name, "license": "CC-BY-4.0", "cover": [image("bafkreiathingcover", "Thing cover")], "previews": [image("bafkreiathingpreview", "Thing preview")], "models": models.iter().map(|m| strong_ref(m)).collect::<Vec<_>>(), "createdAt": "2024-01-01T00:00:00.000Z" }).to_string()) .execute(pool) .await .unwrap(); for (pos, m) in models.iter().enumerate() { sqlx::query( "INSERT OR REPLACE INTO thing_models (thing_uri, model_uri, position) VALUES (?, ?, ?)", ) .bind(&uri) .bind(*m) .bind(pos as i64) .execute(pool) .await .unwrap(); } sqlx::query( "INSERT OR IGNORE INTO content_stats (uri, like_count, save_count, tag_count) VALUES (?, ?, 0, 0)", ) .bind(&uri) .bind(like_count) .execute(pool) .await .unwrap(); uri}
pub(crate) async fn set_thing_created_at(pool: &SqlitePool, uri: &str, created_at: i64) { sqlx::query("UPDATE things SET created_at = ? WHERE uri = ?") .bind(created_at) .bind(uri) .execute(pool) .await .unwrap();}
pub(crate) async fn seed_model( pool: &SqlitePool, did: &str, rkey: &str, name: &str, parts: &[&str],) -> String { let uri = format!("at://{did}/space.polymodel.library.model/{rkey}"); let now = chrono::Utc::now() .timestamp_nanos_opt() .expect("current timestamp must fit in i64 nanoseconds"); sqlx::query( r#"INSERT INTO models (did, rkey, uri, cid, name, summary, created_at, indexed_at, record_json) VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)"#, ) .bind(did) .bind(rkey) .bind(&uri) .bind("cid") .bind(name) .bind(now) .bind(now) .bind(json!({ "name": name, "cover": [image("bafkreiamodelcover", "Model cover")], "previews": [image("bafkreiamodelpreview", "Model preview")], "parts": parts.iter().map(|p| strong_ref(p)).collect::<Vec<_>>(), "createdAt": "2024-01-01T00:00:00.000Z" }).to_string()) .execute(pool) .await .unwrap(); for (pos, p) in parts.iter().enumerate() { sqlx::query( "INSERT OR REPLACE INTO model_parts (model_uri, part_uri, position) VALUES (?, ?, ?)", ) .bind(&uri) .bind(*p) .bind(pos as i64) .execute(pool) .await .unwrap(); } uri}
pub(crate) async fn seed_part(pool: &SqlitePool, did: &str, rkey: &str, name: &str) -> String { let uri = format!("at://{did}/space.polymodel.library.part/{rkey}"); let now = chrono::Utc::now() .timestamp_nanos_opt() .expect("current timestamp must fit in i64 nanoseconds"); sqlx::query( r#"INSERT INTO parts (did, rkey, uri, cid, name, format, file_json, created_at, indexed_at, record_json) VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?)"#, ) .bind(did) .bind(rkey) .bind(&uri) .bind("cid") .bind(name) .bind(json!({"mimeType": "model/stl", "size": 1, "chunks": []}).to_string()) .bind(now) .bind(now) .bind(json!({ "name": name, "file": {"mimeType": "model/stl", "size": 1, "chunks": []}, "previews": [image("bafkreiapartpreview", "Part preview")], "createdAt": "2024-01-01T00:00:00.000Z" }).to_string()) .execute(pool) .await .unwrap(); uri}
pub(crate) async fn seed_identity(pool: &SqlitePool, did: &str, handle: &str) { sqlx::query( "INSERT INTO identities (did, handle, updated_at) VALUES (?, ?, ?) \ ON CONFLICT(did) DO UPDATE SET handle = excluded.handle", ) .bind(did) .bind(handle) .bind( chrono::Utc::now() .timestamp_nanos_opt() .expect("current timestamp must fit in i64 nanoseconds"), ) .execute(pool) .await .unwrap();}
pub(crate) async fn seed_profile(pool: &SqlitePool, did: &str, display: &str) { let avatar = blob("bafkreiavatar").to_string(); sqlx::query( r#"INSERT INTO profiles (did, display_name, description, avatar_json, default_license, pronouns, printers_json, links_json, record_json, indexed_at) VALUES (?, ?, NULL, ?, NULL, NULL, NULL, NULL, ?, ?) ON CONFLICT(did) DO UPDATE SET display_name = excluded.display_name, avatar_json = excluded.avatar_json, record_json = excluded.record_json"#, ) .bind(did) .bind(display) .bind(avatar) .bind(json!({"displayName": display, "avatar": blob("bafkreiavatar")}).to_string()) .bind( chrono::Utc::now() .timestamp_nanos_opt() .expect("current timestamp must fit in i64 nanoseconds"), ) .execute(pool) .await .unwrap();}
pub(crate) async fn seed_list(pool: &SqlitePool, did: &str, rkey: &str, name: &str) -> String { let uri = format!("at://{did}/space.polymodel.graph.list/{rkey}"); let now = chrono::Utc::now() .timestamp_nanos_opt() .expect("current timestamp must fit in i64 nanoseconds"); sqlx::query( r#"INSERT INTO lists (did, rkey, uri, cid, name, description, purpose, created_at, indexed_at, record_json) VALUES (?, ?, ?, ?, ?, 'Print farm staples', 'curation', ?, ?, ?)"#, ) .bind(did) .bind(rkey) .bind(&uri) .bind("bafyreilistcid") .bind(name) .bind(now) .bind(now) .bind(json!({ "name": name, "description": "Print farm staples", "purpose": "curation", "createdAt": "2024-01-01T00:00:00.000Z" }).to_string()) .execute(pool) .await .unwrap(); uri}
pub(crate) async fn body_text(resp: axum::response::Response) -> String { let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20) .await .unwrap(); String::from_utf8(bytes.to_vec()).unwrap()}