diff --git a/openfut-utas-host/src/lib.rs b/openfut-utas-host/src/lib.rs index 5e004da..41e5782 100644 --- a/openfut-utas-host/src/lib.rs +++ b/openfut-utas-host/src/lib.rs @@ -237,7 +237,15 @@ pub fn classify(method: &str, path: &str) -> Route { Some("squad/list") if get => Route::SquadList, Some("squad/active") if get => Route::SquadActive, Some(tail) if get && is_numeric_squad_tail(tail) => Route::SquadActive, - Some("userMassInfo") if get => Route::UserMassInfo, + // The retail client sends BOTH casings: a lowercase `usermassinfo` + // followed immediately by the canonical `userMassInfo`. Matching the + // literal only meant the lowercase one fell through to the Python + // upstream — a 502 here, but on a deployment with Python alive it would + // be ANSWERED there, silently splitting authority away from Rust for a + // route Core owns. Matched case-insensitively for this tail only; the + // rest of the table stays exact, since no other route has shown a + // casing variant. + Some(t) if get && t.eq_ignore_ascii_case("usermassinfo") => Route::UserMassInfo, Some("user/club") if put || post => Route::ClubRename, Some(tail) if tail.starts_with("clientdata/") => Route::ClientData, Some(tail) if get && tail.starts_with("store/purchasegroup") => Route::StorePurchaseGroup, diff --git a/openfut-utas-host/tests/host_test.rs b/openfut-utas-host/tests/host_test.rs index cdefcd2..54fa3fb 100644 --- a/openfut-utas-host/tests/host_test.rs +++ b/openfut-utas-host/tests/host_test.rs @@ -1079,6 +1079,38 @@ fn classify_squad_and_usermassinfo_routes() { classify("GET", "/ut/game/fifa17/userMassInfo"), Route::UserMassInfo ); + // The retail client sends the lowercase tail too, immediately before the + // canonical one. It must reach the SAME Rust-owned handler: falling through + // to Python is a 502 here and, with Python alive, an authority split. + assert_eq!( + classify("GET", "/ut/game/fifa17/usermassinfo"), + Route::UserMassInfo, + "lowercase usermassinfo is a real retail request, observed twice" + ); + assert_eq!( + classify("GET", "/ut/game/fifa17/USERMASSINFO"), + Route::UserMassInfo + ); + // Adjacent tails must NOT be swept up by the case-insensitive arm. + assert_eq!( + classify("GET", "/ut/game/fifa17/usermassinfox"), + Route::Passthrough, + "the alias must match the whole tail, not a prefix" + ); + assert_eq!( + classify("GET", "/ut/game/fifa17/usermass"), + Route::Passthrough + ); + // Method semantics are unchanged: only GET is this route. + assert_eq!( + classify("PUT", "/ut/game/fifa17/usermassinfo"), + Route::Passthrough + ); + assert_eq!( + classify("POST", "/ut/game/fifa17/userMassInfo"), + Route::Passthrough + ); + // GET /squad/active and every numeric GET are the one Core-backed current // squad, matching the oracle's single-squad response regardless of URL id. assert_eq!(