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:
funman300
2026-08-20 16:08:24 +00:00
parent c58e7326a1
commit 658dbe2f24
12 changed files with 447 additions and 186 deletions
+12
View File
@@ -0,0 +1,12 @@
target/
**/target/
*.db
*.db-shm
*.db-wal
.env
.env.local
Dockerfile
.dockerignore
.git
.gitignore
captures/
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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(&params.a) let cap_a = find(&params.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(&params.b) })?;
.ok_or_else(|| BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.b)))?; let cap_b = find(&params.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}") }),
+6 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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()
);
} }