Public Access
617 lines
22 KiB
Rust
617 lines
22 KiB
Rust
//! Integration tests for DefectDojo plugin using wiremock
|
|
//!
|
|
//! These tests require a running PostgreSQL instance.
|
|
//! Set DATABASE_URL environment variable to run them.
|
|
//! They are ignored by default in CI unless DATABASE_URL is set.
|
|
|
|
use charybdis::charybdis::entities::entity::{Metadata, Spec};
|
|
use charybdis::charybdis::entities::Entity;
|
|
use charybdis::database::{ensure_schema, EntityRepository};
|
|
use charybdis::plugins::ResourceHandler;
|
|
use charybdis_defectdojo::{
|
|
DefectDojoClient, DefectDojoConfig, EngagementConfig, OwnerResolutionConfig,
|
|
};
|
|
use serde_json::json;
|
|
use sqlx::PgPool;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use wiremock::matchers::{method, path, query_param};
|
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
|
|
|
async fn setup_db() -> PgPool {
|
|
let url =
|
|
std::env::var("DATABASE_URL").expect("DATABASE_URL must be set for integration tests");
|
|
let pool = PgPool::connect(&url)
|
|
.await
|
|
.expect("Failed to connect to test database");
|
|
ensure_schema(&pool).await.expect("Failed to create schema");
|
|
|
|
// Clean up from previous test runs
|
|
sqlx::query("DELETE FROM entities")
|
|
.execute(&pool)
|
|
.await
|
|
.expect("Failed to clean entities table");
|
|
|
|
pool
|
|
}
|
|
|
|
fn make_component_entity(id: &str, name: &str, owner: &str) -> Entity {
|
|
Entity {
|
|
id: id.to_string(),
|
|
kind: "Component".to_string(),
|
|
annotations: HashMap::new(),
|
|
created_at: None,
|
|
updated_at: None,
|
|
metadata: Some(Metadata::ComponentMetadata(
|
|
charybdis::charybdis::core::ComponentMetadata {
|
|
name: name.to_string(),
|
|
namespace: "default".to_string(),
|
|
description: format!("{} service", name),
|
|
labels: HashMap::new(),
|
|
tags: vec![],
|
|
links: vec![],
|
|
},
|
|
)),
|
|
spec: Some(Spec::ComponentSpec(
|
|
charybdis::charybdis::core::ComponentSpec {
|
|
r#type: "service".to_string(),
|
|
lifecycle: "production".to_string(),
|
|
owner: owner.to_string(),
|
|
system: String::new(),
|
|
subcomponent_of: String::new(),
|
|
depends_on: vec![],
|
|
provides_apis: vec![],
|
|
consumes_apis: vec![],
|
|
},
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn make_config(base_url: &str) -> DefectDojoConfig {
|
|
DefectDojoConfig {
|
|
base_url: base_url.to_string(),
|
|
api_token: "test-token".to_string(),
|
|
default_product_type_id: Some(1),
|
|
auto_create_users: false,
|
|
auto_create_product_types: false,
|
|
default_engagement: EngagementConfig::default(),
|
|
owner_resolution: OwnerResolutionConfig::default(),
|
|
field_mappings: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────
|
|
// Product creation
|
|
// ──────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires DATABASE_URL
|
|
async fn test_product_creation_on_component_create() {
|
|
let pool = setup_db().await;
|
|
let repository = Arc::new(EntityRepository::new(pool.clone()));
|
|
let mock_server = MockServer::start().await;
|
|
|
|
// Mock: POST /api/v2/products/ → returns product with id=42
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/v2/products/"))
|
|
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
|
"id": 42,
|
|
"name": "payment-api",
|
|
"description": "payment-api service",
|
|
"prod_type": 1
|
|
})))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
// Mock: POST /api/v2/engagements/ → returns engagement with id=100
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/v2/engagements/"))
|
|
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
|
"id": 100,
|
|
"name": "CI/CD Scans",
|
|
"product": 42
|
|
})))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
let mut config = make_config(&mock_server.uri());
|
|
config.default_engagement.auto_create = true;
|
|
// Disable owner resolution for this test (no owner group exists)
|
|
config.owner_resolution.assign_all_members = false;
|
|
|
|
let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap();
|
|
let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new(
|
|
client.clone(),
|
|
repository.clone(),
|
|
HashMap::new(),
|
|
));
|
|
|
|
let handler = charybdis_defectdojo::handlers::ProductHandler::new(
|
|
client,
|
|
repository.clone(),
|
|
HashMap::new(),
|
|
config,
|
|
engagement_handler,
|
|
);
|
|
|
|
// Create the entity in the database first (returns entity with real UUID)
|
|
let entity = make_component_entity("", "payment-api", "team-payments");
|
|
let entity = repository
|
|
.create(&entity)
|
|
.await
|
|
.expect("Failed to create entity");
|
|
|
|
// Trigger handler
|
|
let result = handler.handle_create(&entity).await;
|
|
assert!(result.is_ok(), "handle_create failed: {:?}", result.err());
|
|
|
|
// Verify annotations were saved
|
|
let updated = repository.get_by_id(&entity.id).await.unwrap().unwrap();
|
|
assert_eq!(
|
|
updated.annotations.get("defectdojo.com/product-id"),
|
|
Some(&"42".to_string())
|
|
);
|
|
assert_eq!(
|
|
updated.annotations.get("defectdojo.com/engagement-id"),
|
|
Some(&"100".to_string())
|
|
);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────
|
|
// Product update
|
|
// ──────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires DATABASE_URL
|
|
async fn test_product_update_with_existing_product_id() {
|
|
let pool = setup_db().await;
|
|
let repository = Arc::new(EntityRepository::new(pool.clone()));
|
|
let mock_server = MockServer::start().await;
|
|
|
|
// Mock: PUT /api/v2/products/42/ → success
|
|
Mock::given(method("PUT"))
|
|
.and(path("/api/v2/products/42/"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"id": 42,
|
|
"name": "payment-api-updated"
|
|
})))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
let config = make_config(&mock_server.uri());
|
|
let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap();
|
|
let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new(
|
|
client.clone(),
|
|
repository.clone(),
|
|
HashMap::new(),
|
|
));
|
|
|
|
let handler = charybdis_defectdojo::handlers::ProductHandler::new(
|
|
client,
|
|
repository.clone(),
|
|
HashMap::new(),
|
|
config,
|
|
engagement_handler,
|
|
);
|
|
|
|
// Create entity with existing product-id annotation
|
|
let mut entity = make_component_entity("", "payment-api", "team-payments");
|
|
entity
|
|
.annotations
|
|
.insert("defectdojo.com/product-id".to_string(), "42".to_string());
|
|
let entity = repository
|
|
.create(&entity)
|
|
.await
|
|
.expect("Failed to create entity");
|
|
|
|
// Trigger update handler
|
|
let result = handler.handle_update(&entity).await;
|
|
assert!(result.is_ok(), "handle_update failed: {:?}", result.err());
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────
|
|
// Product deletion
|
|
// ──────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires DATABASE_URL
|
|
async fn test_product_deletion() {
|
|
let pool = setup_db().await;
|
|
let repository = Arc::new(EntityRepository::new(pool.clone()));
|
|
let mock_server = MockServer::start().await;
|
|
|
|
// Mock: DELETE /api/v2/products/42/ → 204 No Content
|
|
Mock::given(method("DELETE"))
|
|
.and(path("/api/v2/products/42/"))
|
|
.respond_with(ResponseTemplate::new(204))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
let config = make_config(&mock_server.uri());
|
|
let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap();
|
|
let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new(
|
|
client.clone(),
|
|
repository.clone(),
|
|
HashMap::new(),
|
|
));
|
|
|
|
let handler = charybdis_defectdojo::handlers::ProductHandler::new(
|
|
client,
|
|
repository.clone(),
|
|
HashMap::new(),
|
|
config,
|
|
engagement_handler,
|
|
);
|
|
|
|
// Entity with product-id annotation
|
|
let mut entity = make_component_entity("", "payment-api", "team-payments");
|
|
entity
|
|
.annotations
|
|
.insert("defectdojo.com/product-id".to_string(), "42".to_string());
|
|
let entity = repository
|
|
.create(&entity)
|
|
.await
|
|
.expect("Failed to create entity");
|
|
|
|
// Trigger delete handler
|
|
let result = handler.handle_delete(&entity).await;
|
|
assert!(result.is_ok(), "handle_delete failed: {:?}", result.err());
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────
|
|
// Owner resolution with product member assignment
|
|
// ──────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires DATABASE_URL
|
|
async fn test_owner_resolution_assigns_product_members() {
|
|
let pool = setup_db().await;
|
|
let repository = Arc::new(EntityRepository::new(pool.clone()));
|
|
let mock_server = MockServer::start().await;
|
|
|
|
// Mock: POST /api/v2/products/ → product id=10
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/v2/products/"))
|
|
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
|
"id": 10,
|
|
"name": "orders-api",
|
|
"prod_type": 1
|
|
})))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
// Mock: POST /api/v2/engagements/ → engagement id=20
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/v2/engagements/"))
|
|
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
|
"id": 20,
|
|
"name": "CI/CD Scans",
|
|
"product": 10
|
|
})))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
// Mock: GET /api/v2/users/?email=alice@example.com → found user id=5
|
|
Mock::given(method("GET"))
|
|
.and(path("/api/v2/users/"))
|
|
.and(query_param("email", "alice@example.com"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"count": 1,
|
|
"results": [{"id": 5, "username": "alice@example.com"}]
|
|
})))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
// Mock: GET /api/v2/users/?email=bob@example.com → found user id=7
|
|
Mock::given(method("GET"))
|
|
.and(path("/api/v2/users/"))
|
|
.and(query_param("email", "bob@example.com"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"count": 1,
|
|
"results": [{"id": 7, "username": "bob@example.com"}]
|
|
})))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
// Mock: GET /api/v2/roles/?name=Owner → role id=4
|
|
Mock::given(method("GET"))
|
|
.and(path("/api/v2/roles/"))
|
|
.and(query_param("name", "Owner"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"count": 1,
|
|
"results": [{"id": 4, "name": "Owner"}]
|
|
})))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
// Mock: GET /api/v2/product_members/?product=10&user=5 → not yet a member
|
|
Mock::given(method("GET"))
|
|
.and(path("/api/v2/product_members/"))
|
|
.and(query_param("product", "10"))
|
|
.and(query_param("user", "5"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"count": 0,
|
|
"results": []
|
|
})))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
// Mock: GET /api/v2/product_members/?product=10&user=7 → not yet a member
|
|
Mock::given(method("GET"))
|
|
.and(path("/api/v2/product_members/"))
|
|
.and(query_param("product", "10"))
|
|
.and(query_param("user", "7"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"count": 0,
|
|
"results": []
|
|
})))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
// Mock: POST /api/v2/product_members/ → member created (called twice)
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/v2/product_members/"))
|
|
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
|
"id": 99,
|
|
"product": 10,
|
|
"user": 5,
|
|
"role": 4
|
|
})))
|
|
.expect(2)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
// Set up config with owner resolution enabled
|
|
let mut config = make_config(&mock_server.uri());
|
|
config.default_engagement.auto_create = true;
|
|
config.owner_resolution.user_email_annotation = "keycloak.com/email".to_string();
|
|
config.owner_resolution.defectdojo_lookup_field = "email".to_string();
|
|
config.owner_resolution.assign_all_members = true;
|
|
|
|
// Create Group "backend-team" with members alice and bob
|
|
let group_entity = Entity {
|
|
id: "group-backend".to_string(),
|
|
kind: "Group".to_string(),
|
|
annotations: HashMap::new(),
|
|
created_at: None,
|
|
updated_at: None,
|
|
metadata: Some(Metadata::GroupMetadata(
|
|
charybdis::charybdis::core::GroupMetadata {
|
|
name: "backend-team".to_string(),
|
|
namespace: "default".to_string(),
|
|
description: "Backend team".to_string(),
|
|
labels: HashMap::new(),
|
|
tags: vec![],
|
|
links: vec![],
|
|
},
|
|
)),
|
|
spec: Some(Spec::GroupSpec(charybdis::charybdis::core::GroupSpec {
|
|
r#type: "team".to_string(),
|
|
profile: None,
|
|
parent: String::new(),
|
|
children: vec![],
|
|
members: vec!["alice".to_string(), "bob".to_string()],
|
|
})),
|
|
};
|
|
repository.create(&group_entity).await.unwrap();
|
|
|
|
// Create User "alice" with keycloak email annotation
|
|
let mut alice = Entity {
|
|
id: "user-alice".to_string(),
|
|
kind: "User".to_string(),
|
|
annotations: HashMap::new(),
|
|
created_at: None,
|
|
updated_at: None,
|
|
metadata: Some(Metadata::UserMetadata(
|
|
charybdis::charybdis::core::UserMetadata {
|
|
name: "alice".to_string(),
|
|
namespace: "default".to_string(),
|
|
description: "Alice".to_string(),
|
|
labels: HashMap::new(),
|
|
tags: vec![],
|
|
links: vec![],
|
|
},
|
|
)),
|
|
spec: Some(Spec::UserSpec(charybdis::charybdis::core::UserSpec {
|
|
profile: None,
|
|
member_of: vec!["backend-team".to_string()],
|
|
})),
|
|
};
|
|
alice.annotations.insert(
|
|
"keycloak.com/email".to_string(),
|
|
"alice@example.com".to_string(),
|
|
);
|
|
repository.create(&alice).await.unwrap();
|
|
|
|
// Create User "bob" with keycloak email annotation
|
|
let mut bob = Entity {
|
|
id: "user-bob".to_string(),
|
|
kind: "User".to_string(),
|
|
annotations: HashMap::new(),
|
|
created_at: None,
|
|
updated_at: None,
|
|
metadata: Some(Metadata::UserMetadata(
|
|
charybdis::charybdis::core::UserMetadata {
|
|
name: "bob".to_string(),
|
|
namespace: "default".to_string(),
|
|
description: "Bob".to_string(),
|
|
labels: HashMap::new(),
|
|
tags: vec![],
|
|
links: vec![],
|
|
},
|
|
)),
|
|
spec: Some(Spec::UserSpec(charybdis::charybdis::core::UserSpec {
|
|
profile: None,
|
|
member_of: vec!["backend-team".to_string()],
|
|
})),
|
|
};
|
|
bob.annotations.insert(
|
|
"keycloak.com/email".to_string(),
|
|
"bob@example.com".to_string(),
|
|
);
|
|
repository.create(&bob).await.unwrap();
|
|
|
|
// Create handlers
|
|
let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap();
|
|
let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new(
|
|
client.clone(),
|
|
repository.clone(),
|
|
HashMap::new(),
|
|
));
|
|
|
|
let handler = charybdis_defectdojo::handlers::ProductHandler::new(
|
|
client,
|
|
repository.clone(),
|
|
HashMap::new(),
|
|
config,
|
|
engagement_handler,
|
|
);
|
|
|
|
// Create component owned by "backend-team"
|
|
let component = make_component_entity("", "orders-api", "backend-team");
|
|
let component = repository.create(&component).await.unwrap();
|
|
|
|
// Trigger handler
|
|
let result = handler.handle_create(&component).await;
|
|
assert!(result.is_ok(), "handle_create failed: {:?}", result.err());
|
|
|
|
// Verify annotations
|
|
let updated = repository.get_by_id(&component.id).await.unwrap().unwrap();
|
|
assert_eq!(
|
|
updated.annotations.get("defectdojo.com/product-id"),
|
|
Some(&"10".to_string())
|
|
);
|
|
assert_eq!(
|
|
updated.annotations.get("defectdojo.com/engagement-id"),
|
|
Some(&"20".to_string())
|
|
);
|
|
// Owner member IDs should be set
|
|
assert!(
|
|
updated
|
|
.annotations
|
|
.contains_key("defectdojo.com/owner-member-ids"),
|
|
"Expected owner-member-ids annotation"
|
|
);
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────
|
|
// Engagement auto-creation disabled
|
|
// ──────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires DATABASE_URL
|
|
async fn test_product_creation_without_engagement() {
|
|
let pool = setup_db().await;
|
|
let repository = Arc::new(EntityRepository::new(pool.clone()));
|
|
let mock_server = MockServer::start().await;
|
|
|
|
// Mock: POST /api/v2/products/ → product id=55
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/v2/products/"))
|
|
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
|
"id": 55,
|
|
"name": "simple-service",
|
|
"prod_type": 1
|
|
})))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
// No engagement mock — should NOT be called
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/v2/engagements/"))
|
|
.respond_with(ResponseTemplate::new(201).set_body_json(json!({"id": 999})))
|
|
.expect(0) // MUST NOT be called
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
let mut config = make_config(&mock_server.uri());
|
|
config.default_engagement.auto_create = false;
|
|
|
|
let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap();
|
|
let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new(
|
|
client.clone(),
|
|
repository.clone(),
|
|
HashMap::new(),
|
|
));
|
|
|
|
let handler = charybdis_defectdojo::handlers::ProductHandler::new(
|
|
client,
|
|
repository.clone(),
|
|
HashMap::new(),
|
|
config,
|
|
engagement_handler,
|
|
);
|
|
|
|
let entity = make_component_entity("", "simple-service", "");
|
|
let entity = repository.create(&entity).await.unwrap();
|
|
|
|
let result = handler.handle_create(&entity).await;
|
|
assert!(result.is_ok(), "handle_create failed: {:?}", result.err());
|
|
|
|
// Verify only product-id annotation (no engagement-id)
|
|
let updated = repository.get_by_id(&entity.id).await.unwrap().unwrap();
|
|
assert_eq!(
|
|
updated.annotations.get("defectdojo.com/product-id"),
|
|
Some(&"55".to_string())
|
|
);
|
|
assert!(!updated
|
|
.annotations
|
|
.contains_key("defectdojo.com/engagement-id"));
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────
|
|
// DefectDojo API error handling
|
|
// ──────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires DATABASE_URL
|
|
async fn test_product_creation_handles_api_error() {
|
|
let pool = setup_db().await;
|
|
let repository = Arc::new(EntityRepository::new(pool.clone()));
|
|
let mock_server = MockServer::start().await;
|
|
|
|
// Mock: POST /api/v2/products/ → 400 Bad Request
|
|
Mock::given(method("POST"))
|
|
.and(path("/api/v2/products/"))
|
|
.respond_with(
|
|
ResponseTemplate::new(400)
|
|
.set_body_json(json!({"name": ["This field may not be blank."]})),
|
|
)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
let config = make_config(&mock_server.uri());
|
|
let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap();
|
|
let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new(
|
|
client.clone(),
|
|
repository.clone(),
|
|
HashMap::new(),
|
|
));
|
|
|
|
let handler = charybdis_defectdojo::handlers::ProductHandler::new(
|
|
client,
|
|
repository.clone(),
|
|
HashMap::new(),
|
|
config,
|
|
engagement_handler,
|
|
);
|
|
|
|
let entity = make_component_entity("", "bad-entity", "team-x");
|
|
let entity = repository.create(&entity).await.unwrap();
|
|
|
|
let result = handler.handle_create(&entity).await;
|
|
assert!(result.is_err(), "Expected error on 400 response");
|
|
}
|