link-assistant-router 1.4.6

Link.Assistant.Router — Claude MAX OAuth proxy and token gateway for Anthropic APIs
Documentation
//! In-process coverage for the administrative token handlers.

use axum::Json;
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use http_body_util::BodyExt;

use crate::app_state::AppState;
use crate::token::{ADMIN_SCOPE, IssueRequest};
use crate::token_admin::{
    IssueClientTokenRequest, RotateClientTokenRequest, RotateTokenRequest, issue_client_token,
    rotate_admin_token, rotate_client_token,
};

fn state() -> (AppState, tempfile::TempDir) {
    let data = tempfile::tempdir().expect("temporary state directory");
    let mut state = AppState::for_tests(data.path());
    state.allow_anonymous_admin = true;
    (state, data)
}

fn client_request(client_kind: &str) -> IssueClientTokenRequest {
    IssueClientTokenRequest {
        client_kind: client_kind.into(),
        ttl_hours: None,
        sliding_expiry: None,
        label: None,
        max_requests: None,
    }
}

async fn json(response: axum::response::Response) -> serde_json::Value {
    let bytes = response
        .into_body()
        .collect()
        .await
        .expect("read response")
        .to_bytes();
    serde_json::from_slice(&bytes).expect("JSON response")
}

#[tokio::test]
async fn bound_client_issuance_checks_authority_kind_and_constraints() {
    let (mut state, _data) = state();
    state.allow_anonymous_admin = false;
    let response = issue_client_token(
        State(state.clone()),
        HeaderMap::new(),
        Json(client_request("codex")),
    )
    .await
    .into_response();
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);

    state.allow_anonymous_admin = true;
    for client in ["unknown-client", "cursor"] {
        let response = issue_client_token(
            State(state.clone()),
            HeaderMap::new(),
            Json(client_request(client)),
        )
        .await
        .into_response();
        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
    }

    let mut invalid = client_request("claude");
    invalid.ttl_hours = Some(0);
    let response = issue_client_token(State(state.clone()), HeaderMap::new(), Json(invalid))
        .await
        .into_response();
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);

    let mut request = client_request("claude");
    request.ttl_hours = Some(2);
    request.sliding_expiry = Some(true);
    request.label = Some("bound claude".into());
    request.max_requests = Some(7);
    let response = issue_client_token(State(state.clone()), HeaderMap::new(), Json(request))
        .await
        .into_response();
    assert_eq!(response.status(), StatusCode::OK);
    let body = json(response).await;
    assert_eq!(body["client_kind"], "claude");
    assert_eq!(body["principal_id"], "primary");
    assert_eq!(body["ttl_hours"], 2);
    assert_eq!(body["label"], "bound claude");

    let claims = state
        .token_manager
        .validate_token(body["token"].as_str().expect("issued token"))
        .expect("valid issued token");
    let record = state
        .token_manager
        .store()
        .get(&claims.sub)
        .expect("read record")
        .expect("persisted record");
    assert_eq!(record.client_kind.as_deref(), Some("claude"));
    assert_eq!(record.principal_id.as_deref(), Some("primary"));
    assert_eq!(record.account.as_deref(), Some("primary"));
    assert_eq!(record.max_requests, Some(7));
    assert_eq!(record.sliding_window_seconds, Some(7_200));
}

fn request<'a>(label: &'a str, scope: &'a str) -> IssueRequest<'a> {
    IssueRequest {
        ttl_hours: 1,
        label,
        account: None,
        max_requests: None,
        max_tokens: None,
        rate_limit_per_minute: None,
        scope,
        github_repos: Vec::new(),
        sliding_window_seconds: None,
        client_kind: None,
        principal_id: None,
    }
}

fn rotate_request(id: String) -> RotateClientTokenRequest {
    RotateClientTokenRequest {
        id,
        label: None,
        ttl_hours: None,
        max_requests: None,
        max_tokens: None,
        rate_limit_per_minute: None,
        account: None,
    }
}

#[tokio::test]
async fn client_rotation_rejects_unknown_admin_and_invalid_tokens_then_rotates() {
    let (state, _data) = state();

    let response = rotate_client_token(
        State(state.clone()),
        HeaderMap::new(),
        Json(rotate_request("missing".into())),
    )
    .await
    .into_response();
    assert_eq!(response.status(), StatusCode::NOT_FOUND);

    let (_, admin_id) = state
        .token_manager
        .issue_with_id(&request("admin", ADMIN_SCOPE))
        .expect("issue admin token");
    let response = rotate_client_token(
        State(state.clone()),
        HeaderMap::new(),
        Json(rotate_request(admin_id)),
    )
    .await
    .into_response();
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);

    let (_, invalid_id) = state
        .token_manager
        .issue_with_id(&request("invalid replacement", ""))
        .expect("issue client token");
    let mut invalid = rotate_request(invalid_id);
    invalid.ttl_hours = Some(0);
    let response = rotate_client_token(State(state.clone()), HeaderMap::new(), Json(invalid))
        .await
        .into_response();
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);

    let (_, id) = state
        .token_manager
        .issue_with_id(&request("old", ""))
        .expect("issue client token");
    let mut replacement = rotate_request(id.clone());
    replacement.label = Some("new".into());
    replacement.ttl_hours = Some(2);
    replacement.max_requests = Some(3);
    let response = rotate_client_token(State(state.clone()), HeaderMap::new(), Json(replacement))
        .await
        .into_response();
    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(json(response).await["revoked"], id);
    assert!(
        state
            .token_manager
            .store()
            .get(&id)
            .expect("read old token")
            .expect("old token remains auditable")
            .revoked
    );
}

#[tokio::test]
async fn admin_rotation_requires_proof_of_possession_and_revokes_the_caller() {
    let (state, _data) = state();
    let response = rotate_admin_token(
        State(state.clone()),
        HeaderMap::new(),
        Json(RotateTokenRequest::default()),
    )
    .await
    .into_response();
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);

    let (token, id) = state
        .token_manager
        .issue_with_id(&request("operator", ADMIN_SCOPE))
        .expect("issue admin token");
    let mut headers = HeaderMap::new();
    headers.insert("authorization", format!("Bearer {token}").parse().unwrap());
    let response = rotate_admin_token(
        State(state.clone()),
        headers,
        Json(RotateTokenRequest {
            ttl_hours: Some(2),
            label: Some("replacement operator".into()),
        }),
    )
    .await
    .into_response();
    assert_eq!(response.status(), StatusCode::OK);
    let body = json(response).await;
    assert_eq!(body["revoked"], id);
    assert_eq!(body["scope"], ADMIN_SCOPE);
    assert_eq!(body["label"], "replacement operator");
    assert!(
        state
            .token_manager
            .store()
            .get(&id)
            .expect("read old admin token")
            .expect("old token remains auditable")
            .revoked
    );
}