feat: add test helpers with in-memory DB and test state

This commit is contained in:
Till Wegmueller 2026-04-03 19:27:54 +02:00
parent 1d4873ba75
commit 4b04cf9b76
No known key found for this signature in database
2 changed files with 145 additions and 11 deletions

View file

@ -1,25 +1,93 @@
use async_trait::async_trait;
/// Trait for verifying domain ownership challenges (DNS-01 and HTTP-01).
use crate::config::ChallengeConfig;
/// Trait for challenge verification — allows mocking in tests.
#[async_trait]
pub trait ChallengeVerifier: Send + Sync + std::fmt::Debug {
async fn verify_dns01(&self, domain: &str, expected_token: &str) -> Result<bool, String>;
async fn verify_http01(&self, domain: &str, expected_token: &str) -> Result<bool, String>;
pub trait ChallengeVerifier: Send + Sync {
async fn verify_dns(
&self,
domain: &str,
expected_token: &str,
config: &ChallengeConfig,
) -> Result<bool, String>;
async fn verify_http(
&self,
domain: &str,
expected_token: &str,
config: &ChallengeConfig,
) -> Result<bool, String>;
}
/// Production implementation using real DNS and HTTP lookups.
#[derive(Debug)]
/// Real implementation using DNS lookups and HTTP requests.
pub struct RealChallengeVerifier;
#[async_trait]
impl ChallengeVerifier for RealChallengeVerifier {
async fn verify_dns01(&self, _domain: &str, _expected_token: &str) -> Result<bool, String> {
// TODO: implement with hickory-resolver in a later task
async fn verify_dns(
&self,
domain: &str,
expected_token: &str,
config: &ChallengeConfig,
) -> Result<bool, String> {
use hickory_resolver::TokioResolver;
let resolver = TokioResolver::builder_tokio()
.map_err(|e| format!("resolver error: {e}"))?
.build();
let lookup_name = format!("{}.{}", config.dns_txt_prefix, domain);
let response = resolver
.txt_lookup(&lookup_name)
.await
.map_err(|e| format!("DNS lookup failed: {e}"))?;
for record in response.iter() {
let txt = record.to_string();
if txt.trim_matches('"') == expected_token {
return Ok(true);
}
}
Ok(false)
}
async fn verify_http01(&self, _domain: &str, _expected_token: &str) -> Result<bool, String> {
// TODO: implement with reqwest in a later task
Ok(false)
async fn verify_http(
&self,
domain: &str,
expected_token: &str,
config: &ChallengeConfig,
) -> Result<bool, String> {
let url = format!(
"https://{}/{}/{}",
domain, config.http_well_known_path, expected_token
);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| format!("HTTP client error: {e}"))?;
let response = client
.get(&url)
.send()
.await
.map_err(|e| format!("HTTP request failed: {e}"))?;
Ok(response.status().is_success())
}
}
/// Mock that always succeeds — for testing.
pub struct MockChallengeVerifier;
#[async_trait]
impl ChallengeVerifier for MockChallengeVerifier {
async fn verify_dns(&self, _: &str, _: &str, _: &ChallengeConfig) -> Result<bool, String> {
Ok(true)
}
async fn verify_http(&self, _: &str, _: &str, _: &ChallengeConfig) -> Result<bool, String> {
Ok(true)
}
}

66
tests/common/mod.rs Normal file
View file

@ -0,0 +1,66 @@
use sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseConnection, Statement};
use sea_orm_migration::MigratorTrait;
use std::sync::Arc;
use webfingerd::cache::Cache;
use webfingerd::config::*;
use webfingerd::state::AppState;
pub async fn setup_test_db() -> DatabaseConnection {
let opt = ConnectOptions::new("sqlite::memory:");
let db = Database::connect(opt).await.unwrap();
db.execute(Statement::from_string(
sea_orm::DatabaseBackend::Sqlite,
"PRAGMA journal_mode=WAL".to_string(),
))
.await
.unwrap();
migration::Migrator::up(&db, None).await.unwrap();
db
}
pub fn test_settings() -> Settings {
Settings {
server: ServerConfig {
listen: "127.0.0.1:0".into(),
base_url: "http://localhost:8080".into(),
},
database: DatabaseConfig {
path: ":memory:".into(),
wal_mode: true,
},
cache: CacheConfig {
reaper_interval_secs: 1,
},
rate_limit: RateLimitConfig {
public_rpm: 1000,
api_rpm: 1000,
batch_rpm: 100,
batch_max_links: 500,
},
challenge: ChallengeConfig {
dns_txt_prefix: "_webfinger-challenge".into(),
http_well_known_path: ".well-known/webfinger-verify".into(),
challenge_ttl_secs: 3600,
},
ui: UiConfig {
enabled: false,
session_secret: "test-secret-at-least-32-bytes-long-for-signing".into(),
},
}
}
pub async fn test_state() -> AppState {
test_state_with_settings(test_settings()).await
}
pub async fn test_state_with_settings(settings: Settings) -> AppState {
let db = setup_test_db().await;
let cache = Cache::new();
cache.hydrate(&db).await.unwrap();
AppState {
db,
cache,
settings: Arc::new(settings),
challenge_verifier: Arc::new(webfingerd::challenge::MockChallengeVerifier),
}
}