Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/korp-collectors

tests/every_collector_declares_its_config.rs

//! **THE DECLARATION LAW: a collector that needs credentials NAMES them, on every face
//! it wears.**
//!
//! The owner's part (1), 2026-09-06: "each collector declares the config it must have (env
//! var names or config keys) as data, in one place, next to the collector."
//!
//! # The hole this closes
//!
//! `CollectorConfig::requirements`'s doc comment on `439b845` said a daemon whose key
//! comes from `CollectorCtx::secret` "MUST override this and name the variable, because
//! a daemon whose key is absent can only report `Failed` at the wire". A MUST that
//! nothing checks is a wish. Measured on `439b845`, by enumerating both registries:
//!
//! * the **ingest** registry (`registry_requirements()`) declared named secrets for 17
//!   collectors — `ENTSOE_TOKEN`, `TRAFIKVERKET_API_KEY`, `BOLAGSVERKET_CLIENT_ID` … —
//!   and that is the list korp's object registry renders, the one that showed 21
//!   collectors refusing on the live oden server;
//! * the **daemon** registry (`daemon_registry()`) — the face that owns `run()`, the one
//!   that actually ticks — declared `secrets: []` for **all 22 daemons, without
//!   exception**, including the six that appear in BOTH registries under the same name.
//!
//! So one collector answered "what do you need?" two different ways, and the face that
//! ran was the one that answered wrong: readiness computed `Runnable`, the host offered
//! ▶ Run, and the daemon nagged hourly forever.
//!
//! # Why a test and not just the fix
//!
//! The fix (`Requirements::inheriting_credentials_of`) makes the daemon read the ingest
//! declaration, so the two cannot drift. But a fix is a fact about today and a law is a
//! fact about tomorrow: the next provider added to `registry()` with an `AuthKind` and
//! no `requirements()` override would slip straight back through. This file fails the
//! build for that.
#![cfg(feature = "daemon")]

use std::collections::BTreeMap;

use korp_collectors::{AuthKind, registry, registry_requirements};

/// **Every credential-gated INGEST collector names at least one variable.**
///
/// "Names" is the operative word: `SecretReq::undeclared` produces a requirement with an
/// EMPTY name, which is honest ("I need something, I cannot say what") and is still
/// unusable — an operator cannot set a variable nobody named, which is the whole of
/// The owner's complaint. So an empty name fails here exactly like a missing one.
#[test]
fn every_credential_gated_collector_names_its_variable() {
    let declared: BTreeMap<String, _> = registry_requirements().into_iter().collect();

    let mut gated = 0usize;
    let mut offenders: Vec<String> = Vec::new();
    for c in registry() {
        let meta = c.meta();
        if meta.auth == AuthKind::None {
            continue;
        }
        gated += 1;
        let req = declared.get(&meta.name).expect("registry twin");
        let named: Vec<&str> = req
            .secrets
            .iter()
            .map(|s| s.name.as_str())
            .filter(|n| !n.is_empty())
            .collect();
        // A collector may be gated by a config FIELD rather than a secret — Bolagsverket
        // needs `identitetsbeteckning` as well as its client id. Either is a name an
        // operator can act on; having NEITHER is not.
        if named.is_empty() && req.fields.is_empty() {
            offenders.push(format!(
                "  {} (auth = {}) declares {} secret(s), none of them named, and no \
                 required field",
                meta.name,
                meta.auth.as_str(),
                req.secrets.len(),
            ));
        }
    }

    // ── Anti-vacuity. A sweep that examined nothing passes every assertion below it,
    //    and this crate has had exactly that mistake found in it before (a bencher that
    //    scored 100% because the budget was larger than the fixture). If the registry is
    //    ever feature-gated down to nothing, or `auth` stops being set, this fires. ────
    assert!(
        gated >= 15,
        "only {gated} credential-gated collectors were examined — the sweep is not \
         seeing the registry, so every assertion below it is vacuous",
    );
    assert!(
        offenders.is_empty(),
        "{} credential-gated collector(s) cannot be configured, because nobody can say \
         WHICH variable to set:\n{}\n\nDeclare it: `.with_secrets(vec![SecretReq::new(\
         \"PROVIDER_TOKEN\", \"what it is and where to get one\")])` beside the \
         collector.",
        offenders.len(),
        offenders.join("\n"),
    );
}

/// **The two faces of one collector declare the SAME credentials.**
///
/// This is the anti-drift guard, and it is the one that was RED on `439b845`: six
/// collectors live in both registries under one name and the daemon face declared
/// nothing for all six.
///
/// It compares NAME SETS, not whole `Requirements`: the `source` is deliberately allowed
/// to differ, because a daemon's endpoint comes from its live config (an operator may
/// have pointed it at a mirror) while the ingest twin carries the shipped default.
#[test]
fn a_daemons_credentials_agree_with_its_ingest_twin() {
    let ingest: BTreeMap<String, _> = registry_requirements().into_iter().collect();

    let mut compared = 0usize;
    let mut drifted: Vec<String> = Vec::new();
    for d in korp_collectors::daemon::daemon_registry() {
        let name = d.name().to_string();
        let Some(twin) = ingest.get(&name) else {
            continue; // a daemon with no ingest twin (osm-cut, wfs) — nothing to agree with
        };
        compared += 1;

        let want: Vec<&str> = twin.secrets.iter().map(|s| s.name.as_str()).collect();
        let got: Vec<String> = d
            .requirements()
            .secrets
            .iter()
            .map(|s| s.name.clone())
            .collect();
        if want.iter().any(|w| !got.iter().any(|g| g == w)) {
            drifted.push(format!(
                "  {name}: ingest declares {want:?}, the daemon declares {got:?}",
            ));
        }
    }

    assert!(
        compared >= 6,
        "only {compared} daemon/ingest twins were compared — on 439b845 there were at \
         least six (entsoe, entsoe_day_ahead, entsoe_generation, nordpool, \
         openaq-air-quality, trafikverket-trainpos). Fewer means this guard has stopped \
         looking at the thing it guards.",
    );
    assert!(
        drifted.is_empty(),
        "{} collector(s) answer \"what credentials do you need?\" differently depending \
         on which face you ask. The face that RUNS is the daemon one, so this is a \
         collector reported as runnable that cannot run:\n{}",
        drifted.len(),
        drifted.join("\n"),
    );
}

/// **A collector with no auth declares no secret** — the other direction, and it is not
/// pedantry: a keyless collector that declared a secret would sit at `NeedsConfig`
/// forever and be refused at load by the gate this lane just built. Getting the
/// declaration wrong in THIS direction takes a working collector off the air.
#[test]
fn a_keyless_collector_declares_no_secret() {
    let declared: BTreeMap<String, _> = registry_requirements().into_iter().collect();
    let mut checked = 0usize;
    let mut wrong: Vec<String> = Vec::new();
    for c in registry() {
        let meta = c.meta();
        if meta.auth != AuthKind::None {
            continue;
        }
        checked += 1;
        let req = declared.get(&meta.name).expect("registry twin");
        if !req.secrets.is_empty() {
            wrong.push(format!(
                "  {} declares auth None but requires {:?} — it would refuse at load",
                meta.name,
                req.secrets.iter().map(|s| &s.name).collect::<Vec<_>>(),
            ));
        }
    }
    assert!(checked >= 20, "only {checked} keyless collectors examined — vacuous");
    assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}