#![cfg(feature = "kernel-registry-api")]
use std::sync::Arc;
use axum::body::Body;
use axum::http::{Method, Request, StatusCode};
use http_body_util::BodyExt;
use serde_json::{json, Value};
use tensor_wasm_api::{
build_router_with_kernel_publish_tokens, AppState, AuditConfig, AuthConfig, CorsConfig,
KernelPublishTokens, RateLimitConfig, RateLimiter, TenantConfig, TrustedProxies,
};
use tensor_wasm_jit::registry::{sign_manifest, InMemoryRegistry, KernelManifest, KernelRegistry};
use tower::ServiceExt;
const TEST_KEY: [u8; 32] = [0x42u8; 32];
const PUBLISH_TOKEN: &str = "publish-allowed-token";
const READ_ONLY_TOKEN: &str = "read-only-token";
fn state_with_registry() -> Arc<AppState> {
let registry: Arc<dyn KernelRegistry> = Arc::new(InMemoryRegistry::new(TEST_KEY));
Arc::new(AppState::default().with_kernel_registry(registry))
}
fn signed_manifest(name: &str, version: &str, ptx_text: &str, key: &[u8; 32]) -> KernelManifest {
let digest = *blake3::hash(ptx_text.as_bytes()).as_bytes();
let mut m = KernelManifest::new(
name.to_string(),
version.to_string(),
80,
digest,
[0u8; 32],
0,
"integration-test".to_string(),
);
m.signature = sign_manifest(&m, key);
m
}
async fn body_json(body: Body) -> Value {
let bytes = body
.collect()
.await
.expect("body collects")
.to_bytes()
.to_vec();
serde_json::from_slice(&bytes).expect("body parses as JSON")
}
fn publish_payload(name: &str, version: &str) -> Value {
let ptx = "// fixture ptx\n";
let manifest = signed_manifest(name, version, ptx, &TEST_KEY);
json!({
"manifest": manifest,
"ptx_text": ptx,
})
}
fn router_with(auth_tokens: &[&str], publish_tokens: &[&str]) -> axum::Router {
let auth = if auth_tokens.is_empty() {
AuthConfig::default()
} else {
AuthConfig::from_tokens(auth_tokens.iter().copied())
};
build_router_with_kernel_publish_tokens(
state_with_registry(),
auth,
TenantConfig::default(),
RateLimiter::new(RateLimitConfig::disabled()),
AuditConfig::disabled(),
CorsConfig::default(),
TrustedProxies::default(),
KernelPublishTokens::from_tokens(publish_tokens.iter().copied()),
)
}
#[tokio::test]
async fn dev_mode_post_kernels_returns_403_kernel_publish_disabled_in_dev_mode() {
let router = router_with(&[], &[]);
let req = Request::builder()
.method(Method::POST)
.uri("/kernels")
.header("content-type", "application/json")
.body(Body::from(
serde_json::to_vec(&publish_payload("matmul.f32", "1.0.0")).unwrap(),
))
.expect("request builds");
let resp = router.oneshot(req).await.expect("router serves /kernels");
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
let body = body_json(resp.into_body()).await;
assert_eq!(
body["error"]["kind"], "kernel_publish_disabled_in_dev_mode",
"expected dev-mode rejection, got {body}",
);
}
#[tokio::test]
async fn non_publish_token_post_kernels_returns_403_kernel_publish_scope_required() {
let router = router_with(&[READ_ONLY_TOKEN], &[]);
let req = Request::builder()
.method(Method::POST)
.uri("/kernels")
.header("content-type", "application/json")
.header("Authorization", format!("Bearer {READ_ONLY_TOKEN}"))
.body(Body::from(
serde_json::to_vec(&publish_payload("matmul.f32", "1.0.0")).unwrap(),
))
.expect("request builds");
let resp = router.oneshot(req).await.expect("router serves /kernels");
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
let body = body_json(resp.into_body()).await;
assert_eq!(
body["error"]["kind"], "kernel_publish_scope_required",
"expected publish-scope rejection, got {body}",
);
}
#[tokio::test]
async fn non_publish_token_get_kernels_returns_200() {
let router = router_with(&[READ_ONLY_TOKEN], &[]);
let req = Request::builder()
.method(Method::GET)
.uri("/kernels")
.header("Authorization", format!("Bearer {READ_ONLY_TOKEN}"))
.body(Body::empty())
.expect("request builds");
let resp = router.oneshot(req).await.expect("router serves /kernels");
assert_eq!(resp.status(), StatusCode::OK);
let body = body_json(resp.into_body()).await;
assert!(
body["manifests"].is_array(),
"expected manifests array on list, got {body}",
);
}
#[tokio::test]
async fn publish_token_post_kernels_returns_201() {
let router = router_with(&[PUBLISH_TOKEN], &[PUBLISH_TOKEN]);
let req = Request::builder()
.method(Method::POST)
.uri("/kernels")
.header("content-type", "application/json")
.header("Authorization", format!("Bearer {PUBLISH_TOKEN}"))
.body(Body::from(
serde_json::to_vec(&publish_payload("matmul.f32", "1.0.0")).unwrap(),
))
.expect("request builds");
let resp = router.oneshot(req).await.expect("router serves /kernels");
assert_eq!(resp.status(), StatusCode::CREATED);
let body = body_json(resp.into_body()).await;
assert_eq!(body["name"], "matmul.f32");
assert_eq!(body["version"], "1.0.0");
}
#[tokio::test]
async fn publish_token_not_in_api_allowlist_returns_401() {
let router = router_with(&[READ_ONLY_TOKEN], &[PUBLISH_TOKEN]);
let req = Request::builder()
.method(Method::POST)
.uri("/kernels")
.header("content-type", "application/json")
.header("Authorization", format!("Bearer {PUBLISH_TOKEN}"))
.body(Body::from(
serde_json::to_vec(&publish_payload("matmul.f32", "1.0.0")).unwrap(),
))
.expect("request builds");
let resp = router.oneshot(req).await.expect("router serves /kernels");
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}