use alien_aws_clients::codebuild::*;
use alien_aws_clients::iam::{CreateRoleRequest, IamApi, IamClient};
use alien_client_core::{Error, ErrorData, Result};
use async_trait::async_trait;
use reqwest::Client;
use std::collections::HashSet;
use std::path::PathBuf as StdPathBuf;
use std::sync::Mutex;
use std::time::Duration;
use test_context::{test_context, AsyncTestContext};
use tracing::{info, warn};
use uuid::Uuid;
struct CodeBuildTestContext {
codebuild_client: CodeBuildClient,
iam_client: IamClient,
created_projects: Mutex<HashSet<String>>,
service_role_arn: String,
service_role_name: String,
}
impl CodeBuildTestContext {
fn track_project(&self, name: &str) {
let mut projects = self.created_projects.lock().unwrap();
projects.insert(name.to_string());
info!("๐ Tracking project for cleanup: {}", name);
}
async fn cleanup_project(&self, name: &str) {
info!("๐งน Cleaning up project: {}", name);
let req = DeleteProjectRequest {
name: name.to_string(),
};
match self.codebuild_client.delete_project(req).await {
Ok(_) => info!("โ
Project {} deleted successfully", name),
Err(e) => {
if !matches!(e.error, Some(ErrorData::RemoteResourceNotFound { .. })) {
warn!("Failed to delete project {} during cleanup: {:?}", name, e);
}
}
}
}
async fn create_project_with_retry(
&self,
create_req: CreateProjectRequest,
) -> Result<CreateProjectResponse> {
let mut delay = Duration::from_secs(1);
for attempt in 1..=5 {
match self
.codebuild_client
.create_project(create_req.clone())
.await
{
Ok(response) => return Ok(response),
Err(e) => {
if matches!(e.error, Some(ErrorData::RemoteResourceConflict { .. }))
&& attempt < 5
{
info!(
"Project already exists, retrying in {}s... (attempt {}/5)",
delay.as_secs(),
attempt
);
tokio::time::sleep(delay).await;
delay = delay * 2; continue;
} else {
return Err(e);
}
}
}
}
unreachable!()
}
fn get_project_name(&self) -> String {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
format!(
"alien-test-project-{}-{}",
timestamp,
Uuid::new_v4().simple()
)
}
}
impl AsyncTestContext for CodeBuildTestContext {
async fn setup() -> Self {
let root: StdPathBuf = workspace_root::get_workspace_root();
dotenvy::from_path(root.join(".env.test")).expect("Failed to load .env.test");
tracing_subscriber::fmt::try_init().ok();
let region = std::env::var("AWS_MANAGEMENT_REGION")
.expect("AWS_MANAGEMENT_REGION must be set in .env.test");
let access_key = std::env::var("AWS_MANAGEMENT_ACCESS_KEY_ID")
.expect("AWS_MANAGEMENT_ACCESS_KEY_ID must be set in .env.test");
let secret_key = std::env::var("AWS_MANAGEMENT_SECRET_ACCESS_KEY")
.expect("AWS_MANAGEMENT_SECRET_ACCESS_KEY must be set in .env.test");
let account_id = std::env::var("AWS_MANAGEMENT_ACCOUNT_ID")
.expect("AWS_MANAGEMENT_ACCOUNT_ID must be set in .env.test");
let aws_config = alien_aws_clients::AwsClientConfig {
account_id,
region,
credentials: alien_aws_clients::AwsCredentials::AccessKeys {
access_key_id: access_key,
secret_access_key: secret_key,
session_token: std::env::var("AWS_SESSION_TOKEN").ok(),
},
service_overrides: None,
};
let codebuild_client = CodeBuildClient::new(Client::new(), aws_config.clone());
let iam_client = IamClient::new(Client::new(), aws_config);
let role_name = format!("alien-test-codebuild-role-{}", Uuid::new_v4().simple());
info!("๐ง Creating IAM service role for tests: {}", role_name);
let assume_role_policy = r#"{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "codebuild.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}"#
.to_string();
let role_request = CreateRoleRequest::builder()
.role_name(role_name.clone())
.assume_role_policy_document(assume_role_policy)
.build();
let role = iam_client.create_role(role_request).await.expect(
"Failed to create IAM role for CodeBuild. Check your test credentials and permissions.",
);
let service_role_arn = role.create_role_result.role.arn.clone();
let policy_document = r#"{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}"#
.to_string();
iam_client
.put_role_policy(&role_name, "CodeBuildDefaultPolicy", &policy_document)
.await
.expect("Failed to attach policy to role");
info!("Waiting 10 seconds for IAM propagation...");
tokio::time::sleep(Duration::from_secs(10)).await;
info!("IAM role should be ready.");
Self {
codebuild_client,
iam_client,
created_projects: Mutex::new(HashSet::new()),
service_role_arn,
service_role_name: role_name,
}
}
async fn teardown(self) {
info!("๐งน Starting CodeBuild test cleanup...");
let projects_to_cleanup = self.created_projects.lock().unwrap().clone();
for name in projects_to_cleanup {
self.cleanup_project(&name).await;
}
info!("๐งน Cleaning up IAM role: {}", self.service_role_name);
match self
.iam_client
.delete_role_policy(&self.service_role_name, "CodeBuildDefaultPolicy")
.await
{
Ok(_) => info!("โ
Deleted role policy successfully."),
Err(e) => warn!(
"Failed to delete policy from role {} during cleanup: {:?}",
self.service_role_name, e
),
}
match self.iam_client.delete_role(&self.service_role_name).await {
Ok(_) => info!("โ
Deleted IAM role successfully."),
Err(e) => warn!(
"Failed to delete role {} during cleanup: {:?}",
self.service_role_name, e
),
}
info!("โ
CodeBuild test cleanup completed");
}
}
async fn wait_for_build_to_finish(ctx: &CodeBuildTestContext, build_id: &str) -> String {
info!("Polling build status for build ID: {}", build_id);
for _ in 0..60 {
let get_builds_req = BatchGetBuildsRequest::builder()
.ids(vec![build_id.to_string()])
.build();
let get_builds_res = ctx
.codebuild_client
.batch_get_builds(get_builds_req)
.await
.expect("BatchGetBuilds failed during polling");
if let Some(build) = get_builds_res.builds.as_ref().and_then(|b| b.first()) {
if let Some(status) = build.build_status.as_deref() {
info!("Build {} status: {}", build_id, status);
match status {
"SUCCEEDED" | "FAILED" | "FAULT" | "TIMED_OUT" | "STOPPED" => {
return status.to_string()
}
_ => {} }
}
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
panic!("Build {} did not finish within timeout", build_id);
}
#[test_context(CodeBuildTestContext)]
#[tokio::test]
async fn test_codebuild_project_and_build_lifecycle(ctx: &mut CodeBuildTestContext) {
let project_name = ctx.get_project_name();
info!(
"๐ Testing CodeBuild lifecycle with project: {}",
project_name
);
let create_req = CreateProjectRequest::builder()
.name(project_name.clone())
.service_role(ctx.service_role_arn.clone())
.source(
ProjectSource::builder()
.r#type("NO_SOURCE".to_string())
.buildspec(
r#"version: 0.2
phases:
build:
commands:
- echo "Hello from Alien e2e test!"
"#
.to_string(),
)
.build(),
)
.artifacts(
ProjectArtifacts::builder()
.r#type("NO_ARTIFACTS".to_string())
.build(),
)
.environment(
ProjectEnvironment::builder()
.r#type("LINUX_CONTAINER".to_string())
.image("aws/codebuild/standard:7.0".to_string())
.compute_type("BUILD_GENERAL1_SMALL".to_string())
.build(),
)
.build();
let create_res = ctx
.codebuild_client
.create_project(create_req)
.await
.expect("CreateProject failed");
ctx.track_project(&project_name);
assert_eq!(create_res.project.name.as_ref(), Some(&project_name));
info!("โ
CreateProject successful");
let batch_get_req = BatchGetProjectsRequest::builder()
.names(vec![project_name.clone()])
.build();
let batch_get_res = ctx
.codebuild_client
.batch_get_projects(batch_get_req)
.await
.expect("BatchGetProjects failed");
assert!(batch_get_res
.projects
.as_ref()
.map_or(false, |p| !p.is_empty()));
info!("โ
BatchGetProjects successful");
let update_req = UpdateProjectRequest::builder()
.name(project_name.clone())
.description("Updated description".to_string())
.build();
let update_res = ctx
.codebuild_client
.update_project(update_req)
.await
.expect("UpdateProject failed");
assert_eq!(
update_res.project.description.as_deref(),
Some("Updated description")
);
info!("โ
UpdateProject successful");
let start_build_req = StartBuildRequest::builder()
.project_name(project_name.clone())
.build();
let start_build_res = ctx
.codebuild_client
.start_build(start_build_req)
.await
.expect("StartBuild failed");
let build_id = start_build_res
.build
.id
.clone()
.expect("Build should have an ID");
info!("โ
StartBuild successful, build ID: {}", build_id);
let status = wait_for_build_to_finish(ctx, &build_id).await;
assert_eq!(status, "SUCCEEDED", "Build did not succeed");
info!("โ
BatchGetBuilds polling successful, build SUCCEEDED");
let stop_project_name = ctx.get_project_name();
info!("๐ Testing StopBuild with project: {}", stop_project_name);
let stop_create_req = CreateProjectRequest::builder()
.name(stop_project_name.clone())
.service_role(ctx.service_role_arn.clone())
.source(
ProjectSource::builder()
.r#type("NO_SOURCE".to_string())
.buildspec(
r#"version: 0.2
phases:
build:
commands:
- echo "Starting long build..."
- sleep 120
- echo "This should not be printed."
"#
.to_string(),
)
.build(),
)
.artifacts(
ProjectArtifacts::builder()
.r#type("NO_ARTIFACTS".to_string())
.build(),
)
.environment(
ProjectEnvironment::builder()
.r#type("LINUX_CONTAINER".to_string())
.image("aws/codebuild/standard:7.0".to_string())
.compute_type("BUILD_GENERAL1_SMALL".to_string())
.build(),
)
.build();
ctx.codebuild_client
.create_project(stop_create_req)
.await
.expect("CreateProject for stop test failed");
ctx.track_project(&stop_project_name);
let stop_start_req = StartBuildRequest::builder()
.project_name(stop_project_name.clone())
.build();
let stop_start_res = ctx
.codebuild_client
.start_build(stop_start_req)
.await
.expect("StartBuild for stop test failed");
let stop_build_id = stop_start_res
.build
.id
.clone()
.expect("Stop build should have an ID");
info!("Waiting 5 seconds before stopping build {}", stop_build_id);
tokio::time::sleep(Duration::from_secs(5)).await;
let stop_req = StopBuildRequest::builder()
.id(stop_build_id.clone())
.build();
let stop_res = ctx
.codebuild_client
.stop_build(stop_req)
.await
.expect("StopBuild failed");
assert!(matches!(
stop_res.build.build_status.as_deref(),
Some("STOPPING") | Some("STOPPED")
));
let stop_status = wait_for_build_to_finish(ctx, &stop_build_id).await;
assert_eq!(stop_status, "STOPPED");
info!("โ
StopBuild successful");
let fail_project_name = ctx.get_project_name();
info!("๐ Testing RetryBuild with project: {}", fail_project_name);
let fail_create_req = CreateProjectRequest::builder()
.name(fail_project_name.clone())
.service_role(ctx.service_role_arn.clone())
.source(
ProjectSource::builder()
.r#type("NO_SOURCE".to_string())
.buildspec(
r#"version: 0.2
phases:
build:
commands:
- echo "This build will fail."
- exit 1
"#
.to_string(),
)
.build(),
)
.artifacts(
ProjectArtifacts::builder()
.r#type("NO_ARTIFACTS".to_string())
.build(),
)
.environment(
ProjectEnvironment::builder()
.r#type("LINUX_CONTAINER".to_string())
.image("aws/codebuild/standard:7.0".to_string())
.compute_type("BUILD_GENERAL1_SMALL".to_string())
.build(),
)
.build();
ctx.codebuild_client
.create_project(fail_create_req)
.await
.expect("CreateProject for retry test failed");
ctx.track_project(&fail_project_name);
let fail_start_req = StartBuildRequest::builder()
.project_name(fail_project_name.clone())
.build();
let fail_start_res = ctx
.codebuild_client
.start_build(fail_start_req)
.await
.expect("StartBuild for retry test failed");
let fail_build_id = fail_start_res
.build
.id
.clone()
.expect("Fail build should have an ID");
let fail_status = wait_for_build_to_finish(ctx, &fail_build_id).await;
assert_eq!(fail_status, "FAILED");
info!("โ
Confirmed original build failed");
let retry_req = RetryBuildRequest::builder()
.id(fail_build_id.clone())
.build();
let retry_res = ctx
.codebuild_client
.retry_build(retry_req)
.await
.expect("RetryBuild failed");
let retry_build_id = retry_res.build.id.expect("Retry build should have an ID");
info!("โ
RetryBuild successful, new build ID: {}", retry_build_id);
let retry_status = wait_for_build_to_finish(ctx, &retry_build_id).await;
assert_eq!(retry_status, "FAILED"); info!("โ
Confirmed retried build also failed as expected");
info!("๐ Testing BatchDeleteBuilds");
let delete_req = BatchDeleteBuildsRequest::builder()
.ids(vec![
build_id.clone(),
stop_build_id.clone(),
fail_build_id.clone(),
])
.build();
let delete_res = ctx
.codebuild_client
.batch_delete_builds(delete_req)
.await
.expect("BatchDeleteBuilds failed");
info!(
"BatchDeleteBuilds response - deleted: {:?}, not deleted: {:?}",
delete_res.builds_deleted, delete_res.builds_not_deleted
);
let total_response_builds = delete_res.builds_deleted.as_ref().map_or(0, |v| v.len())
+ delete_res
.builds_not_deleted
.as_ref()
.map_or(0, |v| v.len());
assert_eq!(
total_response_builds, 3,
"Expected response for all 3 build IDs"
);
info!("โ
BatchDeleteBuilds successful");
ctx.cleanup_project(&project_name).await;
let mut projects = ctx.created_projects.lock().unwrap();
projects.remove(&project_name); info!("โ
DeleteProject successful");
}