KU-K1: tests for answer_depends_on_samples and last_failure_was_transport

KeydbSource::answer_depends_on_samples (KSK1) and MultiSource::answer_depends_on_samples
(KSK2, KU3-6 "any(inner)") both default to `true` today, so a keydb-then-online
composition would be re-asked per piece instead of once. OnlineSource::last_failure_was_transport
(J15) defaults to `false`, so a transient DNS/connect/idle failure would never be
retried by `resolve` (J13). Red: these six assertions fail against today's defaults.
This commit is contained in:
Matthew Jackson
2026-09-28 04:12:12 -07:00
parent 18fdab6b77
commit 8f923fd5f9
3 changed files with 115 additions and 0 deletions
+7
View File
@@ -1142,6 +1142,13 @@ mod tests {
assert_eq!(KeydbSource::new("/nonexistent/keydb.cfg").label(), "keydb");
}
// KSK1: keyed by disc hash, so `resolve` asks it once per rip, not once per
// piece (KU §2.3 step 8, N-KU10).
#[test]
fn keydb_source_answer_does_not_depend_on_samples() {
assert!(!KeydbSource::new("/nonexistent/keydb.cfg").answer_depends_on_samples());
}
// A stamp with no mtime (mtime-less FS) or a future mtime (clock skew) must
// not be stuck un-settled forever — it now settles via the inode-identity
// fallback once the observation has aged past the granularity.
+30
View File
@@ -214,6 +214,36 @@ mod tests {
assert!(multi.host_certs(None).is_empty());
}
// A source whose `answer_depends_on_samples` is fixed to the given value —
// the only behaviour KSK2 exercises.
struct FixedDependency(bool);
impl KeySource for FixedDependency {
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
Ok(Vec::new())
}
fn answer_depends_on_samples(&self) -> bool {
self.0
}
}
// KSK2: `MultiSource::answer_depends_on_samples` is `any(inner)`, so nesting
// a sample-dependent online source under a sample-independent keydb still
// gets the per-piece ask (`resolve` must not treat the pair as keydb-only).
#[test]
fn multi_source_answer_depends_on_samples_is_any_inner() {
let all_independent = MultiSource::new(vec![
Box::new(FixedDependency(false)) as Box<dyn KeySource>,
Box::new(FixedDependency(false)) as Box<dyn KeySource>,
]);
assert!(!all_independent.answer_depends_on_samples());
let keydb_then_online = MultiSource::new(vec![
Box::new(FixedDependency(false)) as Box<dyn KeySource>,
Box::new(FixedDependency(true)) as Box<dyn KeySource>,
]);
assert!(keydb_then_online.answer_depends_on_samples());
}
/// The `mkb` generation must reach each inner source so its OWN revocation
/// filter can act; a source that drops everything at a given generation is
/// honoured by the union.
+78
View File
@@ -2838,4 +2838,82 @@ mod tests {
);
assert_eq!(take_last_decode_reachability(), None);
}
// ── KU-K1: `last_failure_was_transport` (J13, J15) ──────────────────────
#[test]
fn last_failure_was_transport_is_false_before_any_query() {
let src = OnlineSource::new("https://keyserver.test/keys", "s3cr3t");
assert!(!src.last_failure_was_transport());
}
// Per J15/J13: a DNS failure never answers, so it is transport-class and gets retried.
#[test]
fn last_failure_was_transport_true_after_dns_failure() {
let src = source_via("kuk1a.test", T_IDLE, || {
Err((GuardFail::Unreachable, "did not resolve".into()))
});
assert!(src.get_unit_keys(&ctx_with_mkb(0)).is_err());
assert!(
src.last_failure_was_transport(),
"a DNS failure is transport-class"
);
}
// A refused connection never answers either — transport-class (J13's "connect").
#[test]
fn last_failure_was_transport_true_after_connect_refused() {
let refused: SocketAddr = ([127, 0, 0, 1], 1).into();
let src = source_via("kuk1b.test", T_IDLE, move || Ok(vec![refused]));
assert!(src.get_unit_keys(&ctx_with_mkb(0)).is_err());
assert!(
src.last_failure_was_transport(),
"a refused connection is transport-class"
);
}
// Stop-design-v5 T16: "no bytes moved" for the idle bound is a transport-class timeout.
#[test]
fn last_failure_was_transport_true_after_idle_timeout() {
let addr = stub_server(Stub::NeverAnswer);
let src = source_via("kuk1c.test", T_IDLE, move || Ok(vec![addr]));
assert!(src.get_unit_keys(&ctx_with_mkb(0)).is_err());
assert!(
src.last_failure_was_transport(),
"an idle stall is transport-class"
);
}
// J15: a 5xx (or any decode reply) IS an answer, so it must never look transport-class —
// that would re-ask a source the service already answered ("never call it twice").
#[test]
fn last_failure_was_transport_false_after_5xx() {
let addr = stub_server(Stub::Answer(503));
let src = source_via("kuk1d.test", T_IDLE, move || Ok(vec![addr]));
assert!(src.get_unit_keys(&ctx_with_mkb(0)).is_err());
assert!(
!src.last_failure_was_transport(),
"the service answered (5xx) — never re-asked"
);
}
// J15: "reset it on success" — a later answer must clear a prior transport verdict.
#[test]
fn last_failure_was_transport_resets_on_success() {
let ok_addr = stub_server(Stub::Answer(200));
let refused: SocketAddr = ([127, 0, 0, 1], 1).into();
let target = Arc::new(Mutex::new(refused));
let for_resolve = target.clone();
let src = source_via("kuk1e.test", T_IDLE, move || {
Ok(vec![*for_resolve.lock().unwrap()])
});
assert!(src.get_unit_keys(&ctx_with_mkb(0)).is_err());
assert!(src.last_failure_was_transport());
*target.lock().unwrap() = ok_addr;
assert!(src.get_unit_keys(&ctx_with_mkb(0)).is_ok());
assert!(
!src.last_failure_was_transport(),
"a success must reset the flag"
);
}
}