The provider resolves credentials through the AWS SDK + * {@code DefaultCredentialsProvider} chain. The first link in that chain is the + * {@code SystemPropertyCredentialsProvider}, which reads the {@code aws.accessKeyId}, + * {@code aws.secretAccessKey} and {@code aws.sessionToken} system properties with + * no network access. By setting those properties to fake static values we + * exercise the whole "sign an STS GetCallerIdentity request and package it as a + * cloud-id token" flow without any real credentials or any outbound request. + * + *
SigV4 signing is a purely local computation, so these tests never contact STS.
+ */
+public class AwsCloudIdProviderTest {
+
+ private static final String FAKE_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE";
+ private static final String FAKE_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
+ private static final String FAKE_SESSION_TOKEN = "FAKESESSIONTOKEN/akeyless-test//////////wEXAMPLE";
+
+ private static final String PROP_ACCESS_KEY = "aws.accessKeyId";
+ private static final String PROP_SECRET_KEY = "aws.secretAccessKey";
+ private static final String PROP_SESSION_TOKEN = "aws.sessionToken";
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private String savedAccessKey;
+ private String savedSecretKey;
+ private String savedSessionToken;
+
+ @Before
+ public void injectFakeCredentials() {
+ // Remember whatever the host/CI environment had so we can restore it.
+ savedAccessKey = System.getProperty(PROP_ACCESS_KEY);
+ savedSecretKey = System.getProperty(PROP_SECRET_KEY);
+ savedSessionToken = System.getProperty(PROP_SESSION_TOKEN);
+
+ System.setProperty(PROP_ACCESS_KEY, FAKE_ACCESS_KEY);
+ System.setProperty(PROP_SECRET_KEY, FAKE_SECRET_KEY);
+ // Session token is set per-test where needed; make sure we start clean.
+ System.clearProperty(PROP_SESSION_TOKEN);
+ }
+
+ @After
+ public void restoreCredentials() {
+ restore(PROP_ACCESS_KEY, savedAccessKey);
+ restore(PROP_SECRET_KEY, savedSecretKey);
+ restore(PROP_SESSION_TOKEN, savedSessionToken);
+ }
+
+ private static void restore(String key, String value) {
+ if (value == null) {
+ System.clearProperty(key);
+ } else {
+ System.setProperty(key, value);
+ }
+ }
+
+ // ---- The decoded token model -------------------------------------------------
+
+ /** Decodes the outer base64+JSON envelope and the inner base64 fields. */
+ private static DecodedToken decode(String cloudId) throws Exception {
+ assertNotNull("cloud id must not be null", cloudId);
+ byte[] outer = Base64.getDecoder().decode(cloudId);
+ JsonNode root = MAPPER.readTree(new String(outer, StandardCharsets.UTF_8));
+
+ DecodedToken token = new DecodedToken();
+ token.method = root.get("sts_request_method").asText();
+ token.url = decodeField(root, "sts_request_url");
+ token.body = decodeField(root, "sts_request_body");
+ token.headersJson = decodeField(root, "sts_request_headers");
+ token.headers = MAPPER.readTree(token.headersJson);
+ return token;
+ }
+
+ private static String decodeField(JsonNode root, String field) {
+ JsonNode node = root.get(field);
+ assertNotNull("missing token field: " + field, node);
+ return new String(Base64.getDecoder().decode(node.asText()), StandardCharsets.UTF_8);
+ }
+
+ private static final class DecodedToken {
+ String method;
+ String url;
+ String body;
+ String headersJson;
+ JsonNode headers;
+
+ /** Case-insensitive header lookup; headers are serialized as name -> [values]. */
+ String header(String name) {
+ Iterator The provider obtains a token from {@code DefaultAzureCredential}, which requires
+ * live Azure AD / IMDS access. Per the task constraints we do NOT drive a real token
+ * fetch here; we only assert what is provable offline: that the provider is
+ * constructible, is wired into the factory, and that the underlying Azure credential
+ * builder used by {@code getCloudId()} constructs without any network call.
+ */
+public class AzureCloudIdProviderTest {
+
+ @Test
+ public void providerIsConstructibleAndImplementsInterface() {
+ AzureCloudIdProvider provider = new AzureCloudIdProvider();
+ assertNotNull(provider);
+ assertTrue(provider instanceof CloudIdProvider);
+ }
+
+ @Test
+ public void factoryReturnsAzureProvider() {
+ CloudIdProvider provider = CloudProviderFactory.getCloudIdProvider("azure_ad");
+ assertTrue(provider instanceof AzureCloudIdProvider);
+ }
+
+ @Test
+ public void defaultAzureCredentialBuilderConstructsWithoutNetwork() {
+ // Mirrors the credential construction performed inside getCloudId(). Building
+ // the credential is a local operation; no token request is issued here.
+ DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build();
+ assertNotNull("DefaultAzureCredential should build offline", credential);
+ }
+}
diff --git a/src/test/java/io/akeyless/cloudid/CloudProviderFactoryTest.java b/src/test/java/io/akeyless/cloudid/CloudProviderFactoryTest.java
new file mode 100644
index 0000000..09262ab
--- /dev/null
+++ b/src/test/java/io/akeyless/cloudid/CloudProviderFactoryTest.java
@@ -0,0 +1,89 @@
+package io.akeyless.cloudid;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Hermetic tests for {@link CloudProviderFactory} dispatch logic.
+ * These tests never touch the network or real credentials; they only assert
+ * that the factory maps access-type strings to the correct provider class and
+ * rejects everything else.
+ */
+public class CloudProviderFactoryTest {
+
+ @Test
+ public void mapsAwsIamToAwsProvider() {
+ CloudIdProvider provider = CloudProviderFactory.getCloudIdProvider("aws_iam");
+ assertNotNull(provider);
+ assertTrue("expected an AwsCloudIdProvider", provider instanceof AwsCloudIdProvider);
+ assertTrue("provider must implement CloudIdProvider", provider instanceof CloudIdProvider);
+ }
+
+ @Test
+ public void mapsAzureAdToAzureProvider() {
+ CloudIdProvider provider = CloudProviderFactory.getCloudIdProvider("azure_ad");
+ assertNotNull(provider);
+ assertTrue("expected an AzureCloudIdProvider", provider instanceof AzureCloudIdProvider);
+ }
+
+ @Test
+ public void mapsGcpToGcpProvider() {
+ CloudIdProvider provider = CloudProviderFactory.getCloudIdProvider("gcp");
+ assertNotNull(provider);
+ assertTrue("expected a GcpCloudIdProvider", provider instanceof GcpCloudIdProvider);
+ }
+
+ @Test
+ public void returnsFreshInstanceEachCall() {
+ CloudIdProvider first = CloudProviderFactory.getCloudIdProvider("aws_iam");
+ CloudIdProvider second = CloudProviderFactory.getCloudIdProvider("aws_iam");
+ assertNotSame("factory must not cache/share provider instances", first, second);
+ }
+
+ @Test
+ public void rejectsUnknownType() {
+ try {
+ CloudProviderFactory.getCloudIdProvider("kubernetes");
+ fail("expected RuntimeException for unsupported type");
+ } catch (RuntimeException e) {
+ assertEquals("Unsupported type: kubernetes", e.getMessage());
+ }
+ }
+
+ @Test
+ public void rejectsEmptyType() {
+ try {
+ CloudProviderFactory.getCloudIdProvider("");
+ fail("expected RuntimeException for empty type");
+ } catch (RuntimeException e) {
+ assertEquals("Unsupported type: ", e.getMessage());
+ }
+ }
+
+ @Test
+ public void rejectsNullType() {
+ try {
+ CloudProviderFactory.getCloudIdProvider(null);
+ fail("expected RuntimeException for null type");
+ } catch (RuntimeException e) {
+ assertEquals("Unsupported type: null", e.getMessage());
+ }
+ }
+
+ @Test
+ public void typeMatchIsCaseSensitive() {
+ // The factory uses exact string equality, so a differently-cased value
+ // must be rejected rather than silently mapped.
+ try {
+ CloudProviderFactory.getCloudIdProvider("AWS_IAM");
+ fail("expected RuntimeException for wrong-case type");
+ } catch (RuntimeException e) {
+ assertEquals("Unsupported type: AWS_IAM", e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/io/akeyless/cloudid/GcpCloudIdProviderTest.java b/src/test/java/io/akeyless/cloudid/GcpCloudIdProviderTest.java
new file mode 100644
index 0000000..6c212d1
--- /dev/null
+++ b/src/test/java/io/akeyless/cloudid/GcpCloudIdProviderTest.java
@@ -0,0 +1,31 @@
+package io.akeyless.cloudid;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Offline tests for {@link GcpCloudIdProvider}.
+ *
+ * {@code getCloudId()} relies on {@code GoogleCredentials.getApplicationDefault()}
+ * and a live GCP identity-token exchange, which cannot be exercised hermetically.
+ * We therefore only assert the offline-provable facts: the provider is constructible,
+ * implements the common interface, and is wired into the factory. A real end-to-end
+ * exchange is covered (credentials-gated) in {@link LiveCloudIdE2ETest}.
+ */
+public class GcpCloudIdProviderTest {
+
+ @Test
+ public void providerIsConstructibleAndImplementsInterface() {
+ GcpCloudIdProvider provider = new GcpCloudIdProvider();
+ assertNotNull(provider);
+ assertTrue(provider instanceof CloudIdProvider);
+ }
+
+ @Test
+ public void factoryReturnsGcpProvider() {
+ CloudIdProvider provider = CloudProviderFactory.getCloudIdProvider("gcp");
+ assertTrue(provider instanceof GcpCloudIdProvider);
+ }
+}
diff --git a/src/test/java/io/akeyless/cloudid/LiveCloudIdE2ETest.java b/src/test/java/io/akeyless/cloudid/LiveCloudIdE2ETest.java
new file mode 100644
index 0000000..f8e00b6
--- /dev/null
+++ b/src/test/java/io/akeyless/cloudid/LiveCloudIdE2ETest.java
@@ -0,0 +1,58 @@
+package io.akeyless.cloudid;
+
+import org.junit.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeNotNull;
+
+/**
+ * Credentials-gated, live-cloud end-to-end tests.
+ *
+ * Every test here is SKIPPED (via JUnit {@code Assume}) unless the corresponding
+ * real cloud credentials are present in the environment. This means the class is
+ * always safe to run in CI with no secrets — it simply reports the tests as skipped —
+ * yet it exercises the real provider flow whenever credentials are available.
+ *
+ * Gating environment variables:
+ *
+ *
+ */
+public class LiveCloudIdE2ETest {
+
+ private static void assertNonEmptyBase64(String cloudId) {
+ assertNotNull("cloud id must not be null", cloudId);
+ assertTrue("cloud id must not be empty", cloudId.length() > 0);
+ // Must be valid base64 (throws IllegalArgumentException otherwise).
+ byte[] decoded = Base64.getDecoder().decode(cloudId);
+ assertTrue("decoded cloud id must not be empty",
+ new String(decoded, StandardCharsets.UTF_8).length() > 0);
+ }
+
+ @Test
+ public void awsLiveGetCloudId() throws Exception {
+ assumeNotNull(System.getenv("AWS_ACCESS_KEY_ID"));
+ String cloudId = CloudProviderFactory.getCloudIdProvider("aws_iam").getCloudId();
+ assertNonEmptyBase64(cloudId);
+ }
+
+ @Test
+ public void azureLiveGetCloudId() throws Exception {
+ assumeNotNull(System.getenv("AZURE_CLIENT_ID"));
+ String cloudId = CloudProviderFactory.getCloudIdProvider("azure_ad").getCloudId();
+ assertNonEmptyBase64(cloudId);
+ }
+
+ @Test
+ public void gcpLiveGetCloudId() throws Exception {
+ assumeNotNull(System.getenv("GOOGLE_APPLICATION_CREDENTIALS"));
+ String cloudId = CloudProviderFactory.getCloudIdProvider("gcp").getCloudId();
+ assertNonEmptyBase64(cloudId);
+ }
+}