feat(utas-host): send X-OpenFUT-Game to Core + end-to-end /club composition test
This commit is contained in:
@@ -118,16 +118,21 @@ pub trait CoreAccess: Send + Sync {
|
||||
}
|
||||
|
||||
/// Default HTTP implementation: `GET {core_url}/collection?…` (plain HTTP JSON,
|
||||
/// the same boundary Bridge uses to reach Core).
|
||||
/// the same boundary Bridge uses to reach Core). Every request carries
|
||||
/// `X-OpenFUT-Game: <game>` so Core resolves the game-scoped active profile — for
|
||||
/// FIFA 17 that is the profile whose inventory is fully asset-mapped, so `/club`
|
||||
/// filters/paginates a wholly renderable set (no post-pagination drops).
|
||||
pub struct HttpCoreClient {
|
||||
base_url: String,
|
||||
game: String,
|
||||
client: reqwest::blocking::Client,
|
||||
}
|
||||
|
||||
impl HttpCoreClient {
|
||||
pub fn new(base_url: impl Into<String>) -> Self {
|
||||
pub fn new(base_url: impl Into<String>, game: impl Into<String>) -> Self {
|
||||
HttpCoreClient {
|
||||
base_url: base_url.into().trim_end_matches('/').to_string(),
|
||||
game: game.into(),
|
||||
client: reqwest::blocking::Client::new(),
|
||||
}
|
||||
}
|
||||
@@ -139,6 +144,7 @@ impl CoreAccess for HttpCoreClient {
|
||||
let resp = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("X-OpenFUT-Game", &self.game)
|
||||
.query(params)
|
||||
.send()
|
||||
.map_err(|e| CoreError::Http(e.to_string()))?;
|
||||
@@ -507,7 +513,10 @@ impl Server {
|
||||
let assets: Arc<dyn ItemIdentityResolver + Send + Sync> =
|
||||
Arc::new(Fifa17IdentityResolver::new(catalog, Arc::new(store)));
|
||||
Ok(Server {
|
||||
core: Arc::new(HttpCoreClient::new(cfg.core_url.clone())),
|
||||
core: Arc::new(HttpCoreClient::new(
|
||||
cfg.core_url.clone(),
|
||||
Fifa17WireItemIdPolicy::GAME,
|
||||
)),
|
||||
entities: Arc::new(entities),
|
||||
assets,
|
||||
pass: Arc::new(PassClient::new(cfg.python_upstream.clone())),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! upstream, route-classification safety, negatives, and an end-to-end socket run.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{BufReader, Read, Write};
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -13,8 +13,8 @@ use openfut_adapter_fifa17::fut::club_response::{CoreOwnedItem, ItemIdentityReso
|
||||
use openfut_adapter_fifa17::fut::entities::Fifa17Entities;
|
||||
use openfut_identity::JsonIdentityStore;
|
||||
use openfut_utas_host::{
|
||||
classify, read_request, CoreAccess, CoreError, CorePage, Fifa17IdentityResolver, PassClient,
|
||||
Route, Server,
|
||||
classify, read_request, CoreAccess, CoreError, CorePage, Fifa17IdentityResolver,
|
||||
HttpCoreClient, PassClient, Route, Server,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::Value;
|
||||
@@ -482,3 +482,93 @@ fn host_does_not_refilter_core_results() {
|
||||
"host must not second-guess Core's filtering"
|
||||
);
|
||||
}
|
||||
|
||||
// ── End-to-end through the REAL resolver + Core over HTTP ────────────────────
|
||||
|
||||
/// A mock OpenFUT Core: records each request's header block and replies with a
|
||||
/// fixed `/collection` body. Lets us prove the host talks to Core over real HTTP
|
||||
/// (game header + query) and composes the real identity resolver.
|
||||
fn spawn_mock_core(collection: Value) -> (String, Recorded) {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let rec: Recorded = Arc::new(Mutex::new(Vec::new()));
|
||||
let rec2 = rec.clone();
|
||||
let body = collection.to_string();
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming() {
|
||||
let mut stream = stream.unwrap();
|
||||
let mut reader = BufReader::new(stream.try_clone().unwrap());
|
||||
let mut headers = String::new();
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
if reader.read_line(&mut line).unwrap() == 0 || line == "\r\n" {
|
||||
break;
|
||||
}
|
||||
headers.push_str(&line);
|
||||
}
|
||||
rec2.lock().push(("GET".into(), headers, vec![]));
|
||||
let resp = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
stream.write_all(resp.as_bytes()).unwrap();
|
||||
}
|
||||
});
|
||||
(format!("http://{addr}"), rec)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn club_end_to_end_through_real_resolver_and_sends_game_header() {
|
||||
// Core returns one asset-mapped item (fifa17_20801) and one synthetic item
|
||||
// with no catalog identity.
|
||||
let collection = serde_json::json!({
|
||||
"collection": [
|
||||
{"owned_card_id":"oc-mapped","effective_position":"ST","card":{"id":"fifa17_20801","overall":94,"position":"ST","nation":"Argentina","league":"Premier League","club":"Chelsea","pace":90,"shooting":92,"passing":81,"dribbling":91,"defending":33,"physical":80}},
|
||||
{"owned_card_id":"oc-synth","effective_position":"CB","card":{"id":"card_synth_1","overall":70,"position":"CB","nation":"England","league":"Premier League","club":"Chelsea","pace":60,"shooting":40,"passing":55,"dribbling":50,"defending":72,"physical":75}}
|
||||
],
|
||||
"total": 2
|
||||
});
|
||||
let (core_url, rec) = spawn_mock_core(collection);
|
||||
|
||||
let catalog = Fifa17CardCatalog::from_json_str(
|
||||
"{\"schema_version\":1,\"game\":\"fifa17\",\"cards\":{\"fifa17_20801\":{\"asset_id\":20801}}}",
|
||||
)
|
||||
.unwrap();
|
||||
let store = JsonIdentityStore::open(unique_store_path()).unwrap();
|
||||
let resolver: Arc<dyn ItemIdentityResolver + Send + Sync> =
|
||||
Arc::new(Fifa17IdentityResolver::new(catalog, Arc::new(store)));
|
||||
let server = Server::new(
|
||||
Arc::new(HttpCoreClient::new(core_url, "fifa17")),
|
||||
Arc::new(entities()),
|
||||
resolver,
|
||||
Arc::new(PassClient::new("http://127.0.0.1:1")),
|
||||
);
|
||||
|
||||
let resp = server.handle("GET", "/ut/game/fifa17/club?level=gold", &[], b"");
|
||||
assert_eq!(resp.status, 200);
|
||||
let body: Value = serde_json::from_slice(&resp.body).unwrap();
|
||||
let items = body["itemData"].as_array().unwrap();
|
||||
assert_eq!(
|
||||
items.len(),
|
||||
1,
|
||||
"mapped item renders; synthetic (no catalog id) dropped"
|
||||
);
|
||||
assert_eq!(
|
||||
items[0]["resourceId"], 20801,
|
||||
"real asset id from the catalog"
|
||||
);
|
||||
assert!(
|
||||
items[0]["id"].as_i64().unwrap() >= 100_000_001,
|
||||
"wire id minted by the store, not a placeholder"
|
||||
);
|
||||
|
||||
// The game header reached Core so it can select the fifa17 (all-mapped) profile.
|
||||
std::thread::sleep(std::time::Duration::from_millis(30));
|
||||
let got = rec.lock().clone();
|
||||
assert!(
|
||||
got.iter()
|
||||
.any(|(_, h, _)| h.to_lowercase().contains("x-openfut-game: fifa17")),
|
||||
"X-OpenFUT-Game: fifa17 sent to Core: {got:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user