use crate::EnvVars;
use crate::codes::{CODES, DoctorCodeDef};
use crate::collect::{LockedMigrationError, verify_locked};
use crate::lint::banned_tokens;
use crate::lock::{Lock, read_lock};
use cratefield_core::lint_card_data;
use cratefield_core::{
HARNESS_API, HARNESS_SIDECARS, Harness, MIN_SECRET_BYTES, Port, SIDECAR_GATEWAY_SECRET,
VentureEnv, deployed_env, env_disagreement, harness_api_mismatch,
};
use cratefield_push_wiring::PushWiring;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
pub const REPORT_SCHEMA: u32 = 1;
#[derive(Debug)]
pub struct DoctorFailure {
pub code: &'static DoctorCodeDef,
pub message: String,
}
#[derive(Debug)]
pub struct DoctorReport {
pub failures: Vec<DoctorFailure>,
}
impl DoctorReport {
#[must_use]
pub fn ok(&self) -> bool {
self.failures.is_empty()
}
pub fn render_json(&self) -> Result<String, serde_json::Error> {
#[derive(serde::Serialize)]
struct Report<'a> {
schema: u32,
ok: bool,
failures: Vec<Failure<'a>>,
}
#[derive(serde::Serialize)]
struct Failure<'a> {
code: &'a str,
message: &'a str,
}
serde_json::to_string(&Report {
schema: REPORT_SCHEMA,
ok: self.ok(),
failures: self
.failures
.iter()
.map(|failure| Failure {
code: failure.code.code,
message: &failure.message,
})
.collect(),
})
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Output {
Human,
Json,
}
pub fn doctor(
harness: &Harness,
migrations_dir: &Path,
allow_no_captcha: Option<&str>,
sidecars: Option<&str>,
) -> Result<(), String> {
let report = run_checks(
harness,
migrations_dir,
allow_no_captcha,
sidecars,
Output::Human,
Configured::from_env(),
);
if report.ok() {
return Ok(());
}
let messages: Vec<String> = report
.failures
.into_iter()
.map(|failure| failure.message)
.collect();
Err(messages.join("\n - "))
}
#[must_use = "call process::exit with the returned ExitCode"]
pub fn doctor_json(
harness: &Harness,
migrations_dir: &Path,
allow_no_captcha: Option<&str>,
sidecars: Option<&str>,
) -> ExitCode {
let report = run_checks(
harness,
migrations_dir,
allow_no_captcha,
sidecars,
Output::Json,
Configured::from_env(),
);
match report.render_json() {
Ok(payload) => println!("{payload}"),
Err(err) => {
eprintln!("fz: cannot serialize the doctor report: {err}");
return ExitCode::FAILURE;
}
}
if report.ok() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
#[must_use]
pub fn doctor_report_json(
harness: &Harness,
migrations_dir: &Path,
allow_no_captcha: Option<&str>,
sidecars: Option<&str>,
) -> DoctorReport {
run_checks(
harness,
migrations_dir,
allow_no_captcha,
sidecars,
Output::Json,
Configured::from_env(),
)
}
fn run_checks(
harness: &Harness,
migrations_dir: &Path,
allow_no_captcha: Option<&str>,
sidecars: Option<&str>,
output: Output,
configured: Configured,
) -> DoctorReport {
let mut failures: Vec<DoctorFailure> = Vec::new();
contract_failures(harness, &mut failures);
self_check_failures(harness, &mut failures);
let env = deployed_env(harness.venture().env, &EnvVars);
if let Some(note) = env_disagreement(harness.venture().env, env)
&& output == Output::Human
{
eprintln!("fz: warning: {note}");
}
if env == VentureEnv::Production {
production_port_checks(
harness,
configured,
&EnvVars,
allow_no_captcha,
output,
&mut failures,
);
}
if let Some(message) = admin_token_failure(configured.admin_token_bytes) {
failures.push(DoctorFailure {
code: &CODES.admin_token_too_short,
message,
});
}
push_checks(harness, env, output, &mut failures);
lockfile_failures(harness, migrations_dir, &mut failures);
undeclared_migration_failures(harness, migrations_dir, &mut failures);
sidecar_failures(harness, sidecars, output, &mut failures);
lint_failures(harness, &mut failures);
DoctorReport { failures }
}
fn contract_failures(harness: &Harness, failures: &mut Vec<DoctorFailure>) {
for module in harness.modules() {
if module.harness_api() != HARNESS_API {
failures.push(DoctorFailure {
code: &CODES.harness_api_mismatch,
message: harness_api_mismatch(module.as_ref()),
});
}
}
}
fn self_check_failures(harness: &Harness, failures: &mut Vec<DoctorFailure>) {
for module in harness.modules() {
for message in module.self_check() {
failures.push(DoctorFailure {
code: &CODES.module_self_check,
message,
});
}
}
}
fn lockfile_failures(harness: &Harness, migrations_dir: &Path, failures: &mut Vec<DoctorFailure>) {
let lock = match read_lock(migrations_dir) {
Ok(lock) => lock,
Err(err) => {
failures.push(DoctorFailure {
code: &CODES.lockfile_unreadable,
message: err,
});
Lock::default()
}
};
for module in harness.modules() {
for migration in module.migrations().sqlite {
let key = format!("{}/{}", module.name(), migration.id);
match lock.get(&key) {
Some(entry) => {
if let Err(err) = verify_locked(migrations_dir, &key, entry) {
let code = match &err {
LockedMigrationError::Missing { .. } => &CODES.locked_migration_missing,
LockedMigrationError::Edited { .. } => &CODES.locked_migration_edited,
};
failures.push(DoctorFailure {
code,
message: err.to_string(),
});
}
}
None => failures.push(DoctorFailure {
code: &CODES.migration_not_collected,
message: format!(
"migration {key} is not collected yet — run `fz migrations collect`"
),
}),
}
}
}
}
fn undeclared_migration_failures(
harness: &Harness,
migrations_dir: &Path,
failures: &mut Vec<DoctorFailure>,
) {
let declared: std::collections::BTreeSet<&str> = harness
.modules()
.iter()
.flat_map(|module| module.migrations().sqlite)
.map(|migration| migration.sql.trim())
.collect();
for dir in venture_dependency_dirs(migrations_dir) {
let dep = dir.file_name().unwrap_or(dir.as_os_str()).to_string_lossy();
for file in shipped_sql_files(&dir) {
let message = match std::fs::read_to_string(&file) {
Ok(body) if declared.contains(body.trim()) => continue,
Ok(_) => format!(
"dependency `{dep}` ships `{}` but no module declares its SQL — it is \
never collected and its tables never exist on D1. Return it from the \
module's `Module::migrations()` (e.g. `include_str!`) so \
`fz migrations collect` ships it, or drop the file",
file.display()
),
Err(err) => format!(
"dependency `{dep}` ships `{}` but it cannot be read ({err}), so it \
cannot be checked against any module's declared SQL",
file.display()
),
};
failures.push(DoctorFailure {
code: &CODES.migration_not_declared,
message,
});
}
}
}
#[must_use]
pub fn venture_dependency_dirs(migrations_dir: &Path) -> Vec<PathBuf> {
let venture = match migrations_dir.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
_ => PathBuf::from("."),
};
let venture = std::fs::canonicalize(&venture).unwrap_or(venture);
let manifest = std::fs::read_to_string(venture.join("Cargo.toml"))
.ok()
.and_then(|text| text.parse::<toml::Table>().ok());
let Some(manifest) = manifest else {
return Vec::new();
};
let mut workspace: Option<PathBuf> = None;
let mut dirs = std::collections::BTreeSet::new();
for deps in dependency_tables(&manifest) {
for (name, dep) in deps {
if let Some(path) = dep.get("path").and_then(toml::Value::as_str) {
dirs.insert(venture.join(path));
} else if dep.get("workspace").and_then(toml::Value::as_bool) == Some(true) {
if workspace.is_none() {
workspace = workspace_root(&venture);
}
if let Some(path) = workspace
.as_deref()
.and_then(|root| workspace_dep_path(root, name))
{
dirs.insert(path);
}
}
}
}
dirs.into_iter().collect()
}
fn dependency_tables(manifest: &toml::Table) -> Vec<&toml::Table> {
let mut tables = Vec::new();
if let Some(deps) = manifest.get("dependencies").and_then(toml::Value::as_table) {
tables.push(deps);
}
if let Some(targets) = manifest.get("target").and_then(toml::Value::as_table) {
for target in targets.values() {
if let Some(deps) = target.get("dependencies").and_then(toml::Value::as_table) {
tables.push(deps);
}
}
}
tables
}
fn workspace_root(venture: &Path) -> Option<PathBuf> {
let mut dir = venture.to_path_buf();
loop {
let is_workspace = std::fs::read_to_string(dir.join("Cargo.toml"))
.ok()
.and_then(|text| text.parse::<toml::Table>().ok())
.is_some_and(|table| table.contains_key("workspace"));
if is_workspace {
return Some(dir);
}
if !dir.pop() || dir.as_os_str().is_empty() {
return None;
}
}
}
fn workspace_dep_path(root: &Path, name: &str) -> Option<PathBuf> {
let table = std::fs::read_to_string(root.join("Cargo.toml"))
.ok()
.and_then(|text| text.parse::<toml::Table>().ok())?;
table
.get("workspace")?
.get("dependencies")?
.get(name)?
.get("path")?
.as_str()
.map(|path| root.join(path))
}
#[must_use]
pub fn shipped_sql_files(dir: &Path) -> Vec<PathBuf> {
let migrations = dir.join("migrations");
let set = if migrations.join("sqlite").is_dir() {
migrations.join("sqlite")
} else if migrations.is_dir() && !migrations.join(crate::lock::LOCK_FILE).exists() {
migrations
} else {
return Vec::new();
};
let mut files: Vec<PathBuf> = std::fs::read_dir(&set)
.map(|entries| {
entries
.flatten()
.map(|entry| entry.path())
.filter(|path| {
path.is_file()
&& path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("sql"))
})
.collect()
})
.unwrap_or_default();
files.sort();
files
}
fn sidecar_failures(
harness: &Harness,
sidecars: Option<&str>,
output: Output,
failures: &mut Vec<DoctorFailure>,
) {
match crate::sidecars::parse(sidecars) {
Ok(mounts) if !mounts.is_empty() => {
for mount in crate::sidecars::shadowing(harness, &mounts) {
failures.push(DoctorFailure {
code: &CODES.sidecar_shadows_module,
message: format!(
"sidecar mount `{}` names a module compiled into this venture; the runtime \
ignores the mount and serves the compiled-in module. Remove it from \
{HARNESS_SIDECARS} or remove the module from the composition",
mount.name
),
});
}
if output == Output::Human {
eprintln!(
"fz: warning: {} is sidecar-mounted, so its tables, migrations and SQL are not \
checked here — they live in its own repository (docs/MOUNTING.md)",
crate::sidecars::listed(&mounts)
);
if std::env::var(SIDECAR_GATEWAY_SECRET)
.ok()
.is_none_or(|value| value.trim().is_empty())
{
eprintln!(
"fz: warning: {SIDECAR_GATEWAY_SECRET} is unset — the host stamps no \
gateway token, so a sidecar that requires one (SIDECAR_REQUIRE_GATEWAY) \
refuses every forwarded request (issue #131)"
);
}
}
}
Ok(_) => {}
Err(errors) => failures.extend(errors.into_iter().map(|message| DoctorFailure {
code: &CODES.sidecar_mount_invalid,
message,
})),
}
}
fn lint_failures(harness: &Harness, failures: &mut Vec<DoctorFailure>) {
for module in harness.modules() {
if !module.migrations().postgres.is_empty() {
continue;
}
for migration in module.migrations().sqlite {
for (token, why) in banned_tokens(migration.sql) {
failures.push(DoctorFailure {
code: &CODES.non_portable_sql,
message: format!(
"module `{}` migration {}: banned token `{token}` — {why}",
module.name(),
migration.id
),
});
}
}
}
for module in harness.modules() {
let migrations = module.migrations();
for migration in migrations.sqlite.iter().chain(migrations.postgres.iter()) {
for (fragment, why) in lint_card_data(migration.sql) {
failures.push(DoctorFailure {
code: &CODES.card_data_in_migration,
message: format!(
"module `{}` migration {}: `{fragment}` {why}",
module.name(),
migration.id
),
});
}
}
}
}
fn push_checks(
harness: &Harness,
env: VentureEnv,
output: Output,
failures: &mut Vec<DoctorFailure>,
) {
let declaration = PushDeclaration::of(harness.modules());
if !declaration.declared {
return;
}
push_wiring_checks(
&cratefield_push_wiring::inspect_push(&EnvVars),
env,
declaration,
output,
failures,
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PushDeclaration {
declared: bool,
required: bool,
}
impl PushDeclaration {
fn of(modules: &[std::sync::Arc<dyn cratefield_core::Module>]) -> Self {
Self::from_ports(
modules
.iter()
.map(|module| (module.requires(), module.optional())),
)
}
fn from_ports<'a>(modules: impl Iterator<Item = (&'a [Port], &'a [Port])>) -> Self {
let mut declaration = Self {
declared: false,
required: false,
};
for (requires, optional) in modules {
if requires.contains(&Port::Push) {
declaration.declared = true;
declaration.required = true;
} else if optional.contains(&Port::Push) {
declaration.declared = true;
}
}
declaration
}
}
fn push_wiring_checks(
wiring: &PushWiring,
env: VentureEnv,
declaration: PushDeclaration,
output: Output,
failures: &mut Vec<DoctorFailure>,
) {
match wiring.check(env) {
Err(problems) => failures.push(DoctorFailure {
code: &CODES.push_transport_misconfigured,
message: problems,
}),
Ok(()) => {
if output == Output::Human {
for problem in wiring.problems() {
eprintln!("fz: warning: {problem}");
}
}
}
}
if declaration.required && env == VentureEnv::Production && !wiring.any_routed() {
failures.push(DoctorFailure {
code: &CODES.push_required_but_unrouted,
message: "production venture has modules that require the Push port but no push \
transport is configured — every send would answer NotConfigured. Set one \
transport's variables (docs/PUSH-ENV.md), or drop the modules that require \
push. If this venture provides Push from an adapter of its own rather than \
from the environment, that adapter is not visible here (issue #191)"
.to_owned(),
});
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Configured {
pub(crate) auth_issuer: bool,
pub(crate) auth_client_id: bool,
pub(crate) stripe_webhook_secret: bool,
pub(crate) admin_token_bytes: Option<usize>,
}
impl Configured {
fn from_env() -> Self {
Self {
auth_issuer: set("AUTH_ISSUER"),
auth_client_id: set("AUTH_CLIENT_ID"),
stripe_webhook_secret: set("STRIPE_WEBHOOK_SECRET"),
admin_token_bytes: std::env::var("ADMIN_TOKEN")
.ok()
.filter(|value| !value.is_empty())
.map(|value| value.len()),
}
}
}
fn production_port_checks(
harness: &Harness,
configured: Configured,
config: &dyn cratefield_core::Config,
allow_no_captcha: Option<&str>,
output: Output,
failures: &mut Vec<DoctorFailure>,
) {
let guards = cratefield_core::WriteGuards::collect(harness.modules());
if let Some(message) = captcha_production_failure(
&guards,
cratefield_core::captcha_effective(harness.runtime()),
) {
match cratefield_core::stated_reason(allow_no_captcha) {
Some(reason) => {
tracing::warn!(
control = "captcha",
reason,
modules = guards.captcha_modules.join(","),
"production abuse control overridden by an operator"
);
if output == Output::Human {
eprintln!("fz: warning: captcha override accepted ({reason}): {message}");
eprintln!(
"fz: note: this override is recorded; it is for previews only and must \
not stand in for a Captcha port in production"
);
}
}
None => failures.push(DoctorFailure {
code: &CODES.captcha_not_effective,
message,
}),
}
}
if let Some(message) = auth_production_failure(
harness
.modules()
.iter()
.any(|module| module.requires().contains(&Port::Auth)),
configured.auth_issuer,
configured.auth_client_id,
) {
failures.push(DoctorFailure {
code: &CODES.auth_not_configured,
message,
});
}
let payments: Vec<String> = guards
.payments_signature_modules
.iter()
.map(|module| {
guards.signature_module_named(module, cratefield_core::SignatureVerification::Payments)
})
.collect();
if let Some(message) = payments_webhook_failure(&payments, configured.stripe_webhook_secret) {
failures.push(DoctorFailure {
code: &CODES.payments_webhook_secret_missing,
message,
});
}
if let Some(message) = hmac_webhook_failure(&guards.hmac_signature_routes(), config) {
failures.push(DoctorFailure {
code: &CODES.hmac_webhook_secret_missing,
message,
});
}
if let Some(message) = rate_limiter_production_failure(
guards.needs_rate_limiter(),
cratefield_core::rate_limiter_effective(harness.runtime()),
&guards
.rate_limiter_modules
.iter()
.map(|(module, routes)| format!("{module} ({})", routes.join(", ")))
.collect::<Vec<_>>(),
) {
match cratefield_core::unlimited_public_routes_override(config) {
Some(reason) => {
tracing::warn!(
control = "rate-limiter",
reason,
"production abuse control overridden by an operator"
);
if output == Output::Human {
eprintln!("fz: warning: rate-limiter override accepted ({reason}): {message}");
eprintln!(
"fz: note: this override is recorded; resolve the RateLimiter binding \
and remove HARNESS_ALLOW_UNLIMITED_PUBLIC_ROUTES before serving \
production traffic"
);
}
}
None => failures.push(DoctorFailure {
code: &CODES.rate_limiter_not_effective,
message,
}),
}
}
}
fn set(key: &str) -> bool {
std::env::var(key).is_ok_and(|value| !value.trim().is_empty())
}
fn auth_production_failure(
needs_auth: bool,
issuer_set: bool,
client_id_set: bool,
) -> Option<String> {
if !needs_auth || (issuer_set && client_id_set) {
return None;
}
let mut missing = Vec::new();
if !issuer_set {
missing.push("AUTH_ISSUER");
}
if !client_id_set {
missing.push("AUTH_CLIENT_ID");
}
Some(format!(
"a module requires the Auth port and {} is not set; every route that needs a signed-in \
caller will answer 503",
missing.join(" and ")
))
}
fn captcha_production_failure(
guards: &cratefield_core::WriteGuards,
captcha_effective: bool,
) -> Option<String> {
if guards.needs_captcha() && !captcha_effective {
Some(format!(
"production venture has captcha-guarded public writes from [{}] but the Captcha port \
is not effectively configured — provide Turnstile with its secret and expected \
hostname (architecture section 11; issue #133)",
guards.captcha_modules.join(", ")
))
} else {
None
}
}
fn payments_webhook_failure(
payments_routes: &[String],
webhook_secret_present: bool,
) -> Option<String> {
(!payments_routes.is_empty() && !webhook_secret_present).then(|| {
format!(
"production venture has signature-guarded routes [{}] but STRIPE_WEBHOOK_SECRET is \
unset — webhook signatures cannot be verified and forged events would be trusted \
(issue #102)",
payments_routes.join(", ")
)
})
}
fn hmac_webhook_failure(
hmac_modules: &[(String, String, Vec<String>)],
config: &dyn cratefield_core::Config,
) -> Option<String> {
let missing: Vec<String> = hmac_modules
.iter()
.filter(|(_, key, _)| config.get(key).is_none_or(|value| value.trim().is_empty()))
.map(|(_, key, routes)| {
if routes.is_empty() {
key.clone()
} else {
format!("{key} on {}", routes.join(", "))
}
})
.collect();
(!missing.is_empty()).then(|| {
format!(
"production venture verifies webhook signatures with the webhook_signature HMAC \
scheme but {} is not set — those handlers refuse every delivery until the \
provider's signing secret is configured (issue #533)",
missing.join(", ")
)
})
}
fn rate_limiter_production_failure(
needs_rate_limiter: bool,
rate_limiter_effective: bool,
declaring: &[String],
) -> Option<String> {
if needs_rate_limiter && !rate_limiter_effective {
let (who, cost) = if declaring.is_empty() {
(
"takes public writes or admin routes".to_owned(),
"admin bearer routes have no brute-force backstop behind the limiter and \
every public write runs without a budget"
.to_owned(),
)
} else {
(
format!(
"declares RateLimiter for public routes from [{}]",
declaring.join(", ")
),
"those routes — and any public write or admin route beside them — run \
without a budget"
.to_owned(),
)
};
Some(format!(
"production venture {who} but the RateLimiter port is not resolved: {cost} — \
resolve the binding so the runtime actually hands over a limiter, or set \
HARNESS_ALLOW_UNLIMITED_PUBLIC_ROUTES to a reason to serve unlimited \
(issues #437, #562)"
))
} else {
None
}
}
fn admin_token_failure(admin_token_bytes: Option<usize>) -> Option<String> {
let bytes = admin_token_bytes?;
(bytes < MIN_SECRET_BYTES).then(|| {
format!(
"ADMIN_TOKEN is {bytes} bytes but must be at least {MIN_SECRET_BYTES} when set — \
the boot refuses it in every environment, so this deploy cannot start. Lengthen \
it, or remove it to keep the admin routes disabled (issue #437)"
)
})
}
#[cfg(test)]
mod tests {
use super::{
CODES, DoctorFailure, DoctorReport, Output, PushDeclaration, admin_token_failure,
captcha_production_failure, hmac_webhook_failure, payments_webhook_failure,
push_wiring_checks, rate_limiter_production_failure,
};
use cratefield_core::{
Config, ConfigError, Harness, MapConfig, Migrations, Module, ModuleContext, Port, Runtime,
Venture, VentureEnv, WriteGuards,
};
use cratefield_push_wiring::{PUSH_ENV, PushKey, PushWiring, inspect_push};
use std::sync::Arc;
pub(super) struct NeedsAuth;
pub(super) struct Nothing;
macro_rules! bare_module {
($ty:ty, $name:literal, $ports:expr) => {
impl Module for $ty {
fn name(&self) -> &'static str {
$name
}
fn version(&self) -> &'static str {
"0.0.0"
}
fn requires(&self) -> &'static [Port] {
$ports
}
fn migrations(&self) -> Migrations {
Migrations::default()
}
fn validate_config(&self, _cfg: &dyn Config) -> Result<(), ConfigError> {
Ok(())
}
fn router(&self, _ctx: ModuleContext) -> cratefield_core::axum::Router {
cratefield_core::axum::Router::new()
}
}
};
}
pub(super) struct TakesWebhooks;
bare_module!(NeedsAuth, "needs-auth", &[Port::Auth]);
bare_module!(Nothing, "nothing", &[]);
impl Module for TakesWebhooks {
fn name(&self) -> &'static str {
"takes-webhooks"
}
fn version(&self) -> &'static str {
"0.0.0"
}
fn requires(&self) -> &'static [Port] {
&[Port::Payments]
}
fn migrations(&self) -> Migrations {
Migrations::default()
}
fn validate_config(&self, _cfg: &dyn Config) -> Result<(), ConfigError> {
Ok(())
}
fn surface(&self) -> cratefield_core::Surface {
Self::surface()
}
fn router(&self, _ctx: ModuleContext) -> cratefield_core::axum::Router {
cratefield_core::axum::Router::new()
}
}
impl TakesWebhooks {
fn surface() -> cratefield_core::Surface {
cratefield_core::Surface::new().action(
cratefield_core::Action::post("receive", "/receive")
.policy(cratefield_core::RoutePolicy::Signature)
.outcome(cratefield_core::Outcome::Json),
)
}
}
pub(super) struct TwoVerifiers;
impl Module for TwoVerifiers {
fn name(&self) -> &'static str {
"billing"
}
fn version(&self) -> &'static str {
"0.0.0"
}
fn requires(&self) -> &'static [Port] {
&[Port::Payments]
}
fn migrations(&self) -> Migrations {
Migrations::default()
}
fn validate_config(&self, _cfg: &dyn Config) -> Result<(), ConfigError> {
Ok(())
}
fn surface(&self) -> cratefield_core::Surface {
Self::surface()
}
fn router(&self, _ctx: ModuleContext) -> cratefield_core::axum::Router {
cratefield_core::axum::Router::new()
}
}
impl TwoVerifiers {
fn surface() -> cratefield_core::Surface {
cratefield_core::Surface::new()
.action(
cratefield_core::Action::post("webhook-stripe", "/webhooks/stripe")
.policy(cratefield_core::RoutePolicy::Signature)
.outcome(cratefield_core::Outcome::Json),
)
.action(
cratefield_core::Action::post("webhook-revenuecat", "/webhooks/revenuecat")
.policy(cratefield_core::RoutePolicy::Signature)
.verification(cratefield_core::SignatureVerification::Hmac {
secret: "REVENUECAT_WEBHOOK_SECRET",
})
.outcome(cratefield_core::Outcome::Json),
)
}
}
struct AllPorts;
impl Runtime for AllPorts {
fn provides(&self) -> Vec<Port> {
Port::ALL.to_vec()
}
}
pub(super) fn harness_of(module: Arc<dyn Module>) -> Harness {
Harness::builder()
.venture(
Venture::new("acme", "acme.test")
.cors_origins(["https://acme.test"])
.env(VentureEnv::Production),
)
.module_arc(module)
.runtime(AllPorts)
.build()
.expect("the fixture composes")
}
const OPTIONAL: PushDeclaration = PushDeclaration {
declared: true,
required: false,
};
const REQUIRED: PushDeclaration = PushDeclaration {
declared: true,
required: true,
};
fn guards(needs_captcha: bool) -> WriteGuards {
WriteGuards {
captcha_modules: if needs_captcha {
vec!["writer".to_owned()]
} else {
Vec::new()
},
signature_modules: Vec::new(),
signed_link_modules: Vec::new(),
..WriteGuards::default()
}
}
#[test]
fn a_venture_that_needs_no_caller_is_not_asked_for_an_issuer() {
assert_eq!(super::auth_production_failure(false, false, false), None);
}
#[test]
fn a_module_that_must_know_who_is_calling_needs_an_issuer_in_production() {
let refusal = super::auth_production_failure(true, false, false)
.expect("a module needs a caller and nothing can name one");
assert!(refusal.contains("AUTH_ISSUER"), "{refusal}");
assert!(refusal.contains("AUTH_CLIENT_ID"), "{refusal}");
assert!(refusal.contains("503"), "say what goes wrong: {refusal}");
}
#[test]
fn half_a_verifier_is_still_a_failure_and_names_the_half() {
let missing_id = super::auth_production_failure(true, true, false).expect("half");
assert!(missing_id.contains("AUTH_CLIENT_ID"), "{missing_id}");
assert!(!missing_id.contains("AUTH_ISSUER"), "{missing_id}");
let missing_issuer = super::auth_production_failure(true, false, true).expect("half");
assert!(missing_issuer.contains("AUTH_ISSUER"), "{missing_issuer}");
assert!(
!missing_issuer.contains("AUTH_CLIENT_ID"),
"{missing_issuer}"
);
}
#[test]
fn a_configured_verifier_is_no_failure() {
assert_eq!(super::auth_production_failure(true, true, true), None);
}
#[test]
fn captcha_rule_keys_on_declared_policies_and_effectiveness() {
let failure = captcha_production_failure(&guards(true), false).expect("fails closed");
assert!(failure.contains("writer"));
assert!(failure.contains("effectively configured"));
assert!(captcha_production_failure(&guards(true), true).is_none());
assert!(captcha_production_failure(&guards(false), false).is_none());
for nothing in ["", " "] {
assert!(
cratefield_core::stated_reason(Some(nothing)).is_none(),
"an empty reason counted as an acceptance: {nothing:?}"
);
}
assert_eq!(
cratefield_core::stated_reason(Some(" previewing the new form ")),
Some("previewing the new form"),
"a real reason still overrides, trimmed"
);
}
#[test]
fn payments_in_production_needs_a_webhook_secret() {
let payments = vec!["billing (POST /v1/billing/webhooks/stripe)".to_owned()];
let failure = payments_webhook_failure(&payments, false).expect("no secret fails");
assert!(
failure.contains("POST /v1/billing/webhooks/stripe"),
"{failure}"
);
assert!(payments_webhook_failure(&payments, true).is_none());
assert!(payments_webhook_failure(&[], false).is_none());
let modules = vec![(
"pos".to_owned(),
"POS_WEBHOOK_SECRET".to_owned(),
vec!["POST /webhook".to_owned()],
)];
let failure = hmac_webhook_failure(&modules, &MapConfig::default()).expect("unset fails");
assert!(failure.contains("POS_WEBHOOK_SECRET"), "{failure}");
assert!(failure.contains("POST /webhook"), "{failure}");
assert!(
hmac_webhook_failure(
&modules,
&MapConfig::from_pairs([("POS_WEBHOOK_SECRET", " ")])
)
.is_some()
);
assert!(
hmac_webhook_failure(
&modules,
&MapConfig::from_pairs([("POS_WEBHOOK_SECRET", "x")])
)
.is_none()
);
assert!(hmac_webhook_failure(&[], &MapConfig::default()).is_none());
}
#[test]
fn a_venture_with_public_writes_or_admin_routes_needs_a_resolved_limiter() {
let failure =
rate_limiter_production_failure(true, false, &[]).expect("an unlimited venture fails");
assert!(failure.contains("RateLimiter"), "{failure}");
assert!(
failure.contains("HARNESS_ALLOW_UNLIMITED_PUBLIC_ROUTES"),
"the hatch is named the way the captcha rule names its own: {failure}"
);
assert!(rate_limiter_production_failure(true, true, &[]).is_none());
assert!(rate_limiter_production_failure(false, false, &[]).is_none());
}
#[test]
fn a_doctor_limiter_failure_names_the_declaring_modules() {
let failure = rate_limiter_production_failure(
true,
false,
&["waitlist (POST /v1/waitlist, GET /v1/waitlist/search)".to_owned()],
)
.expect("a declaring venture fails");
assert!(failure.contains("declares RateLimiter"), "{failure}");
assert!(
failure.contains("waitlist (POST /v1/waitlist, GET /v1/waitlist/search)"),
"{failure}"
);
}
#[test]
fn an_absent_admin_token_is_not_a_failure() {
assert_eq!(admin_token_failure(None), None);
}
#[test]
fn a_set_but_short_admin_token_fails_at_the_floor_the_boot_enforces() {
let refusal = admin_token_failure(Some(8)).expect("a short token");
assert!(refusal.contains("ADMIN_TOKEN"), "{refusal}");
assert!(refusal.contains("32"), "name the floor: {refusal}");
assert!(
refusal.contains("every environment"),
"the rule is not production-only, and says so: {refusal}"
);
assert_eq!(admin_token_failure(Some(32)), None);
assert_eq!(admin_token_failure(Some(64)), None);
}
#[test]
fn report_renders_the_json_contract() {
let report = DoctorReport {
failures: vec![
DoctorFailure {
code: &CODES.captcha_not_effective,
message: "production venture has captcha-guarded writes".to_owned(),
},
DoctorFailure {
code: &CODES.payments_webhook_secret_missing,
message: "STRIPE_WEBHOOK_SECRET is unset".to_owned(),
},
],
};
let payload = report.render_json().expect("serializes");
assert_eq!(
payload,
"{\"schema\":1,\"ok\":false,\"failures\":[\
{\"code\":\"captcha-not-effective\",\"message\":\"production venture has \
captcha-guarded writes\"},\
{\"code\":\"payments-webhook-secret-missing\",\"message\":\"STRIPE_WEBHOOK_SECRET \
is unset\"}]}"
);
assert!(!report.ok());
let clean = DoctorReport {
failures: Vec::new(),
};
assert_eq!(
clean.render_json().expect("serializes"),
"{\"schema\":1,\"ok\":true,\"failures\":[]}"
);
assert!(clean.ok());
}
fn half_wired() -> PushWiring {
let var = PUSH_ENV
.iter()
.find(|var| var.required)
.expect("the table has a required variable");
let wiring = inspect_push(&MapConfig::from_pairs([(
var.name().to_owned(),
"set-but-alone".to_owned(),
)]));
assert!(
!wiring.problems().is_empty(),
"one variable of a transport is a half-wired transport: {}",
wiring.summary()
);
wiring
}
#[test]
fn a_half_wired_transport_fails_the_doctor_in_production_only() {
let wiring = half_wired();
let mut failures = Vec::new();
push_wiring_checks(
&wiring,
VentureEnv::Production,
OPTIONAL,
Output::Human,
&mut failures,
);
assert_eq!(failures.len(), 1, "{failures:?}");
for problem in wiring.problems() {
assert!(failures[0].message.contains(&problem), "{failures:?}");
}
assert_eq!(failures[0].code.code, "push-transport-misconfigured");
for env in [VentureEnv::Development, VentureEnv::Staging] {
let mut failures = Vec::new();
push_wiring_checks(&wiring, env, OPTIONAL, Output::Human, &mut failures);
assert!(failures.is_empty(), "{env:?}: {failures:?}");
}
}
#[test]
fn a_venture_that_wires_no_push_at_all_is_silent() {
let wiring = inspect_push(&MapConfig::from_pairs(Vec::<(String, String)>::new()));
let mut failures = Vec::new();
push_wiring_checks(
&wiring,
VentureEnv::Production,
OPTIONAL,
Output::Human,
&mut failures,
);
assert!(failures.is_empty(), "{failures:?}");
assert!(wiring.problems().is_empty(), "{:?}", wiring.problems());
}
#[test]
fn a_module_that_requires_push_needs_a_transport_that_can_send() {
let nothing = inspect_push(&MapConfig::from_pairs(Vec::<(String, String)>::new()));
assert!(!nothing.any_routed());
let mut failures = Vec::new();
push_wiring_checks(
¬hing,
VentureEnv::Production,
REQUIRED,
Output::Human,
&mut failures,
);
assert_eq!(failures.len(), 1, "{failures:?}");
assert!(
failures[0].message.contains("require the Push port"),
"{failures:?}"
);
assert_eq!(failures[0].code.code, "push-required-but-unrouted");
for env in [VentureEnv::Development, VentureEnv::Staging] {
let mut failures = Vec::new();
push_wiring_checks(¬hing, env, REQUIRED, Output::Human, &mut failures);
assert!(failures.is_empty(), "{env:?}: {failures:?}");
}
let mut failures = Vec::new();
push_wiring_checks(
¬hing,
VentureEnv::Production,
OPTIONAL,
Output::Human,
&mut failures,
);
assert!(failures.is_empty(), "{failures:?}");
}
#[test]
fn the_push_rules_are_gated_on_what_the_venture_declares() {
let none = PushDeclaration::from_ports([(&[Port::Db][..], &[][..])].into_iter());
assert!(!none.declared);
assert!(!none.required);
let optional =
PushDeclaration::from_ports([(&[Port::Db][..], &[Port::Push][..])].into_iter());
assert!(optional.declared);
assert!(
!optional.required,
"a module that only *may* use push does not make one mandatory"
);
let required = PushDeclaration::from_ports(
[(&[Port::Db][..], &[][..]), (&[Port::Push][..], &[][..])].into_iter(),
);
assert!(required.declared);
assert!(required.required);
}
#[test]
fn a_configured_transport_satisfies_a_module_that_requires_push() {
let key = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg\
cXMgRpW+eLn7ZvCx\nIuTdd8csWMZ69azlRzS0dy2FN6GhRANCAATJ6GazR2lhWcC3JYsazLR0\
uWOyDKrC\nmeP4HPWghRmfoa4z3Ux7mG3Ylz+auRaBukKGicSdSvVG+jGeQwr3fNag\n\
-----END PRIVATE KEY-----";
let wiring = inspect_push(&MapConfig::from_pairs([
(PushKey::VapidPrivateKey.name().to_owned(), key.to_owned()),
(
PushKey::VapidSubject.name().to_owned(),
"mailto:ops@example.test".to_owned(),
),
]));
assert!(wiring.any_routed(), "{}", wiring.summary());
let mut failures = Vec::new();
push_wiring_checks(
&wiring,
VentureEnv::Production,
REQUIRED,
Output::Human,
&mut failures,
);
assert!(failures.is_empty(), "{failures:?}");
}
}
#[cfg(test)]
mod production_wiring {
use super::tests::{NeedsAuth, Nothing, harness_of};
use super::{CODES, Configured, DoctorFailure, Output, production_port_checks, run_checks};
fn failures(
module: std::sync::Arc<dyn cratefield_core::Module>,
configured: Configured,
) -> Vec<String> {
let harness = harness_of(module);
let mut out = Vec::new();
production_port_checks(
&harness,
configured,
&cratefield_core::MapConfig::default(),
None,
Output::Json,
&mut out,
);
out.iter()
.map(|failure: &DoctorFailure| failure.code.code.to_owned())
.collect()
}
fn messages(
module: std::sync::Arc<dyn cratefield_core::Module>,
configured: Configured,
config: &dyn cratefield_core::Config,
) -> Vec<(String, String)> {
let harness = harness_of(module);
let mut out = Vec::new();
production_port_checks(&harness, configured, config, None, Output::Json, &mut out);
out.iter()
.map(|failure| (failure.code.code.to_owned(), failure.message.clone()))
.collect()
}
const NONE_SET: Configured = Configured {
auth_issuer: false,
auth_client_id: false,
stripe_webhook_secret: false,
admin_token_bytes: None,
};
#[test]
fn a_module_needing_a_caller_with_no_issuer_is_reported() {
assert!(
failures(std::sync::Arc::new(NeedsAuth), NONE_SET)
.contains(&CODES.auth_not_configured.code.to_owned()),
"the rule is correct and nothing calls it"
);
}
#[test]
fn the_same_module_with_an_issuer_is_not() {
let configured = Configured {
auth_issuer: true,
auth_client_id: true,
..NONE_SET
};
assert!(
!failures(std::sync::Arc::new(NeedsAuth), configured)
.contains(&CODES.auth_not_configured.code.to_owned())
);
}
#[test]
fn a_webhook_receiver_with_no_secret_is_reported() {
assert!(
failures(std::sync::Arc::new(super::tests::TakesWebhooks), NONE_SET)
.contains(&CODES.payments_webhook_secret_missing.code.to_owned()),
"the payments rule is correct and nothing calls it"
);
}
#[test]
fn the_same_receiver_with_a_secret_is_not() {
let configured = Configured {
stripe_webhook_secret: true,
..NONE_SET
};
assert!(
!failures(std::sync::Arc::new(super::tests::TakesWebhooks), configured)
.contains(&CODES.payments_webhook_secret_missing.code.to_owned())
);
}
#[test]
fn a_module_needing_no_caller_is_never_asked() {
assert!(
!failures(std::sync::Arc::new(Nothing), NONE_SET)
.contains(&CODES.auth_not_configured.code.to_owned())
);
}
#[test]
fn a_two_verifier_module_names_the_route_each_webhook_rule_blocks() {
use cratefield_core::MapConfig;
let message = |pairs: &[(String, String)], code: &str| -> Option<String> {
pairs
.iter()
.find(|(found, _)| found == code)
.map(|(_, message)| message.clone())
};
let two = || std::sync::Arc::new(super::tests::TwoVerifiers);
let stripe_set = Configured {
stripe_webhook_secret: true,
..NONE_SET
};
let secret = MapConfig::from_pairs([("BILLING_REVENUECAT_WEBHOOK_SECRET", "whsec-dummy")]);
let both = messages(two(), stripe_set, &secret);
assert!(message(&both, "payments-webhook-secret-missing").is_none());
assert!(message(&both, "hmac-webhook-secret-missing").is_none());
let no_stripe = messages(two(), NONE_SET, &secret);
let payment = message(&no_stripe, "payments-webhook-secret-missing").expect("fires");
assert!(
payment.contains("POST /v1/billing/webhooks/stripe"),
"{payment}"
);
assert!(
!payment.contains("POST /v1/billing/webhooks/revenuecat"),
"{payment}"
);
let no_revenuecat = messages(two(), stripe_set, &MapConfig::default());
let hmac = message(&no_revenuecat, "hmac-webhook-secret-missing").expect("fires");
assert!(
hmac.contains("POST /v1/billing/webhooks/revenuecat"),
"{hmac}"
);
assert!(!hmac.contains("POST /v1/billing/webhooks/stripe"), "{hmac}");
}
fn run_report(configured: Configured) -> Vec<String> {
let harness = harness_of(std::sync::Arc::new(Nothing));
run_checks(
&harness,
std::path::Path::new("no-migrations-here"),
None,
None,
Output::Json,
configured,
)
.failures
.iter()
.map(|failure: &DoctorFailure| failure.code.code.to_owned())
.collect()
}
#[test]
fn a_short_admin_token_reaches_the_report() {
let configured = Configured {
admin_token_bytes: Some(4),
..NONE_SET
};
assert_eq!(
run_report(configured),
vec![CODES.admin_token_too_short.code.to_owned()],
"the rule is correct and nothing calls it"
);
}
#[test]
fn an_absent_admin_token_leaves_the_report_clean() {
assert!(
run_report(NONE_SET).is_empty(),
"{:?}",
run_report(NONE_SET)
);
}
}