#![cfg(test)]
use alien_bindings::{
traits::{
ArtifactRegistry, ArtifactRegistryPermissions, AwsCrossAccountAccess, BindingsProviderApi,
ComputeServiceType, CrossAccountAccess, CrossAccountPermissions, GcpCrossAccountAccess,
},
BindingsProvider,
};
#[cfg(feature = "grpc")]
use alien_bindings::{grpc::run_grpc_server, providers::grpc_provider::GrpcBindingsProvider};
use alien_core::bindings::BindingValue;
use async_trait::async_trait;
use base64::{engine::general_purpose::STANDARD as base64_standard, Engine as _};
use rstest::rstest;
use serde_json;
use std::path::PathBuf as StdPathBuf;
use std::sync::Mutex;
use std::{collections::HashMap, env, sync::Arc, time::Duration};
use tempfile::TempDir;
use test_context::AsyncTestContext;
use tokio::net::TcpListener;
use tokio::task::JoinHandle;
use uuid::Uuid;
use workspace_root::get_workspace_root;
#[cfg(feature = "aws")]
use {alien_aws_clients::AwsClientConfig, reqwest::Client};
#[cfg(feature = "gcp")]
use alien_gcp_clients::{artifactregistry::ArtifactRegistryClient, GcpClientConfig};
#[cfg(feature = "azure")]
use alien_azure_clients::{
containerregistry::{AzureContainerRegistryClient, ContainerRegistryApi},
AzureClientConfig,
};
const GRPC_BINDING_NAME: &str = "test-grpc-artifact-registry-binding";
fn load_test_env() {
let root: StdPathBuf = get_workspace_root();
dotenvy::from_path(root.join(".env.test")).expect("Failed to load .env.test");
}
#[async_trait]
pub trait ArtifactRegistryTestContext: AsyncTestContext + Send + Sync {
async fn get_artifact_registry(&self) -> Arc<dyn ArtifactRegistry>;
fn provider_name(&self) -> &'static str;
}
struct LocalProviderArtifactRegistryTestContext {
artifact_registry: Arc<dyn ArtifactRegistry>,
_temp_dir: TempDir,
_registry_handle: JoinHandle<()>,
}
impl AsyncTestContext for LocalProviderArtifactRegistryTestContext {
async fn setup() -> Self {
load_test_env();
let binding_name = "test-local-artifact-registry";
let temp_dir = tempfile::tempdir()
.expect("Failed to create temp dir for local artifact registry test");
#[cfg(all(feature = "local", unix))]
{
use container_registry::ContainerRegistry;
let mut registry = ContainerRegistry::builder().build_for_testing();
registry.bind(([127, 0, 0, 1], 0).into());
let running_registry = registry.run_in_background();
let bound_addr = running_registry.bound_addr();
let registry_endpoint = format!("localhost:{}", bound_addr.port());
let registry_handle = tokio::spawn(async move {
let _guard = running_registry;
tokio::time::sleep(Duration::from_secs(300)).await;
});
let binding = alien_core::bindings::ArtifactRegistryBinding::local(
registry_endpoint.clone(),
None,
);
let mut env_map: HashMap<String, String> = env::vars().collect();
let binding_json =
serde_json::to_string(&binding).expect("Failed to serialize binding");
env_map.insert(
alien_core::bindings::binding_env_var_name(binding_name),
binding_json,
);
env_map.insert("ALIEN_DEPLOYMENT_TYPE".to_string(), "local".to_string());
let provider = Arc::new(
BindingsProvider::from_env(env_map)
.await
.expect("Failed to load bindings provider"),
);
let artifact_registry = provider
.load_artifact_registry(binding_name)
.await
.unwrap_or_else(|e| {
panic!(
"Failed to load Local artifact registry for binding '{}' using registry endpoint '{}': {:?}",
binding_name, registry_endpoint, e
)
});
Self {
artifact_registry,
_temp_dir: temp_dir,
_registry_handle: registry_handle,
}
}
#[cfg(all(feature = "local", not(unix)))]
{
panic!("Local artifact registry tests require Unix because container-registry uses symlinks");
}
#[cfg(not(feature = "local"))]
{
panic!("Local feature is required for LocalProviderArtifactRegistryTestContext");
}
}
async fn teardown(self) {
self._registry_handle.abort();
}
}
#[async_trait]
impl ArtifactRegistryTestContext for LocalProviderArtifactRegistryTestContext {
async fn get_artifact_registry(&self) -> Arc<dyn ArtifactRegistry> {
self.artifact_registry.clone()
}
fn provider_name(&self) -> &'static str {
"local"
}
}
#[cfg(feature = "grpc")]
struct GrpcProviderArtifactRegistryTestContext {
artifact_registry: Arc<dyn ArtifactRegistry>,
_server_handle:
JoinHandle<Result<(), alien_error::AlienError<alien_bindings::error::ErrorData>>>,
_temp_data_dir: TempDir,
_registry_handle: JoinHandle<()>,
}
#[cfg(feature = "grpc")]
impl AsyncTestContext for GrpcProviderArtifactRegistryTestContext {
async fn setup() -> Self {
load_test_env();
let temp_data_dir =
tempfile::tempdir().expect("Failed to create temp dir for gRPC server test");
#[cfg(all(feature = "local", unix))]
{
use container_registry::ContainerRegistry;
let mut registry = ContainerRegistry::builder().build_for_testing();
registry.bind(([127, 0, 0, 1], 0).into());
let running_registry = registry.run_in_background();
let bound_addr = running_registry.bound_addr();
let registry_endpoint = format!("localhost:{}", bound_addr.port());
let registry_handle = tokio::spawn(async move {
let _guard = running_registry;
tokio::time::sleep(Duration::from_secs(300)).await;
});
let server_binding = alien_core::bindings::ArtifactRegistryBinding::local(
registry_endpoint.clone(),
None,
);
let mut server_provider_env_map: HashMap<String, String> = env::vars().collect();
let server_binding_json =
serde_json::to_string(&server_binding).expect("Failed to serialize server binding");
server_provider_env_map.insert(
alien_core::bindings::binding_env_var_name(GRPC_BINDING_NAME),
server_binding_json,
);
server_provider_env_map
.insert("ALIEN_DEPLOYMENT_TYPE".to_string(), "local".to_string());
let local_provider_for_server = Arc::new(
BindingsProvider::from_env(server_provider_env_map)
.await
.expect("Failed to load bindings provider"),
);
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("Failed to bind to random port");
let addr = listener.local_addr().expect("Failed to get local address");
drop(listener);
let server_addr_str = addr.to_string();
let server_addr_for_spawn = server_addr_str.clone();
let server_handle = tokio::spawn(async move {
let handles = run_grpc_server(local_provider_for_server, &server_addr_for_spawn)
.await
.unwrap();
handles
.readiness_receiver
.await
.expect("Server should become ready");
handles.server_task.await.unwrap()
});
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let mut service_provider_env_map: HashMap<String, String> = env::vars().collect();
service_provider_env_map.insert(
"ALIEN_BINDINGS_GRPC_ADDRESS".to_string(),
server_addr_str.clone(),
);
service_provider_env_map
.insert("ALIEN_DEPLOYMENT_TYPE".to_string(), "grpc".to_string());
let grpc_provider = GrpcBindingsProvider::new_with_env(service_provider_env_map)
.expect("Failed to load bindings provider");
let artifact_registry_client = grpc_provider
.load_artifact_registry(GRPC_BINDING_NAME)
.await
.unwrap_or_else(|e| {
panic!(
"Failed to load Grpc artifact registry for binding '{}' using ALIEN_BINDINGS_GRPC_ADDRESS='{}': {:?}",
GRPC_BINDING_NAME, server_addr_str, e
)
});
Self {
artifact_registry: artifact_registry_client,
_server_handle: server_handle,
_temp_data_dir: temp_data_dir,
_registry_handle: registry_handle,
}
}
#[cfg(all(feature = "local", not(unix)))]
{
panic!("Local artifact registry tests require Unix because container-registry uses symlinks");
}
#[cfg(not(feature = "local"))]
{
panic!("Local feature is required for GrpcProviderArtifactRegistryTestContext");
}
}
async fn teardown(self) {
self._server_handle.abort();
self._registry_handle.abort();
}
}
#[cfg(feature = "grpc")]
#[async_trait]
impl ArtifactRegistryTestContext for GrpcProviderArtifactRegistryTestContext {
async fn get_artifact_registry(&self) -> Arc<dyn ArtifactRegistry> {
self.artifact_registry.clone()
}
fn provider_name(&self) -> &'static str {
"grpc"
}
}
#[cfg(feature = "aws")]
struct AwsProviderArtifactRegistryTestContext {
artifact_registry: Arc<dyn ArtifactRegistry>,
}
#[cfg(feature = "aws")]
impl AsyncTestContext for AwsProviderArtifactRegistryTestContext {
async fn setup() -> Self {
load_test_env();
let binding_name = "test-aws-artifact-registry";
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 pull_role_arn = std::env::var("ALIEN_TEST_AWS_ECR_PULL_ROLE_ARN").ok();
let push_role_arn = std::env::var("ALIEN_TEST_AWS_ECR_PUSH_ROLE_ARN").ok();
let binding = alien_core::bindings::ArtifactRegistryBinding::ecr(
"test".to_string(),
pull_role_arn,
push_role_arn,
);
let mut env_map: HashMap<String, String> = HashMap::new();
env_map.insert("AWS_REGION".to_string(), region);
env_map.insert("AWS_ACCESS_KEY_ID".to_string(), access_key);
env_map.insert("AWS_SECRET_ACCESS_KEY".to_string(), secret_key);
env_map.insert("AWS_ACCOUNT_ID".to_string(), account_id);
env_map.insert("ALIEN_DEPLOYMENT_TYPE".to_string(), "aws".to_string());
let binding_json = serde_json::to_string(&binding).expect("Failed to serialize binding");
env_map.insert(
alien_core::bindings::binding_env_var_name(binding_name),
binding_json,
);
let provider = BindingsProvider::from_env(env_map)
.await
.expect("Failed to load bindings provider");
let artifact_registry = provider
.load_artifact_registry(binding_name)
.await
.unwrap_or_else(|e| {
panic!(
"Failed to load AWS artifact registry for binding '{}': {:?}",
binding_name, e
)
});
Self { artifact_registry }
}
}
#[cfg(feature = "aws")]
#[async_trait]
impl ArtifactRegistryTestContext for AwsProviderArtifactRegistryTestContext {
async fn get_artifact_registry(&self) -> Arc<dyn ArtifactRegistry> {
self.artifact_registry.clone()
}
fn provider_name(&self) -> &'static str {
"aws"
}
}
#[cfg(feature = "gcp")]
struct GcpProviderArtifactRegistryTestContext {
artifact_registry: Arc<dyn ArtifactRegistry>,
}
#[cfg(feature = "gcp")]
impl AsyncTestContext for GcpProviderArtifactRegistryTestContext {
async fn setup() -> Self {
load_test_env();
let binding_name = "test-gcp-artifact-registry";
let service_account_key_json = env::var("GOOGLE_MANAGEMENT_SERVICE_ACCOUNT_KEY")
.expect("GOOGLE_MANAGEMENT_SERVICE_ACCOUNT_KEY must be set in .env.test");
let gcp_region = env::var("GOOGLE_MANAGEMENT_REGION")
.expect("GOOGLE_MANAGEMENT_REGION must be set in .env.test");
let credential_value: serde_json::Value = serde_json::from_str(&service_account_key_json)
.expect("GOOGLE_MANAGEMENT_SERVICE_ACCOUNT_KEY must be valid JSON");
let client_email = credential_value
.get("client_email")
.and_then(|v| v.as_str())
.expect("client_email field must be present in service account key")
.to_string();
let binding = alien_core::bindings::ArtifactRegistryBinding::gar(
"test-repo",
Some(client_email.clone()),
Some(client_email),
);
let mut env_map: HashMap<String, String> = HashMap::new();
env_map.insert(
"GOOGLE_SERVICE_ACCOUNT_KEY".to_string(),
service_account_key_json,
);
env_map.insert("GCP_REGION".to_string(), gcp_region);
env_map.insert("ALIEN_DEPLOYMENT_TYPE".to_string(), "gcp".to_string());
let binding_json = serde_json::to_string(&binding).expect("Failed to serialize binding");
env_map.insert(
alien_core::bindings::binding_env_var_name(binding_name),
binding_json,
);
let provider = BindingsProvider::from_env(env_map)
.await
.expect("Failed to load bindings provider");
let artifact_registry = provider
.load_artifact_registry(binding_name)
.await
.unwrap_or_else(|e| {
panic!(
"Failed to load GCP artifact registry for binding '{}': {:?}",
binding_name, e
)
});
Self { artifact_registry }
}
}
#[cfg(feature = "gcp")]
#[async_trait]
impl ArtifactRegistryTestContext for GcpProviderArtifactRegistryTestContext {
async fn get_artifact_registry(&self) -> Arc<dyn ArtifactRegistry> {
self.artifact_registry.clone()
}
fn provider_name(&self) -> &'static str {
"gcp"
}
}
#[cfg(feature = "azure")]
struct AzureProviderArtifactRegistryTestContext {
artifact_registry: Arc<dyn ArtifactRegistry>,
}
#[cfg(feature = "azure")]
impl AsyncTestContext for AzureProviderArtifactRegistryTestContext {
async fn setup() -> Self {
load_test_env();
let binding_name = "test-azure-artifact-registry";
let resource_group = env::var("ALIEN_TEST_AZURE_RESOURCE_GROUP");
if resource_group.is_err() {
panic!(
"Skipping Azure artifact registry test: ALIEN_TEST_AZURE_RESOURCE_GROUP not set."
);
}
let resource_group_name = resource_group.unwrap();
let registry_name = env::var("ALIEN_TEST_AZURE_REGISTRY_NAME").expect(
"Skipping Azure artifact registry test: ALIEN_TEST_AZURE_REGISTRY_NAME not set.",
);
let binding = alien_core::bindings::ArtifactRegistryBinding::acr(
registry_name,
resource_group_name.clone(),
);
let mut env_map: HashMap<String, String> = HashMap::new();
env_map.insert(
"AZURE_TENANT_ID".to_string(),
env::var("AZURE_MANAGEMENT_TENANT_ID").unwrap(),
);
env_map.insert(
"AZURE_CLIENT_ID".to_string(),
env::var("AZURE_MANAGEMENT_CLIENT_ID").unwrap(),
);
env_map.insert(
"AZURE_CLIENT_SECRET".to_string(),
env::var("AZURE_MANAGEMENT_CLIENT_SECRET").unwrap(),
);
env_map.insert(
"AZURE_SUBSCRIPTION_ID".to_string(),
env::var("AZURE_MANAGEMENT_SUBSCRIPTION_ID").unwrap(),
);
env_map.insert("AZURE_RESOURCE_GROUP".to_string(), resource_group_name);
env_map.insert("ALIEN_DEPLOYMENT_TYPE".to_string(), "azure".to_string());
let binding_json = serde_json::to_string(&binding).expect("Failed to serialize binding");
env_map.insert(
alien_core::bindings::binding_env_var_name(binding_name),
binding_json,
);
let provider = BindingsProvider::from_env(env_map)
.await
.expect("Failed to load bindings provider");
let artifact_registry = provider
.load_artifact_registry(binding_name)
.await
.unwrap_or_else(|e| {
panic!(
"Failed to load Azure artifact registry for binding '{}': {:?}",
binding_name, e
)
});
Self { artifact_registry }
}
}
#[cfg(feature = "azure")]
#[async_trait]
impl ArtifactRegistryTestContext for AzureProviderArtifactRegistryTestContext {
async fn get_artifact_registry(&self) -> Arc<dyn ArtifactRegistry> {
self.artifact_registry.clone()
}
fn provider_name(&self) -> &'static str {
"azure"
}
}
#[cfg(feature = "kubernetes")]
struct KubernetesProviderArtifactRegistryTestContext {
artifact_registry: Arc<dyn ArtifactRegistry>,
}
#[cfg(feature = "kubernetes")]
impl AsyncTestContext for KubernetesProviderArtifactRegistryTestContext {
async fn setup() -> Self {
load_test_env();
let binding_name = "test-k8s-artifact-registry";
let mut env_map: HashMap<String, String> = env::vars().collect();
env_map.insert(
format!(
"ALIEN_{}_NAMESPACE",
binding_name.replace('-', "_").to_uppercase()
),
"default".to_string(),
);
env_map.insert(
"ALIEN_DEPLOYMENT_TYPE".to_string(),
"kubernetes".to_string(),
);
let provider = BindingsProvider::from_env(env_map)
.await
.expect("Failed to load bindings provider");
let artifact_registry = provider
.load_artifact_registry(binding_name)
.await
.unwrap_or_else(|e| {
if let Some(alien_bindings::error::ErrorData::OperationNotSupported { .. }) = e.error {
panic!("SKIP: Kubernetes provider does not support artifact registry operations yet");
}
panic!("Failed to load Kubernetes artifact registry for binding '{}': {:?}", binding_name, e)
});
Self { artifact_registry }
}
}
#[cfg(feature = "kubernetes")]
#[async_trait]
impl ArtifactRegistryTestContext for KubernetesProviderArtifactRegistryTestContext {
async fn get_artifact_registry(&self) -> Arc<dyn ArtifactRegistry> {
self.artifact_registry.clone()
}
fn provider_name(&self) -> &'static str {
"kubernetes"
}
}
fn get_real_service_account_email() -> Option<String> {
if let Ok(credential_json) = env::var("GOOGLE_MANAGEMENT_SERVICE_ACCOUNT_KEY") {
if let Ok(credential_value) = serde_json::from_str::<serde_json::Value>(&credential_json) {
if let Some(email) = credential_value
.get("client_email")
.and_then(|v| v.as_str())
{
return Some(format!("serviceAccount:{}", email));
}
}
}
None
}
fn generate_unique_repo_name(provider_name: &str) -> String {
format!("alien-test-{}-{}", provider_name, Uuid::new_v4().simple())
}
async fn wait_for_repository_ready(
artifact_registry: &Arc<dyn ArtifactRegistry>,
repo_id: &str,
provider_name: &str,
max_wait_seconds: u64,
) -> bool {
let start_time = std::time::Instant::now();
let max_duration = Duration::from_secs(max_wait_seconds);
loop {
match artifact_registry.get_repository(repo_id).await {
Ok(_repository) => {
return true;
}
Err(e) => {
if let Some(alien_bindings::error::ErrorData::ResourceNotFound { .. }) = e.error {
if start_time.elapsed() > max_duration {
panic!(
"[{}] Repository {} did not become ready within {} seconds",
provider_name, repo_id, max_wait_seconds
);
}
tokio::time::sleep(Duration::from_secs(2)).await;
} else {
panic!("[{}] Failed to get repository: {:?}", provider_name, e);
}
}
}
}
}
#[rstest]
#[cfg_attr(feature = "local", case::local(LocalProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "grpc", case::grpc(GrpcProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "aws", case::aws(AwsProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "azure", case::azure(AzureProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "gcp", case::gcp(GcpProviderArtifactRegistryTestContext::setup().await))]
#[cfg(any(
feature = "local",
feature = "grpc",
feature = "aws",
feature = "azure",
feature = "gcp"
))]
#[tokio::test]
async fn test_create_repository_and_get_status(#[case] ctx: impl ArtifactRegistryTestContext) {
let artifact_registry = ctx.get_artifact_registry().await;
let provider_name = ctx.provider_name();
let repo_name = generate_unique_repo_name(provider_name);
let create_response = artifact_registry
.create_repository(&repo_name)
.await
.unwrap_or_else(|e| {
panic!(
"[{}] Failed to create repository '{}': {:?}",
provider_name, repo_name, e
)
});
assert!(
!create_response.name.is_empty(),
"[{}] Repository name should not be empty",
provider_name
);
if matches!(provider_name, "aws" | "gcp" | "azure") {
let is_ready = wait_for_repository_ready(
&artifact_registry,
&create_response.name,
provider_name,
300,
)
.await;
assert!(
is_ready,
"[{}] Repository should eventually be ready",
provider_name
);
}
let repository = artifact_registry
.get_repository(&create_response.name)
.await
.unwrap_or_else(|e| {
panic!(
"[{}] Failed to get repository details: {:?}",
provider_name, e
)
});
println!(
"[{}] Successfully created repository '{}' with URI: {:?}",
provider_name, repo_name, repository.uri
);
artifact_registry
.delete_repository(&create_response.name)
.await
.ok();
}
#[rstest]
#[cfg_attr(feature = "local", case::local(LocalProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "grpc", case::grpc(GrpcProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "aws", case::aws(AwsProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "azure", case::azure(AzureProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "gcp", case::gcp(GcpProviderArtifactRegistryTestContext::setup().await))]
#[cfg(any(
feature = "local",
feature = "grpc",
feature = "aws",
feature = "azure",
feature = "gcp"
))]
#[tokio::test]
async fn test_add_remove_cross_account_access(#[case] ctx: impl ArtifactRegistryTestContext) {
let artifact_registry = ctx.get_artifact_registry().await;
let provider_name = ctx.provider_name();
let repo_name = generate_unique_repo_name(provider_name);
let create_response = artifact_registry
.create_repository(&repo_name)
.await
.unwrap_or_else(|e| {
panic!(
"[{}] Failed to create repository '{}': {:?}",
provider_name, repo_name, e
)
});
if matches!(provider_name, "gcp") {
wait_for_repository_ready(
&artifact_registry,
&create_response.name,
provider_name,
300,
)
.await;
}
let (cross_account_access_1, cross_account_access_2) = match provider_name {
"aws" => {
let access_1 = CrossAccountAccess::Aws(AwsCrossAccountAccess {
account_ids: vec!["123456789012".to_string()],
allowed_service_types: vec![ComputeServiceType::Worker],
role_arns: vec!["arn:aws:iam::123456789012:role/test-role-1".to_string()],
regions: vec![],
});
let access_2 = CrossAccountAccess::Aws(AwsCrossAccountAccess {
account_ids: vec!["987654321098".to_string()],
allowed_service_types: vec![ComputeServiceType::Worker],
role_arns: vec!["arn:aws:iam::987654321098:role/test-role-2".to_string()],
regions: vec![],
});
(access_1, access_2)
}
"gcp" => {
let service_accounts = if let Some(real_sa) = get_real_service_account_email() {
let email = real_sa.strip_prefix("serviceAccount:").unwrap_or(&real_sa);
vec![email.to_string()]
} else {
vec!["test1@example.com".to_string()]
};
let access_1 = CrossAccountAccess::Gcp(GcpCrossAccountAccess {
project_numbers: vec!["123456789012".to_string()],
allowed_service_types: vec![ComputeServiceType::Worker],
service_account_emails: service_accounts,
});
let access_2 = CrossAccountAccess::Gcp(GcpCrossAccountAccess {
project_numbers: vec!["987654321098".to_string()],
allowed_service_types: vec![ComputeServiceType::Worker],
service_account_emails: vec!["test2@example.com".to_string()],
});
(access_1, access_2)
}
"azure" => {
println!(
"[{}] Testing OperationNotSupported for cross-account access",
provider_name
);
match artifact_registry
.get_cross_account_access(&create_response.name)
.await
{
Ok(_) => panic!(
"[{}] Expected OperationNotSupported error for get_cross_account_access",
provider_name
),
Err(e) => {
if let Some(alien_bindings::error::ErrorData::OperationNotSupported {
operation,
reason,
}) = e.error
{
println!(
"[{}] ✓ Correctly received OperationNotSupported for get: {} - {}",
provider_name, operation, reason
);
} else {
panic!(
"[{}] Expected OperationNotSupported error for get, got: {:?}",
provider_name, e
);
}
}
}
let test_access = CrossAccountAccess::Aws(AwsCrossAccountAccess {
account_ids: vec!["123456789012".to_string()],
allowed_service_types: vec![ComputeServiceType::Worker],
role_arns: vec!["arn:aws:iam::123456789012:role/test-role".to_string()],
regions: vec![],
});
match artifact_registry
.add_cross_account_access(&create_response.name, test_access.clone())
.await
{
Ok(()) => panic!("[{}] Expected OperationNotSupported error", provider_name),
Err(e) => {
if let Some(alien_bindings::error::ErrorData::OperationNotSupported {
operation,
reason,
}) = e.error
{
println!(
"[{}] ✓ Correctly received OperationNotSupported for add: {} - {}",
provider_name, operation, reason
);
} else {
panic!(
"[{}] Expected OperationNotSupported error, got: {:?}",
provider_name, e
);
}
}
}
match artifact_registry
.remove_cross_account_access(&create_response.name, test_access)
.await
{
Ok(()) => panic!(
"[{}] Expected OperationNotSupported error for remove",
provider_name
),
Err(e) => {
if let Some(alien_bindings::error::ErrorData::OperationNotSupported {
operation,
reason,
}) = e.error
{
println!(
"[{}] ✓ Correctly received OperationNotSupported for remove: {} - {}",
provider_name, operation, reason
);
} else {
panic!(
"[{}] Expected OperationNotSupported error for remove, got: {:?}",
provider_name, e
);
}
}
}
artifact_registry
.delete_repository(&create_response.name)
.await
.ok();
return;
}
"local" | "grpc" => {
println!(
"[{}] Testing OperationNotSupported for cross-account access",
provider_name
);
match artifact_registry
.get_cross_account_access(&create_response.name)
.await
{
Ok(_) => panic!(
"[{}] Expected OperationNotSupported error for get_cross_account_access",
provider_name
),
Err(e) => {
if let Some(alien_bindings::error::ErrorData::OperationNotSupported {
operation,
reason,
}) = e.error
{
println!(
"[{}] ✓ Correctly received OperationNotSupported for get: {} - {}",
provider_name, operation, reason
);
} else {
panic!(
"[{}] Expected OperationNotSupported error for get, got: {:?}",
provider_name, e
);
}
}
}
let test_access = CrossAccountAccess::Aws(AwsCrossAccountAccess {
account_ids: vec!["123456789012".to_string()],
allowed_service_types: vec![ComputeServiceType::Worker],
role_arns: vec!["arn:aws:iam::123456789012:role/test-role".to_string()],
regions: vec![],
});
match artifact_registry
.add_cross_account_access(&create_response.name, test_access.clone())
.await
{
Ok(()) => panic!("[{}] Expected OperationNotSupported error", provider_name),
Err(e) => {
if let Some(alien_bindings::error::ErrorData::OperationNotSupported {
operation,
reason,
}) = e.error
{
println!(
"[{}] ✓ Correctly received OperationNotSupported for add: {} - {}",
provider_name, operation, reason
);
} else {
panic!(
"[{}] Expected OperationNotSupported error, got: {:?}",
provider_name, e
);
}
}
}
match artifact_registry
.remove_cross_account_access(&create_response.name, test_access)
.await
{
Ok(()) => panic!(
"[{}] Expected OperationNotSupported error for remove",
provider_name
),
Err(e) => {
if let Some(alien_bindings::error::ErrorData::OperationNotSupported {
operation,
reason,
}) = e.error
{
println!(
"[{}] ✓ Correctly received OperationNotSupported for remove: {} - {}",
provider_name, operation, reason
);
} else {
panic!(
"[{}] Expected OperationNotSupported error for remove, got: {:?}",
provider_name, e
);
}
}
}
artifact_registry
.delete_repository(&create_response.name)
.await
.ok();
return;
}
_ => panic!("Unknown provider: {}", provider_name),
};
match artifact_registry
.get_cross_account_access(&create_response.name)
.await
{
Ok(initial_permissions) => {
println!(
"[{}] ✓ Retrieved initial cross-account access: {:?}",
provider_name, initial_permissions
);
match (&initial_permissions.access, provider_name) {
(CrossAccountAccess::Aws(aws_access), "aws") => {
let is_empty = aws_access.account_ids.is_empty()
&& aws_access.role_arns.is_empty()
&& aws_access.allowed_service_types.is_empty();
if is_empty {
println!(
"[{}] ✓ Initial AWS cross-account access is empty as expected",
provider_name
);
} else {
println!(
"[{}] ⚠ Initial AWS cross-account access is not empty: {:?}",
provider_name, aws_access
);
}
}
(CrossAccountAccess::Gcp(gcp_access), "gcp") => {
let is_empty = gcp_access.project_numbers.is_empty()
&& gcp_access.service_account_emails.is_empty()
&& gcp_access.allowed_service_types.is_empty();
if is_empty {
println!(
"[{}] ✓ Initial GCP cross-account access is empty as expected",
provider_name
);
} else {
println!(
"[{}] ⚠ Initial GCP cross-account access is not empty: {:?}",
provider_name, gcp_access
);
}
}
_ => {
println!(
"[{}] ⚠ Unexpected initial cross-account access format",
provider_name
);
}
}
}
Err(e) => {
println!(
"[{}] ⚠ Failed to get initial cross-account access: {:?}",
provider_name, e
);
}
}
match artifact_registry
.add_cross_account_access(&create_response.name, cross_account_access_1.clone())
.await
{
Ok(()) => {
println!(
"[{}] ✓ Successfully added first cross-account access",
provider_name
);
match artifact_registry
.get_cross_account_access(&create_response.name)
.await
{
Ok(permissions_after_first) => {
println!(
"[{}] ✓ Retrieved cross-account access after adding first: {:?}",
provider_name, permissions_after_first
);
match artifact_registry
.add_cross_account_access(
&create_response.name,
cross_account_access_2.clone(),
)
.await
{
Ok(()) => {
println!(
"[{}] ✓ Successfully added second cross-account access",
provider_name
);
match artifact_registry
.get_cross_account_access(&create_response.name)
.await
{
Ok(permissions_after_both) => {
println!("[{}] ✓ Retrieved cross-account access after adding both: {:?}", provider_name, permissions_after_both);
match (&permissions_after_both.access, provider_name) {
(CrossAccountAccess::Aws(aws_access), "aws") => {
let has_multiple = aws_access.account_ids.len() >= 2
|| aws_access.role_arns.len() >= 2;
if has_multiple {
println!("[{}] ✓ AWS cross-account access contains multiple entries", provider_name);
} else {
println!("[{}] ⚠ AWS cross-account access may not contain both entries: {:?}", provider_name, aws_access);
}
}
(CrossAccountAccess::Gcp(gcp_access), "gcp") => {
let has_multiple = gcp_access.project_numbers.len()
>= 2
|| gcp_access.service_account_emails.len() >= 2;
if has_multiple {
println!("[{}] ✓ GCP cross-account access contains multiple entries", provider_name);
} else {
println!("[{}] ⚠ GCP cross-account access may not contain both entries: {:?}", provider_name, gcp_access);
}
}
_ => {
println!(
"[{}] ⚠ Unexpected cross-account access format",
provider_name
);
}
}
match artifact_registry
.remove_cross_account_access(
&create_response.name,
cross_account_access_1,
)
.await
{
Ok(()) => {
println!("[{}] ✓ Successfully removed first cross-account access", provider_name);
match artifact_registry
.get_cross_account_access(&create_response.name)
.await
{
Ok(permissions_after_remove_first) => {
println!("[{}] ✓ Retrieved cross-account access after removing first: {:?}", provider_name, permissions_after_remove_first);
match artifact_registry
.remove_cross_account_access(
&create_response.name,
cross_account_access_2,
)
.await
{
Ok(()) => {
println!("[{}] ✓ Successfully removed second cross-account access", provider_name);
match artifact_registry
.get_cross_account_access(
&create_response.name,
)
.await
{
Ok(final_permissions) => {
println!("[{}] ✓ Retrieved final cross-account access: {:?}", provider_name, final_permissions);
match (
&final_permissions.access,
provider_name,
) {
(
CrossAccountAccess::Aws(
aws_access,
),
"aws",
) => {
let is_empty = aws_access
.account_ids
.is_empty()
&& aws_access
.role_arns
.is_empty()
&& aws_access
.allowed_service_types
.is_empty();
if is_empty {
println!("[{}] ✓ Final AWS cross-account access is empty - all access properly removed", provider_name);
} else {
println!("[{}] ⚠ Final AWS cross-account access is not empty: {:?}", provider_name, aws_access);
}
}
(
CrossAccountAccess::Gcp(
gcp_access,
),
"gcp",
) => {
let is_empty = gcp_access
.project_numbers
.is_empty()
&& gcp_access
.service_account_emails
.is_empty()
&& gcp_access
.allowed_service_types
.is_empty();
if is_empty {
println!("[{}] ✓ Final GCP cross-account access is empty - all access properly removed", provider_name);
} else {
println!("[{}] ⚠ Final GCP cross-account access is not empty: {:?}", provider_name, gcp_access);
}
}
_ => {
println!("[{}] ⚠ Unexpected final cross-account access format", provider_name);
}
}
}
Err(e) => {
println!("[{}] ⚠ Failed to get final cross-account access: {:?}", provider_name, e);
}
}
}
Err(e) => {
println!("[{}] ⚠ Failed to remove second cross-account access: {:?}", provider_name, e);
}
}
}
Err(e) => {
println!("[{}] ⚠ Failed to get cross-account access after removing first: {:?}", provider_name, e);
}
}
}
Err(e) => {
println!("[{}] ⚠ Failed to remove first cross-account access: {:?}", provider_name, e);
}
}
}
Err(e) => {
println!("[{}] ⚠ Failed to get cross-account access after adding both: {:?}", provider_name, e);
}
}
}
Err(e) => {
println!(
"[{}] ⚠ Failed to add second cross-account access: {:?}",
provider_name, e
);
}
}
}
Err(e) => {
println!(
"[{}] ⚠ Failed to get cross-account access after adding first: {:?}",
provider_name, e
);
}
}
}
Err(e) => {
println!("[{}] ⚠ Failed to add first cross-account access (acceptable for some test environments): {:?}", provider_name, e);
println!("[{}] This is acceptable when the test environment doesn't have IAM permission management capabilities", provider_name);
}
}
artifact_registry
.delete_repository(&create_response.name)
.await
.ok();
}
#[rstest]
#[cfg_attr(feature = "local", case::local(LocalProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "grpc", case::grpc(GrpcProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "azure", case::azure(AzureProviderArtifactRegistryTestContext::setup().await))]
#[cfg(any(feature = "local", feature = "grpc", feature = "azure"))]
#[tokio::test]
async fn test_generate_credentials(#[case] ctx: impl ArtifactRegistryTestContext) {
let artifact_registry = ctx.get_artifact_registry().await;
let provider_name = ctx.provider_name();
let repo_name = generate_unique_repo_name(provider_name);
let create_response = artifact_registry
.create_repository(&repo_name)
.await
.unwrap_or_else(|e| {
panic!(
"[{}] Failed to create repository '{}': {:?}",
provider_name, repo_name, e
)
});
if matches!(provider_name, "gcp") {
wait_for_repository_ready(
&artifact_registry,
&create_response.name,
provider_name,
300,
)
.await;
}
let credentials_result = artifact_registry
.generate_credentials(
&create_response.name,
ArtifactRegistryPermissions::Pull,
Some(3600),
)
.await
.unwrap_or_else(|e| {
panic!(
"[{}] Failed to generate credentials for repository '{}': {:?}",
provider_name, repo_name, e
)
});
artifact_registry
.delete_repository(&create_response.name)
.await
.ok();
}
#[rstest]
#[cfg_attr(feature = "local", case::local(LocalProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "grpc", case::grpc(GrpcProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "aws", case::aws(AwsProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "azure", case::azure(AzureProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "gcp", case::gcp(GcpProviderArtifactRegistryTestContext::setup().await))]
#[cfg(any(
feature = "local",
feature = "grpc",
feature = "aws",
feature = "azure",
feature = "gcp"
))]
#[tokio::test]
async fn test_delete_repository(#[case] ctx: impl ArtifactRegistryTestContext) {
let artifact_registry = ctx.get_artifact_registry().await;
let provider_name = ctx.provider_name();
let repo_name = generate_unique_repo_name(provider_name);
let create_response = artifact_registry
.create_repository(&repo_name)
.await
.unwrap_or_else(|e| {
panic!(
"[{}] Failed to create repository '{}': {:?}",
provider_name, repo_name, e
)
});
if matches!(provider_name, "gcp") {
wait_for_repository_ready(
&artifact_registry,
&create_response.name,
provider_name,
300,
)
.await;
}
artifact_registry
.delete_repository(&create_response.name)
.await
.unwrap_or_else(|e| {
panic!(
"[{}] Failed to delete repository '{}': {:?}",
provider_name, repo_name, e
)
});
println!(
"[{}] Successfully deleted repository '{}'",
provider_name, repo_name
);
}
#[rstest]
#[cfg_attr(feature = "local", case::local(LocalProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "grpc", case::grpc(GrpcProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "aws", case::aws(AwsProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "azure", case::azure(AzureProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "gcp", case::gcp(GcpProviderArtifactRegistryTestContext::setup().await))]
#[cfg(any(
feature = "local",
feature = "grpc",
feature = "aws",
feature = "azure",
feature = "gcp"
))]
#[tokio::test]
async fn test_full_repository_lifecycle(#[case] ctx: impl ArtifactRegistryTestContext) {
let artifact_registry = ctx.get_artifact_registry().await;
let provider_name = ctx.provider_name();
let repo_name = generate_unique_repo_name(provider_name);
println!(
"[{}] Starting full lifecycle test for repository '{}'",
provider_name, repo_name
);
let create_response = artifact_registry
.create_repository(&repo_name)
.await
.unwrap_or_else(|e| {
panic!(
"[{}] Failed to create repository '{}': {:?}",
provider_name, repo_name, e
)
});
println!("[{}] ✓ Created repository", provider_name);
let get_response = artifact_registry
.get_repository(&create_response.name)
.await
.unwrap_or_else(|e| {
panic!(
"[{}] get_repository must accept the routable name from \
create_repository as-is. Provider returned: {:?}",
provider_name, e
)
});
assert_eq!(
get_response.name, create_response.name,
"[{}] get_repository must round-trip the routable name unchanged",
provider_name
);
if matches!(provider_name, "gcp") {
let is_ready = wait_for_repository_ready(
&artifact_registry,
&create_response.name,
provider_name,
300,
)
.await;
assert!(is_ready);
}
println!("[{}] ✓ Repository is ready", provider_name);
if matches!(provider_name, "aws" | "gcp") {
let cross_account_access = match provider_name {
"aws" => CrossAccountAccess::Aws(AwsCrossAccountAccess {
account_ids: vec!["123456789012".to_string()],
allowed_service_types: vec![ComputeServiceType::Worker],
role_arns: vec!["arn:aws:iam::123456789012:role/test-role".to_string()],
regions: vec![],
}),
"gcp" => {
let service_accounts = if let Some(real_sa) = get_real_service_account_email() {
let email = real_sa.strip_prefix("serviceAccount:").unwrap_or(&real_sa);
vec![email.to_string()]
} else {
vec!["test@example.com".to_string()]
};
CrossAccountAccess::Gcp(GcpCrossAccountAccess {
project_numbers: vec!["123456789012".to_string()],
allowed_service_types: vec![ComputeServiceType::Worker],
service_account_emails: service_accounts,
})
}
_ => unreachable!(),
};
match artifact_registry
.add_cross_account_access(&create_response.name, cross_account_access.clone())
.await
{
Ok(()) => {
println!("[{}] ✓ Added cross-account access", provider_name);
match artifact_registry
.get_cross_account_access(&create_response.name)
.await
{
Ok(retrieved_permissions) => {
println!(
"[{}] ✓ Retrieved cross-account access: {:?}",
provider_name, retrieved_permissions
);
match artifact_registry
.remove_cross_account_access(
&create_response.name,
cross_account_access,
)
.await
{
Ok(()) => {
println!("[{}] ✓ Removed cross-account access", provider_name);
match artifact_registry
.get_cross_account_access(&create_response.name)
.await
{
Ok(empty_permissions) => {
println!("[{}] ✓ Verified cross-account access was removed: {:?}", provider_name, empty_permissions);
}
Err(_) => {
println!("[{}] ⚠ Could not verify removal (acceptable for some test environments)", provider_name);
}
}
}
Err(_) => {
println!("[{}] ⚠ Could not remove cross-account access (acceptable for some test environments)", provider_name);
}
}
}
Err(_) => {
println!("[{}] ⚠ Could not retrieve cross-account access (acceptable for some test environments)", provider_name);
}
}
}
Err(_) => {
println!("[{}] ⚠ Could not add cross-account access (acceptable for some test environments)", provider_name);
}
}
} else {
println!(
"[{}] ✓ Skipped cross-account access (not supported for this provider)",
provider_name
);
}
if let Ok(credentials) = artifact_registry
.generate_credentials(
&create_response.name,
ArtifactRegistryPermissions::Pull,
Some(3600),
)
.await
{
println!(
"[{}] ✓ Generated pull credentials: username={}",
provider_name, credentials.username
);
} else {
println!("[{}] ⚠ Credential generation not supported", provider_name);
}
artifact_registry
.delete_repository(&create_response.name)
.await
.unwrap_or_else(|e| panic!("[{}] Failed to delete repository: {:?}", provider_name, e));
println!("[{}] ✓ Deleted repository", provider_name);
println!(
"[{}] ✅ Full lifecycle test completed successfully",
provider_name
);
}
#[rstest]
#[cfg_attr(feature = "local", case::local(LocalProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "grpc", case::grpc(GrpcProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "aws", case::aws(AwsProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "azure", case::azure(AzureProviderArtifactRegistryTestContext::setup().await))]
#[cfg_attr(feature = "gcp", case::gcp(GcpProviderArtifactRegistryTestContext::setup().await))]
#[cfg(any(
feature = "local",
feature = "grpc",
feature = "aws",
feature = "azure",
feature = "gcp"
))]
#[tokio::test]
async fn test_get_nonexistent_repository_returns_404(
#[case] ctx: impl ArtifactRegistryTestContext,
) {
let artifact_registry = ctx.get_artifact_registry().await;
let provider_name = ctx.provider_name();
let nonexistent_repo = format!("alien-test-nonexistent-{}", Uuid::new_v4().simple());
let expected_repo_name = if provider_name == "aws" {
format!("test-{}", nonexistent_repo)
} else {
nonexistent_repo.clone()
};
println!(
"[{}] Testing get_repository for non-existent repo '{}'",
provider_name, nonexistent_repo
);
let result = artifact_registry.get_repository(&nonexistent_repo).await;
assert!(
result.is_err(),
"[{}] Expected error when getting non-existent repository",
provider_name
);
let error = result.unwrap_err();
println!("[{}] Error: {:?}", provider_name, error);
match &error.error {
Some(alien_bindings::error::ErrorData::ResourceNotFound { resource_id }) => {
assert_eq!(resource_id, &expected_repo_name);
println!("[{}] ✓ Correctly returned ResourceNotFound", provider_name);
}
Some(alien_bindings::error::ErrorData::RemoteResourceNotFound {
resource_name, ..
}) => {
assert_eq!(resource_name, &expected_repo_name);
println!(
"[{}] ✓ Correctly returned RemoteResourceNotFound",
provider_name
);
}
other => {
panic!(
"[{}] Expected ResourceNotFound or RemoteResourceNotFound error, got: {:?}",
provider_name, other
);
}
}
assert_eq!(
error.http_status_code,
Some(404),
"[{}] HTTP status code should be 404 for not found, got: {:?}",
provider_name,
error.http_status_code
);
println!("[{}] ✓ HTTP status code is correctly 404", provider_name);
}