Something went wrong. Try again.
A lexicon-driven AppView for ATProto. forked from gamesgamesgamesgames.games/happyview
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444//! End-to-end tests for the `Actor` Lua API.//!//! Requires `TEST_DATABASE_URL` to be set; ignored otherwise.
mod common;
use atrium_identity::did::{CommonDidResolver, CommonDidResolverConfig};use atrium_identity::handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig};use atrium_oauth::{ AtprotoLocalhostClientMetadata, DefaultHttpClient, KnownScope, OAuthClientConfig, OAuthResolverConfig, Scope,};use happyview::AppState;use happyview::config::Config;use happyview::db::DatabaseBackend;use happyview::lexicon::LexiconRegistry;use happyview::lua::actor_api::register_actor_api;use mlua::Lua;use serial_test::serial;use std::sync::Arc;use tokio::sync::watch;
use common::db;
async fn test_state(pool: sqlx::AnyPool, backend: DatabaseBackend) -> AppState { let config = Config { host: "127.0.0.1".into(), port: 3000, database_url: String::new(), database_backend: backend, public_url: String::new(), session_secret: "test-secret".into(), jetstream_url: String::new(), relay_url: String::new(), plc_url: String::new(), static_dir: String::new(), event_log_retention_days: 30, app_name: None, logo_uri: None, tos_uri: None, policy_uri: None, token_encryption_key: None, default_rate_limit_capacity: 100, default_rate_limit_refill_rate: 2.0, }; let (tx, _) = watch::channel(vec![]); let (labeler_tx, _) = watch::channel(()); let atrium_http = std::sync::Arc::new(DefaultHttpClient::default()); let did_resolver = CommonDidResolver::new(CommonDidResolverConfig { plc_directory_url: "https://plc.directory".into(), http_client: std::sync::Arc::clone(&atrium_http), }); let handle_resolver = AtprotoHandleResolver::new(AtprotoHandleResolverConfig { dns_txt_resolver: happyview::dns::NativeDnsResolver::new(), http_client: atrium_http, }); let oauth_pool = db::test_pool().await; let oauth = atrium_oauth::OAuthClient::new(OAuthClientConfig { client_metadata: AtprotoLocalhostClientMetadata { redirect_uris: Some(vec!["http://127.0.0.1:0/auth/callback".into()]), scopes: Some(vec![Scope::Known(KnownScope::Atproto)]), }, keys: None, state_store: happyview::auth::oauth_store::DbStateStore::new(oauth_pool.clone(), backend), session_store: happyview::auth::oauth_store::DbSessionStore::new(oauth_pool, backend), resolver: OAuthResolverConfig { did_resolver, handle_resolver, authorization_server_metadata: Default::default(), protected_resource_metadata: Default::default(), }, }) .expect("Failed to create test OAuth client");
AppState { config, http: reqwest::Client::new(), db: pool.clone(), db_backend: backend, lexicons: LexiconRegistry::new(), collections_tx: tx, labeler_subscriptions_tx: labeler_tx, labeler_identity: std::sync::Arc::new(tokio::sync::RwLock::new(None)), labeler_identity_tx: tokio::sync::watch::channel(()).0, labeler_definitions: happyview::labeler::definitions::LabelDefinitionRegistry::new(), emitted_label_tx: happyview::labeler::emitter::channel(), rate_limiter: happyview::rate_limit::RateLimiter::new( happyview::rate_limit::RateLimitDefaults { query_cost: 1, procedure_cost: 1, proxy_cost: 1, }, ), 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-for-tests-only-not-production", ), plugin_registry: std::sync::Arc::new(happyview::plugin::PluginRegistry::new()), wasm_runtime: std::sync::Arc::new( happyview::plugin::WasmRuntime::new().expect("wasm runtime"), ), attestation_signer: None, official_registry: std::sync::Arc::new(tokio::sync::RwLock::new( happyview::plugin::official_registry::OfficialRegistryState::default(), )), official_registry_config: happyview::plugin::official_registry::RegistryConfig::production( ), domain_cache: happyview::domain::DomainCache::new(), }}
fn setup_lua(state: &AppState, caller_did: Option<String>) -> Lua { let lua = Lua::new(); register_actor_api(&lua, Arc::new(state.clone()), caller_did).unwrap(); lua}
// ---------------------------------------------------------------------------// Tests// ---------------------------------------------------------------------------
#[tokio::test]#[serial]#[ignore]async fn actor_get_returns_nil_when_unset() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, Some("did:plc:alice".into()));
let result: mlua::Value = lua .load(r#"return Actor:get("role")"#) .eval_async() .await .unwrap(); assert!(result.is_nil());}
#[tokio::test]#[serial]#[ignore]async fn actor_set_then_get_string() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, Some("did:plc:alice".into()));
let result: String = lua .load( r#" Actor:set("role", "moderator") return Actor:get("role") "#, ) .eval_async() .await .unwrap(); assert_eq!(result, "moderator");}
#[tokio::test]#[serial]#[ignore]async fn actor_round_trips_table_value() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, Some("did:plc:alice".into()));
let result: mlua::Table = lua .load( r#" Actor:set("limits", { rate = 100, burst = 200 }) return Actor:get("limits") "#, ) .eval_async() .await .unwrap(); assert_eq!(result.get::<i64>("rate").unwrap(), 100); assert_eq!(result.get::<i64>("burst").unwrap(), 200);}
#[tokio::test]#[serial]#[ignore]async fn actor_has_reflects_existence() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, Some("did:plc:alice".into()));
let result: mlua::Table = lua .load( r#" Actor:set("beta", true) return { yes = Actor:has("beta"), no = Actor:has("missing") } "#, ) .eval_async() .await .unwrap(); assert!(result.get::<bool>("yes").unwrap()); assert!(!result.get::<bool>("no").unwrap());}
#[tokio::test]#[serial]#[ignore]async fn actor_metadata_returns_all_keys() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, Some("did:plc:alice".into()));
let result: mlua::Table = lua .load( r#" Actor:set("role", "moderator") Actor:set("tier", "pro") Actor:set("limits", { rate = 100 }) return Actor:metadata() "#, ) .eval_async() .await .unwrap(); assert_eq!(result.get::<String>("role").unwrap(), "moderator"); assert_eq!(result.get::<String>("tier").unwrap(), "pro"); let limits: mlua::Table = result.get("limits").unwrap(); assert_eq!(limits.get::<i64>("rate").unwrap(), 100);}
#[tokio::test]#[serial]#[ignore]async fn actor_delete_returns_bool() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, Some("did:plc:alice".into()));
let result: mlua::Table = lua .load( r#" Actor:set("doomed", "bye") local first = Actor:delete("doomed") local second = Actor:delete("doomed") local after = Actor:get("doomed") return { first = first, second = second, after = after } "#, ) .eval_async() .await .unwrap(); assert!(result.get::<bool>("first").unwrap()); assert!(!result.get::<bool>("second").unwrap()); let after: mlua::Value = result.get("after").unwrap(); assert!(after.is_nil());}
#[tokio::test]#[serial]#[ignore]async fn actor_clear_wipes_all_keys() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, Some("did:plc:alice".into()));
let result: mlua::Table = lua .load( r#" Actor:set("a", 1) Actor:set("b", 2) Actor:set("c", 3) local n = Actor:clear() local meta = Actor:metadata() local count = 0 for _ in pairs(meta) do count = count + 1 end return { removed = n, after_count = count } "#, ) .eval_async() .await .unwrap(); assert_eq!(result.get::<i64>("removed").unwrap(), 3); assert_eq!(result.get::<i64>("after_count").unwrap(), 0);}
#[tokio::test]#[serial]#[ignore]async fn actor_call_targets_other_did() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, Some("did:plc:alice".into()));
let result: mlua::Table = lua .load( r#" local bob = Actor("did:plc:bob") bob:set("tier", "pro") return { bob_tier = bob:get("tier"), alice_tier = Actor:get("tier"), bob_did = bob.did, self_did = Actor.did, } "#, ) .eval_async() .await .unwrap(); assert_eq!(result.get::<String>("bob_tier").unwrap(), "pro"); let alice_tier: mlua::Value = result.get("alice_tier").unwrap(); assert!(alice_tier.is_nil(), "alice should not see bob's metadata"); assert_eq!(result.get::<String>("bob_did").unwrap(), "did:plc:bob"); assert_eq!(result.get::<String>("self_did").unwrap(), "did:plc:alice");}
#[tokio::test]#[serial]#[ignore]async fn actor_unauthenticated_reads_are_nil() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, None);
let result: mlua::Table = lua .load( r#" return { has = Actor:has("anything"), get = Actor:get("anything"), did = Actor.did, } "#, ) .eval_async() .await .unwrap(); assert!(!result.get::<bool>("has").unwrap()); let g: mlua::Value = result.get("get").unwrap(); assert!(g.is_nil()); let d: mlua::Value = result.get("did").unwrap(); assert!(d.is_nil());}
#[tokio::test]#[serial]#[ignore]async fn actor_unauthenticated_writes_error() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, None);
let err: Result<mlua::Value, _> = lua.load(r#"Actor:set("k", "v")"#).eval_async().await; assert!(err.is_err()); let msg = err.unwrap_err().to_string(); assert!(msg.contains("no DID"), "got: {msg}");}
#[tokio::test]#[serial]#[ignore]async fn actor_unauthenticated_can_target_specific_did() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, None);
// Even with no caller, scripts can reach into specific DIDs (typical // hook scenario where the record author DID is exposed via globals). let result: String = lua .load( r#" Actor("did:plc:carol"):set("greeting", "hello") return Actor("did:plc:carol"):get("greeting") "#, ) .eval_async() .await .unwrap(); assert_eq!(result, "hello");}
#[tokio::test]#[serial]#[ignore]async fn actor_set_overwrites() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, Some("did:plc:alice".into()));
let result: String = lua .load( r#" Actor:set("tier", "free") Actor:set("tier", "pro") return Actor:get("tier") "#, ) .eval_async() .await .unwrap(); assert_eq!(result, "pro");}
#[tokio::test]#[serial]#[ignore]async fn actor_rejects_empty_did_in_call() { let pool = db::test_pool().await; let backend = db::test_backend(); db::truncate_all(&pool).await; let state = test_state(pool, backend).await; let lua = setup_lua(&state, Some("did:plc:alice".into()));
let err: Result<mlua::Value, _> = lua.load(r#"return Actor("")"#).eval_async().await; assert!(err.is_err()); let msg = err.unwrap_err().to_string(); assert!(msg.contains("non-empty"), "got: {msg}");}