324b137862
CI / Check (pull_request) Successful in 8m14s
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped
Supersedes #82. Deletes the inline JWT middleware in compliance-agent (now stale — missing JWKS refresh from #84 and tenant extraction from #83) and imports require_jwt_auth, require_tenant_status, JwksState from compliance_core::auth. Wires the status gate into the server's layer stack: Extension(jwks_state) → require_jwt_auth → require_tenant_status → handler. Adds the integration test from #82, retargeted to compliance_core::auth::require_tenant_status. Test plan - cargo fmt --all clean - cargo clippy --workspace --exclude compliance-dashboard -- -D warnings clean (matches baseline) - cargo test -p compliance-core --lib — 7 tests pass - cargo test -p compliance-agent --lib — 228 tests pass - cargo test -p compliance-agent --test tenant_status_middleware — 6 tests pass - scripts/smoke.sh against live certifai KC — 15/15 cells pass (anon, bogus, active×2, trial, frozen, archived × {GET/health, GET/echo, POST/echo}) Caveats - M7.1 only — status gate + claim extraction. Per-collection tenant_id scoping (M7.2) still pending; agent will still serve any Active/Trial tenant's data to any caller until the ~38 query call-sites use compliance_core::db::tenant_filter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
123 lines
3.5 KiB
Rust
123 lines
3.5 KiB
Rust
//! M7.1 — integration tests for `compliance_core::auth::require_tenant_status`.
|
|
//!
|
|
//! Exercises the middleware end-to-end through an Axum router so we
|
|
//! catch wiring bugs (extension propagation, method matching) that pure
|
|
//! unit tests would miss.
|
|
|
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
|
|
|
use axum::{
|
|
body::Body,
|
|
extract::Request,
|
|
http::{Method, StatusCode},
|
|
middleware::{from_fn, Next},
|
|
response::Response,
|
|
routing::{get, post},
|
|
Router,
|
|
};
|
|
use compliance_core::{auth::require_tenant_status, TenantContext, TenantStatus};
|
|
use tower::ServiceExt;
|
|
|
|
fn ctx_with(status: TenantStatus) -> TenantContext {
|
|
TenantContext {
|
|
tenant_id: "t-1".to_string(),
|
|
tenant_slug: "acme".to_string(),
|
|
org_roles: vec![],
|
|
products: vec![],
|
|
plan: "starter".to_string(),
|
|
status,
|
|
user_id: "u-1".to_string(),
|
|
user_name: None,
|
|
}
|
|
}
|
|
|
|
fn router_with_ctx(ctx: Option<TenantContext>) -> Router {
|
|
let injector = move |mut req: Request, next: Next| {
|
|
let ctx = ctx.clone();
|
|
async move {
|
|
if let Some(c) = ctx {
|
|
req.extensions_mut().insert(c);
|
|
}
|
|
next.run(req).await
|
|
}
|
|
};
|
|
|
|
Router::new()
|
|
.route("/r", get(|| async { "read" }))
|
|
.route("/w", post(|| async { "write" }))
|
|
.layer(from_fn(require_tenant_status))
|
|
.layer(from_fn(injector))
|
|
}
|
|
|
|
async fn call(router: Router, method: Method, path: &str) -> Response {
|
|
let req = Request::builder()
|
|
.method(method)
|
|
.uri(path)
|
|
.body(Body::empty())
|
|
.expect("request build");
|
|
router.oneshot(req).await.expect("oneshot")
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn active_tenant_can_read_and_write() {
|
|
let r = router_with_ctx(Some(ctx_with(TenantStatus::Active)));
|
|
assert_eq!(
|
|
call(r.clone(), Method::GET, "/r").await.status(),
|
|
StatusCode::OK
|
|
);
|
|
assert_eq!(call(r, Method::POST, "/w").await.status(), StatusCode::OK);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn trial_tenant_can_read_and_write() {
|
|
let r = router_with_ctx(Some(ctx_with(TenantStatus::Trial)));
|
|
assert_eq!(
|
|
call(r.clone(), Method::GET, "/r").await.status(),
|
|
StatusCode::OK
|
|
);
|
|
assert_eq!(call(r, Method::POST, "/w").await.status(), StatusCode::OK);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn demo_tenant_can_read_and_write() {
|
|
let r = router_with_ctx(Some(ctx_with(TenantStatus::Demo)));
|
|
assert_eq!(
|
|
call(r.clone(), Method::GET, "/r").await.status(),
|
|
StatusCode::OK
|
|
);
|
|
assert_eq!(call(r, Method::POST, "/w").await.status(), StatusCode::OK);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn frozen_tenant_can_read_but_not_write() {
|
|
let r = router_with_ctx(Some(ctx_with(TenantStatus::Frozen)));
|
|
assert_eq!(
|
|
call(r.clone(), Method::GET, "/r").await.status(),
|
|
StatusCode::OK
|
|
);
|
|
assert_eq!(
|
|
call(r, Method::POST, "/w").await.status(),
|
|
StatusCode::PAYMENT_REQUIRED
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn archived_tenant_is_gone_on_every_method() {
|
|
let r = router_with_ctx(Some(ctx_with(TenantStatus::Archived)));
|
|
assert_eq!(
|
|
call(r.clone(), Method::GET, "/r").await.status(),
|
|
StatusCode::GONE
|
|
);
|
|
assert_eq!(call(r, Method::POST, "/w").await.status(), StatusCode::GONE);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn no_context_passes_through() {
|
|
let r = router_with_ctx(None);
|
|
assert_eq!(
|
|
call(r.clone(), Method::GET, "/r").await.status(),
|
|
StatusCode::OK
|
|
);
|
|
assert_eq!(call(r, Method::POST, "/w").await.status(), StatusCode::OK);
|
|
}
|