use std::sync::Arc;
use std::time::Duration;
use axum::body::Body;
use axum::http::{Method, Request, StatusCode};
use http_body_util::BodyExt as _;
use link_assistant_router::admin::AdminClaim;
use link_assistant_router::app_state::AppState;
use link_assistant_router::cli::Cli;
use link_assistant_router::model_catalog::ModelCatalogCache;
use link_assistant_router::oauth::OAuthProvider;
use link_assistant_router::providers::ProviderStore;
use link_assistant_router::refresh::TokenCache;
use link_assistant_router::token::TokenManager;
use lino_arguments::Parser as _;
use serde_json::Value;
use tower::ServiceExt as _;
const FIXTURE_MODEL: &str = "gpt-5";
struct Fixture {
file: String,
client: String,
method: String,
path: String,
carrier: String,
headers: Vec<(String, String)>,
body: Value,
}
fn load_fixtures() -> Vec<Fixture> {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/clients");
let mut fixtures = Vec::new();
for entry in std::fs::read_dir(&dir).expect("fixture directory exists") {
let path = entry.expect("readable fixture entry").path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
let file = path
.file_name()
.and_then(|n| n.to_str())
.expect("UTF-8 fixture name")
.to_string();
let raw = std::fs::read_to_string(&path).expect("read fixture");
let value: Value = serde_json::from_str(&raw)
.unwrap_or_else(|error| panic!("{file} is not valid JSON: {error}"));
let headers = value["headers"]
.as_object()
.unwrap_or_else(|| panic!("{file} has no headers object"))
.iter()
.map(|(name, value)| {
(
name.clone(),
value
.as_str()
.expect("header value is a string")
.to_string(),
)
})
.collect();
fixtures.push(Fixture {
client: value["client"].as_str().expect("client").to_string(),
method: value["method"].as_str().expect("method").to_string(),
path: value["path"]
.as_str()
.expect("path")
.replace("{model}", FIXTURE_MODEL),
carrier: value["credential_carrier"]
.as_str()
.expect("credential_carrier")
.to_string(),
headers,
body: replace_model(&value["body"]),
file,
});
}
assert!(!fixtures.is_empty(), "no client fixtures were loaded");
fixtures
}
fn replace_model(body: &Value) -> Value {
match body {
Value::String(text) => Value::String(text.replace("{model}", FIXTURE_MODEL)),
Value::Array(items) => Value::Array(items.iter().map(replace_model).collect()),
Value::Object(fields) => Value::Object(
fields
.iter()
.map(|(key, value)| (key.clone(), replace_model(value)))
.collect(),
),
other => other.clone(),
}
}
fn test_app(dir: &std::path::Path) -> (axum::Router, String) {
let dir_arg = dir.to_str().expect("UTF-8 test path");
let config = Cli::try_parse_from(vec![
"router",
"--token-secret",
"fixture-secret",
"--data-dir",
dir_arg,
"--upstream-base-url",
"http://127.0.0.1:9",
])
.expect("test CLI parses")
.into_config()
.expect("test config is valid");
let token_manager = TokenManager::new("fixture-secret");
let token = token_manager
.issue_token(1, "fixture client")
.expect("issue client token");
let state = AppState {
client: reqwest::Client::new(),
token_manager,
oauth_provider: OAuthProvider::new(dir_arg),
account_router: None,
subscription_reader: None,
subscription_base_url: None,
subscription_readers: vec![],
model_catalogs: Arc::new(ModelCatalogCache::new()),
subscription_cache: Arc::new(TokenCache::new()),
upstream_base_url: config.upstream_base_url.clone(),
upstream_provider: config.upstream_provider,
gonka: None,
bridge_model: None,
bridge_model_policy: link_assistant_router::bridge_selection::BridgeModelPolicy::default(),
crater: None,
openai_compatible: config.openai_compatible.clone(),
provider_store: ProviderStore::open(dir, "fixture-secret").expect("provider store"),
logger: log_lazy::LogLazy::new(),
admin: Arc::new(AdminClaim::load(
Some("fixture-admin".to_string()),
dir,
Duration::from_secs(60),
)),
admin_key: Some("fixture-admin".to_string()),
allow_anonymous_admin: false,
metrics: Arc::new(link_assistant_router::metrics::Metrics::default()),
audit: Arc::new(link_assistant_router::audit::AuditLog::disabled()),
request_log: Arc::new(link_assistant_router::request_log::RequestLog::new(
dir.join("requests"),
1024 * 1024,
)),
activitypub_actor_base_url: "https://router.test".to_string(),
activitypub_public_key_pem:
link_assistant_router::config::default_activitypub_public_key_pem(),
mpp: config.mpp.clone(),
login_manager: link_assistant_router::login::LoginManager::new(config.login.clone()),
github: link_assistant_router::github_proxy::GitHubProxyConfig::default(),
max_proxy_request_bytes: link_assistant_router::config::DEFAULT_MAX_PROXY_REQUEST_BYTES,
};
(
link_assistant_router::server_router::router(state, &config),
token,
)
}
async fn replay(fixture: &Fixture, credential: Option<&str>) -> (StatusCode, Value) {
let dir = tempfile::tempdir().expect("tempdir");
let (app, token) = test_app(dir.path());
let credential = credential.map_or(token, str::to_string);
let mut request = Request::builder()
.method(Method::from_bytes(fixture.method.as_bytes()).expect("method"))
.uri(&fixture.path);
for (name, value) in &fixture.headers {
request = request.header(name, value);
}
let value = if fixture.carrier.eq_ignore_ascii_case("authorization") {
format!("Bearer {credential}")
} else {
credential
};
let request = request
.header(&fixture.carrier, value)
.body(Body::from(
serde_json::to_vec(&fixture.body).expect("serialize fixture body"),
))
.expect("build fixture request");
let response = app.oneshot(request).await.expect("router response");
let status = response.status();
let body = response
.into_body()
.collect()
.await
.expect("response body")
.to_bytes();
(status, serde_json::from_slice(&body).unwrap_or(Value::Null))
}
#[tokio::test]
async fn every_recorded_client_authenticates_with_its_own_carrier() {
for fixture in load_fixtures() {
let (status, body) = replay(&fixture, None).await;
assert!(
status != StatusCode::UNAUTHORIZED && status != StatusCode::FORBIDDEN,
"{} ({}) was refused at the credential check via {}: {status} {body}",
fixture.file,
fixture.client,
fixture.carrier
);
}
}
#[tokio::test]
async fn every_recorded_client_reaches_a_route() {
for fixture in load_fixtures() {
let (status, body) = replay(&fixture, None).await;
if status != StatusCode::NOT_FOUND {
continue;
}
let message = serde_json::to_string(&body).unwrap_or_default();
assert!(
!message.contains("route not found"),
"{} ({}) hit no route at {} {}: {message}",
fixture.file,
fixture.client,
fixture.method,
fixture.path
);
}
}
#[tokio::test]
async fn every_recorded_client_is_refused_an_invalid_credential() {
for fixture in load_fixtures() {
let (status, _) = replay(&fixture, Some("la_sk_not_a_real_token")).await;
assert_eq!(
status,
StatusCode::UNAUTHORIZED,
"{} ({}) accepted an invalid credential in {}",
fixture.file,
fixture.client,
fixture.carrier
);
}
}
#[tokio::test]
async fn vendor_specific_headers_do_not_disturb_routing() {
for fixture in load_fixtures() {
let (with_headers, _) = replay(&fixture, None).await;
let stripped = Fixture {
headers: fixture
.headers
.iter()
.filter(|(name, _)| name == "content-type" || name == "anthropic-version")
.cloned()
.collect(),
..fixture
};
let (without_headers, _) = replay(&stripped, None).await;
assert_eq!(
with_headers, without_headers,
"{} routed differently once its vendor headers were removed",
stripped.file
);
}
}