Skip to main content

atrg_testing/
test_app.rs

1//! Test application builder — in-memory everything, no network.
2
3use atrg_auth::session;
4use atrg_core::config::{AppConfig, AuthConfig, Config, DatabaseConfig};
5use atrg_core::state::AppState;
6use sqlx::SqlitePool;
7use std::sync::Arc;
8
9/// Build a test [`AppState`] backed by in-memory SQLite.
10///
11/// Migrations are applied automatically. No outbound network calls are made.
12pub async fn test_state() -> AppState {
13    let db = atrg_db::connect("sqlite::memory:").await.expect("test db");
14    atrg_db::run_internal_migrations(&db)
15        .await
16        .expect("migrations");
17
18    let config = Config {
19        app: AppConfig {
20            name: "test-app".into(),
21            host: "127.0.0.1".into(),
22            port: 3000,
23            secret_key: "test-secret-key-at-least-32-chars!!".into(),
24            cors_origins: vec![],
25            environment: "development".into(),
26        },
27        auth: AuthConfig {
28            client_id: "http://localhost:3000/client-metadata.json".into(),
29            redirect_uri: "http://localhost:3000/auth/callback".into(),
30            scope: "atproto transition:generic".into(),
31        },
32        database: DatabaseConfig {
33            url: "sqlite::memory:".into(),
34        },
35        jetstream: None,
36        firehose: None,
37        feed_generator: None,
38        labeler: None,
39        rate_limit: None,
40    };
41
42    AppState {
43        config: Arc::new(config),
44        db,
45        http: reqwest::Client::new(),
46        identity: Arc::new(atrg_identity::IdentityResolver::with_defaults(
47            reqwest::Client::new(),
48        )),
49    }
50}
51
52/// Build a test [`AppState`] and an [`atrg_core::AtrgApp`] ready for `oneshot` testing.
53pub async fn test_app() -> (atrg_core::AtrgApp, AppState) {
54    let state = test_state().await;
55    let app = atrg_core::AtrgApp::new();
56    (app, state)
57}
58
59/// Seed a session into the test database for a given user.
60///
61/// Returns the session ID that can be used in `Authorization: Bearer <id>`
62/// or `Cookie: atrg_session=<id>` headers.
63pub async fn seed_session(pool: &SqlitePool, did: &str, handle: &str) -> String {
64    let session_id = session::generate_session_id();
65    let expires = std::time::SystemTime::now()
66        .duration_since(std::time::UNIX_EPOCH)
67        .expect("system clock before UNIX epoch")
68        .as_secs() as i64
69        + 86400; // 24 hours
70
71    session::create_session(
72        pool,
73        &session_id,
74        did,
75        handle,
76        "test_access_token",
77        Some("test_refresh_token"),
78        expires,
79    )
80    .await
81    .expect("seed session");
82
83    session_id
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[tokio::test]
91    async fn test_state_works() {
92        let state = test_state().await;
93        assert_eq!(state.config.app.name, "test-app");
94    }
95
96    #[tokio::test]
97    async fn test_app_works() {
98        let (_app, state) = test_app().await;
99        assert_eq!(state.config.app.name, "test-app");
100    }
101
102    #[tokio::test]
103    async fn seed_and_find_session() {
104        let state = test_state().await;
105        let sid = seed_session(&state.db, "did:plc:test", "test.user").await;
106        assert!(!sid.is_empty());
107
108        let found = session::find_session(&state.db, &sid).await.unwrap();
109        assert!(found.is_some());
110        let s = found.unwrap();
111        assert_eq!(s.did, "did:plc:test");
112        assert_eq!(s.handle, "test.user");
113    }
114}