Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 91 additions & 21 deletions src/controllers/trustpub/tokens/exchange/github_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ fn default_claims() -> FullGitHubClaims {
.build()
}

// ============================================================================
// Success cases and token generation tests
// ============================================================================

#[tokio::test(flavor = "multi_thread")]
async fn test_happy_path() -> anyhow::Result<()> {
let client = prepare().await?;
Expand Down Expand Up @@ -129,6 +133,31 @@ async fn test_happy_path_with_ignored_environment() -> anyhow::Result<()> {
Ok(())
}

/// Check that the owner name, repository name, and environment are accepted in
/// a case-insensitive manner.
#[tokio::test(flavor = "multi_thread")]
async fn test_case_insensitive() -> anyhow::Result<()> {
let client = prepare_with_config(|c| c.environment = Some("Prod")).await?;

let claims = FullGitHubClaims::builder()
.owner_id(OWNER_ID)
.owner_name("RUST-lanG")
.repository_name("foo-RS")
.workflow_filename(WORKFLOW_FILENAME)
.environment("PROD")
.build();

let body = claims.as_exchange_body()?;
let response = client.post::<()>(URL, body).await;
assert_snapshot!(response.status(), @"200 OK");

Ok(())
}

// ============================================================================
// JWT decode and validation tests
// ============================================================================

#[tokio::test(flavor = "multi_thread")]
async fn test_broken_jwt() -> anyhow::Result<()> {
let client = prepare().await?;
Expand Down Expand Up @@ -249,6 +278,10 @@ async fn test_invalid_audience() -> anyhow::Result<()> {
Ok(())
}

// ============================================================================
// JTI replay prevention tests
// ============================================================================

/// Test that OIDC tokens can only be exchanged once
#[tokio::test(flavor = "multi_thread")]
async fn test_token_reuse() -> anyhow::Result<()> {
Expand All @@ -268,6 +301,10 @@ async fn test_token_reuse() -> anyhow::Result<()> {
Ok(())
}

// ============================================================================
// Repository parsing tests
// ============================================================================

#[tokio::test(flavor = "multi_thread")]
async fn test_invalid_repository() -> anyhow::Result<()> {
let client = prepare().await?;
Expand All @@ -283,6 +320,10 @@ async fn test_invalid_repository() -> anyhow::Result<()> {
Ok(())
}

// ============================================================================
// Workflow filename extraction tests
// ============================================================================

#[tokio::test(flavor = "multi_thread")]
async fn test_invalid_workflow() -> anyhow::Result<()> {
let client = prepare().await?;
Expand All @@ -298,6 +339,10 @@ async fn test_invalid_workflow() -> anyhow::Result<()> {
Ok(())
}

// ============================================================================
// Repository owner ID validation tests
// ============================================================================

#[tokio::test(flavor = "multi_thread")]
async fn test_invalid_owner_id() -> anyhow::Result<()> {
let client = prepare().await?;
Expand All @@ -313,6 +358,10 @@ async fn test_invalid_owner_id() -> anyhow::Result<()> {
Ok(())
}

// ============================================================================
// Config lookup tests
// ============================================================================

#[tokio::test(flavor = "multi_thread")]
async fn test_missing_config() -> anyhow::Result<()> {
let (_app, client, _cookie) = TestApp::full()
Expand All @@ -328,50 +377,71 @@ async fn test_missing_config() -> anyhow::Result<()> {
Ok(())
}

// ============================================================================
// Repository owner ID verification (resurrection protection) tests
// ============================================================================

#[tokio::test(flavor = "multi_thread")]
async fn test_missing_environment() -> anyhow::Result<()> {
let client = prepare_with_config(|c| c.environment = Some("prod")).await?;
async fn test_repository_owner_id_mismatch() -> anyhow::Result<()> {
let client = prepare_with_config(|c| {
c.repository_owner_id = 999; // Different from OWNER_ID (42)
})
.await?;

let body = default_claims().as_exchange_body()?;
let response = client.post::<()>(URL, body).await;
assert_snapshot!(response.status(), @"400 Bad Request");
assert_snapshot!(response.json(), @r#"{"errors":[{"detail":"The Trusted Publishing config for repository `rust-lang/foo-rs` requires an environment, but the JWT does not specify one. Expected environments: `prod`"}]}"#);
assert_snapshot!(response.json(), @r#"{"errors":[{"detail":"The Trusted Publishing config for repository `rust-lang/foo-rs` does not match the repository owner ID (42) in the JWT. Expected owner IDs: 999. Please recreate the Trusted Publishing config to update the repository owner ID."}]}"#);

Ok(())
}

// ============================================================================
// Workflow filename matching tests
// ============================================================================

#[tokio::test(flavor = "multi_thread")]
async fn test_wrong_environment() -> anyhow::Result<()> {
let client = prepare_with_config(|c| c.environment = Some("prod")).await?;
async fn test_workflow_filename_mismatch() -> anyhow::Result<()> {
let client = prepare_with_config(|c| {
c.workflow_filename = "different.yml";
})
.await?;

let mut claims = default_claims();
claims.environment = Some("not-prod".into());
let body = default_claims().as_exchange_body()?;
let response = client.post::<()>(URL, body).await;
assert_snapshot!(response.status(), @"400 Bad Request");
assert_snapshot!(response.json(), @r#"{"errors":[{"detail":"The Trusted Publishing config for repository `rust-lang/foo-rs` does not match the workflow filename `publish.yml` in the JWT. Expected workflow filenames: `different.yml`"}]}"#);

let body = claims.as_exchange_body()?;
Ok(())
}

// ============================================================================
// Environment matching tests
// ============================================================================

#[tokio::test(flavor = "multi_thread")]
async fn test_missing_environment() -> anyhow::Result<()> {
let client = prepare_with_config(|c| c.environment = Some("prod")).await?;

let body = default_claims().as_exchange_body()?;
let response = client.post::<()>(URL, body).await;
assert_snapshot!(response.status(), @"400 Bad Request");
assert_snapshot!(response.json(), @r#"{"errors":[{"detail":"The Trusted Publishing config for repository `rust-lang/foo-rs` does not match the environment `not-prod` in the JWT. Expected environments: `prod`"}]}"#);
assert_snapshot!(response.json(), @r#"{"errors":[{"detail":"The Trusted Publishing config for repository `rust-lang/foo-rs` requires an environment, but the JWT does not specify one. Expected environments: `prod`"}]}"#);

Ok(())
}

/// Check that the owner name, repository name, and environment are accepted in
/// a case-insensitive manner.
#[tokio::test(flavor = "multi_thread")]
async fn test_case_insensitive() -> anyhow::Result<()> {
let client = prepare_with_config(|c| c.environment = Some("Prod")).await?;
async fn test_wrong_environment() -> anyhow::Result<()> {
let client = prepare_with_config(|c| c.environment = Some("prod")).await?;

let claims = FullGitHubClaims::builder()
.owner_id(OWNER_ID)
.owner_name("RUST-lanG")
.repository_name("foo-RS")
.workflow_filename(WORKFLOW_FILENAME)
.environment("PROD")
.build();
let mut claims = default_claims();
claims.environment = Some("not-prod".into());

let body = claims.as_exchange_body()?;
let response = client.post::<()>(URL, body).await;
assert_snapshot!(response.status(), @"200 OK");
assert_snapshot!(response.status(), @"400 Bad Request");
assert_snapshot!(response.json(), @r#"{"errors":[{"detail":"The Trusted Publishing config for repository `rust-lang/foo-rs` does not match the environment `not-prod` in the JWT. Expected environments: `prod`"}]}"#);

Ok(())
}