diff --git a/openfut-utas-host/src/lib.rs b/openfut-utas-host/src/lib.rs index c72278b..8d47a97 100644 --- a/openfut-utas-host/src/lib.rs +++ b/openfut-utas-host/src/lib.rs @@ -292,8 +292,32 @@ pub fn classify(method: &str, path: &str) -> Route { Some("champion") if get => Route::FeatureOffEmpty, Some("clubUser") if get => Route::FeatureOffEmpty, Some("user/list") if get => Route::FeatureOffEmpty, - Some("item/resource") if get => Route::ItemDefs, - Some("defid") if get => Route::ItemDefs, + Some(t) if get && (t == "item/resource" || t.starts_with("item/resource?")) => { + Route::ItemDefs + } + Some(t) if get && (t == "defid" || t.starts_with("defid?")) => Route::ItemDefs, + // `GET ut/%s/item` is FutViewCards (deser `0x1801293d0`, top-level + // `itemData` via the shared card element `0x18013fe00`). The oracle + // answers it with `defs_route` — the SAME handler as `item/resource` and + // `defid`: parse every integer out of the query (`idList=a,b,c`, + // `definitionId=`, `resourceId=`) and return one definition per id, or + // `{"itemData": []}` when the query carries none + // (`tools/utas_server.py:1103`). Claiming it is byte-identical parity. + // + // It was unclaimed, so it fell through to the Python upstream. That is + // invisible in production, where the oracle answers, and shows up only on + // staging as a 502 — the same dead-route class already found four times + // (season/list, watchList, the handle_static_ack tails, tournament/user). + // + // The `?` forms are not decoration: `ut_tail` does NOT strip the query, so + // a bare equality arm silently misses every real request, which is exactly + // how this route stayed unclaimed. The oracle's own pattern is + // `item(\?|$)` (`tools/utas_server.py:1419`) and this mirrors it. + // + // MUST stay below `item/resource` (matched first) and must not swallow + // `item/`, which is DELETE-only Quick Sell, nor PUT `item`, which is + // FutMoveCard on the economy path. + Some(t) if get && (t == "item" || t.starts_with("item?")) => Route::ItemDefs, Some(t) if get && (t == "marketdata" || t.starts_with("marketdata/")) => Route::MarketData, _ => Route::Passthrough, } @@ -7045,6 +7069,52 @@ mod tests { Route::Passthrough ); } + /// `GET ut/%s/item` (FutViewCards) MUST be claimed, and claiming it must not + /// disturb the three other verbs that share the `item` prefix. + /// + /// This route was unclaimed and fell through to the Python upstream, which is + /// invisible in production (the oracle answers) and appears only on staging as + /// a 502. That is the recurring dead-route defect in this project, so the + /// point of this test is that a handler existing is not the same as a request + /// reaching it. + #[test] + fn get_item_is_claimed_and_the_other_item_verbs_are_unaffected() { + // FutViewCards, with and without the definition query the client builds. + assert_eq!(classify("GET", "/ut/game/fifa17/item"), Route::ItemDefs); + assert_eq!( + classify("GET", "/ut/game/fifa17/item?idList=100000003,100000004"), + Route::ItemDefs + ); + // v2 sku form resolves identically. + assert_eq!(classify("GET", "/ut/v2/game/fifa17/item"), Route::ItemDefs); + // The more specific definition routes still win, and — the bug this test + // exists for — they must survive a query string too. `ut_tail` does not + // strip the query, so an equality-only arm misses every real request while + // looking correct in a no-query unit test. + assert_eq!( + classify("GET", "/ut/game/fifa17/item/resource"), + Route::ItemDefs + ); + assert_eq!( + classify("GET", "/ut/game/fifa17/item/resource?resourceId=5003012"), + Route::ItemDefs + ); + assert_eq!( + classify("GET", "/ut/game/fifa17/defid?definitionId=200389"), + Route::ItemDefs + ); + // PUT `item` is FutMoveCard on the economy path, NOT a definition read. + assert_eq!(classify("PUT", "/ut/game/fifa17/item"), Route::Passthrough); + assert_eq!( + classify_economy("PUT", "/ut/game/fifa17/item"), + Some(EconomyRoute::MoveItems) + ); + // DELETE `item/` is single-card Quick Sell and must not be swallowed. + assert_eq!( + classify_economy("DELETE", "/ut/game/fifa17/item/100000240"), + Some(EconomyRoute::QuickSellPath) + ); + } /// Retail FIFA 17 issues part of the item family under `/ut/v2/game/`, so the /// same three verbs must land identically under both prefixes: `ut_tail`