Skip to main content

max / makenotwork

6.5 KB · 182 lines History Blame Raw
1 //! Fixtures shared by the gate modules' test suites.
2 //!
3 //! The three `*_ctx` builders each construct a full [`GateCtx`], and each is
4 //! used from more than one sibling suite, which is what makes this a shared
5 //! module rather than three private copies.
6
7 use super::GateCtx;
8 use super::log::GateLog;
9 use crate::domain::{GateKind, GateRunId, TierId};
10 use crate::events;
11 use chrono::Utc;
12 use sqlx::SqlitePool;
13 use sqlx::sqlite::SqlitePoolOptions;
14 use std::collections::HashMap;
15 use std::path::PathBuf;
16
17 pub(super) fn target(dir: &str) -> crate::config::TestTarget {
18 crate::config::TestTarget {
19 dir: std::path::PathBuf::from(dir),
20 aux_repo: None,
21 features: Vec::new(),
22 all_features: false,
23 scratch_db: false,
24 }
25 }
26
27 /// `target()` above, but resolved against an aux repo's checkout.
28 pub(super) fn aux_target(dir: &str, repo: &str) -> crate::config::TestTarget {
29 crate::config::TestTarget {
30 aux_repo: Some(repo.to_string()),
31 ..target(dir)
32 }
33 }
34
35 /// True when the URL's host parses as a domain rather than an IP literal,
36 /// which is the distinction `Url::domain()` draws and WebAuthn depends on.
37 pub(super) fn url_host_is_a_domain(url: &str) -> bool {
38 let after = url.split("://").nth(1).unwrap_or("");
39 let host = after.split(['/', '?', '#']).next().unwrap_or("");
40 let host = host.rsplit('@').next().unwrap_or(host);
41 let host = if let Some(rest) = host.strip_prefix('[') {
42 rest.split(']').next().unwrap_or("")
43 } else {
44 host.split(':').next().unwrap_or("")
45 };
46 !host.is_empty() && host.parse::<std::net::IpAddr>().is_err()
47 }
48
49 pub(super) fn resolving_ctx(worktree: &str, aux: &[(&str, &str)]) -> GateCtx {
50 GateCtx {
51 public_url: None,
52 pool: SqlitePool::connect_lazy("sqlite::memory:").unwrap(),
53 cfg: std::sync::Arc::new(crate::config::AppConfig::for_tests()),
54 tier: TierId::new("host"),
55 version: "0.1.0".parse().unwrap(),
56 worktree: Some(PathBuf::from(worktree)),
57 bundle: None,
58 events: events::channel(),
59 nodes: Vec::new(),
60 build_id: None,
61 aux_dirs: aux
62 .iter()
63 .map(|(n, d)| ((*n).to_string(), PathBuf::from(d)))
64 .collect(),
65 }
66 }
67
68 /// A `GateCtx` over `worktree` with the given frontend projects configured.
69 /// No DB, no artifact — `code_smoke_frontends` touches neither.
70 pub(super) async fn frontend_ctx(worktree: &std::path::Path, dirs: &[&str]) -> GateCtx {
71 let mut cfg = crate::config::AppConfig::for_tests();
72 cfg.frontend_builds = dirs
73 .iter()
74 .map(|d| crate::config::FrontendBuild {
75 dir: PathBuf::from(d),
76 script: "build".into(),
77 })
78 .collect();
79 cfg.logs_root = worktree.join("logs");
80 GateCtx {
81 public_url: None,
82 pool: SqlitePoolOptions::new()
83 .max_connections(1)
84 .connect("sqlite::memory:")
85 .await
86 .unwrap(),
87 cfg: std::sync::Arc::new(cfg),
88 tier: TierId::new("host"),
89 version: "0.1.0".parse().unwrap(),
90 worktree: Some(worktree.to_path_buf()),
91 bundle: None,
92 events: events::channel(),
93 nodes: Vec::new(),
94 build_id: None,
95 aux_dirs: HashMap::new(),
96 }
97 }
98
99 /// A `GateCtx` for the `migration_dry_run` freshness checks: a migrated
100 /// in-memory pool (so `backups` exists) and a scratch URL set, so the gate
101 /// reaches the backup lookup instead of bailing on config. Nothing here
102 /// touches postgres — every assertion below blocks before `reset_scratch`.
103 pub(super) async fn dry_run_ctx(worktree: &std::path::Path, max_age_hours: u32) -> GateCtx {
104 let mut cfg = crate::config::AppConfig::for_tests();
105 cfg.scratch_db_url = Some("postgres:///sando_scratch".into());
106 cfg.backup_max_age_hours = max_age_hours;
107 cfg.logs_root = worktree.join("logs");
108 let pool = SqlitePoolOptions::new()
109 .max_connections(1)
110 .connect("sqlite::memory:")
111 .await
112 .unwrap();
113 crate::db::migrate(&pool).await.unwrap();
114 GateCtx {
115 public_url: None,
116 pool,
117 cfg: std::sync::Arc::new(cfg),
118 tier: TierId::new("host"),
119 version: "0.1.0".parse().unwrap(),
120 worktree: Some(worktree.to_path_buf()),
121 bundle: None,
122 events: events::channel(),
123 nodes: Vec::new(),
124 build_id: None,
125 aux_dirs: HashMap::new(),
126 }
127 }
128
129 /// Record a `server` backup row fetched `hours_ago`, as `/backup/fetch` would.
130 pub(super) async fn seed_backup(ctx: &GateCtx, hours_ago: i64) {
131 seed_named_backup(ctx, "server", hours_ago).await;
132 }
133
134 /// Record a backup row for one named dump.
135 pub(super) async fn seed_named_backup(ctx: &GateCtx, name: &str, hours_ago: i64) {
136 let at = (Utc::now() - chrono::Duration::hours(hours_ago)).to_rfc3339();
137 sqlx::query(
138 "INSERT INTO backups (name, fetched_at, source, local_path, byte_size)
139 VALUES (?, ?, 'file:///x.sql.gz', '/tmp/sando-test-backup.sql.gz', 1000000)",
140 )
141 .bind(name)
142 .bind(at)
143 .execute(&ctx.pool)
144 .await
145 .unwrap();
146 }
147
148 /// The multithreaded check, as `sando-daemon.toml` configures it.
149 pub(super) fn mt_check() -> crate::config::MigrationCheck {
150 crate::config::MigrationCheck {
151 dir: std::path::PathBuf::from("multithreaded/migrations"),
152 backup: "multithreaded".into(),
153 scratch_db: Some("sando_scratch_mt".into()),
154 owner_role: Some("multithreaded".into()),
155 }
156 }
157
158 /// Re-point a `dry_run_ctx` at one check, keeping its pool and scratch URL.
159 pub(super) fn with_check(ctx: &mut GateCtx, check: crate::config::MigrationCheck) {
160 let mut cfg = crate::config::AppConfig::for_tests();
161 cfg.scratch_db_url = ctx.cfg.scratch_db_url.clone();
162 cfg.backup_max_age_hours = ctx.cfg.backup_max_age_hours;
163 cfg.logs_root = ctx.cfg.logs_root.clone();
164 cfg.migration_checks = vec![check];
165 ctx.cfg = std::sync::Arc::new(cfg);
166 }
167
168 /// A `code_smoke` live log over `ctx.cfg.logs_root`, for the helpers that
169 /// take one. `GateRunId(0)` never matches a real row; nothing reads the
170 /// chunk events in these tests.
171 pub(super) async fn test_gate_log(ctx: &GateCtx) -> GateLog {
172 GateLog::open(ctx, GateRunId(0), GateKind::CodeSmoke).await
173 }
174
175 /// Close `log` (flushing it) and read back what it wrote on disk.
176 pub(super) async fn read_gate_log(ctx: &GateCtx, log: GateLog) -> String {
177 log.close().await;
178 tokio::fs::read_to_string(ctx.log_path(GateKind::CodeSmoke))
179 .await
180 .expect("the gate log must exist on disk")
181 }
182