1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
//! Coverage for the M4 system-check framework: the shape of
//! `framework_checks()`, the dot-delimited check-id convention, the
//! `settings.required` check's pass/fail surface, `run_all` over an
//! empty slice, and one end-to-end pass through `AppBuilder::build()`
//! that drives a real `BuildError::SystemCheckFailed` out of phase 4.
//!
//! The build-lifecycle test is intentionally the only call to
//! `App::builder().build()` in this file. `build()` writes to
//! process-wide OnceLocks (settings, db, backend); within one test
//! binary it can only succeed past phase 3 once. Cargo's parallel
//! runner gives each `tests/*.rs` file its own binary, so this file is
//! isolated from `tests/builder.rs` and friends.
//!
//! See `crates/umbral-core/src/check.rs` for the framework being tested.
use std::collections::HashMap;
use umbral_core::backend::SqliteBackend;
use umbral_core::check::{CheckContext, Severity, framework_checks, run_all};
use umbral_core::settings::{Environment, Settings};
/// The literal default secret_key from `crate::settings::default_secret_key`.
/// Duplicated here (rather than reaching for the private const in check.rs)
/// because that's the contract end users will be tripping over: if anyone
/// renames it, the test should fail loudly so the docs and check.rs stay
/// in sync.
const INSECURE_DEV_SECRET_KEY: &str = "umbral-insecure-dev-key-change-me";
/// Helper: build a Settings struct in one place so each test only has to
/// describe the deltas that matter for what it's asserting. The default
/// here mirrors the figment defaults the production loader emits.
fn make_settings(environment: Environment, secret_key: &str) -> Settings {
Settings {
database_url: "sqlite::memory:".to_string(),
databases: HashMap::new(),
max_form_body_bytes: Some(16 * 1024 * 1024),
secret_key: secret_key.to_string(),
environment,
allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
log_level: "info".to_string(),
db_max_connections: 10,
db_acquire_timeout_secs: 30,
db_min_connections: 0,
db_idle_timeout_secs: Some(600),
db_max_lifetime_secs: Some(1800),
db_test_before_acquire: true,
bind_addr: "127.0.0.1:8000".to_string(),
time_zone: None,
static_url: "/static/".to_string(),
static_root: "staticfiles/".to_string(),
extra: HashMap::new(),
}
}
/// At M4 the framework ships at least the `settings.required` check;
/// `framework_checks()` returning an empty Vec would mean the catalogue
/// was wired off by accident.
#[test]
fn framework_checks_returns_non_empty_vec() {
let checks = framework_checks();
assert!(
!checks.is_empty(),
"framework_checks() should ship at least one check at M4; got an empty Vec",
);
}
/// Pin the convention that every check id is a stable dot-delimited
/// string. Operators are expected to grep these out of logs and error
/// reports, so the format has to stay greppable: at minimum the id
/// contains a `.` separator, and the prefix is one of the recognised
/// namespaces.
#[test]
fn framework_check_ids_are_dot_delimited_stable_strings() {
let allowed_prefixes = [
"settings.",
"backend.",
"field.",
"model.",
"plugin.",
"route.",
];
for check in framework_checks() {
assert!(
check.id.contains('.'),
"check id `{}` should be dot-delimited (`namespace.name`)",
check.id,
);
assert!(
allowed_prefixes
.iter()
.any(|prefix| check.id.starts_with(prefix)),
"check id `{}` should start with a recognised namespace prefix \
(one of {allowed_prefixes:?}); add the new namespace here if intentional",
check.id,
);
}
}
/// In Dev the insecure default is fine — that's the whole point of
/// having a dev default — so `settings.required` must stay silent. Run
/// the full framework catalogue (not just one check) to catch any other
/// check that mis-fires on a sane Dev profile.
#[test]
fn settings_required_passes_when_dev_environment() {
let settings = make_settings(Environment::Dev, INSECURE_DEV_SECRET_KEY);
let ctx = CheckContext {
backend: &SqliteBackend,
settings: &settings,
provides_storage: true,
registered_plugin_names: &[],
};
let findings = run_all(&ctx, &framework_checks());
assert!(
findings.is_empty(),
"Dev profile with default secret_key should produce zero findings; got {findings:#?}",
);
}
/// A Prod app that overrode the secret_key is the supported production
/// posture, so `settings.required` must report nothing. Other checks
/// (e.g. allowed_hosts) might still warn here, but no finding should be
/// at Severity::Error.
#[test]
fn settings_required_passes_when_secret_key_overridden_in_prod() {
let settings = make_settings(Environment::Prod, "real-secret-not-the-default");
let ctx = CheckContext {
backend: &SqliteBackend,
settings: &settings,
provides_storage: true,
registered_plugin_names: &[],
};
let findings = run_all(&ctx, &framework_checks());
let errors: Vec<_> = findings
.iter()
.filter(|f| f.severity == Severity::Error)
.collect();
assert!(
errors.is_empty(),
"Prod with overridden secret should produce zero Error-severity findings; got {errors:#?}",
);
}
/// The core failure mode the check exists for: Prod + the literal dev
/// default secret_key. Must surface at least one finding tagged
/// `settings.required` at `Severity::Error`, since this is what the
/// builder turns into `BuildError::SystemCheckFailed`.
#[test]
fn settings_required_errors_when_default_secret_in_prod() {
let settings = make_settings(Environment::Prod, INSECURE_DEV_SECRET_KEY);
let ctx = CheckContext {
backend: &SqliteBackend,
settings: &settings,
provides_storage: true,
registered_plugin_names: &[],
};
let findings = run_all(&ctx, &framework_checks());
let hit = findings
.iter()
.find(|f| f.check_id == "settings.required" && f.severity == Severity::Error);
assert!(
hit.is_some(),
"Prod + default secret_key should produce a `settings.required` Error finding; got {findings:#?}",
);
}
/// `run_all` over an empty slice should be a no-op returning an empty
/// Vec. Easy to break by accident if someone later adds an "always
/// include framework_checks" shortcut inside `run_all`.
#[test]
fn run_all_handles_empty_checks() {
let settings = make_settings(Environment::Dev, INSECURE_DEV_SECRET_KEY);
let ctx = CheckContext {
backend: &SqliteBackend,
settings: &settings,
provides_storage: true,
registered_plugin_names: &[],
};
let findings = run_all(&ctx, &[]);
assert!(
findings.is_empty(),
"run_all over an empty slice should return an empty Vec; got {findings:#?}",
);
}
/// End-to-end coverage: drive `App::builder().build()` with a Settings
/// configured for Prod and the literal dev-default secret_key. Phase 4
/// must reject the build and return `BuildError::SystemCheckFailed`,
/// and the carried findings must include the `settings.required` one.
///
/// This is the only call to `.build()` in this file by design: it
/// writes to process-wide OnceLocks, and within one test binary only
/// one such call can pass phase 3. See the file-level docstring.
#[tokio::test]
async fn system_check_failed_build_returns_build_error_system_check_failed() {
let settings = make_settings(Environment::Prod, INSECURE_DEV_SECRET_KEY);
let pool = umbral_core::db::connect_sqlite("sqlite::memory:")
.await
.expect("in-memory sqlite should always connect");
let result = umbral_core::app::App::builder()
.settings(settings)
.database("default", pool)
.build();
let err = result
.err()
.expect("Prod + default secret_key should fail build at phase 4");
let findings = match err {
umbral_core::app::BuildError::SystemCheckFailed { findings } => findings,
other => panic!("expected BuildError::SystemCheckFailed, got {other:?}"),
};
assert!(
findings
.iter()
.any(|f| f.check_id == "settings.required" && f.severity == Severity::Error),
"SystemCheckFailed findings should include the settings.required Error finding; got {findings:#?}",
);
}