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

nordisk/korp-collectors

tests/collectors_auth.rs

//! **Auth-resolution contract** — every registered collector must resolve its
//! [`AuthConfig`] through the ASYNC seam, so an OAuth2 client-credentials source
//! is a supported configuration rather than a hard error.
//!
//! `AuthConfig::resolve()` (sync) deliberately refuses
//! `OAuth2ClientCredentials` — the token is a network round-trip — and returns
//! "…use AuthConfig::resolve_async()". Any collector that calls the SYNC form in
//! its `collect()` therefore aborts before it makes a single request when the
//! operator configures OAuth2, with an error telling them to call an API the
//! collector does not expose. `resolve_async` delegates every non-OAuth variant
//! straight back to `resolve`, so calling it is behaviourally free.
//!
//! This sweep is deterministic and network-free: the OAuth2 config names env
//! vars that are guaranteed unset, so `resolve_async` fails at the env read
//! (BEFORE the token fetch) with a *different*, correct error. What is asserted
//! is that no collector produces the sync-resolve refusal.
//!
//! The registry this sweeps is `parsers`-gated, so this target is too (the same
//! self-gate the csv/osm/mainframe/znippy suites carry) — without it the whole
//! test binary fails to COMPILE under `--no-default-features` rather than being
//! skipped as not-applicable.
#![cfg(feature = "parsers")]

use korp_collectors::{AuthConfig, BatchSink, Bounds, CollectorCfg, registry};

/// The refusal `AuthConfig::resolve()` returns for the OAuth2 variant. A
/// collector emitting this has resolved auth on the wrong (sync) seam.
const SYNC_REFUSAL: &str = "use AuthConfig::resolve_async()";

/// Env var names that must not exist in the environment, so the token fetch is
/// never attempted and the test stays offline.
const UNSET_ID_ENV: &str = "KORP_COLLECTORS_TEST_OAUTH_ID_DO_NOT_SET";
const UNSET_SECRET_ENV: &str = "KORP_COLLECTORS_TEST_OAUTH_SECRET_DO_NOT_SET";

#[test]
fn no_collector_rejects_oauth2_on_the_sync_auth_seam() {
    assert!(
        std::env::var_os(UNSET_ID_ENV).is_none(),
        "test precondition: {UNSET_ID_ENV} must be unset"
    );

    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("tokio runtime");

    let mut offenders: Vec<String> = Vec::new();
    let mut checked = 0usize;

    for collector in registry() {
        let name = collector.meta().name.to_string();
        // Port 9 (discard) on loopback: any collector that gets PAST auth
        // resolution fails fast on connect instead of hitting a real endpoint.
        let cfg = CollectorCfg::new("http://127.0.0.1:9/", "t").with_auth(
            AuthConfig::OAuth2ClientCredentials {
                token_url: "http://127.0.0.1:9/token".into(),
                id_env: UNSET_ID_ENV.into(),
                secret_env: UNSET_SECRET_ENV.into(),
            },
        );
        let sink = BatchSink::memory();
        let err = rt
            .block_on(collector.collect(&cfg, Bounds::default(), &sink))
            .err()
            .map(|e| e.to_string())
            .unwrap_or_default();
        if err.contains(SYNC_REFUSAL) {
            offenders.push(name);
        }
        checked += 1;
    }

    assert!(checked > 0, "registry() must yield collectors to sweep");
    assert!(
        offenders.is_empty(),
        "{} collector(s) resolve auth synchronously, so OAuth2 client-credentials is a hard \
         error for them — switch `cfg.auth.resolve()?` to `cfg.auth.resolve_async().await?`: {:?}",
        offenders.len(),
        offenders
    );
}

/// The complement: with OAuth2 configured and the credential env vars absent,
/// the error a collector surfaces must be the MISSING-CREDENTIAL one — proof
/// that resolution reached the async seam and got as far as reading env.
#[test]
fn oauth2_without_env_credentials_reports_the_missing_var() {
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("tokio runtime");

    let cfg = CollectorCfg::new("http://127.0.0.1:9/", "t").with_auth(
        AuthConfig::OAuth2ClientCredentials {
            token_url: "http://127.0.0.1:9/token".into(),
            id_env: UNSET_ID_ENV.into(),
            secret_env: UNSET_SECRET_ENV.into(),
        },
    );
    // WFS is one of the previously-sync collectors and does nothing before auth.
    let wfs = korp_collectors::by_name("wfs").expect("wfs collector registered");
    let err = rt
        .block_on(wfs.collect(&cfg, Bounds::default(), &BatchSink::memory()))
        .expect_err("no credentials in env → collect must fail")
        .to_string();
    assert!(
        err.contains(UNSET_ID_ENV) && err.contains("missing credential env var"),
        "expected the missing-credential error from resolve_async, got: {err}"
    );
}