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}"
);
}