//! Fixtures shared by the gate modules' test suites. //! //! The three `*_ctx` builders each construct a full [`GateCtx`], and each is //! used from more than one sibling suite, which is what makes this a shared //! module rather than three private copies. use super::GateCtx; use super::log::GateLog; use crate::domain::{GateKind, GateRunId, TierId}; use crate::events; use chrono::Utc; use sqlx::SqlitePool; use sqlx::sqlite::SqlitePoolOptions; use std::collections::HashMap; use std::path::PathBuf; pub(super) fn target(dir: &str) -> crate::config::TestTarget { crate::config::TestTarget { dir: std::path::PathBuf::from(dir), aux_repo: None, features: Vec::new(), all_features: false, scratch_db: false, } } /// `target()` above, but resolved against an aux repo's checkout. pub(super) fn aux_target(dir: &str, repo: &str) -> crate::config::TestTarget { crate::config::TestTarget { aux_repo: Some(repo.to_string()), ..target(dir) } } /// True when the URL's host parses as a domain rather than an IP literal, /// which is the distinction `Url::domain()` draws and WebAuthn depends on. pub(super) fn url_host_is_a_domain(url: &str) -> bool { let after = url.split("://").nth(1).unwrap_or(""); let host = after.split(['/', '?', '#']).next().unwrap_or(""); let host = host.rsplit('@').next().unwrap_or(host); let host = if let Some(rest) = host.strip_prefix('[') { rest.split(']').next().unwrap_or("") } else { host.split(':').next().unwrap_or("") }; !host.is_empty() && host.parse::().is_err() } pub(super) fn resolving_ctx(worktree: &str, aux: &[(&str, &str)]) -> GateCtx { GateCtx { public_url: None, pool: SqlitePool::connect_lazy("sqlite::memory:").unwrap(), cfg: std::sync::Arc::new(crate::config::AppConfig::for_tests()), tier: TierId::new("host"), version: "0.1.0".parse().unwrap(), worktree: Some(PathBuf::from(worktree)), bundle: None, events: events::channel(), nodes: Vec::new(), build_id: None, aux_dirs: aux .iter() .map(|(n, d)| ((*n).to_string(), PathBuf::from(d))) .collect(), } } /// A `GateCtx` over `worktree` with the given frontend projects configured. /// No DB, no artifact — `code_smoke_frontends` touches neither. pub(super) async fn frontend_ctx(worktree: &std::path::Path, dirs: &[&str]) -> GateCtx { let mut cfg = crate::config::AppConfig::for_tests(); cfg.frontend_builds = dirs .iter() .map(|d| crate::config::FrontendBuild { dir: PathBuf::from(d), script: "build".into(), }) .collect(); cfg.logs_root = worktree.join("logs"); GateCtx { public_url: None, pool: SqlitePoolOptions::new() .max_connections(1) .connect("sqlite::memory:") .await .unwrap(), cfg: std::sync::Arc::new(cfg), tier: TierId::new("host"), version: "0.1.0".parse().unwrap(), worktree: Some(worktree.to_path_buf()), bundle: None, events: events::channel(), nodes: Vec::new(), build_id: None, aux_dirs: HashMap::new(), } } /// A `GateCtx` for the `migration_dry_run` freshness checks: a migrated /// in-memory pool (so `backups` exists) and a scratch URL set, so the gate /// reaches the backup lookup instead of bailing on config. Nothing here /// touches postgres — every assertion below blocks before `reset_scratch`. pub(super) async fn dry_run_ctx(worktree: &std::path::Path, max_age_hours: u32) -> GateCtx { let mut cfg = crate::config::AppConfig::for_tests(); cfg.scratch_db_url = Some("postgres:///sando_scratch".into()); cfg.backup_max_age_hours = max_age_hours; cfg.logs_root = worktree.join("logs"); let pool = SqlitePoolOptions::new() .max_connections(1) .connect("sqlite::memory:") .await .unwrap(); crate::db::migrate(&pool).await.unwrap(); GateCtx { public_url: None, pool, cfg: std::sync::Arc::new(cfg), tier: TierId::new("host"), version: "0.1.0".parse().unwrap(), worktree: Some(worktree.to_path_buf()), bundle: None, events: events::channel(), nodes: Vec::new(), build_id: None, aux_dirs: HashMap::new(), } } /// Record a `server` backup row fetched `hours_ago`, as `/backup/fetch` would. pub(super) async fn seed_backup(ctx: &GateCtx, hours_ago: i64) { seed_named_backup(ctx, "server", hours_ago).await; } /// Record a backup row for one named dump. pub(super) async fn seed_named_backup(ctx: &GateCtx, name: &str, hours_ago: i64) { let at = (Utc::now() - chrono::Duration::hours(hours_ago)).to_rfc3339(); sqlx::query( "INSERT INTO backups (name, fetched_at, source, local_path, byte_size) VALUES (?, ?, 'file:///x.sql.gz', '/tmp/sando-test-backup.sql.gz', 1000000)", ) .bind(name) .bind(at) .execute(&ctx.pool) .await .unwrap(); } /// The multithreaded check, as `sando-daemon.toml` configures it. pub(super) fn mt_check() -> crate::config::MigrationCheck { crate::config::MigrationCheck { dir: std::path::PathBuf::from("multithreaded/migrations"), backup: "multithreaded".into(), scratch_db: Some("sando_scratch_mt".into()), owner_role: Some("multithreaded".into()), } } /// Re-point a `dry_run_ctx` at one check, keeping its pool and scratch URL. pub(super) fn with_check(ctx: &mut GateCtx, check: crate::config::MigrationCheck) { let mut cfg = crate::config::AppConfig::for_tests(); cfg.scratch_db_url = ctx.cfg.scratch_db_url.clone(); cfg.backup_max_age_hours = ctx.cfg.backup_max_age_hours; cfg.logs_root = ctx.cfg.logs_root.clone(); cfg.migration_checks = vec![check]; ctx.cfg = std::sync::Arc::new(cfg); } /// A `code_smoke` live log over `ctx.cfg.logs_root`, for the helpers that /// take one. `GateRunId(0)` never matches a real row; nothing reads the /// chunk events in these tests. pub(super) async fn test_gate_log(ctx: &GateCtx) -> GateLog { GateLog::open(ctx, GateRunId(0), GateKind::CodeSmoke).await } /// Close `log` (flushing it) and read back what it wrote on disk. pub(super) async fn read_gate_log(ctx: &GateCtx, log: GateLog) -> String { log.close().await; tokio::fs::read_to_string(ctx.log_path(GateKind::CodeSmoke)) .await .expect("the gate log must exist on disk") }