wip(bridge): retained FIFA request/response mapper + docker packaging
RETAINED PRE-EXISTING WIP (brought forward after verification, not authored
here, not production-ready). Substantive mapper.rs proxy expansion plus a
multi-stage Dockerfile/.dockerignore (rustls, self-signed cert at startup).
No secrets/captures staged. Preserved off the detached base c58e7326a1.
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
target/
|
||||||
|
**/target/
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
captures/
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# OpenFUT Bridge — Claude Code project context
|
# OpenFUT Bridge — Claude Code project context
|
||||||
|
|
||||||
|
> ⚠️ **Legacy / historical (FIFA 23).** OpenFUT's working target is now **FIFA 17**; this bridge (FIFA 23 integration) is superseded per `../docs/PROJECT_STATE.md`. Kept for reference. Canonical server: `../fifa17-recon/docker/fifa17-python` (`docker compose up -d`).
|
||||||
|
|
||||||
Read this first. It's the standing context for every task in this project. Each
|
Read this first. It's the standing context for every task in this project. Each
|
||||||
working session will give you ONE bounded task plus a verification clause; this
|
working session will give you ONE bounded task plus a verification clause; this
|
||||||
file is the background that stays true across all of them.
|
file is the background that stays true across all of them.
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
authors = ["OpenFUT Contributors"]
|
authors = ["OpenFUT Contributors"]
|
||||||
description = "FIFA 23 integration layer and reverse-engineering proxy"
|
description = "FIFA 23 integration layer and reverse-engineering proxy"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/openfut/openfut-bridge"
|
repository = "https://git.aleshym.co/funman300/OpenFUT-Bridge.git"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "openfut_bridge"
|
name = "openfut_bridge"
|
||||||
|
|||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
# ---- OpenFUT Bridge: FIFA integration / reverse-engineering proxy ----
|
||||||
|
# Multi-stage build; slim Debian runtime. reqwest uses rustls-tls and the
|
||||||
|
# bridge self-signs its own cert at startup (rcgen) — no OpenSSL needed.
|
||||||
|
|
||||||
|
FROM rust:1-bookworm AS builder
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY Cargo.toml Cargo.lock* ./
|
||||||
|
RUN mkdir -p src src/bin \
|
||||||
|
&& echo 'fn main() {}' > src/main.rs \
|
||||||
|
&& echo '' > src/lib.rs \
|
||||||
|
&& echo 'fn main() {}' > src/bin/replay.rs \
|
||||||
|
&& cargo build --release --bin openfut-bridge 2>/dev/null || true
|
||||||
|
RUN rm -rf src
|
||||||
|
|
||||||
|
COPY src ./src
|
||||||
|
RUN touch src/main.rs src/lib.rs \
|
||||||
|
&& cargo build --release --bin openfut-bridge
|
||||||
|
|
||||||
|
# ---- Runtime ----
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN useradd --system --uid 10002 --create-home --home-dir /app openfut
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /build/target/release/openfut-bridge /usr/local/bin/openfut-bridge
|
||||||
|
|
||||||
|
# Captures are written at runtime — keep them on a named volume.
|
||||||
|
RUN mkdir -p /app/captures && chown openfut:openfut /app/captures
|
||||||
|
|
||||||
|
USER openfut
|
||||||
|
|
||||||
|
ENV BRIDGE_LISTEN_ADDR=0.0.0.0:8443 \
|
||||||
|
CORE_URL=http://core:8080 \
|
||||||
|
CAPTURES_DIR=/app/captures \
|
||||||
|
PLACEHOLDER_MODE=true \
|
||||||
|
TLS_ENABLED=true \
|
||||||
|
RUST_LOG=openfut_bridge=info,tower_http=info
|
||||||
|
|
||||||
|
EXPOSE 8443
|
||||||
|
VOLUME ["/app/captures"]
|
||||||
|
|
||||||
|
# The bridge serves TLS; -k because the cert is self-signed. /_bridge/health
|
||||||
|
# is the bridge's own admin endpoint (not a proxied FIFA route).
|
||||||
|
HEALTHCHECK --interval=15s --timeout=4s --start-period=8s --retries=5 \
|
||||||
|
CMD curl -fsSk https://127.0.0.1:8443/_bridge/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["openfut-bridge"]
|
||||||
+8
-2
@@ -59,9 +59,15 @@ async fn main() -> ExitCode {
|
|||||||
for capture in &captures {
|
for capture in &captures {
|
||||||
let result = replay_one(&client, bridge_url, capture).await;
|
let result = replay_one(&client, bridge_url, capture).await;
|
||||||
match result {
|
match result {
|
||||||
Ok(status) => println!(" [{}] {} {} → {status}", capture.id, capture.method, capture.path),
|
Ok(status) => println!(
|
||||||
|
" [{}] {} {} → {status}",
|
||||||
|
capture.id, capture.method, capture.path
|
||||||
|
),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(" [{}] {} {} → ERROR: {e}", capture.id, capture.method, capture.path);
|
eprintln!(
|
||||||
|
" [{}] {} {} → ERROR: {e}",
|
||||||
|
capture.id, capture.method, capture.path
|
||||||
|
);
|
||||||
failures += 1;
|
failures += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-6
@@ -102,7 +102,7 @@ async fn serve_tls(
|
|||||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (tcp, _peer) = listener.accept().await?;
|
let (tcp, peer) = listener.accept().await?;
|
||||||
let acceptor = acceptor.clone();
|
let acceptor = acceptor.clone();
|
||||||
let app = app.clone();
|
let app = app.clone();
|
||||||
|
|
||||||
@@ -110,18 +110,21 @@ async fn serve_tls(
|
|||||||
let tls_stream = match acceptor.accept(tcp).await {
|
let tls_stream = match acceptor.accept(tcp).await {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("TLS handshake failed: {e}");
|
if e.kind() == std::io::ErrorKind::UnexpectedEof {
|
||||||
|
tracing::debug!(%peer, "TCP probe closed before ClientHello");
|
||||||
|
} else {
|
||||||
|
tracing::warn!(%peer, "TLS handshake failed: {e}");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let io = TokioIo::new(tls_stream);
|
let io = TokioIo::new(tls_stream);
|
||||||
let svc = hyper::service::service_fn(
|
let svc =
|
||||||
move |req: hyper::Request<hyper::body::Incoming>| {
|
hyper::service::service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
|
||||||
let app = app.clone();
|
let app = app.clone();
|
||||||
async move { app.oneshot(req.map(axum::body::Body::new)).await }
|
async move { app.oneshot(req.map(axum::body::Body::new)).await }
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if let Err(e) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
|
if let Err(e) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
|
||||||
.serve_connection(io, svc)
|
.serve_connection(io, svc)
|
||||||
|
|||||||
+269
-133
@@ -22,363 +22,495 @@ struct ExactRoute {
|
|||||||
const EXACT: &[ExactRoute] = &[
|
const EXACT: &[ExactRoute] = &[
|
||||||
// ── Auth ─────────────────────────────────────────────────────────────────
|
// ── Auth ─────────────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/auth",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/auth/local",
|
ea_path: "/ut/auth",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/auth/local",
|
||||||
notes: "FUT login → Core local auth",
|
notes: "FUT login → Core local auth",
|
||||||
},
|
},
|
||||||
// ── Profile / Settings ───────────────────────────────────────────────────
|
// ── Profile / Settings ───────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/user/settings",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/profile",
|
ea_path: "/ut/game/fut/user/settings",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/profile",
|
||||||
notes: "FUT user settings → Core profile",
|
notes: "FUT user settings → Core profile",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "PUT", ea_path: "/ut/game/fut/user/settings",
|
ea_method: "PUT",
|
||||||
core_method: "PUT", core_path: "/settings",
|
ea_path: "/ut/game/fut/user/settings",
|
||||||
|
core_method: "PUT",
|
||||||
|
core_path: "/settings",
|
||||||
notes: "FUT update settings → Core settings",
|
notes: "FUT update settings → Core settings",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/user/accountinfo",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/profile",
|
ea_path: "/ut/game/fut/user/accountinfo",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/profile",
|
||||||
notes: "FUT account info → Core profile",
|
notes: "FUT account info → Core profile",
|
||||||
},
|
},
|
||||||
// ── Club / Mass info ──────────────────────────────────────────────────────
|
// ── Club / Mass info ──────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/usermassinfo",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/club",
|
ea_path: "/ut/game/fut/usermassinfo",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/club",
|
||||||
notes: "FUT mass info (club + coins) → Core club",
|
notes: "FUT mass info (club + coins) → Core club",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/club",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/club",
|
ea_path: "/ut/game/fut/club",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/club",
|
||||||
notes: "FUT club info → Core club",
|
notes: "FUT club info → Core club",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "PUT", ea_path: "/ut/game/fut/club",
|
ea_method: "PUT",
|
||||||
core_method: "PUT", core_path: "/club",
|
ea_path: "/ut/game/fut/club",
|
||||||
|
core_method: "PUT",
|
||||||
|
core_path: "/club",
|
||||||
notes: "FUT update club → Core update club",
|
notes: "FUT update club → Core update club",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/club/stats",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/statistics",
|
ea_path: "/ut/game/fut/club/stats",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/statistics",
|
||||||
notes: "FUT club stats → Core statistics",
|
notes: "FUT club stats → Core statistics",
|
||||||
},
|
},
|
||||||
// ── Cards / Collection ────────────────────────────────────────────────────
|
// ── Cards / Collection ────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/item",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/collection",
|
ea_path: "/ut/game/fut/item",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/collection",
|
||||||
notes: "FUT collection → Core owned cards",
|
notes: "FUT collection → Core owned cards",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/item/search",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/cards",
|
ea_path: "/ut/game/fut/item/search",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/cards",
|
||||||
notes: "FUT item search → Core card catalogue (query params forwarded)",
|
notes: "FUT item search → Core card catalogue (query params forwarded)",
|
||||||
},
|
},
|
||||||
// ── Squad ─────────────────────────────────────────────────────────────────
|
// ── Squad ─────────────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/squad/active",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/squad",
|
ea_path: "/ut/game/fut/squad/active",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/squad",
|
||||||
notes: "FUT active squad → Core squad",
|
notes: "FUT active squad → Core squad",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/squad/0",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/squad",
|
ea_path: "/ut/game/fut/squad/0",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/squad",
|
||||||
notes: "FUT squad by slot 0 → Core squad (first squad)",
|
notes: "FUT squad by slot 0 → Core squad (first squad)",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "PUT", ea_path: "/ut/game/fut/squad/active",
|
ea_method: "PUT",
|
||||||
core_method: "POST", core_path: "/squad",
|
ea_path: "/ut/game/fut/squad/active",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/squad",
|
||||||
notes: "FUT save squad → Core save squad",
|
notes: "FUT save squad → Core save squad",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/squad/chemistry",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/squad",
|
ea_path: "/ut/game/fut/squad/chemistry",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/squad",
|
||||||
notes: "FUT squad chemistry → Core squad (chemistry included in response)",
|
notes: "FUT squad chemistry → Core squad (chemistry included in response)",
|
||||||
},
|
},
|
||||||
// ── Packs ─────────────────────────────────────────────────────────────────
|
// ── Packs ─────────────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/store/packdetails",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/packs",
|
ea_path: "/ut/game/fut/store/packdetails",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/packs",
|
||||||
notes: "FUT pack store → Core pack list",
|
notes: "FUT pack store → Core pack list",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/store/purchase",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/packs/buy",
|
ea_path: "/ut/game/fut/store/purchase",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/packs/buy",
|
||||||
notes: "FUT pack purchase → Core pack buy",
|
notes: "FUT pack purchase → Core pack buy",
|
||||||
},
|
},
|
||||||
// ── Transfer Market ───────────────────────────────────────────────────────
|
// ── Transfer Market ───────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/transfermarket",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/market",
|
ea_path: "/ut/game/fut/transfermarket",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/market",
|
||||||
notes: "FUT transfer market search → Core NPC market",
|
notes: "FUT transfer market search → Core NPC market",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/trade/bid",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/market/buy",
|
ea_path: "/ut/game/fut/trade/bid",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/market/buy",
|
||||||
notes: "FUT bid/buy now → Core market buy",
|
notes: "FUT bid/buy now → Core market buy",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/trade/watchlist",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/market",
|
ea_path: "/ut/game/fut/trade/watchlist",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/market",
|
||||||
notes: "FUT watchlist → Core market (approximation)",
|
notes: "FUT watchlist → Core market (approximation)",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/trade/tradepile",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/market/my-listings",
|
ea_path: "/ut/game/fut/trade/tradepile",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/market/my-listings",
|
||||||
notes: "FUT trade pile (my listings) → Core my-listings",
|
notes: "FUT trade pile (my listings) → Core my-listings",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/auctionhouse",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/market/sell",
|
ea_path: "/ut/game/fut/auctionhouse",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/market/sell",
|
||||||
notes: "FUT list card on AH → Core sell card",
|
notes: "FUT list card on AH → Core sell card",
|
||||||
},
|
},
|
||||||
// ── Objectives ────────────────────────────────────────────────────────────
|
// ── Objectives ────────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/objectives",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/objectives",
|
ea_path: "/ut/game/fut/objectives",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/objectives",
|
||||||
notes: "FUT objectives → Core objectives list",
|
notes: "FUT objectives → Core objectives list",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/objectives/claim",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/objectives/claim",
|
ea_path: "/ut/game/fut/objectives/claim",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/objectives/claim",
|
||||||
notes: "FUT claim objective → Core claim",
|
notes: "FUT claim objective → Core claim",
|
||||||
},
|
},
|
||||||
// ── Events ────────────────────────────────────────────────────────────────
|
// ── Events ────────────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/events",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/events",
|
ea_path: "/ut/game/fut/events",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/events",
|
||||||
notes: "FUT events → Core events list",
|
notes: "FUT events → Core events list",
|
||||||
},
|
},
|
||||||
// ── Matches / Squad Battles ───────────────────────────────────────────────
|
// ── Matches / Squad Battles ───────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/squadbattle/opponent",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/matches/opponent",
|
ea_path: "/ut/game/fut/squadbattle/opponent",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/matches/opponent",
|
||||||
notes: "Squad battles opponent → Core match opponent generator",
|
notes: "Squad battles opponent → Core match opponent generator",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/result",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/matches/result",
|
ea_path: "/ut/game/fut/result",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/matches/result",
|
||||||
notes: "FUT match result submit → Core match result",
|
notes: "FUT match result submit → Core match result",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/matches",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/matches",
|
ea_path: "/ut/game/fut/matches",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/matches",
|
||||||
notes: "FUT match history → Core match list",
|
notes: "FUT match history → Core match list",
|
||||||
},
|
},
|
||||||
// ── SBC ──────────────────────────────────────────────────────────────────
|
// ── SBC ──────────────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/sbc",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/sbc",
|
ea_path: "/ut/game/fut/sbc",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/sbc",
|
||||||
notes: "FUT SBC list → Core SBC list",
|
notes: "FUT SBC list → Core SBC list",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/sbc/challenges",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/sbc",
|
ea_path: "/ut/game/fut/sbc/challenges",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/sbc",
|
||||||
notes: "FUT SBC challenges → Core SBC list",
|
notes: "FUT SBC challenges → Core SBC list",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/sbc/submission",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/sbc/submit",
|
ea_path: "/ut/game/fut/sbc/submission",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/sbc/submit",
|
||||||
notes: "FUT SBC submission → Core SBC submit",
|
notes: "FUT SBC submission → Core SBC submit",
|
||||||
},
|
},
|
||||||
// ── Draft ─────────────────────────────────────────────────────────────────
|
// ── Draft ─────────────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/draft",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/draft/squad",
|
ea_path: "/ut/game/fut/draft",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/draft/squad",
|
||||||
notes: "FUT draft view → Core draft squad",
|
notes: "FUT draft view → Core draft squad",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/draft/new",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/draft/start",
|
ea_path: "/ut/game/fut/draft/new",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/draft/start",
|
||||||
notes: "FUT new draft → Core draft start",
|
notes: "FUT new draft → Core draft start",
|
||||||
},
|
},
|
||||||
// ── Division / Season ─────────────────────────────────────────────────────
|
// ── Division / Season ─────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/division/rivals",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/division",
|
ea_path: "/ut/game/fut/division/rivals",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/division",
|
||||||
notes: "FUT Division Rivals status → Core division",
|
notes: "FUT Division Rivals status → Core division",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/division/rivals/claim",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/rivals/claim-weekly",
|
ea_path: "/ut/game/fut/division/rivals/claim",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/rivals/claim-weekly",
|
||||||
notes: "FUT rivals weekly claim → Core rivals claim",
|
notes: "FUT rivals weekly claim → Core rivals claim",
|
||||||
},
|
},
|
||||||
// ── FUT Champions ─────────────────────────────────────────────────────────
|
// ── FUT Champions ─────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/champs",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/fut-champs",
|
ea_path: "/ut/game/fut/champs",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/fut-champs",
|
||||||
notes: "FUT Champions status → Core fut-champs",
|
notes: "FUT Champions status → Core fut-champs",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/champs/start",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/fut-champs/start",
|
ea_path: "/ut/game/fut/champs/start",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/fut-champs/start",
|
||||||
notes: "FUT Champions start week → Core fut-champs start",
|
notes: "FUT Champions start week → Core fut-champs start",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/champs/history",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/fut-champs/history",
|
ea_path: "/ut/game/fut/champs/history",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/fut-champs/history",
|
||||||
notes: "FUT Champions history → Core history",
|
notes: "FUT Champions history → Core history",
|
||||||
},
|
},
|
||||||
// ── Card Upgrades ─────────────────────────────────────────────────────────
|
// ── Card Upgrades ─────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/chemistry",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/chemistry-styles",
|
ea_path: "/ut/game/fut/chemistry",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/chemistry-styles",
|
||||||
notes: "FUT chemistry styles → Core chemistry styles list",
|
notes: "FUT chemistry styles → Core chemistry styles list",
|
||||||
},
|
},
|
||||||
// ── Notifications ─────────────────────────────────────────────────────────
|
// ── Notifications ─────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/notification",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/notifications",
|
ea_path: "/ut/game/fut/notification",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/notifications",
|
||||||
notes: "FUT notifications → Core notifications",
|
notes: "FUT notifications → Core notifications",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/notifications",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/notifications",
|
ea_path: "/ut/game/fut/notifications",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/notifications",
|
||||||
notes: "FUT notifications (plural form) → Core notifications",
|
notes: "FUT notifications (plural form) → Core notifications",
|
||||||
},
|
},
|
||||||
// ── Squad list ────────────────────────────────────────────────────────────
|
// ── Squad list ────────────────────────────────────────────────────────────
|
||||||
// ── Division / Season history / Leaderboard ───────────────────────────────
|
// ── Division / Season history / Leaderboard ───────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/division/history",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/division/history",
|
ea_path: "/ut/game/fut/division/history",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/division/history",
|
||||||
notes: "FUT division history → Core season history",
|
notes: "FUT division history → Core season history",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/division/leaderboard",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/division/leaderboard",
|
ea_path: "/ut/game/fut/division/leaderboard",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/division/leaderboard",
|
||||||
notes: "FUT division leaderboard → Core seeded NPC leaderboard",
|
notes: "FUT division leaderboard → Core seeded NPC leaderboard",
|
||||||
},
|
},
|
||||||
// ── Market trade history ──────────────────────────────────────────────────
|
// ── Market trade history ──────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/trade/history",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/market/trade-history",
|
ea_path: "/ut/game/fut/trade/history",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/market/trade-history",
|
||||||
notes: "FUT trade history → Core market trade history",
|
notes: "FUT trade history → Core market trade history",
|
||||||
},
|
},
|
||||||
// ── Daily check-in ────────────────────────────────────────────────────────
|
// ── Daily check-in ────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/dailyObjective",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/club/checkin",
|
ea_path: "/ut/game/fut/dailyObjective",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/club/checkin",
|
||||||
notes: "FUT daily objective status → Core check-in status",
|
notes: "FUT daily objective status → Core check-in status",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/dailyObjective/claim",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/club/checkin",
|
ea_path: "/ut/game/fut/dailyObjective/claim",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/club/checkin",
|
||||||
notes: "FUT daily objective claim → Core check-in claim",
|
notes: "FUT daily objective claim → Core check-in claim",
|
||||||
},
|
},
|
||||||
// ── Club milestones ───────────────────────────────────────────────────────
|
// ── Club milestones ───────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/milestones",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/club/milestones",
|
ea_path: "/ut/game/fut/milestones",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/club/milestones",
|
||||||
notes: "FUT milestones → Core club milestones",
|
notes: "FUT milestones → Core club milestones",
|
||||||
},
|
},
|
||||||
// ── Squad list ────────────────────────────────────────────────────────────
|
// ── Squad list ────────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/squad/list",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/squads",
|
ea_path: "/ut/game/fut/squad/list",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/squads",
|
||||||
notes: "FUT squad list → Core all squads",
|
notes: "FUT squad list → Core all squads",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "DELETE", ea_path: "/ut/game/fut/squad/active",
|
ea_method: "DELETE",
|
||||||
core_method: "DELETE", core_path: "/squad",
|
ea_path: "/ut/game/fut/squad/active",
|
||||||
|
core_method: "DELETE",
|
||||||
|
core_path: "/squad",
|
||||||
notes: "FUT delete active squad → Core delete squad (best-effort)",
|
notes: "FUT delete active squad → Core delete squad (best-effort)",
|
||||||
},
|
},
|
||||||
// ── Achievements / Trophies ───────────────────────────────────────────────
|
// ── Achievements / Trophies ───────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/trophies",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/achievements",
|
ea_path: "/ut/game/fut/trophies",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/achievements",
|
||||||
notes: "FUT trophies → Core achievements",
|
notes: "FUT trophies → Core achievements",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/trophy",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/achievements",
|
ea_path: "/ut/game/fut/trophy",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/achievements",
|
||||||
notes: "FUT trophy (singular) → Core achievements",
|
notes: "FUT trophy (singular) → Core achievements",
|
||||||
},
|
},
|
||||||
// ── Rivals extra endpoints ────────────────────────────────────────────────
|
// ── Rivals extra endpoints ────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/rivals/rank",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/division",
|
ea_path: "/ut/game/fut/rivals/rank",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/division",
|
||||||
notes: "FUT rivals rank → Core division",
|
notes: "FUT rivals rank → Core division",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/rivals/result",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/matches/result",
|
ea_path: "/ut/game/fut/rivals/result",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/matches/result",
|
||||||
notes: "FUT rivals match result → Core match result",
|
notes: "FUT rivals match result → Core match result",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/rivals/leaderboard",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/statistics",
|
ea_path: "/ut/game/fut/rivals/leaderboard",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/statistics",
|
||||||
notes: "FUT rivals leaderboard → Core statistics (offline approximation)",
|
notes: "FUT rivals leaderboard → Core statistics (offline approximation)",
|
||||||
},
|
},
|
||||||
// ── Objectives sub-groups ─────────────────────────────────────────────────
|
// ── Objectives sub-groups ─────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/objectives/group",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/objectives",
|
ea_path: "/ut/game/fut/objectives/group",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/objectives",
|
||||||
notes: "FUT objectives group → Core objectives",
|
notes: "FUT objectives group → Core objectives",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/objectives/daily",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/objectives",
|
ea_path: "/ut/game/fut/objectives/daily",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/objectives",
|
||||||
notes: "FUT daily objectives → Core objectives",
|
notes: "FUT daily objectives → Core objectives",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/objectives/weekly",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/objectives",
|
ea_path: "/ut/game/fut/objectives/weekly",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/objectives",
|
||||||
notes: "FUT weekly objectives → Core objectives",
|
notes: "FUT weekly objectives → Core objectives",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/objectives/group/claim",
|
ea_method: "POST",
|
||||||
core_method: "POST", core_path: "/objectives/claim",
|
ea_path: "/ut/game/fut/objectives/group/claim",
|
||||||
|
core_method: "POST",
|
||||||
|
core_path: "/objectives/claim",
|
||||||
notes: "FUT objectives group claim → Core objectives claim",
|
notes: "FUT objectives group claim → Core objectives claim",
|
||||||
},
|
},
|
||||||
// ── Catalogue / Card search ───────────────────────────────────────────────
|
// ── Catalogue / Card search ───────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/catalogue/item",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/cards",
|
ea_path: "/ut/game/fut/catalogue/item",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/cards",
|
||||||
notes: "FUT catalogue items → Core card catalogue",
|
notes: "FUT catalogue items → Core card catalogue",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/catalogue",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/cards",
|
ea_path: "/ut/game/fut/catalogue",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/cards",
|
||||||
notes: "FUT catalogue → Core card catalogue",
|
notes: "FUT catalogue → Core card catalogue",
|
||||||
},
|
},
|
||||||
// ── Store / Pricing ───────────────────────────────────────────────────────
|
// ── Store / Pricing ───────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/store/pricetiers",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/packs/store",
|
ea_path: "/ut/game/fut/store/pricetiers",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/packs/store",
|
||||||
notes: "FUT price tiers → Core pack store definitions",
|
notes: "FUT price tiers → Core pack store definitions",
|
||||||
},
|
},
|
||||||
// ── Consumables / Chemistry / Fitness ─────────────────────────────────────
|
// ── Consumables / Chemistry / Fitness ─────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/consumables",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/chemistry-styles",
|
ea_path: "/ut/game/fut/consumables",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/chemistry-styles",
|
||||||
notes: "FUT consumables → Core chemistry styles (approximation)",
|
notes: "FUT consumables → Core chemistry styles (approximation)",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "POST", ea_path: "/ut/game/fut/fitness",
|
ea_method: "POST",
|
||||||
core_method: "GET", core_path: "/club",
|
ea_path: "/ut/game/fut/fitness",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/club",
|
||||||
notes: "FUT fitness apply → Core club (placeholder, fitness not tracked)",
|
notes: "FUT fitness apply → Core club (placeholder, fitness not tracked)",
|
||||||
},
|
},
|
||||||
// ── Loan items ────────────────────────────────────────────────────────────
|
// ── Loan items ────────────────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/loanitems",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/collection",
|
ea_path: "/ut/game/fut/loanitems",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/collection",
|
||||||
notes: "FUT loan items → Core collection (client filters is_loan)",
|
notes: "FUT loan items → Core collection (client filters is_loan)",
|
||||||
},
|
},
|
||||||
// ── Customization / Kit ───────────────────────────────────────────────────
|
// ── Customization / Kit ───────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/customization",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/settings",
|
ea_path: "/ut/game/fut/customization",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/settings",
|
||||||
notes: "FUT customization → Core settings",
|
notes: "FUT customization → Core settings",
|
||||||
},
|
},
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "PUT", ea_path: "/ut/game/fut/customization",
|
ea_method: "PUT",
|
||||||
core_method: "PUT", core_path: "/settings",
|
ea_path: "/ut/game/fut/customization",
|
||||||
|
core_method: "PUT",
|
||||||
|
core_path: "/settings",
|
||||||
notes: "FUT save customization → Core save settings",
|
notes: "FUT save customization → Core save settings",
|
||||||
},
|
},
|
||||||
// ── Active messages / MOTD ────────────────────────────────────────────────
|
// ── Active messages / MOTD ────────────────────────────────────────────────
|
||||||
ExactRoute {
|
ExactRoute {
|
||||||
ea_method: "GET", ea_path: "/ut/game/fut/activeMessage",
|
ea_method: "GET",
|
||||||
core_method: "GET", core_path: "/notifications",
|
ea_path: "/ut/game/fut/activeMessage",
|
||||||
|
core_method: "GET",
|
||||||
|
core_path: "/notifications",
|
||||||
notes: "FUT active messages → Core notifications (mapped to nearest equivalent)",
|
notes: "FUT active messages → Core notifications (mapped to nearest equivalent)",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -732,7 +864,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_total_exact_routes_count() {
|
fn test_total_exact_routes_count() {
|
||||||
assert!(EXACT.len() >= 61, "expected at least 61 exact mappings, got {}", EXACT.len());
|
assert!(
|
||||||
|
EXACT.len() >= 61,
|
||||||
|
"expected at least 61 exact mappings, got {}",
|
||||||
|
EXACT.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Phase 22 new mappings ─────────────────────────────────────────────────
|
// ── Phase 22 new mappings ─────────────────────────────────────────────────
|
||||||
|
|||||||
+7
-2
@@ -107,8 +107,13 @@ pub async fn catch_all_handler(
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut capture =
|
let mut capture = CapturedRequest::new(
|
||||||
CapturedRequest::new(&method, &path, query.as_deref(), headers.clone(), body_str.clone());
|
&method,
|
||||||
|
&path,
|
||||||
|
query.as_deref(),
|
||||||
|
headers.clone(),
|
||||||
|
body_str.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let (response_body, status_code): (Value, u16) =
|
let (response_body, status_code): (Value, u16) =
|
||||||
if let Some(mapping) = map_to_core(&method, &path) {
|
if let Some(mapping) = map_to_core(&method, &path) {
|
||||||
|
|||||||
+21
-21
@@ -158,10 +158,12 @@ pub async fn get_capture_diff(
|
|||||||
captures.iter().find(|c| c.id == id)
|
captures.iter().find(|c| c.id == id)
|
||||||
};
|
};
|
||||||
|
|
||||||
let cap_a = find(¶ms.a)
|
let cap_a = find(¶ms.a).ok_or_else(|| {
|
||||||
.ok_or_else(|| BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.a)))?;
|
BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.a))
|
||||||
let cap_b = find(¶ms.b)
|
})?;
|
||||||
.ok_or_else(|| BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.b)))?;
|
let cap_b = find(¶ms.b).ok_or_else(|| {
|
||||||
|
BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.b))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Structural JSON diff of request bodies
|
// Structural JSON diff of request bodies
|
||||||
let body_diff = diff_json_strings(cap_a.body.as_deref(), cap_b.body.as_deref());
|
let body_diff = diff_json_strings(cap_a.body.as_deref(), cap_b.body.as_deref());
|
||||||
@@ -184,13 +186,17 @@ pub async fn get_capture_diff(
|
|||||||
.headers
|
.headers
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(k, va)| {
|
.filter_map(|(k, va)| {
|
||||||
cap_b.headers.iter().find(|(kb, _)| kb == k).and_then(|(_, vb)| {
|
cap_b
|
||||||
if va != vb {
|
.headers
|
||||||
Some(json!({ "header": k, "a": va, "b": vb }))
|
.iter()
|
||||||
} else {
|
.find(|(kb, _)| kb == k)
|
||||||
None
|
.and_then(|(_, vb)| {
|
||||||
}
|
if va != vb {
|
||||||
})
|
Some(json!({ "header": k, "a": va, "b": vb }))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -226,10 +232,8 @@ fn diff_json_strings(a: Option<&str>, b: Option<&str>) -> Value {
|
|||||||
return json!({ "same": true });
|
return json!({ "same": true });
|
||||||
}
|
}
|
||||||
// Try to parse as JSON objects for a structured diff
|
// Try to parse as JSON objects for a structured diff
|
||||||
let va: Option<serde_json::Map<String, Value>> =
|
let va: Option<serde_json::Map<String, Value>> = serde_json::from_str(a_str).ok();
|
||||||
serde_json::from_str(a_str).ok();
|
let vb: Option<serde_json::Map<String, Value>> = serde_json::from_str(b_str).ok();
|
||||||
let vb: Option<serde_json::Map<String, Value>> =
|
|
||||||
serde_json::from_str(b_str).ok();
|
|
||||||
|
|
||||||
match (va, vb) {
|
match (va, vb) {
|
||||||
(Some(ma), Some(mb)) => {
|
(Some(ma), Some(mb)) => {
|
||||||
@@ -282,8 +286,7 @@ pub async fn post_replay_capture(
|
|||||||
State(state): State<ProxyState>,
|
State(state): State<ProxyState>,
|
||||||
Path(capture_id): Path<String>,
|
Path(capture_id): Path<String>,
|
||||||
) -> BridgeResult<Json<Value>> {
|
) -> BridgeResult<Json<Value>> {
|
||||||
let captures =
|
let captures = load_all_captures(&state.config.captures_dir).map_err(BridgeError::Internal)?;
|
||||||
load_all_captures(&state.config.captures_dir).map_err(BridgeError::Internal)?;
|
|
||||||
|
|
||||||
let capture = captures
|
let capture = captures
|
||||||
.iter()
|
.iter()
|
||||||
@@ -305,10 +308,7 @@ pub async fn post_replay_capture(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok((body, status)) => (
|
Ok((body, status)) => (status, crate::shaper::shape_response(&m.core_path, body)),
|
||||||
status,
|
|
||||||
crate::shaper::shape_response(&m.core_path, body),
|
|
||||||
),
|
|
||||||
Err(e) => (
|
Err(e) => (
|
||||||
502u16,
|
502u16,
|
||||||
json!({ "error": format!("core forwarding failed: {e}") }),
|
json!({ "error": format!("core forwarding failed: {e}") }),
|
||||||
|
|||||||
@@ -69,7 +69,12 @@ pub async fn get_guide(State(state): State<ProxyState>) -> Html<String> {
|
|||||||
.unwrap_or("localhost");
|
.unwrap_or("localhost");
|
||||||
let tls = state.config.tls_enabled;
|
let tls = state.config.tls_enabled;
|
||||||
let scheme = if tls { "https" } else { "http" };
|
let scheme = if tls { "https" } else { "http" };
|
||||||
let port = state.config.listen_addr.split(':').next_back().unwrap_or("8080");
|
let port = state
|
||||||
|
.config
|
||||||
|
.listen_addr
|
||||||
|
.split(':')
|
||||||
|
.next_back()
|
||||||
|
.unwrap_or("8080");
|
||||||
let bridge_url = format!("{scheme}://{host}:{port}");
|
let bridge_url = format!("{scheme}://{host}:{port}");
|
||||||
let core_url = &state.config.core_url;
|
let core_url = &state.config.core_url;
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -157,10 +157,7 @@ fn shape_squad_response(core: Value) -> Value {
|
|||||||
/// Wraps Core market response in the FUT transfer market envelope.
|
/// Wraps Core market response in the FUT transfer market envelope.
|
||||||
fn shape_market_response(core: Value) -> Value {
|
fn shape_market_response(core: Value) -> Value {
|
||||||
let listings = core.get("listings").cloned().unwrap_or(json!([]));
|
let listings = core.get("listings").cloned().unwrap_or(json!([]));
|
||||||
let total = core
|
let total = core.get("total").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
.get("total")
|
|
||||||
.and_then(|v| v.as_u64())
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
json!({
|
json!({
|
||||||
"auctionInfo": listings,
|
"auctionInfo": listings,
|
||||||
|
|||||||
+59
-16
@@ -1,3 +1,4 @@
|
|||||||
|
use axum::routing::{any, delete, get, post};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
http::{Request, StatusCode},
|
http::{Request, StatusCode},
|
||||||
@@ -9,7 +10,6 @@ use openfut_bridge::{
|
|||||||
proxy::ProxyState,
|
proxy::ProxyState,
|
||||||
routes,
|
routes,
|
||||||
};
|
};
|
||||||
use axum::routing::{any, delete, get, post};
|
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
// ── Unit tests ────────────────────────────────────────────────────────────────
|
// ── Unit tests ────────────────────────────────────────────────────────────────
|
||||||
@@ -96,9 +96,15 @@ fn build_test_app() -> axum::Router {
|
|||||||
.route("/_bridge/dashboard", get(routes::health::get_dashboard))
|
.route("/_bridge/dashboard", get(routes::health::get_dashboard))
|
||||||
.route("/_bridge/captures", get(routes::admin::get_captures))
|
.route("/_bridge/captures", get(routes::admin::get_captures))
|
||||||
.route("/_bridge/captures", delete(routes::admin::delete_captures))
|
.route("/_bridge/captures", delete(routes::admin::delete_captures))
|
||||||
.route("/_bridge/unknown", get(routes::admin::get_unknown_endpoints))
|
.route(
|
||||||
|
"/_bridge/unknown",
|
||||||
|
get(routes::admin::get_unknown_endpoints),
|
||||||
|
)
|
||||||
.route("/_bridge/status", get(routes::admin::get_endpoint_status))
|
.route("/_bridge/status", get(routes::admin::get_endpoint_status))
|
||||||
.route("/_bridge/captures/:id/replay", post(routes::admin::post_replay_capture))
|
.route(
|
||||||
|
"/_bridge/captures/:id/replay",
|
||||||
|
post(routes::admin::post_replay_capture),
|
||||||
|
)
|
||||||
.fallback(any(openfut_bridge::proxy::catch_all_handler))
|
.fallback(any(openfut_bridge::proxy::catch_all_handler))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
@@ -107,11 +113,18 @@ fn build_test_app() -> axum::Router {
|
|||||||
async fn test_bridge_health_endpoint() {
|
async fn test_bridge_health_endpoint() {
|
||||||
let app = build_test_app();
|
let app = build_test_app();
|
||||||
let resp = app
|
let resp = app
|
||||||
.oneshot(Request::builder().uri("/_bridge/health").body(Body::empty()).unwrap())
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri("/_bridge/health")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||||
assert_eq!(json["status"], "ok");
|
assert_eq!(json["status"], "ok");
|
||||||
assert_eq!(json["service"], "openfut-bridge");
|
assert_eq!(json["service"], "openfut-bridge");
|
||||||
@@ -131,7 +144,9 @@ async fn test_bridge_placeholder_mode_returns_ok() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||||
assert_eq!(json["status"], "ok");
|
assert_eq!(json["status"], "ok");
|
||||||
assert!(json["openfut_note"].is_string());
|
assert!(json["openfut_note"].is_string());
|
||||||
@@ -150,7 +165,9 @@ async fn test_bridge_captures_endpoint_returns_list() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||||
assert!(json["captures"].is_array());
|
assert!(json["captures"].is_array());
|
||||||
assert!(json["total"].is_number());
|
assert!(json["total"].is_number());
|
||||||
@@ -170,7 +187,9 @@ async fn test_bridge_delete_captures() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||||
assert!(json["deleted"].is_number());
|
assert!(json["deleted"].is_number());
|
||||||
}
|
}
|
||||||
@@ -188,7 +207,9 @@ async fn test_bridge_status_endpoint() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||||
assert!(json["endpoints"].is_array());
|
assert!(json["endpoints"].is_array());
|
||||||
}
|
}
|
||||||
@@ -208,15 +229,25 @@ async fn test_dashboard_returns_html() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::OK);
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
let ct = resp.headers().get("content-type").unwrap().to_str().unwrap();
|
let ct = resp
|
||||||
|
.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap();
|
||||||
assert!(ct.contains("text/html"), "expected text/html, got {ct}");
|
assert!(ct.contains("text/html"), "expected text/html, got {ct}");
|
||||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||||
assert!(html.contains("OpenFUT Dashboard"), "title missing");
|
assert!(html.contains("OpenFUT Dashboard"), "title missing");
|
||||||
assert!(html.contains("const CORE ="), "core URL injection missing");
|
assert!(html.contains("const CORE ="), "core URL injection missing");
|
||||||
// Placeholder URL injected in test mode
|
// Placeholder URL injected in test mode
|
||||||
assert!(html.contains("127.0.0.1:9999"), "core URL not injected");
|
assert!(html.contains("127.0.0.1:9999"), "core URL not injected");
|
||||||
assert!(!html.contains("{{CORE_URL}}"), "template placeholder was not replaced");
|
assert!(
|
||||||
|
!html.contains("{{CORE_URL}}"),
|
||||||
|
"template placeholder was not replaced"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -231,7 +262,9 @@ async fn test_dashboard_contains_key_sections() {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||||
// Verify all major tab sections are present
|
// Verify all major tab sections are present
|
||||||
assert!(html.contains("tab-club"), "club tab missing");
|
assert!(html.contains("tab-club"), "club tab missing");
|
||||||
@@ -250,8 +283,14 @@ async fn test_dashboard_contains_key_sections() {
|
|||||||
assert!(html.contains("tab-statistics"), "statistics tab missing");
|
assert!(html.contains("tab-statistics"), "statistics tab missing");
|
||||||
assert!(html.contains("tab-catalog"), "card catalog tab missing");
|
assert!(html.contains("tab-catalog"), "card catalog tab missing");
|
||||||
assert!(html.contains("tab-settings"), "settings tab missing");
|
assert!(html.contains("tab-settings"), "settings tab missing");
|
||||||
assert!(html.contains("tab-notifications"), "notifications tab missing");
|
assert!(
|
||||||
assert!(html.contains("tab-achievements"), "achievements tab missing");
|
html.contains("tab-notifications"),
|
||||||
|
"notifications tab missing"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
html.contains("tab-achievements"),
|
||||||
|
"achievements tab missing"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -272,5 +311,9 @@ async fn test_tls_cert_generation() {
|
|||||||
async fn test_tls_acceptor_construction() {
|
async fn test_tls_acceptor_construction() {
|
||||||
let (cert_pem, key_pem) = openfut_bridge::tls::generate_self_signed_cert().unwrap();
|
let (cert_pem, key_pem) = openfut_bridge::tls::generate_self_signed_cert().unwrap();
|
||||||
let acceptor = openfut_bridge::tls::make_tls_acceptor(&cert_pem, &key_pem);
|
let acceptor = openfut_bridge::tls::make_tls_acceptor(&cert_pem, &key_pem);
|
||||||
assert!(acceptor.is_ok(), "acceptor construction failed: {:?}", acceptor.err());
|
assert!(
|
||||||
|
acceptor.is_ok(),
|
||||||
|
"acceptor construction failed: {:?}",
|
||||||
|
acceptor.err()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user