use serde_json::{Value, json};
use std::{fs, os::unix::fs::PermissionsExt, process::Command, time::Duration};
#[test]
fn fresh_setup_guides_sign_in_configuration_before_creating_resources() {
let dir = tempfile::tempdir().unwrap();
let executable = dir.path().join("gcloud");
fs::write(&executable, include_str!("fixtures/gcloud.py")).unwrap();
fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
let state = dir.path().join("cloud.json");
let output = capulus::process::CaptureOptions::default()
.validate()
.unwrap()
.run(
Command::new(env!("CARGO_BIN_EXE_aegis-admin"))
.env("HOME", dir.path())
.env_remove("SUDO_USER")
.env("AEGIS_TEST_CLOUD_STATE", &state)
.env(
"PATH",
format!(
"{}:{}",
dir.path().display(),
std::env::var("PATH").unwrap()
),
)
.args([
"--progress",
"plain",
"--color",
"never",
"setup",
"--project",
"aegis-fresh-test",
"--region",
"australia-southeast1",
"--yes",
"--no-enroll",
"--oauth",
]),
None,
)
.unwrap();
assert!(!output.status.success());
assert!(output.stdout.is_empty());
assert!(!output.stderr.contains('\x1b'));
for expected in [
"Use operator@example.com in the Google Cloud Console.",
"https://console.cloud.google.com/auth/overview?project=aegis-fresh-test",
"https://console.cloud.google.com/auth/clients?project=aegis-fresh-test",
"Audience → Test users",
"https://aegis-api-1234567890.australia-southeast1.run.app/v2/oauth/callback",
"--oauth-client FILE",
] {
assert!(output.stderr.contains(expected), "{}", output.stderr);
}
let cloud: Value = serde_json::from_slice(&fs::read(state).unwrap()).unwrap();
let calls = cloud["calls"].as_array().unwrap();
assert_eq!(calls.len(), 3);
assert_eq!(calls[0][0], "services");
assert!(
calls[0]
.as_array()
.unwrap()
.contains(&json!("cloudresourcemanager.googleapis.com"))
);
assert!(
calls[0]
.as_array()
.unwrap()
.contains(&json!("cloudbilling.googleapis.com"))
);
assert_eq!(calls[1][0], "projects");
assert_eq!(calls[1][1], "describe");
assert_eq!(calls[2][0], "auth");
assert_eq!(calls[2][1], "list");
assert!(
!dir.path()
.join(".aegis/deployments/aegis-fresh-test.json")
.exists()
);
}
#[test]
fn administration_help_and_validation_do_not_require_a_saved_endpoint() {
let dir = tempfile::tempdir().unwrap();
for args in [
vec!["--help"],
vec!["setup", "--help"],
vec!["authorize", "--help"],
vec!["configure", "--help"],
vec!["namespace-template"],
] {
let result = capulus::process::CaptureOptions::default()
.validate()
.unwrap()
.run(
Command::new(env!("CARGO_BIN_EXE_aegis-admin"))
.args(args)
.env("HOME", dir.path()),
None,
)
.unwrap();
assert!(result.status.success(), "{}", result.stderr);
assert!(!result.stdout.contains("api.hoek.io"));
}
}
#[test]
#[ignore = "requires a loopback FIRESTORE_EMULATOR_HOST"]
fn externally_hosted_configuration_and_discovery_do_not_provision_cloud_resources() {
assert!(
std::env::var("FIRESTORE_EMULATOR_HOST")
.unwrap()
.starts_with("127.0.0.1:")
);
let dir = tempfile::tempdir().unwrap();
let executable = dir.path().join("gcloud");
fs::write(&executable, include_str!("fixtures/gcloud.py")).unwrap();
fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
let oauth = dir.path().join("oauth.json");
fs::write(
&oauth,
serde_json::to_vec(&json!({"web": {
"client_id": "external-client", "client_secret": "external-secret-never-log",
"redirect_uris": ["https://fleet.example/custom/oauth/callback"]
}}))
.unwrap(),
)
.unwrap();
let project = format!(
"aegis-external-{}",
&uuid::Uuid::new_v4().simple().to_string()[..12]
);
let state = dir.path().join("cloud.json");
for discover in [false, false, true] {
let mut command = Command::new(env!("CARGO_BIN_EXE_aegis-admin"));
command
.env("HOME", dir.path())
.env_remove("SUDO_USER")
.env("AEGIS_TEST_CLOUD_STATE", &state)
.env(
"PATH",
format!(
"{}:{}",
dir.path().display(),
std::env::var("PATH").unwrap()
),
)
.args(["--progress", "plain"]);
if discover {
command.args(["connect", "personal"]);
} else {
command.args(["configure", "--endpoint", "https://fleet.example/custom"]);
command.arg("--oauth-client").arg(&oauth);
}
command.args(["--project", &project, "--database", "custom-database"]);
let output = capulus::process::CaptureOptions::default()
.validate()
.unwrap()
.run(&mut command, None)
.unwrap();
assert!(output.status.success(), "{}", output.stderr);
assert!(!output.stderr.contains("external-secret-never-log"));
assert!(output.stdout.is_empty());
}
let cloud: Value = serde_json::from_slice(&fs::read(state).unwrap()).unwrap();
assert!(
cloud["calls"]
.as_array()
.unwrap()
.iter()
.all(|call| call[0] == "auth" && call[1] == "print-access-token")
);
let context = fs::read_to_string(dir.path().join(".aegis/context.toml")).unwrap();
assert!(context.contains("https://fleet.example/custom/namespaces/personal"));
}
#[test]
#[ignore = "requires a loopback FIRESTORE_EMULATOR_HOST"]
fn setup_resumes_after_deploy_failure_without_replacing_keys_or_secrets() {
setup_resume(None, true);
}
#[test]
#[ignore = "requires a loopback FIRESTORE_EMULATOR_HOST"]
fn proxied_setup_retains_cloud_run_authentication() {
setup_resume(Some("proxy@example-project.iam.gserviceaccount.com"), true);
}
fn setup_resume(proxy_invoker: Option<&str>, oauth_enabled: bool) {
assert!(
std::env::var("FIRESTORE_EMULATOR_HOST")
.unwrap()
.starts_with("127.0.0.1:")
);
let dir = tempfile::tempdir().unwrap();
let executable = dir.path().join("gcloud");
fs::write(&executable, include_str!("fixtures/gcloud.py")).unwrap();
fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
let oauth = dir.path().join("oauth.json");
fs::write(
&oauth,
serde_json::to_vec(
&json!({"web": {"client_id":"test-client", "client_secret":"test-secret-never-log",
"redirect_uris":["https://fleet.example/custom/oauth/callback"]}}),
)
.unwrap(),
)
.unwrap();
let project = format!(
"aegis-wizard-{}",
&uuid::Uuid::new_v4().simple().to_string()[..12]
);
let image = format!("ghcr.io/khoek/aegis-api@sha256:{}", "a".repeat(64));
let state = dir.path().join("cloud.json");
for attempt in 0..3 {
let mut command = Command::new(env!("CARGO_BIN_EXE_aegis-admin"));
command
.env("HOME", dir.path())
.env_remove("SUDO_USER")
.env("AEGIS_TEST_CLOUD_STATE", &state)
.env(
"PATH",
format!(
"{}:{}",
dir.path().display(),
std::env::var("PATH").unwrap()
),
)
.args([
"--progress",
"plain",
"--color",
"never",
"setup",
"--project",
&project,
"--yes",
"--no-enroll",
]);
if let Some(invoker) = proxy_invoker {
command
.env("AEGIS_TEST_PROXY_INVOKER", invoker)
.args(["--proxy-invoker", invoker]);
}
if attempt == 0 {
command.args([
"--region",
"us-central1",
"--endpoint",
"https://fleet.example/custom",
"--image",
&image,
]);
if oauth_enabled {
command.arg("--oauth-client").arg(&oauth);
}
} else if attempt == 2 && oauth_enabled {
command.arg("--oauth-client").arg(&oauth);
}
let output = capulus::process::CaptureOptions {
timeout: Duration::from_secs(90),
..Default::default()
}
.validate()
.unwrap()
.run(&mut command, None)
.unwrap();
assert!(!output.status.success());
assert!(
output.stderr.contains("simulated image pull failure"),
"{}",
output.stderr
);
assert!(output.stderr.contains("Resources and keys are retained"));
if attempt == 0 {
assert!(
output.stderr.contains("GCP still reports SERVICE_DISABLED"),
"{}",
output.stderr
);
assert!(
output.stderr.contains("IAM access is not yet usable"),
"{}",
output.stderr
);
}
assert!(output.stderr.contains("API runtime access verified"));
assert!(!output.stderr.contains("test-secret-never-log"));
assert!(!output.stderr.contains('\x1b'));
assert!(output.stdout.is_empty(), "{}", output.stdout);
let receipt = dir
.path()
.join(".aegis/deployments")
.join(format!("{project}.json"));
let mut receipt_value: Value =
serde_json::from_slice(&fs::read(&receipt).unwrap()).unwrap();
assert_eq!(
receipt_value["secret_version"],
if oauth_enabled {
json!("1")
} else {
Value::Null
}
);
assert_eq!(receipt_value["config"]["login"].is_null(), !oauth_enabled);
assert_eq!(
receipt_value["completed"],
json!(["GCP resources", "Identity and certificate authorities"])
);
if attempt == 1 {
receipt_value["secret_version"] = Value::Null;
fs::write(receipt, serde_json::to_vec(&receipt_value).unwrap()).unwrap();
}
}
let state: Value = serde_json::from_slice(&fs::read(state).unwrap()).unwrap();
assert_eq!(state["secret_versions"], u64::from(oauth_enabled));
let calls = state["calls"].as_array().unwrap();
assert!(calls.iter().any(|call| {
call[0] == "iam"
&& call[1] == "service-accounts"
&& call[2] == "add-iam-policy-binding"
&& call[3] == format!("aegis-api@{project}.iam.gserviceaccount.com")
&& call
.as_array()
.unwrap()
.contains(&json!("--role=roles/iam.serviceAccountTokenCreator"))
}));
if !oauth_enabled {
for call in state["calls"].as_array().unwrap() {
assert_ne!(call[0], "secrets");
assert!(
!call
.as_array()
.unwrap()
.contains(&json!("secretmanager.googleapis.com"))
);
assert!(!call.as_array().unwrap().contains(&json!("--set-secrets")));
}
}
}
#[test]
#[ignore = "requires a loopback FIRESTORE_EMULATOR_HOST"]
fn default_setup_resumes_without_oauth_or_secret_manager() {
setup_resume(None, false);
}