use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use async_trait::async_trait;
use base64::Engine as _;
use proto_blue_crypto::{K256Keypair, Keypair as _, Signer as _, format_multikey};
use crate::auth::did::{DidDocument, DidResolver, ResolveError, VerificationMethod};
use crate::xrpc_gateway::auth::XrpcAuthService;
use crate::xrpc_gateway::config::XrpcGatewayConfig;
pub const TEST_PRIV_HEX: &str = "b7e3f1c9a2d84ef50712436589bc1d8f023147b68cafed94a8b603c7159d4e2a";
pub const SERVICE_DID: &str = "did:web:cairn.example.com";
pub const ISSUER_DID: &str = "did:plc:issuer0000000000000000000";
pub const FIXED_NOW: i64 = 1_700_000_000;
pub fn test_keypair() -> K256Keypair {
K256Keypair::from_private_key(&hex::decode(TEST_PRIV_HEX).unwrap()).unwrap()
}
pub fn test_did_doc(did: &str) -> DidDocument {
DidDocument {
id: did.to_string(),
verification_method: vec![VerificationMethod {
id: format!("{did}#atproto"),
r#type: "Multikey".into(),
public_key_multibase: format_multikey(
"ES256K",
&test_keypair().public_key_compressed(),
),
}],
}
}
pub struct MockResolver(Mutex<HashMap<String, DidDocument>>);
impl MockResolver {
pub fn with_doc(did: &str, doc: DidDocument) -> Arc<Self> {
let mut m = HashMap::new();
m.insert(did.to_string(), doc);
Arc::new(Self(Mutex::new(m)))
}
}
#[async_trait]
impl DidResolver for MockResolver {
async fn resolve(&self, did: &str) -> Result<DidDocument, ResolveError> {
let snapshot = self.0.lock().unwrap().get(did).cloned();
snapshot.ok_or(ResolveError::BadStatus(404))
}
}
pub fn build_jwt(claims: &serde_json::Value, alg_header: &str) -> String {
let header = serde_json::json!({"alg": alg_header, "typ": "JWT"});
let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
let h = engine.encode(header.to_string());
let p = engine.encode(claims.to_string());
let signing_input = format!("{h}.{p}");
let sig = test_keypair().sign(signing_input.as_bytes()).unwrap();
format!("{h}.{p}.{}", engine.encode(sig))
}
pub fn valid_claims(lxm: &str) -> serde_json::Value {
serde_json::json!({
"iss": ISSUER_DID,
"aud": SERVICE_DID,
"exp": FIXED_NOW + 60,
"iat": FIXED_NOW - 5,
"jti": "jti-fixture-1",
"lxm": lxm,
})
}
pub fn fixed_clock() -> Arc<dyn Fn() -> i64 + Send + Sync> {
Arc::new(|| FIXED_NOW)
}
pub fn fixture_config() -> XrpcGatewayConfig {
XrpcGatewayConfig {
enabled: true,
service_did: SERVICE_DID.into(),
clock_skew_tolerance: Duration::from_secs(30),
replay_cache_ttl: Duration::from_secs(90),
}
}
pub fn build_service() -> Arc<XrpcAuthService> {
let resolver = MockResolver::with_doc(ISSUER_DID, test_did_doc(ISSUER_DID));
Arc::new(XrpcAuthService::with_clock(
fixture_config(),
resolver,
fixed_clock(),
))
}
pub async fn build_test_pool() -> sqlx::Pool<sqlx::Sqlite> {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("xrpc-gateway-test.db");
let pool = crate::storage::open(&path).await.unwrap();
Box::leak(Box::new(dir));
let m_did = "did:plc:test_moderator";
crate::xrpc_gateway::add_known_caller(&pool, ISSUER_DID, Some("test"), m_did)
.await
.unwrap();
crate::xrpc_gateway::add_trusted_pds(&pool, ISSUER_DID, Some("test"), m_did)
.await
.unwrap();
pool
}
pub fn build_replay_cache() -> Arc<crate::xrpc_gateway::XrpcReplayCache> {
Arc::new(crate::xrpc_gateway::XrpcReplayCache::new(
Duration::from_secs(90),
))
}
pub async fn build_handler_state(
pool: sqlx::Pool<sqlx::Sqlite>,
) -> crate::xrpc_gateway::XrpcGatewayState {
let priv_bytes = hex::decode(TEST_PRIV_HEX).unwrap();
let key = crate::signing_key::SigningKey::from_bytes(priv_bytes.try_into().unwrap());
let reason_vocabulary = test_vocab(&[
crate::xrpc_gateway::XRPC_GATEWAY_DEFAULT_REASON_CODE,
"spam",
"harassment",
]);
let strike_policy = crate::moderation::policy::StrikePolicy::defaults();
let label_emission_policy = crate::labels::policy::LabelEmissionPolicy::defaults();
let policy_automation_policy = crate::policy::automation::PolicyAutomationPolicy::defaults();
let writer = crate::writer::spawn(
pool.clone(),
key,
SERVICE_DID.to_string(),
None,
crate::server::RetentionConfig::default(),
reason_vocabulary,
strike_policy,
label_emission_policy,
policy_automation_policy,
)
.await
.expect("spawn test writer");
crate::xrpc_gateway::XrpcGatewayState {
writer,
pool,
service_did: SERVICE_DID.to_string(),
}
}
fn test_vocab(entries: &[&str]) -> crate::moderation::reasons::ReasonVocabulary {
let map: serde_json::Map<String, serde_json::Value> = entries
.iter()
.map(|id| {
(
(*id).to_string(),
serde_json::json!({
"base_weight": 1,
"severe": false,
"description": "test fixture",
}),
)
})
.collect();
let v = serde_json::json!({
"service_did": SERVICE_DID,
"service_endpoint": "https://labeler.example",
"db_path": "/var/lib/cairn/cairn.db",
"signing_key_path": "/etc/cairn/signing-key.hex",
"moderation_reasons": serde_json::Value::Object(map),
});
let cfg: crate::config::Config = serde_json::from_value(v).expect("test config deserializes");
crate::moderation::reasons::ReasonVocabulary::from_config(&cfg).expect("test vocab")
}