feat(fifa17): own non-economy static + security-question routes in Rust
Migrate 5 non-economy UTAS route families from the Python oracle proxy to
Rust host ownership: user/accountinfo, settings, leaderboards/options,
match/reset, and phishing/{trusteddevice,question,validate}.
- adapter fut::non_economy: pure IO-free shapers matching the observed prod
oracle bodies + a verbatim port of security_question_route (stateless ack;
answer never stored/compared; trusted-device is an invariant constant).
- host: Route variants + classify() arms + owner=RUST dispatch; the
security-question X-UT-SID gate reuses SessionStore::session_known.
- tests: 9 adapter unit tests (contract) + host non_economy_route_ownership
(classify + classify_economy negatives).
This commit is contained in:
@@ -10,6 +10,7 @@ pub mod economy;
|
|||||||
pub mod economy_policy;
|
pub mod economy_policy;
|
||||||
pub mod entities;
|
pub mod entities;
|
||||||
pub mod item;
|
pub mod item;
|
||||||
|
pub mod non_economy;
|
||||||
pub mod owned_query;
|
pub mod owned_query;
|
||||||
pub mod pack_content;
|
pub mod pack_content;
|
||||||
pub mod squad;
|
pub mod squad;
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
//! FIFA17 non-economy presentation routes, migrated from the Python oracle.
|
||||||
|
//!
|
||||||
|
//! Pure, IO-free shapers that reproduce the **observed production** oracle
|
||||||
|
//! contract (`fifa17-recon/tools/utas_server.py`) for the non-economy UTAS
|
||||||
|
//! routes a Rust host can own without any Core or account state:
|
||||||
|
//!
|
||||||
|
//! * `GET …/user/accountinfo` → `{}` (FUT_ACCOUNTINFO off)
|
||||||
|
//! * `GET …/settings` → `{"configs":[]}` (FUT_SETTINGS off)
|
||||||
|
//! * `GET …/leaderboards/options` → `{}` (FUT_MODES off)
|
||||||
|
//! * `PUT …/match/reset` → `{}` (Tier-B no-op ack)
|
||||||
|
//! * `GET/POST/PUT …/phishing/{trusteddevice,question,validate}` — the retired
|
||||||
|
//! FUT security-question service, a stateless acknowledgement.
|
||||||
|
//!
|
||||||
|
//! These match the bodies the live prod oracle actually returns under the
|
||||||
|
//! production environment (`FUT_TRADING=1 FUT_PILESIZES=1 FUT_TRADEABLE=1`, none
|
||||||
|
//! of the feature flags set). They carry no persisted state: the security
|
||||||
|
//! record the oracle seeds (`{version:1,verified:true}`) is log-only — the
|
||||||
|
//! response is invariant — so a faithful Rust owner needs no persistence.
|
||||||
|
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
/// `GET …/user/accountinfo` — production oracle returns an empty object.
|
||||||
|
pub fn accountinfo_body() -> Value {
|
||||||
|
json!({})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `GET …/settings` — production oracle returns an empty config list.
|
||||||
|
pub fn settings_body() -> Value {
|
||||||
|
json!({ "configs": [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `GET …/leaderboards/options` — production oracle (FUT_MODES off) returns an
|
||||||
|
/// empty object; the retail client ignores the body.
|
||||||
|
pub fn leaderboard_options_body() -> Value {
|
||||||
|
json!({})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `PUT …/match/reset` — Tier-B no-op acknowledgement.
|
||||||
|
pub fn match_reset_body() -> Value {
|
||||||
|
json!({})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The phishing/security-question action, parsed from the URL tail.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SecurityAction {
|
||||||
|
TrustedDevice,
|
||||||
|
Question,
|
||||||
|
Validate,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `phishing/<action>` selector from a `…/phishing/<action>` path tail.
|
||||||
|
pub fn parse_security_action(tail: &str) -> SecurityAction {
|
||||||
|
let last = tail.trim_end_matches('/').rsplit('/').next().unwrap_or("");
|
||||||
|
match last {
|
||||||
|
"trusteddevice" => SecurityAction::TrustedDevice,
|
||||||
|
"question" => SecurityAction::Question,
|
||||||
|
"validate" => SecurityAction::Validate,
|
||||||
|
_ => SecurityAction::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A well-formed FUT phishing token is an opaque 32-char lowercase/uppercase hex
|
||||||
|
/// value (`_PHISHING_HEX32.fullmatch` in the oracle).
|
||||||
|
fn is_hex32(s: &str) -> bool {
|
||||||
|
s.len() == 32 && s.bytes().all(|b| b.is_ascii_hexdigit())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reproduce `utas_server.py::security_question_route` verbatim.
|
||||||
|
///
|
||||||
|
/// The retired FUT security-question service is a stateless acknowledgement: it
|
||||||
|
/// validates request shape (session, 32-hex device id, 32-hex answer, numeric
|
||||||
|
/// question) and returns fixed bodies. The answer is a client-transformed opaque
|
||||||
|
/// value that is **never stored or compared**; the trusted-device response is an
|
||||||
|
/// invariant `verified/trusted` constant.
|
||||||
|
///
|
||||||
|
/// * `session_known` — whether `X-UT-SID` maps to an open Rust session.
|
||||||
|
/// * `device_id` / `question` / `answer` — decoded query parameters (`""`/`None`
|
||||||
|
/// when absent).
|
||||||
|
///
|
||||||
|
/// Returns `(http_status, body)`.
|
||||||
|
pub fn security_question_response(
|
||||||
|
method: &str,
|
||||||
|
action: SecurityAction,
|
||||||
|
session_known: bool,
|
||||||
|
device_id: &str,
|
||||||
|
question: Option<&str>,
|
||||||
|
answer: Option<&str>,
|
||||||
|
) -> (u16, Value) {
|
||||||
|
let is = |m: &str| method.eq_ignore_ascii_case(m);
|
||||||
|
if !session_known {
|
||||||
|
return (400, json!({ "reason": "invalid_session" }));
|
||||||
|
}
|
||||||
|
if !is_hex32(device_id) {
|
||||||
|
return (400, json!({ "reason": "malformed_request" }));
|
||||||
|
}
|
||||||
|
match action {
|
||||||
|
SecurityAction::TrustedDevice => {
|
||||||
|
if !is("GET") {
|
||||||
|
return (405, json!({ "reason": "method_not_allowed" }));
|
||||||
|
}
|
||||||
|
(
|
||||||
|
200,
|
||||||
|
json!({ "changed": false, "exists": true, "locked": false, "trusted": true }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SecurityAction::Question if is("GET") => (
|
||||||
|
200,
|
||||||
|
json!({ "question": 0, "attempts": 5, "recoverAttempts": 0 }),
|
||||||
|
),
|
||||||
|
SecurityAction::Question if is("POST") || is("PUT") => {
|
||||||
|
let q = question.unwrap_or("");
|
||||||
|
let a = answer.unwrap_or("");
|
||||||
|
if q.is_empty() || !q.bytes().all(|b| b.is_ascii_digit()) || !is_hex32(a) {
|
||||||
|
return (400, json!({ "reason": "malformed_request" }));
|
||||||
|
}
|
||||||
|
(200, json!({}))
|
||||||
|
}
|
||||||
|
SecurityAction::Validate if is("POST") => {
|
||||||
|
let a = answer.unwrap_or("");
|
||||||
|
if !is_hex32(a) {
|
||||||
|
return (400, json!({ "reason": "malformed_request" }));
|
||||||
|
}
|
||||||
|
(200, json!({}))
|
||||||
|
}
|
||||||
|
_ => (405, json!({ "reason": "method_not_allowed" })),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const DEV: &str = "6236375476659cd0f6c780e728774b71"; // 32-hex (live deviceId)
|
||||||
|
const ANS: &str = "0123456789abcdef0123456789abcdef";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_bodies_match_oracle() {
|
||||||
|
assert_eq!(accountinfo_body(), json!({}));
|
||||||
|
assert_eq!(settings_body(), json!({ "configs": [] }));
|
||||||
|
assert_eq!(leaderboard_options_body(), json!({}));
|
||||||
|
assert_eq!(match_reset_body(), json!({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn action_parse() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_security_action("phishing/trusteddevice"),
|
||||||
|
SecurityAction::TrustedDevice
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_security_action("phishing/question/"),
|
||||||
|
SecurityAction::Question
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_security_action("phishing/validate"),
|
||||||
|
SecurityAction::Validate
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_security_action("phishing/other"),
|
||||||
|
SecurityAction::Unknown
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trusted_device_verified_constant() {
|
||||||
|
let (s, b) =
|
||||||
|
security_question_response("GET", SecurityAction::TrustedDevice, true, DEV, None, None);
|
||||||
|
assert_eq!(s, 200);
|
||||||
|
assert_eq!(
|
||||||
|
b,
|
||||||
|
json!({ "changed": false, "exists": true, "locked": false, "trusted": true })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_session_is_400_invalid_session() {
|
||||||
|
let (s, b) = security_question_response(
|
||||||
|
"GET",
|
||||||
|
SecurityAction::TrustedDevice,
|
||||||
|
false,
|
||||||
|
DEV,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(s, 400);
|
||||||
|
assert_eq!(b, json!({ "reason": "invalid_session" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_device_id_is_malformed() {
|
||||||
|
for bad in [
|
||||||
|
"",
|
||||||
|
"xyz",
|
||||||
|
"6236375476659cd0f6c780e728774b7",
|
||||||
|
"not-hex-not-hex-not-hex-not-hexx",
|
||||||
|
] {
|
||||||
|
let (s, b) = security_question_response(
|
||||||
|
"GET",
|
||||||
|
SecurityAction::TrustedDevice,
|
||||||
|
true,
|
||||||
|
bad,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(s, 400, "device {bad:?}");
|
||||||
|
assert_eq!(b, json!({ "reason": "malformed_request" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trusted_device_wrong_method_405() {
|
||||||
|
let (s, _) = security_question_response(
|
||||||
|
"POST",
|
||||||
|
SecurityAction::TrustedDevice,
|
||||||
|
true,
|
||||||
|
DEV,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(s, 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn question_get_returns_prompt() {
|
||||||
|
let (s, b) =
|
||||||
|
security_question_response("GET", SecurityAction::Question, true, DEV, None, None);
|
||||||
|
assert_eq!(s, 200);
|
||||||
|
assert_eq!(
|
||||||
|
b,
|
||||||
|
json!({ "question": 0, "attempts": 5, "recoverAttempts": 0 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn question_setup_validates_shape() {
|
||||||
|
// valid numeric question + 32-hex answer
|
||||||
|
let (s, b) = security_question_response(
|
||||||
|
"POST",
|
||||||
|
SecurityAction::Question,
|
||||||
|
true,
|
||||||
|
DEV,
|
||||||
|
Some("0"),
|
||||||
|
Some(ANS),
|
||||||
|
);
|
||||||
|
assert_eq!(s, 200);
|
||||||
|
assert_eq!(b, json!({}));
|
||||||
|
// empty question -> malformed
|
||||||
|
let (s, _) = security_question_response(
|
||||||
|
"POST",
|
||||||
|
SecurityAction::Question,
|
||||||
|
true,
|
||||||
|
DEV,
|
||||||
|
Some(""),
|
||||||
|
Some(ANS),
|
||||||
|
);
|
||||||
|
assert_eq!(s, 400);
|
||||||
|
// non-digit question -> malformed
|
||||||
|
let (s, _) = security_question_response(
|
||||||
|
"PUT",
|
||||||
|
SecurityAction::Question,
|
||||||
|
true,
|
||||||
|
DEV,
|
||||||
|
Some("x"),
|
||||||
|
Some(ANS),
|
||||||
|
);
|
||||||
|
assert_eq!(s, 400);
|
||||||
|
// bad answer -> malformed
|
||||||
|
let (s, _) = security_question_response(
|
||||||
|
"POST",
|
||||||
|
SecurityAction::Question,
|
||||||
|
true,
|
||||||
|
DEV,
|
||||||
|
Some("0"),
|
||||||
|
Some("short"),
|
||||||
|
);
|
||||||
|
assert_eq!(s, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_checks_answer() {
|
||||||
|
let (s, b) = security_question_response(
|
||||||
|
"POST",
|
||||||
|
SecurityAction::Validate,
|
||||||
|
true,
|
||||||
|
DEV,
|
||||||
|
None,
|
||||||
|
Some(ANS),
|
||||||
|
);
|
||||||
|
assert_eq!(s, 200);
|
||||||
|
assert_eq!(b, json!({}));
|
||||||
|
let (s, _) = security_question_response(
|
||||||
|
"POST",
|
||||||
|
SecurityAction::Validate,
|
||||||
|
true,
|
||||||
|
DEV,
|
||||||
|
None,
|
||||||
|
Some("nope"),
|
||||||
|
);
|
||||||
|
assert_eq!(s, 400);
|
||||||
|
// wrong method for validate
|
||||||
|
let (s, _) =
|
||||||
|
security_question_response("GET", SecurityAction::Validate, true, DEV, None, Some(ANS));
|
||||||
|
assert_eq!(s, 405);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ use openfut_adapter_fifa17::fut::economy_policy::{
|
|||||||
match_reward_total, result_from_end_reason, MatchResult,
|
match_reward_total, result_from_end_reason, MatchResult,
|
||||||
};
|
};
|
||||||
use openfut_adapter_fifa17::fut::entities::Fifa17Entities;
|
use openfut_adapter_fifa17::fut::entities::Fifa17Entities;
|
||||||
|
use openfut_adapter_fifa17::fut::non_economy;
|
||||||
use openfut_adapter_fifa17::fut::owned_query::{
|
use openfut_adapter_fifa17::fut::owned_query::{
|
||||||
is_special_rareflag, map_to_core, parse_club_query, MapError,
|
is_special_rareflag, map_to_core, parse_club_query, MapError,
|
||||||
};
|
};
|
||||||
@@ -101,6 +102,17 @@ pub enum Route {
|
|||||||
/// economy body, with the empty-My-Packs topology overlaid from the Rust
|
/// economy body, with the empty-My-Packs topology overlaid from the Rust
|
||||||
/// session mode (the 65534 sentinel is stripped for a verified clean-v1 SID).
|
/// session mode (the 65534 sentinel is stripped for a verified clean-v1 SID).
|
||||||
StorePurchaseGroup,
|
StorePurchaseGroup,
|
||||||
|
/// `GET …/user/accountinfo` — Rust-owned static `{}` (production oracle body).
|
||||||
|
AccountInfo,
|
||||||
|
/// `GET …/settings` — Rust-owned static `{"configs":[]}`.
|
||||||
|
Settings,
|
||||||
|
/// `GET …/leaderboards/options` — Rust-owned static `{}`.
|
||||||
|
LeaderboardOptions,
|
||||||
|
/// `PUT …/match/reset` — Rust-owned no-op ack `{}`.
|
||||||
|
MatchReset,
|
||||||
|
/// `GET/POST/PUT …/phishing/{trusteddevice,question,validate}` — the retired
|
||||||
|
/// FUT security-question service, owned in Rust as a stateless ack.
|
||||||
|
SecurityQuestion,
|
||||||
/// Anything else — proxied verbatim to the Python oracle.
|
/// Anything else — proxied verbatim to the Python oracle.
|
||||||
Passthrough,
|
Passthrough,
|
||||||
}
|
}
|
||||||
@@ -135,6 +147,11 @@ pub fn classify(method: &str, path: &str) -> Route {
|
|||||||
Some("userMassInfo") if get => Route::UserMassInfo,
|
Some("userMassInfo") if get => Route::UserMassInfo,
|
||||||
Some(tail) if get && tail.starts_with("store/purchasegroup") => Route::StorePurchaseGroup,
|
Some(tail) if get && tail.starts_with("store/purchasegroup") => Route::StorePurchaseGroup,
|
||||||
Some(tail) if put && is_numeric_squad_tail(tail) => Route::SquadReplace,
|
Some(tail) if put && is_numeric_squad_tail(tail) => Route::SquadReplace,
|
||||||
|
Some("user/accountinfo") if get => Route::AccountInfo,
|
||||||
|
Some("settings") if get => Route::Settings,
|
||||||
|
Some("leaderboards/options") if get => Route::LeaderboardOptions,
|
||||||
|
Some("match/reset") if put => Route::MatchReset,
|
||||||
|
Some(tail) if tail.starts_with("phishing/") => Route::SecurityQuestion,
|
||||||
_ => Route::Passthrough,
|
_ => Route::Passthrough,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2223,6 +2240,23 @@ impl Server {
|
|||||||
Route::StorePurchaseGroup => {
|
Route::StorePurchaseGroup => {
|
||||||
self.handle_store_purchasegroup(method, target, headers, body, client_ip)
|
self.handle_store_purchasegroup(method, target, headers, body, client_ip)
|
||||||
}
|
}
|
||||||
|
Route::AccountInfo => {
|
||||||
|
eprintln!("utas-host owner=RUST route=accountinfo status=200");
|
||||||
|
json_status(200, &non_economy::accountinfo_body())
|
||||||
|
}
|
||||||
|
Route::Settings => {
|
||||||
|
eprintln!("utas-host owner=RUST route=settings status=200");
|
||||||
|
json_status(200, &non_economy::settings_body())
|
||||||
|
}
|
||||||
|
Route::LeaderboardOptions => {
|
||||||
|
eprintln!("utas-host owner=RUST route=leaderboards-options status=200");
|
||||||
|
json_status(200, &non_economy::leaderboard_options_body())
|
||||||
|
}
|
||||||
|
Route::MatchReset => {
|
||||||
|
eprintln!("utas-host owner=RUST route=match-reset status=200");
|
||||||
|
json_status(200, &non_economy::match_reset_body())
|
||||||
|
}
|
||||||
|
Route::SecurityQuestion => self.handle_security_question(method, target, headers),
|
||||||
Route::Passthrough => {
|
Route::Passthrough => {
|
||||||
let resp = match self.pass.forward(method, target, headers, body) {
|
let resp = match self.pass.forward(method, target, headers, body) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@@ -2361,6 +2395,51 @@ impl Server {
|
|||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `GET/POST/PUT …/phishing/{trusteddevice,question,validate}` — the retired
|
||||||
|
/// FUT security-question service, owned entirely in Rust (no proxy, no Core).
|
||||||
|
/// Stateless: the client-transformed answer is never stored or compared, and
|
||||||
|
/// the trusted-device response is an invariant verified/trusted constant. The
|
||||||
|
/// `X-UT-SID` session gate mirrors the oracle — an unknown session is a 400
|
||||||
|
/// `invalid_session`; a malformed 32-hex device id/answer is `malformed_request`.
|
||||||
|
fn handle_security_question(
|
||||||
|
&self,
|
||||||
|
method: &str,
|
||||||
|
target: &str,
|
||||||
|
headers: &[(String, String)],
|
||||||
|
) -> WireResponse {
|
||||||
|
fn query_param(query: &str, key: &str) -> Option<String> {
|
||||||
|
query.split('&').find_map(|kv| {
|
||||||
|
let (k, v) = kv.split_once('=')?;
|
||||||
|
(k == key).then(|| v.to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let path = target.split('?').next().unwrap_or(target);
|
||||||
|
let query = target.split_once('?').map(|(_, q)| q).unwrap_or("");
|
||||||
|
let tail = ut_tail(path).unwrap_or("");
|
||||||
|
let action = non_economy::parse_security_action(tail);
|
||||||
|
let sid = header(headers, "x-ut-sid").unwrap_or("");
|
||||||
|
let known = self.sessions.lock().unwrap().session_known(sid);
|
||||||
|
let device_id = query_param(query, "deviceId").unwrap_or_default();
|
||||||
|
let question = query_param(query, "question");
|
||||||
|
let answer = query_param(query, "answer");
|
||||||
|
let (status, body) = non_economy::security_question_response(
|
||||||
|
method,
|
||||||
|
action,
|
||||||
|
known,
|
||||||
|
&device_id,
|
||||||
|
question.as_deref(),
|
||||||
|
answer.as_deref(),
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"utas-host owner=RUST route=security-question status={} action={:?} sid={} known={}",
|
||||||
|
status,
|
||||||
|
action,
|
||||||
|
fifa17_sidlog(sid),
|
||||||
|
known
|
||||||
|
);
|
||||||
|
json_status(status, &body)
|
||||||
|
}
|
||||||
|
|
||||||
/// Serve forever on `addr` (thread-per-connection, HTTP/1.1 keep-alive).
|
/// Serve forever on `addr` (thread-per-connection, HTTP/1.1 keep-alive).
|
||||||
pub fn serve(&self, addr: &str) -> std::io::Result<()> {
|
pub fn serve(&self, addr: &str) -> std::io::Result<()> {
|
||||||
let listener = TcpListener::bind(addr)?;
|
let listener = TcpListener::bind(addr)?;
|
||||||
@@ -3358,4 +3437,60 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_economy_route_ownership() {
|
||||||
|
// The non-economy routes newly owned by Rust must classify to their arm,
|
||||||
|
// and lookalikes must stay Passthrough (proxied to Python).
|
||||||
|
let owned: &[(&str, &str, Route)] = &[
|
||||||
|
(
|
||||||
|
"GET",
|
||||||
|
"/ut/game/fifa17/user/accountinfo",
|
||||||
|
Route::AccountInfo,
|
||||||
|
),
|
||||||
|
("GET", "/ut/game/fifa17/settings", Route::Settings),
|
||||||
|
(
|
||||||
|
"GET",
|
||||||
|
"/ut/game/fifa17/leaderboards/options",
|
||||||
|
Route::LeaderboardOptions,
|
||||||
|
),
|
||||||
|
("PUT", "/ut/game/fifa17/match/reset", Route::MatchReset),
|
||||||
|
(
|
||||||
|
"GET",
|
||||||
|
"/ut/game/fifa17/phishing/trusteddevice",
|
||||||
|
Route::SecurityQuestion,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"POST",
|
||||||
|
"/ut/game/fifa17/phishing/question",
|
||||||
|
Route::SecurityQuestion,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"POST",
|
||||||
|
"/ut/game/fifa17/phishing/validate",
|
||||||
|
Route::SecurityQuestion,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (m, p, want) in owned {
|
||||||
|
assert_eq!(classify(m, p), *want, "OWN: {m} {p}");
|
||||||
|
}
|
||||||
|
// Still Python (not yet migrated) / lookalikes / wrong method.
|
||||||
|
let proxied: &[(&str, &str)] = &[
|
||||||
|
("GET", "/ut/game/fifa17/hub"),
|
||||||
|
("GET", "/ut/game/fifa17/club/stats/year"),
|
||||||
|
("PUT", "/ut/game/fifa17/clientdata/userHubData"),
|
||||||
|
("POST", "/openfut/account/sync"),
|
||||||
|
("GET", "/ut/game/fifa17/settingsfoo"),
|
||||||
|
("POST", "/ut/game/fifa17/match/reset"), // match/reset is PUT-only
|
||||||
|
("GET", "/ut/game/fifa17/match/reset"),
|
||||||
|
("PUT", "/ut/game/fifa17/user/accountinfo"),
|
||||||
|
];
|
||||||
|
for (m, p) in proxied {
|
||||||
|
assert_eq!(classify(m, p), Route::Passthrough, "PROXY: {m} {p}");
|
||||||
|
}
|
||||||
|
// These non-economy routes must NOT be economy-classified.
|
||||||
|
for (m, p, _) in owned {
|
||||||
|
assert_eq!(classify_economy(m, p), None, "NON-ECON: {m} {p}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user