b1bc7a764e
Next component in the migration order (Roster -> LSX -> UTAS). Adapter layer
only: no host, no runtime replacement, nothing armed.
The response is shaped as much by http.server.BaseHTTPRequestHandler as by the
oracle's handler code, so it is captured over the wire rather than reasoned
about:
* HTTP/1.0 status line -- protocol_version is left at its default, so the
reply is 1.0 even though the client asks for 1.1
* send_response injects Server: and Date: BEFORE the handler's own headers
* POST answers with headers only: the handler writes the body `if method ==
"GET"`, so a POST advertises Content-Length: 67 and then sends nothing
That last one is preserved, not corrected. It looks like a bug, but "obviously a
bug" has been the wrong call before in this port, and a test now asserts it so a
future cleanup has to argue with something.
Date and Server are volatile and are MASKED in the fixture rather than dropped,
so their presence and position are still asserted. Server is additionally
recorded verbatim: it carries the container's Python version, so a drift away
from roster::ORACLE_SERVER fails a test instead of silently changing every byte
we emit.
generate_roster.py --check FAILS when it cannot reach the oracle rather than
passing, and mutation-testing the mutation harness itself caught two "surviving"
mutations that were really sed no-ops. With application verified, all four
mutations (header order, Content-Length, XML body, Connection) are killed.
122 lines
4.0 KiB
Rust
122 lines
4.0 KiB
Rust
//! The roster response, held against bytes captured from the live oracle.
|
|
//!
|
|
//! The fixture masks the two volatile fields (`Date:`, `Server:`) and records
|
|
//! the observed `Server` string separately, so this can assert the full byte
|
|
//! layout while still failing loudly if the oracle's Python version drifts away
|
|
//! from the adapter's `ORACLE_SERVER` constant.
|
|
|
|
use openfut_adapter_fifa17::roster::{self, Method};
|
|
|
|
const MASK: &str = "<MASKED>";
|
|
|
|
fn fixture() -> String {
|
|
let path = format!("{}/fixtures/roster.json", env!("CARGO_MANIFEST_DIR"));
|
|
std::fs::read_to_string(&path)
|
|
.unwrap_or_else(|e| panic!("roster fixtures missing at {path}: {e}"))
|
|
}
|
|
|
|
/// Minimal extraction: the fixture is written by our own generator and is a
|
|
/// flat object, so a JSON dependency would be overkill in a crate that has none.
|
|
fn field(text: &str, key: &str) -> String {
|
|
let needle = format!("\"{key}\"");
|
|
let at = text
|
|
.find(&needle)
|
|
.unwrap_or_else(|| panic!("fixture has no {key}"));
|
|
let rest = &text[at + needle.len()..];
|
|
let colon = rest.find(':').expect("key: value");
|
|
let open = rest[colon..].find('"').expect("value opens") + colon;
|
|
let close = rest[open + 1..].find('"').expect("value closes");
|
|
rest[open + 1..open + 1 + close].to_string()
|
|
}
|
|
|
|
fn expected(method: &str) -> Vec<u8> {
|
|
// Scope the search to the responses object so a key never matches elsewhere.
|
|
let text = fixture();
|
|
let at = text.find("\"responses\"").expect("responses");
|
|
let hex = field(&text[at..], method);
|
|
(0..hex.len())
|
|
.step_by(2)
|
|
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).expect("hex"))
|
|
.collect()
|
|
}
|
|
|
|
/// Re-apply the generator's masking so the comparison is like-for-like.
|
|
fn mask(raw: &[u8]) -> Vec<u8> {
|
|
let text = String::from_utf8_lossy(raw);
|
|
let masked: String = text
|
|
.split("\r\n")
|
|
.map(|line| {
|
|
if line.starts_with("Date: ") {
|
|
format!("Date: {MASK}")
|
|
} else if line.starts_with("Server: ") {
|
|
format!("Server: {MASK}")
|
|
} else {
|
|
line.to_string()
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\r\n");
|
|
masked.into_bytes()
|
|
}
|
|
|
|
fn check(method: Method, name: &str) {
|
|
let got = roster::roster_response(
|
|
method,
|
|
roster::ORACLE_SERVER,
|
|
"Tue, 11 Aug 2026 05:15:13 GMT",
|
|
);
|
|
assert_eq!(
|
|
String::from_utf8_lossy(&mask(&got)),
|
|
String::from_utf8_lossy(&expected(name)),
|
|
"{name} differs from the oracle"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn get_matches_the_oracle() {
|
|
check(Method::Get, "GET");
|
|
}
|
|
|
|
#[test]
|
|
fn head_matches_the_oracle() {
|
|
check(Method::Head, "HEAD");
|
|
}
|
|
|
|
/// Including the quirk: headers advertising a body that never arrives.
|
|
#[test]
|
|
fn post_matches_the_oracle() {
|
|
check(Method::Post, "POST");
|
|
}
|
|
|
|
/// If the container's Python changes, `ORACLE_SERVER` is stale and every
|
|
/// response this adapter builds is wrong in a byte the oracle would have got
|
|
/// right. The fixture records what was actually observed so that drift is a
|
|
/// test failure rather than a silent divergence.
|
|
#[test]
|
|
fn the_server_constant_still_matches_the_observed_oracle() {
|
|
let observed = field(&fixture(), "observed_server");
|
|
assert_eq!(
|
|
roster::ORACLE_SERVER, observed,
|
|
"roster::ORACLE_SERVER is stale — the oracle now sends {observed:?}. \
|
|
Regenerate fixtures and update the constant."
|
|
);
|
|
}
|
|
|
|
/// The masking must not be able to hide a real difference. If `Date:` were
|
|
/// dropped rather than masked, a response missing it entirely would still pass.
|
|
#[test]
|
|
fn masking_does_not_hide_a_missing_header() {
|
|
let good = roster::roster_response(Method::Get, roster::ORACLE_SERVER, "X");
|
|
let without_date: Vec<u8> = String::from_utf8_lossy(&good)
|
|
.split("\r\n")
|
|
.filter(|l| !l.starts_with("Date: "))
|
|
.collect::<Vec<_>>()
|
|
.join("\r\n")
|
|
.into_bytes();
|
|
assert_ne!(
|
|
mask(&good),
|
|
mask(&without_date),
|
|
"masking collapsed a missing Date into a match"
|
|
);
|
|
}
|