diff --git a/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/client/AbstractIamIT.java b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/client/AbstractIamIT.java index 8e73d4bc2..f24cf8780 100644 --- a/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/client/AbstractIamIT.java +++ b/iam/iam-client/src/test/java/com/salesforce/multicloudj/iam/client/AbstractIamIT.java @@ -4,6 +4,8 @@ import com.salesforce.multicloudj.iam.driver.AbstractIam; import com.salesforce.multicloudj.iam.model.PolicyDocument; import com.salesforce.multicloudj.iam.model.Statement; +import com.salesforce.multicloudj.iam.model.CreateOptions; +import com.salesforce.multicloudj.iam.model.TrustConfiguration; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -13,6 +15,7 @@ import org.junit.jupiter.api.TestInstance; import java.util.List; +import java.util.Optional; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public abstract class AbstractIamIT { @@ -33,6 +36,10 @@ public interface Harness extends AutoCloseable { String getIamEndpoint(); + String getTrustedPrincipal(); + + String getTestIdentityName(); + default String getPolicyVersion() { return ""; } @@ -41,12 +48,14 @@ default String getPolicyVersion() { List getTestPolicyActions(); - String getTestPolicyName(); - } + String getTestPolicyName(); + } protected abstract Harness createHarness(); private Harness harness; + private AbstractIam iam; + private IamClient iamClient; /** * Initializes the WireMock server before all tests. @@ -73,27 +82,29 @@ public void shutdownWireMockServer() throws Exception { @BeforeEach public void setupTestEnvironment() { TestsUtil.startWireMockRecording(harness.getIamEndpoint()); + iam = harness.createIamDriver(true); + iamClient = new IamClient(iam); } /** * Cleans up the test environment after each test. */ @AfterEach - public void cleanupTestEnvironment() { + public void cleanupTestEnvironment() throws Exception { TestsUtil.stopWireMockRecording(); + if (iamClient != null) { + iamClient.close(); // closes underlying AbstractIam + } } @Test public void testAttachInlinePolicy() { - AbstractIam iam = harness.createIamDriver(true); - IamClient iamClient = new IamClient(iam); - - Statement.StatementBuilder statementBuilder = Statement.builder() + Statement.StatementBuilder statementBuilder = Statement.builder() .effect(harness.getTestPolicyEffect()); for (String action : harness.getTestPolicyActions()) { statementBuilder.action(action); } - + PolicyDocument policyDocument = PolicyDocument.builder() .version(harness.getPolicyVersion()) .statement(statementBuilder.build()) @@ -109,10 +120,7 @@ public void testAttachInlinePolicy() { @Test public void testGetInlinePolicyDetails() { - AbstractIam iam = harness.createIamDriver(true); - IamClient iamClient = new IamClient(iam); - - PolicyDocument policyDocument = PolicyDocument.builder() + PolicyDocument policyDocument = PolicyDocument.builder() .version(harness.getPolicyVersion()) .statement(Statement.builder() .effect(harness.getTestPolicyEffect()) @@ -139,15 +147,12 @@ public void testGetInlinePolicyDetails() { @Test public void testGetAttachedPolicies() { - AbstractIam iam = harness.createIamDriver(true); - IamClient iamClient = new IamClient(iam); - - Statement.StatementBuilder statementBuilder = Statement.builder() + Statement.StatementBuilder statementBuilder = Statement.builder() .effect(harness.getTestPolicyEffect()); for (String action : harness.getTestPolicyActions()) { statementBuilder.action(action); } - + PolicyDocument policyDocument = PolicyDocument.builder() .version(harness.getPolicyVersion()) .statement(statementBuilder.build()) @@ -171,10 +176,7 @@ public void testGetAttachedPolicies() { @Test public void testRemovePolicy() { - AbstractIam iam = harness.createIamDriver(true); - IamClient iamClient = new IamClient(iam); - - PolicyDocument policyDocument = PolicyDocument.builder() + PolicyDocument policyDocument = PolicyDocument.builder() .version(harness.getPolicyVersion()) .statement(Statement.builder() .effect(harness.getTestPolicyEffect()) @@ -189,11 +191,244 @@ public void testRemovePolicy() { harness.getIdentityName() ); - iamClient.removePolicy( - harness.getIdentityName(), - harness.getTestPolicyName(), - harness.getTenantId(), - harness.getRegion() - ); - } + iamClient.removePolicy( + harness.getIdentityName(), + harness.getTestPolicyName(), + harness.getTenantId(), + harness.getRegion() + ); + } + + private void cleanUpIdentity(String identity) { + try { + iamClient.deleteIdentity( + identity, + harness.getTenantId(), + harness.getRegion() + ); + } catch (Exception e) { + // Ignore + } + } + + /** + * Tests creating an identity without trust configuration. + */ + @Test + public void testCreateIdentityWithoutTrustConfig() { + String identityName = harness.getTestIdentityName(); + String identityId = iamClient.createIdentity( + identityName, + "Test identity for MultiCloudJ integration tests", + harness.getTenantId(), + harness.getRegion(), + Optional.empty(), + Optional.empty() + ); + + Assertions.assertNotNull(identityId, "Identity ID should not be null"); + Assertions.assertFalse(identityId.isEmpty(), "Identity ID should not be empty"); + + cleanUpIdentity(identityName); + } + + /** + * Tests creating an identity with trust configuration. + */ + @Test + public void testCreateIdentityWithTrustConfig() { + String identityName = harness.getTestIdentityName() + "Trusted"; + TrustConfiguration trustConfig = TrustConfiguration.builder() + .addTrustedPrincipal(harness.getTrustedPrincipal()) + .build(); + + String identityId = iamClient.createIdentity( + identityName, + "Test identity with trust configuration", + harness.getTenantId(), + harness.getRegion(), + Optional.of(trustConfig), + Optional.empty() + ); + + Assertions.assertNotNull(identityId, "Identity ID should not be null"); + Assertions.assertFalse(identityId.isEmpty(), "Identity ID should not be empty"); + + cleanUpIdentity(identityName); + } + + /** + * Tests creating an identity with CreateOptions. + */ + @Test + public void testCreateIdentityWithOptions() { + String identityName = harness.getTestIdentityName() + "Options"; + CreateOptions options = CreateOptions.builder().build(); + + String identityId = iamClient.createIdentity( + identityName, + "Test identity with options", + harness.getTenantId(), + harness.getRegion(), + Optional.empty(), + Optional.of(options) + ); + + Assertions.assertNotNull(identityId, "Identity ID should not be null"); + Assertions.assertFalse(identityId.isEmpty(), "Identity ID should not be empty"); + + cleanUpIdentity(identityName); + } + + /** + * Tests creating an identity with null description. + */ + @Test + public void testCreateIdentityWithNullDescription() { + String identityName = harness.getTestIdentityName() + "NoDesc"; + + String identityId = iamClient.createIdentity( + identityName, + null, + harness.getTenantId(), + harness.getRegion(), + Optional.empty(), + Optional.empty() + ); + + Assertions.assertNotNull(identityId, "Identity ID should not be null"); + Assertions.assertFalse(identityId.isEmpty(), "Identity ID should not be empty"); + + cleanUpIdentity(identityName); + } + + /** + * Tests getting an identity by name. + */ + @Test + public void testGetIdentity() throws InterruptedException { + String identityName = harness.getTestIdentityName() + "Get"; + // First create an identity + String identityId = iamClient.createIdentity( + identityName, + "Test identity for get operation", + harness.getTenantId(), + harness.getRegion(), + Optional.empty(), + Optional.empty() + ); + + // sleep for 500ms + Thread.sleep(500); + + // Then retrieve it + String retrievedIdentity = iamClient.getIdentity( + harness.getTestIdentityName() + "Get", + harness.getTenantId(), + harness.getRegion() + ); + + Assertions.assertNotNull(retrievedIdentity, "Retrieved identity should not be null"); + Assertions.assertFalse(retrievedIdentity.isEmpty(), "Retrieved identity should not be empty"); + + cleanUpIdentity(identityName); + } + + /** + * Tests that the provider ID is correctly set. + */ + @Test + public void testProviderId() { + Assertions.assertNotNull(iam.getProviderId(), "Provider ID should not be null"); + Assertions.assertEquals(harness.getProviderId(), iam.getProviderId(), + "Provider ID should match the expected value"); + } + + /** + * Tests exception mapping for provider-specific exceptions. + */ + @Test + public void testExceptionMapping() { + // Test with a generic exception + Throwable genericException = new RuntimeException("Generic error"); + Class exceptionClass = + iam.getException(genericException); + + Assertions.assertNotNull(exceptionClass, "Exception class should not be null"); + Assertions.assertEquals( + com.salesforce.multicloudj.common.exceptions.UnknownException.class, + exceptionClass, + "Generic exceptions should map to UnknownException" + ); + } + + /** + * Tests deleting an identity. + */ + @Test + public void testDeleteIdentity() { + // First create an identity + String identityId = iamClient.createIdentity( + harness.getTestIdentityName() + "Delete", + "Test identity for delete operation", + harness.getTenantId(), + harness.getRegion(), + Optional.empty(), + Optional.empty() + ); + + Assertions.assertNotNull(identityId, "Identity ID should not be null"); + + // Then delete it - should not throw any exception + Assertions.assertDoesNotThrow(() -> + iamClient.deleteIdentity( + harness.getTestIdentityName() + "Delete", + harness.getTenantId(), + harness.getRegion() + ) + ); + } + + /** + * Tests the complete lifecycle: create, get, and delete an identity. + */ + @Test + public void testIdentityLifecycle() throws InterruptedException { + String testIdentityName = harness.getTestIdentityName() + "LifeCycle"; + + // Step 1: Create an identity + String identityId = iamClient.createIdentity( + testIdentityName, + "Test identity for lifecycle test", + harness.getTenantId(), + harness.getRegion(), + Optional.empty(), + Optional.empty() + ); + + Assertions.assertNotNull(identityId, "Identity ID should not be null after creation"); + Assertions.assertFalse(identityId.isEmpty(), "Identity ID should not be empty after creation"); + + // Step 2: Get the identity to verify it exists + Thread.sleep(500); + + String retrievedIdentity = iamClient.getIdentity( + testIdentityName, + harness.getTenantId(), + harness.getRegion() + ); + + Assertions.assertNotNull(retrievedIdentity, "Retrieved identity should not be null"); + Assertions.assertFalse(retrievedIdentity.isEmpty(), "Retrieved identity should not be empty"); + + // Step 3: Delete the identity + Assertions.assertDoesNotThrow(() -> + iamClient.deleteIdentity( + testIdentityName, + harness.getTenantId(), + harness.getRegion() + ), + "Deleting identity should not throw an exception" + ); + } } diff --git a/iam/iam-gcp/pom.xml b/iam/iam-gcp/pom.xml index 2ba25ea05..d74ea6c06 100644 --- a/iam/iam-gcp/pom.xml +++ b/iam/iam-gcp/pom.xml @@ -58,6 +58,12 @@ 3.12.1 test + + org.wiremock + wiremock-grpc-extension + 0.11.0 + test + com.salesforce.multicloudj iam-client diff --git a/iam/iam-gcp/src/main/java/com/salesforce/multicloudj/iam/gcp/GcpIam.java b/iam/iam-gcp/src/main/java/com/salesforce/multicloudj/iam/gcp/GcpIam.java index 4d312680a..503a48a0c 100644 --- a/iam/iam-gcp/src/main/java/com/salesforce/multicloudj/iam/gcp/GcpIam.java +++ b/iam/iam-gcp/src/main/java/com/salesforce/multicloudj/iam/gcp/GcpIam.java @@ -51,7 +51,7 @@ public GcpIam(Builder builder) { /** * Creates a new GCP service account with optional trust configuration. - * + * *

This method creates a service account in the specified GCP project. If trust configuration * is provided, it also grants the roles/iam.serviceAccountTokenCreator role to the specified * trusted principals, enabling them to impersonate this service account. @@ -469,7 +469,7 @@ private Policy removeBinding(Policy policy, String role, String member) { /** * Deletes a service account from the specified GCP project. - * + * *

This method permanently removes a service account and all its associated IAM bindings. * The operation cannot be undone. The method accepts either a service account ID or full * email address as input and constructs the appropriate resource name for the API call. @@ -499,7 +499,7 @@ protected void doDeleteIdentity(String identityName, String tenantId, String reg /** * Retrieves service account metadata from the specified GCP project. - * + * *

This method fetches details of an existing service account and returns its email address * as the unique identifier. The method accepts either a service account ID or full email address * as input and constructs the appropriate resource name for the API call. diff --git a/iam/iam-gcp/src/test/java/com/salesforce/multicloudj/iam/gcp/GcpIamIT.java b/iam/iam-gcp/src/test/java/com/salesforce/multicloudj/iam/gcp/GcpIamIT.java index 997bce3b7..f2223ad64 100644 --- a/iam/iam-gcp/src/test/java/com/salesforce/multicloudj/iam/gcp/GcpIamIT.java +++ b/iam/iam-gcp/src/test/java/com/salesforce/multicloudj/iam/gcp/GcpIamIT.java @@ -1,128 +1,619 @@ package com.salesforce.multicloudj.iam.gcp; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; import com.google.api.gax.rpc.TransportChannelProvider; import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.iam.admin.v1.IAMClient; import com.google.cloud.iam.admin.v1.IAMSettings; +import com.google.cloud.iam.admin.v1.stub.IAMStubSettings; import com.google.cloud.resourcemanager.v3.ProjectsClient; import com.google.cloud.resourcemanager.v3.ProjectsSettings; -import com.salesforce.multicloudj.common.gcp.GcpConstants; +import com.google.iam.admin.v1.ServiceAccount; +import com.google.iam.v1.Policy; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Empty; +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; import com.salesforce.multicloudj.common.gcp.util.MockGoogleCredentialsFactory; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.ForwardingClientCallListener; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import com.salesforce.multicloudj.common.gcp.GcpConstants; import com.salesforce.multicloudj.common.gcp.util.TestsUtilGcp; import com.salesforce.multicloudj.iam.client.AbstractIamIT; import com.salesforce.multicloudj.iam.driver.AbstractIam; import org.junit.jupiter.api.Assertions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wiremock.grpc.GrpcExtensionFactory; +import org.wiremock.grpc.dsl.WireMockGrpcService; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static org.wiremock.grpc.dsl.WireMockGrpc.message; +import static org.wiremock.grpc.dsl.WireMockGrpc.method; +/** + * Integration tests for GcpIam with gRPC record/replay support. + * + *

NOTE: GCP IAM Admin API is gRPC-only. Unlike other conformance tests that use + * HTTP/HTTPS proxy-based WireMock recording, this test uses: + *

+ * + *

The test automatically generates proto descriptors and manages WireMock gRPC lifecycle. + */ public class GcpIamIT extends AbstractIamIT { - @Override - protected Harness createHarness() { - return new HarnessImpl(); - } - - public static class HarnessImpl implements AbstractIamIT.Harness { - ProjectsClient projectsClient; - IAMClient iamClient; - int port = ThreadLocalRandom.current().nextInt(1000, 10000); - - @Override - public AbstractIam createIamDriver(boolean useValidCredentials) { - boolean isRecordingEnabled = System.getProperty("record") != null; - TransportChannelProvider channelProvider = TestsUtilGcp.getTransportChannelProvider(port); - ProjectsSettings.Builder projectsSettingsBuilder = ProjectsSettings.newBuilder() - .setTransportChannelProvider(channelProvider); - try { - if (isRecordingEnabled && useValidCredentials) { - projectsClient = ProjectsClient.create(projectsSettingsBuilder.build()); - IAMSettings.Builder iamSettingsBuilder = IAMSettings.newBuilder(); - iamClient = IAMClient.create(iamSettingsBuilder.build()); - return new GcpIam.Builder() - .withProjectsClient(projectsClient) - .withIamClient(iamClient) - .build(); - } else { - GoogleCredentials mockCreds = MockGoogleCredentialsFactory.createMockCredentials(); - projectsSettingsBuilder.setCredentialsProvider(FixedCredentialsProvider.create(mockCreds)); - projectsClient = ProjectsClient.create(projectsSettingsBuilder.build()); - IAMSettings.Builder iamSettingsBuilder = IAMSettings.newBuilder() - .setCredentialsProvider(FixedCredentialsProvider.create(mockCreds)); - iamClient = IAMClient.create(iamSettingsBuilder.build()); - return new GcpIam.Builder() - .withProjectsClient(projectsClient) - .withIamClient(iamClient) - .build(); - } - } catch (IOException e) { - Assertions.fail("Failed to create GCP clients", e); - return null; - } - } - - @Override - public String getIdentityName() { - return "serviceAccount:chameleon@substrate-sdk-gcp-poc1.iam.gserviceaccount.com"; - } - - @Override - public String getTenantId() { - return "projects/substrate-sdk-gcp-poc1"; - } - - @Override - public String getRegion() { - return "us-west1"; - } - - @Override - public String getProviderId() { - return GcpConstants.PROVIDER_ID; - } - - @Override - public int getPort() { - return port; - } - - @Override - public List getWiremockExtensions() { - return List.of("com.salesforce.multicloudj.iam.gcp.util.IamJsonResponseTransformer"); - } - - @Override - public String getIamEndpoint() { - return "https://cloudresourcemanager.googleapis.com"; - } - - - @Override - public String getTestPolicyEffect() { - return "Allow"; - } - - @Override - public List getTestPolicyActions() { - return List.of("roles/storage.objectViewer", "roles/storage.objectCreator"); - } - - @Override - public String getTestPolicyName() { - return "roles/storage.objectViewer"; - } - - @Override - public void close() { - if (projectsClient != null) { - projectsClient.close(); - } - if (iamClient != null) { - iamClient.close(); - } - } - } -} + private static final Logger logger = LoggerFactory.getLogger(GcpIamIT.class); + private static WireMockServer grpcWireMockServer; + private static WireMockGrpcService mockIamService; + private static final String GRPC_DIR = "src/test/resources/grpc"; + private static final String RECORDINGS_DIR = "src/test/resources/recordings"; + + /** + * Sets up WireMock server with gRPC extension for replay mode. + * Synchronized to prevent race conditions if called from multiple test instances. + */ + private static synchronized void setupGrpcWireMock(int port) throws IOException { + // Double-check if already set up + if (grpcWireMockServer != null && grpcWireMockServer.isRunning()) { + return; + } + // 1. Prepare directories and clean up old descriptor files + Files.createDirectories(Paths.get(GRPC_DIR)); + + // Delete any existing descriptor files to avoid corruption + Path grpcDir = Paths.get(GRPC_DIR); + if (Files.exists(grpcDir)) { + Files.list(grpcDir) + .filter(path -> path.toString().endsWith(".dsc")) + .forEach(path -> { + try { + Files.delete(path); + logger.info("Deleted old descriptor: {}", path); + } catch (IOException e) { + logger.error("Failed to delete: {}", path, e); + } + }); + } + + // 2. Generate proto descriptor BEFORE starting WireMock + saveProtoDescriptor(); + + // 3. Start WireMock with gRPC Extension (plaintext) + WireMockConfiguration config = WireMockConfiguration.wireMockConfig() + .port(port) + .extensions(new GrpcExtensionFactory()); + + grpcWireMockServer = new WireMockServer(config); + grpcWireMockServer.start(); + + logger.info("gRPC WireMock started on port: {}", port); + + // 4. Initialize WireMockGrpcService for the IAM service + mockIamService = new WireMockGrpcService( + new WireMock(port), + "google.iam.admin.v1.IAM" + ); + logger.info("Initialized WireMockGrpcService for google.iam.admin.v1.IAM"); + + // 5. Load stubs from recorded JSON files + loadAllStubs(); + } + + /** + * Loads all stub files from the recordings directory. + */ + private static void loadAllStubs() throws IOException { + Path recordingsDir = Paths.get(RECORDINGS_DIR); + if (!Files.exists(recordingsDir)) { + logger.info("No recordings directory found: {}", recordingsDir); + logger.info("Run tests with -Drecord first to create recordings"); + return; + } + + int stubsLoaded = 0; + int stubsFailed = 0; + + // Load all JSON files as stubs + try (Stream paths = Files.walk(recordingsDir)) { + List jsonFiles = paths.filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".json")) + .collect(java.util.stream.Collectors.toList()); + + logger.info("Found {} recording files", jsonFiles.size()); + + for (Path path : jsonFiles) { + try { + loadStubFromFile(path); + stubsLoaded++; + } catch (IOException e) { + stubsFailed++; + logger.error("Failed to load stub: {}", path, e); + } + } + } + + logger.info("Successfully loaded {} stubs, {} failed", stubsLoaded, stubsFailed); + logger.info("Total stub mappings in WireMock: {}", grpcWireMockServer.getStubMappings().size()); + } + + /** + * Loads a single stub file and registers it with WireMock. + * File naming convention: --.json + */ + private static void loadStubFromFile(Path filePath) throws IOException { + String fileName = filePath.getFileName().toString(); + + // Read the response JSON + String jsonContent = new String(Files.readAllBytes(filePath)); + + logger.info("Processing stub file: {}", fileName); + logger.info(" JSON content length: {} bytes", jsonContent.length()); + + JsonNode root = OBJECT_MAPPER.readTree(jsonContent); + String methodNameFromJson = root.path("methodName").asText(null); + JsonNode requestNode = root.path("request"); + JsonNode responseNode = root.path("response"); + + if (methodNameFromJson == null) { + logger.info("Skipping file with unexpected format: {}", fileName); + return; + } + + // Convert inner JSON nodes back to JSON strings + String requestJson = requestNode.isMissingNode() + ? null + : OBJECT_MAPPER.writeValueAsString(requestNode); + + String responseJson = responseNode.isMissingNode() + ? null + : OBJECT_MAPPER.writeValueAsString(responseNode); + + // Determine the response type based on method name and parse accordingly + Message responseMessage = parseResponseForMethod(methodNameFromJson, responseJson); + + if (responseMessage == null) { + logger.error(" ERROR: Unknown method type, skipping: {}", methodNameFromJson); + return; + } + + logger.info(" Parsed message type: {}", responseMessage.getClass().getSimpleName()); + // Register stub with WireMock + mockIamService.stubFor( + method(methodNameFromJson) + .withRequestMessage(equalToJson(requestJson)) + .willReturn(message(responseMessage)) + ); + + logger.info(" ✓ Successfully registered stub for: {}", methodNameFromJson); + } + + private static final ObjectMapper OBJECT_MAPPER = createObjectMapper(); + + private static ObjectMapper createObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + return mapper; + } + + /** + * Extracts the value for the given top-level key from a JSON string. + * + * @param jsonString JSON like: {"name":"foo", "projectId":"bar"} + * @param key key to extract, e.g. "name" + * @return value as String, or null if missing / parse error + */ + public static String extractValueForKey(String jsonString, String key) { + if (jsonString == null || key == null || key.isEmpty()) { + return null; + } + try { + JsonNode root = OBJECT_MAPPER.readTree(jsonString); + JsonNode node = root.get(key); + return (node != null && !node.isNull()) ? node.asText() : null; + } catch (Exception e) { + return null; + } + } + + /** + * Extracts the service account name (local-part) from a full GCP service account resource name. + * + * Example: + * input: "projects/substrate-sdk-gcp-poc1/serviceAccounts/test-sa-get@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" + * output: "test-sa-get" + * + * @param fullName full resource name or email + * @return service account name, or null if it cannot be extracted + */ + private static String extractServiceAccountIdFromFullName(String fullName) { + if (fullName == null || fullName.isEmpty()) { + return ""; + } + + // Last path segment: could be "test-sa-get@project.iam.gserviceaccount.com" + String lastSegment = fullName; + int slashIndex = fullName.lastIndexOf('/'); + if (slashIndex >= 0 && slashIndex + 1 < fullName.length()) { + lastSegment = fullName.substring(slashIndex + 1); + } + + // Take part before '@' if it's an email + int atIndex = lastSegment.indexOf('@'); + if (atIndex > 0) { + return lastSegment.substring(0, atIndex); + } + + // If there's no '@', assume the whole last segment is the name + return lastSegment; + } + + /** + * Parses response JSON into the appropriate protobuf message type. + */ + private static Message parseResponseForMethod(String methodName, String jsonContent) throws IOException { + try { + switch (methodName) { + case "CreateServiceAccount": + case "GetServiceAccount": + ServiceAccount.Builder saBuilder = ServiceAccount.newBuilder(); + JsonFormat.parser().merge(jsonContent, saBuilder); + return saBuilder.build(); + + case "DeleteServiceAccount": + // DeleteServiceAccount returns Empty + Empty.Builder emptyBuilder = Empty.newBuilder(); + JsonFormat.parser().merge(jsonContent, emptyBuilder); + return emptyBuilder.build(); + + case "GetIamPolicy": + case "SetIamPolicy": + // IAM Policy methods return Policy + Policy.Builder policyBuilder = Policy.newBuilder(); + JsonFormat.parser().merge(jsonContent, policyBuilder); + return policyBuilder.build(); + + default: + logger.warn("Unknown method type: {}", methodName); + return null; + } + } catch (Exception e) { + throw new IOException("Failed to parse response for " + methodName, e); + } + } + + /** + * Saves the proto descriptor file that WireMock needs to parse gRPC requests. + */ + private static void saveProtoDescriptor() throws IOException { + Descriptors.FileDescriptor fileDescriptor = ServiceAccount.getDescriptor().getFile(); + + DescriptorProtos.FileDescriptorSet.Builder descriptorSetBuilder = + DescriptorProtos.FileDescriptorSet.newBuilder(); + + Set addedFiles = new HashSet<>(); + addFileDescriptorWithDependencies(fileDescriptor, descriptorSetBuilder, addedFiles); + + DescriptorProtos.FileDescriptorSet descriptorSet = descriptorSetBuilder.build(); + + Path descriptorPath = Paths.get(GRPC_DIR, "iam_admin.dsc"); + Files.write(descriptorPath, descriptorSet.toByteArray()); + + logger.info("Proto descriptor saved: {}", descriptorPath.toAbsolutePath()); + logger.info("Descriptor contains {} files", descriptorSet.getFileCount()); + } + + /** + * Recursively adds file descriptor and dependencies. + */ + private static void addFileDescriptorWithDependencies( + Descriptors.FileDescriptor fileDescriptor, + DescriptorProtos.FileDescriptorSet.Builder builder, + Set addedFiles) { + + String fileName = fileDescriptor.getName(); + if (addedFiles.contains(fileName)) { + return; + } + + for (Descriptors.FileDescriptor dependency : fileDescriptor.getDependencies()) { + addFileDescriptorWithDependencies(dependency, builder, addedFiles); + } + + builder.addFile(fileDescriptor.toProto()); + addedFiles.add(fileName); + } + + @Override + protected Harness createHarness() { + return new HarnessImpl(); + } + + /** + * Harness implementation for GCP IAM integration tests with gRPC WireMock support. + */ + public static class HarnessImpl implements AbstractIamIT.Harness { + IAMClient iamClient; + ProjectsClient projectsClient; + // Parent starts HTTP WireMock on this port, we use a different port for gRPC + int port = ThreadLocalRandom.current().nextInt(1000, 10000); + int grpcPort = ThreadLocalRandom.current().nextInt(10000, 20000); + + private static final String TEST_PROJECT_ID = "projects/substrate-sdk-gcp-poc1"; + private static final String TEST_REGION = "us-west1"; + private static final String TEST_IDENTITY_NAME = "testSa"; + private static final String IAM_ENDPOINT = "https://cloudresourcemanager.googleapis.com"; + private static final String TRUSTED_PRINCIPAL = "chameleon@substrate-sdk-gcp-poc1.iam.gserviceaccount.com"; + + @Override + public AbstractIam createIamDriver(boolean useValidCredentials) { + boolean isRecordMode = System.getProperty("record") != null; + TransportChannelProvider channelProvider = TestsUtilGcp.getTransportChannelProvider(port); + ProjectsSettings.Builder projectsSettingsBuilder = ProjectsSettings.newBuilder() + .setTransportChannelProvider(channelProvider); + try { + if (isRecordMode && useValidCredentials) { + // RECORD mode: Use gRPC interceptor to capture and save responses + logger.info("--- RECORD MODE: Connecting to Real GCP with Recording ---"); + Files.createDirectories(Paths.get(RECORDINGS_DIR)); + + // Create IAM client with recording interceptor + RecordingInterceptor recordingInterceptor = new RecordingInterceptor(); + + IAMStubSettings.Builder stubSettingsBuilder = IAMStubSettings.newBuilder(); + stubSettingsBuilder.setTransportChannelProvider( + InstantiatingGrpcChannelProvider.newBuilder() + .setInterceptorProvider(() -> List.of(recordingInterceptor)) + .build() + ); + + iamClient = IAMClient.create(IAMSettings.create(stubSettingsBuilder.build())); + projectsClient = ProjectsClient.create(projectsSettingsBuilder.build()); + return new GcpIam.Builder() + .withProjectsClient(projectsClient) + .withIamClient(iamClient) + .build(); + } else { + // REPLAY mode: Setup gRPC WireMock if not already done, then connect + logger.info("--- REPLAY MODE: Connecting to WireMock gRPC ---"); + + // Lazy initialization of gRPC WireMock + if (grpcWireMockServer == null || !grpcWireMockServer.isRunning()) { + setupGrpcWireMock(grpcPort); + } + + IAMStubSettings.Builder stubSettingsBuilder = IAMStubSettings.newBuilder(); + stubSettingsBuilder.setEndpoint("localhost:" + grpcPort); + stubSettingsBuilder.setCredentialsProvider(NoCredentialsProvider.create()); + + // Use plaintext for WireMock gRPC connections + stubSettingsBuilder.setTransportChannelProvider( + InstantiatingGrpcChannelProvider.newBuilder() + .setChannelConfigurator(io.grpc.ManagedChannelBuilder::usePlaintext) + .build() + ); + + iamClient = IAMClient.create(IAMSettings.create(stubSettingsBuilder.build())); + GoogleCredentials mockCreds = MockGoogleCredentialsFactory.createMockCredentials(); + projectsSettingsBuilder.setCredentialsProvider(FixedCredentialsProvider.create(mockCreds)); + projectsClient = ProjectsClient.create(projectsSettingsBuilder.build()); + return new GcpIam.Builder() + .withProjectsClient(projectsClient) + .withIamClient(iamClient) + .build(); + } + } catch (IOException e) { + Assertions.fail("Failed to create IAM client", e); + return null; + } + } + + @Override + public String getTenantId() { + return TEST_PROJECT_ID; + } + + @Override + public String getRegion() { + return TEST_REGION; + } + + @Override + public String getTestIdentityName() { + return TEST_IDENTITY_NAME; + } + + @Override + public String getIamEndpoint() { + return IAM_ENDPOINT; + } + + @Override + public String getProviderId() { + return GcpConstants.PROVIDER_ID; + } + + @Override + public int getPort() { + // Port is used by parent class to start WireMock server + // for HTTP-based policy API recording via WireMock proxy. + return port; + } + + @Override + public List getWiremockExtensions() { + // HTTP-based policy APIs use JSON transformer for record/replay. + return List.of("com.salesforce.multicloudj.iam.gcp.util.IamJsonResponseTransformer"); + } + + @Override + public String getTrustedPrincipal() { + return TRUSTED_PRINCIPAL; + } + + @Override + public String getIdentityName() { + return "serviceAccount:chameleon@substrate-sdk-gcp-poc1.iam.gserviceaccount.com"; + } + + @Override + public String getTestPolicyEffect() { + return "Allow"; + } + + @Override + public List getTestPolicyActions() { + return List.of("roles/storage.objectViewer", "roles/storage.objectCreator"); + } + + @Override + public String getTestPolicyName() { + return "roles/storage.objectViewer"; + } + + @Override + public void close() { + if (iamClient != null) { + iamClient.close(); + } + if (projectsClient != null) { + projectsClient.close(); + } + // Clean up gRPC WireMock server if it's running + if (grpcWireMockServer != null && grpcWireMockServer.isRunning()) { + grpcWireMockServer.stop(); + logger.info("Stopped gRPC WireMock server"); + } + } + } + + /** + * gRPC ClientInterceptor that records responses to JSON files. + * This enables the record/replay pattern for gRPC APIs. + */ + static class RecordingInterceptor implements ClientInterceptor { + private int callCounter = 0; + + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + // 1. We hold the request message here so the listener can access it later + AtomicReference requestCapture = new AtomicReference<>(); + + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + + @Override + public void sendMessage(ReqT message) { + // Capture the outgoing request + requestCapture.set(message); + super.sendMessage(message); + } + + @Override + public void start(Listener responseListener, Metadata headers) { + // Wrap the listener to intercept responses + Listener recordingListener = new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { + + @Override + public void onMessage(RespT message) { + // Save the response + saveRequestResponse(method.getFullMethodName(), (Message)(requestCapture.get()), (Message) message); + super.onMessage(message); + } + }; + + super.start(recordingListener, headers); + } + }; + } + + /** + * Saves a gRPC response to a JSON file. + * Method name format: google.iam.admin.v1.IAM/CreateServiceAccount + */ + private void saveRequestResponse(String fullMethodName, Message request, Message response) { + try { + // Extract method name from full path (e.g., "CreateServiceAccount" from "google.iam.admin.v1.IAM/CreateServiceAccount") + String methodName = fullMethodName.substring(fullMethodName.lastIndexOf('/') + 1); + + String responseJson = JsonFormat.printer().print(response); + String requestJson = JsonFormat.printer().print(request); + + // Parse the request/response strings into JSON trees + JsonNode requestNode = OBJECT_MAPPER.readTree(requestJson); + JsonNode responseNode = OBJECT_MAPPER.readTree(responseJson); + + // Build the wrapper object + ObjectNode root = OBJECT_MAPPER.createObjectNode(); + root.put("methodName", methodName); + root.set("request", requestNode); + root.set("response", responseNode); + + // Serialize back to a JSON string + String requestResponseJson = OBJECT_MAPPER.writeValueAsString(root); + + String serviceAccountId = extractServiceAccountId(methodName, requestJson, responseJson); + String filename; + if (!serviceAccountId.isEmpty()) { + filename = String.format("%s-%s-%04d.json", methodName, serviceAccountId, callCounter++); + } else { + filename = String.format("%s-%04d.json", methodName, callCounter++); + } + Path filePath = Paths.get(RECORDINGS_DIR, filename); + Files.write(filePath, requestResponseJson.getBytes()); + logger.info("Recorded: {}", filename); + } catch (IOException e) { + logger.error("Failed to record response for {}", fullMethodName, e); + } + } + + private String extractServiceAccountId(String methodName, String requestJson, String responseJson) { + + switch (methodName) { + case "CreateServiceAccount": + return extractValueForKey(requestJson, "accountId"); + case "DeleteServiceAccount": + case "GetServiceAccount": + return extractServiceAccountIdFromFullName(extractValueForKey(requestJson, "name")); + case "SetIamPolicy": + case "GetIamPolicy": + return extractServiceAccountIdFromFullName(extractValueForKey(requestJson, "resource")); + default: + throw new IllegalArgumentException("Unknown method: " + methodName); + } + } + } +} diff --git a/iam/iam-gcp/src/test/resources/grpc/iam_admin.dsc b/iam/iam-gcp/src/test/resources/grpc/iam_admin.dsc new file mode 100644 index 000000000..8478928fe Binary files /dev/null and b/iam/iam-gcp/src/test/resources/grpc/iam_admin.dsc differ diff --git a/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSa-0000.json b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSa-0000.json new file mode 100644 index 000000000..7ae776d9e --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSa-0000.json @@ -0,0 +1,21 @@ +{ + "methodName" : "CreateServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1", + "accountId" : "testSa", + "serviceAccount" : { + "displayName" : "testSa", + "description" : "Test identity for MultiCloudJ integration tests" + } + }, + "response" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSa@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "projectId" : "substrate-sdk-gcp-poc1", + "uniqueId" : "102748697421896258849", + "email" : "testSa@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "displayName" : "testSa", + "etag" : "MDEwMjE5MjA=", + "description" : "Test identity for MultiCloudJ integration tests", + "oauth2ClientId" : "102748697421896258849" + } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaDelete-0000.json b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaDelete-0000.json new file mode 100644 index 000000000..306ae7efb --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaDelete-0000.json @@ -0,0 +1,21 @@ +{ + "methodName" : "CreateServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1", + "accountId" : "testSaDelete", + "serviceAccount" : { + "displayName" : "testSaDelete", + "description" : "Test identity for delete operation" + } + }, + "response" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaDelete@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "projectId" : "substrate-sdk-gcp-poc1", + "uniqueId" : "114427331964098776950", + "email" : "testSaDelete@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "displayName" : "testSaDelete", + "etag" : "MDEwMjE5MjA=", + "description" : "Test identity for delete operation", + "oauth2ClientId" : "114427331964098776950" + } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaGet-0000.json b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaGet-0000.json new file mode 100644 index 000000000..a85ce2784 --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaGet-0000.json @@ -0,0 +1,21 @@ +{ + "methodName" : "CreateServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1", + "accountId" : "testSaGet", + "serviceAccount" : { + "displayName" : "testSaGet", + "description" : "Test identity for get operation" + } + }, + "response" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaGet@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "projectId" : "substrate-sdk-gcp-poc1", + "uniqueId" : "107637792717662016720", + "email" : "testSaGet@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "displayName" : "testSaGet", + "etag" : "MDEwMjE5MjA=", + "description" : "Test identity for get operation", + "oauth2ClientId" : "107637792717662016720" + } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaLifeCycle-0000.json b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaLifeCycle-0000.json new file mode 100644 index 000000000..4e526c483 --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaLifeCycle-0000.json @@ -0,0 +1,21 @@ +{ + "methodName" : "CreateServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1", + "accountId" : "testSaLifeCycle", + "serviceAccount" : { + "displayName" : "testSaLifeCycle", + "description" : "Test identity for lifecycle test" + } + }, + "response" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaLifeCycle@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "projectId" : "substrate-sdk-gcp-poc1", + "uniqueId" : "114276556953663474442", + "email" : "testSaLifeCycle@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "displayName" : "testSaLifeCycle", + "etag" : "MDEwMjE5MjA=", + "description" : "Test identity for lifecycle test", + "oauth2ClientId" : "114276556953663474442" + } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaNoDesc-0000.json b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaNoDesc-0000.json new file mode 100644 index 000000000..45c8f5911 --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaNoDesc-0000.json @@ -0,0 +1,19 @@ +{ + "methodName" : "CreateServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1", + "accountId" : "testSaNoDesc", + "serviceAccount" : { + "displayName" : "testSaNoDesc" + } + }, + "response" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaNoDesc@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "projectId" : "substrate-sdk-gcp-poc1", + "uniqueId" : "102855534879409764604", + "email" : "testSaNoDesc@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "displayName" : "testSaNoDesc", + "etag" : "MDEwMjE5MjA=", + "oauth2ClientId" : "102855534879409764604" + } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaOptions-0000.json b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaOptions-0000.json new file mode 100644 index 000000000..a080d60a0 --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaOptions-0000.json @@ -0,0 +1,21 @@ +{ + "methodName" : "CreateServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1", + "accountId" : "testSaOptions", + "serviceAccount" : { + "displayName" : "testSaOptions", + "description" : "Test identity with options" + } + }, + "response" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaOptions@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "projectId" : "substrate-sdk-gcp-poc1", + "uniqueId" : "100474553911983032550", + "email" : "testSaOptions@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "displayName" : "testSaOptions", + "etag" : "MDEwMjE5MjA=", + "description" : "Test identity with options", + "oauth2ClientId" : "100474553911983032550" + } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaTrusted-0000.json b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaTrusted-0000.json new file mode 100644 index 000000000..9deae40d0 --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/CreateServiceAccount-testSaTrusted-0000.json @@ -0,0 +1,21 @@ +{ + "methodName" : "CreateServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1", + "accountId" : "testSaTrusted", + "serviceAccount" : { + "displayName" : "testSaTrusted", + "description" : "Test identity with trust configuration" + } + }, + "response" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaTrusted@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "projectId" : "substrate-sdk-gcp-poc1", + "uniqueId" : "114349486479818249800", + "email" : "testSaTrusted@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "displayName" : "testSaTrusted", + "etag" : "MDEwMjE5MjA=", + "description" : "Test identity with trust configuration", + "oauth2ClientId" : "114349486479818249800" + } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSa-0001.json b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSa-0001.json new file mode 100644 index 000000000..c85cd4a2c --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSa-0001.json @@ -0,0 +1,7 @@ +{ + "methodName" : "DeleteServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSa@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" + }, + "response" : { } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaDelete-0001.json b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaDelete-0001.json new file mode 100644 index 000000000..af6005dda --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaDelete-0001.json @@ -0,0 +1,7 @@ +{ + "methodName" : "DeleteServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaDelete@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" + }, + "response" : { } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaGet-0002.json b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaGet-0002.json new file mode 100644 index 000000000..2fbadbdb8 --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaGet-0002.json @@ -0,0 +1,7 @@ +{ + "methodName" : "DeleteServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaGet@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" + }, + "response" : { } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaLifeCycle-0002.json b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaLifeCycle-0002.json new file mode 100644 index 000000000..c657cb3d1 --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaLifeCycle-0002.json @@ -0,0 +1,7 @@ +{ + "methodName" : "DeleteServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaLifeCycle@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" + }, + "response" : { } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaNoDesc-0001.json b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaNoDesc-0001.json new file mode 100644 index 000000000..0736a750f --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaNoDesc-0001.json @@ -0,0 +1,7 @@ +{ + "methodName" : "DeleteServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaNoDesc@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" + }, + "response" : { } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaOptions-0001.json b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaOptions-0001.json new file mode 100644 index 000000000..737ecb44a --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaOptions-0001.json @@ -0,0 +1,7 @@ +{ + "methodName" : "DeleteServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaOptions@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" + }, + "response" : { } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaTrusted-0003.json b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaTrusted-0003.json new file mode 100644 index 000000000..e6a0a5718 --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/DeleteServiceAccount-testSaTrusted-0003.json @@ -0,0 +1,7 @@ +{ + "methodName" : "DeleteServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaTrusted@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" + }, + "response" : { } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/GetIamPolicy-testSaTrusted-0001.json b/iam/iam-gcp/src/test/resources/recordings/GetIamPolicy-testSaTrusted-0001.json new file mode 100644 index 000000000..457e8129a --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/GetIamPolicy-testSaTrusted-0001.json @@ -0,0 +1,9 @@ +{ + "methodName" : "GetIamPolicy", + "request" : { + "resource" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaTrusted@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" + }, + "response" : { + "etag" : "ACAB" + } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/GetServiceAccount-testSaGet-0001.json b/iam/iam-gcp/src/test/resources/recordings/GetServiceAccount-testSaGet-0001.json new file mode 100644 index 000000000..7e947ca57 --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/GetServiceAccount-testSaGet-0001.json @@ -0,0 +1,16 @@ +{ + "methodName" : "GetServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaGet@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" + }, + "response" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaGet@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "projectId" : "substrate-sdk-gcp-poc1", + "uniqueId" : "107637792717662016720", + "email" : "testSaGet@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "displayName" : "testSaGet", + "etag" : "MDEwMjE5MjA=", + "description" : "Test identity for get operation", + "oauth2ClientId" : "107637792717662016720" + } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/GetServiceAccount-testSaLifeCycle-0000.json b/iam/iam-gcp/src/test/resources/recordings/GetServiceAccount-testSaLifeCycle-0000.json new file mode 100644 index 000000000..f67488b28 --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/GetServiceAccount-testSaLifeCycle-0000.json @@ -0,0 +1,16 @@ +{ + "methodName" : "GetServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaLifeCycle@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" + }, + "response" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaLifeCycle@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "projectId" : "substrate-sdk-gcp-poc1", + "uniqueId" : "112054972166957401551", + "email" : "testSaLifeCycle@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "displayName" : "testSaLifeCycle", + "etag" : "MDEwMjE5MjA=", + "description" : "Test identity for lifecycle test", + "oauth2ClientId" : "112054972166957401551" + } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/GetServiceAccount-testSaLifeCycle-0001.json b/iam/iam-gcp/src/test/resources/recordings/GetServiceAccount-testSaLifeCycle-0001.json new file mode 100644 index 000000000..a2db6597e --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/GetServiceAccount-testSaLifeCycle-0001.json @@ -0,0 +1,16 @@ +{ + "methodName" : "GetServiceAccount", + "request" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaLifeCycle@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" + }, + "response" : { + "name" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaLifeCycle@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "projectId" : "substrate-sdk-gcp-poc1", + "uniqueId" : "114276556953663474442", + "email" : "testSaLifeCycle@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "displayName" : "testSaLifeCycle", + "etag" : "MDEwMjE5MjA=", + "description" : "Test identity for lifecycle test", + "oauth2ClientId" : "114276556953663474442" + } +} \ No newline at end of file diff --git a/iam/iam-gcp/src/test/resources/recordings/SetIamPolicy-testSaTrusted-0002.json b/iam/iam-gcp/src/test/resources/recordings/SetIamPolicy-testSaTrusted-0002.json new file mode 100644 index 000000000..1c47bd4ed --- /dev/null +++ b/iam/iam-gcp/src/test/resources/recordings/SetIamPolicy-testSaTrusted-0002.json @@ -0,0 +1,21 @@ +{ + "methodName" : "SetIamPolicy", + "request" : { + "resource" : "projects/substrate-sdk-gcp-poc1/serviceAccounts/testSaTrusted@substrate-sdk-gcp-poc1.iam.gserviceaccount.com", + "policy" : { + "etag" : "ACAB", + "bindings" : [ { + "role" : "roles/iam.serviceAccountTokenCreator", + "members" : [ "serviceAccount:chameleon@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" ] + } ] + } + }, + "response" : { + "version" : 1, + "etag" : "BwZGEqqsKlI=", + "bindings" : [ { + "role" : "roles/iam.serviceAccountTokenCreator", + "members" : [ "serviceAccount:chameleon@substrate-sdk-gcp-poc1.iam.gserviceaccount.com" ] + } ] + } +} \ No newline at end of file diff --git a/multicloudj-common/src/test/java/com/salesforce/multicloudj/common/util/common/TestsUtil.java b/multicloudj-common/src/test/java/com/salesforce/multicloudj/common/util/common/TestsUtil.java index 8e3a638f2..9c8b7eaab 100644 --- a/multicloudj-common/src/test/java/com/salesforce/multicloudj/common/util/common/TestsUtil.java +++ b/multicloudj-common/src/test/java/com/salesforce/multicloudj/common/util/common/TestsUtil.java @@ -22,6 +22,7 @@ import java.util.Base64; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; import static com.github.tomakehurst.wiremock.client.WireMock.recordSpec; import static com.salesforce.multicloudj.common.util.common.TestsUtil.TruncateRequestBodyTransformer.TRUNCATE_MATCHER_REQUST_BODY_OVER; @@ -48,9 +49,12 @@ public StubMapping transform(StubMapping stubMapping, FileSource files, Paramete // See if any of the existing body patterns exceed our length limit for(ContentPattern pattern : bodyPatterns) { if(pattern.getExpected().length() > truncateMatcherRequestBodyOver){ - // We've exceeded our desired matcher length, so truncate it + // We've exceeded our desired matcher length, so truncate it. + // The truncated substring may start with regex metacharacters like '{', + // so we must escape it before constructing a RegexPattern. String truncatedString = pattern.getExpected().substring(0, truncateMatcherRequestBodyOver); - newPatterns.add(new RegexPattern("^" + truncatedString +"*")); + String escaped = Pattern.quote(truncatedString); + newPatterns.add(new RegexPattern("^" + escaped + ".*")); } }