From defff229ca3f03bce9800866f586912450b4ece6 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 28 May 2026 17:14:50 +0300 Subject: [PATCH 1/3] Issue-131 feat: add IAM role support for S3 uploads --- README.md | 23 +- java-reporter-core/pom.xml | 6 +- .../core/constants/ArtifactPropertyNames.java | 2 + .../core/constants/CommonConstants.java | 2 +- .../core/constants/CredentialConstants.java | 2 + .../artifact/client/S3ClientFactory.java | 135 ++++++++--- .../credential/CredentialsManager.java | 10 + .../artifact/credential/S3Credentials.java | 18 ++ .../artifact/client/S3ClientFactoryTest.java | 210 ++++++++++++++++++ java-reporter-cucumber/pom.xml | 4 +- java-reporter-junit/pom.xml | 4 +- java-reporter-karate/pom.xml | 4 +- java-reporter-testng/pom.xml | 4 +- pom.xml | 2 +- testomat-allure-adapter/pom.xml | 4 +- 15 files changed, 372 insertions(+), 58 deletions(-) create mode 100644 java-reporter-core/src/test/java/io/testomat/core/artifact/client/S3ClientFactoryTest.java diff --git a/README.md b/README.md index 46e78267..a9731fc9 100644 --- a/README.md +++ b/README.md @@ -339,16 +339,19 @@ Artifacts are stored in external S3 buckets. S3 Access can be configured in **tw > NOTE: Environment variables(env/jvm/testomatio.properties) take precedence over server-provided credentials. -| Setting | Description | Default | -|-------------------------------|--------------------------------------------------|-------------| -| `testomatio.artifact.disable` | Completely disable artifact uploading | `false` | -| `testomatio.artifact.private` | Keep artifacts private (no public URLs) | `false` | -| `testomatio.step.artifacts.enabled` | Enables uploading artifacts for test steps | `false` | -| `s3.force-path-style` | Use path-style URLs for S3-compatible storage | `false` | -| `s3.endpoint` | Custom endpoint to be used with force-path-style | `false` | -| `s3.bucket` | Provides bucket name for configuration | | -| `s3.access-key-id` | Access key for the bucket | | -| `s3.region` | Bucket region | `us-west-1` | +| Setting | Description | Default | +|-------------------------------------|-------------------------------------------------------|-------------| +| `testomatio.artifact.disable` | Completely disable artifact uploading | `false` | +| `testomatio.artifact.private` | Keep artifacts private (no public URLs) | `false` | +| `testomatio.step.artifacts.enabled` | Enables uploading artifacts for test steps | `false` | +| `s3.force-path-style` | Use path-style URLs for S3-compatible storage | `false` | +| `s3.endpoint` | Custom endpoint to be used with force-path-style | `false` | +| `s3.bucket` | Provides bucket name for configuration | | +| `s3.access-key-id` | Access key for the bucket | | +| `s3.secret.access-key-id` | Secret access key for the bucket | | +| `s3.region` | Bucket region | `us-west-1` | +| `s3.assume.role.arn` | AWS IAM role ARN used for AssumeRole authentication | | +| `s3.assume.role.external.id` | External ID for AssumeRole authentication | | **Note**: S3 credentials can be configured either in properties file or provided automatically on Testomat.io UI. Environment variables take precedence over server-provided credentials. diff --git a/java-reporter-core/pom.xml b/java-reporter-core/pom.xml index 97da8231..23f0ee69 100644 --- a/java-reporter-core/pom.xml +++ b/java-reporter-core/pom.xml @@ -7,7 +7,7 @@ io.testomat java-reporter-core - 0.12.1 + 0.13.0 jar Testomat.io Reporter Core @@ -80,6 +80,10 @@ software.amazon.awssdk s3 + + software.amazon.awssdk + sts + diff --git a/java-reporter-core/src/main/java/io/testomat/core/constants/ArtifactPropertyNames.java b/java-reporter-core/src/main/java/io/testomat/core/constants/ArtifactPropertyNames.java index f90f8b85..0f08cc87 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/constants/ArtifactPropertyNames.java +++ b/java-reporter-core/src/main/java/io/testomat/core/constants/ArtifactPropertyNames.java @@ -6,6 +6,8 @@ public class ArtifactPropertyNames { public static final String SECRET_ACCESS_KEY_PROPERTY_NAME = "s3.secret.access-key-id"; public static final String REGION_PROPERTY_NAME = "s3.region"; public static final String ENDPOINT_PROPERTY_NAME = "s3.endpoint"; + public static final String ASSUME_ROLE_ARN_PROPERTY_NAME = "s3.assume.role.arn"; + public static final String ASSUME_ROLE_EXTERNAL_ID_PROPERTY_NAME = "s3.assume.role.external.id"; public static final String FORCE_PATH_PROPERTY_NAME = "s3.force-path-style"; diff --git a/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java b/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java index 70805fba..65a73617 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java +++ b/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java @@ -1,7 +1,7 @@ package io.testomat.core.constants; public class CommonConstants { - public static final String REPORTER_VERSION = "0.12.1"; + public static final String REPORTER_VERSION = "0.13.0"; public static final String TESTS_STRING = "tests"; public static final String API_KEY_STRING = "api_key"; diff --git a/java-reporter-core/src/main/java/io/testomat/core/constants/CredentialConstants.java b/java-reporter-core/src/main/java/io/testomat/core/constants/CredentialConstants.java index e90dbcaf..ba355fa6 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/constants/CredentialConstants.java +++ b/java-reporter-core/src/main/java/io/testomat/core/constants/CredentialConstants.java @@ -8,6 +8,8 @@ public class CredentialConstants { public static final String ACCESS_KEY_ID = "ACCESS_KEY_ID"; public static final String BUCKET = "BUCKET"; public static final String REGION = "REGION"; + public static final String EXTERNAL_ID = "EXTERNAL_ID"; + public static final String ARN = "ARN"; public static final String ENDPOINT = "ENDPOINT"; public static final String FORCE_PATH = "FORCE_PATH_STYLE"; diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java index 180213ca..5e264dd5 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java @@ -10,6 +10,8 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3ClientBuilder; import software.amazon.awssdk.services.s3.S3Configuration; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider; /** * Factory for creating configured S3Client instances with custom endpoint support. @@ -19,57 +21,120 @@ public class S3ClientFactory { /** * Creates a configured S3Client based on current credentials and settings. + * Priority: + * 1. IAM Role (if roleArn configured) + * 2. Static access key / secret key * * @return configured S3Client instance * @throws IllegalArgumentException if credentials are invalid or missing */ public S3Client createS3Client() { - S3Credentials s3Credentials = CredentialsManager.getCredentials(); + S3Credentials s3 = CredentialsManager.getCredentials(); - S3ClientBuilder builder = S3Client.builder(); + Region region = resolveRegion(s3); - AwsCredentialsProvider credentialsProvider; - if (s3Credentials.getAccessKeyId() != null && s3Credentials.getSecretAccessKey() != null) { - if (s3Credentials.getAccessKeyId().trim().isEmpty() || s3Credentials.getSecretAccessKey().trim().isEmpty()) { - throw new IllegalArgumentException("Access key and secret access key cannot be empty"); - } - AwsBasicCredentials credentials = AwsBasicCredentials.create(s3Credentials.getAccessKeyId().trim(), s3Credentials.getSecretAccessKey().trim()); - credentialsProvider = StaticCredentialsProvider.create(credentials); - } else { - throw new IllegalArgumentException("S3 credentials (access key and secret access key) must be configured"); + AwsCredentialsProvider provider = + buildCredentialsProvider(s3, region); + + S3ClientBuilder builder = S3Client.builder() + .credentialsProvider(provider) + .region(region); + + configureEndpoint(builder, s3); + + return builder.build(); + } + + /** + * Builds AWS credentials provider. + */ + private AwsCredentialsProvider buildCredentialsProvider(S3Credentials s3, Region region) { + boolean useIamRole = s3.getRoleArn() != null && !s3.getRoleArn().isBlank(); + + if (useIamRole) { + return buildIamRoleProvider(s3, region); } - builder.credentialsProvider(credentialsProvider); - if (s3Credentials.getRegion() != null && !s3Credentials.getRegion().trim().isEmpty()) { - try { - builder.region(Region.of(s3Credentials.getRegion().trim())); - } catch (Exception e) { - throw new IllegalArgumentException("Invalid region: " + s3Credentials.getRegion(), e); + return buildStaticCredentialsProvider(s3); + } + + /** + * Creates AssumeRole credentials provider. + */ + private AwsCredentialsProvider buildIamRoleProvider(S3Credentials s3, Region region) { + StsClient stsClient = StsClient.builder() + .region(region) + .build(); + + return StsAssumeRoleCredentialsProvider.builder() + .stsClient(stsClient) + .refreshRequest(request -> { + request.roleArn(s3.getRoleArn().trim()); + request.roleSessionName("testomat-s3-upload"); + + if (s3.getExternalId() != null && !s3.getExternalId().isBlank()) { + request.externalId(s3.getExternalId().trim()); + } + }).build(); + } + + /** + * Creates static credentials provider. + */ + private AwsCredentialsProvider buildStaticCredentialsProvider(S3Credentials s3) { + + if (s3.getAccessKeyId() == null || s3.getAccessKeyId().isBlank()) { + throw new IllegalArgumentException("AWS access key is missing"); + } + + if (s3.getSecretAccessKey() == null || s3.getSecretAccessKey().isBlank()) { + throw new IllegalArgumentException("AWS secret key is missing"); + } + + AwsBasicCredentials credentials = + AwsBasicCredentials.create( + s3.getAccessKeyId().trim(), + s3.getSecretAccessKey().trim()); + + return StaticCredentialsProvider.create(credentials); + } + + /** + * Resolves AWS region. + */ + private Region resolveRegion(S3Credentials s3) { + try { + if (s3.getRegion() == null || s3.getRegion().isBlank()) { + return Region.US_EAST_1; } - } else { - builder.region(Region.US_EAST_1); + return Region.of(s3.getRegion().trim()); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid AWS region: " + s3.getRegion(), e); } + } + + /** + * Configures custom endpoint and path-style access. + */ + private void configureEndpoint(S3ClientBuilder builder, S3Credentials s3) { + boolean hasCustomEndpoint = s3.getCustomEndpoint() != null && !s3.getCustomEndpoint().isBlank(); - if (s3Credentials.getCustomEndpoint() != null && !s3Credentials.getCustomEndpoint().trim().isEmpty()) { + if (hasCustomEndpoint) { try { - builder.endpointOverride(URI.create(s3Credentials.getCustomEndpoint().trim())); + builder.endpointOverride(URI.create(s3.getCustomEndpoint().trim())); } catch (Exception e) { - throw new IllegalArgumentException("Invalid endpoint URL: " + s3Credentials.getCustomEndpoint(), e); - } - - S3Configuration s3Config = S3Configuration.builder() - .pathStyleAccessEnabled(s3Credentials.isForcePath()) - .build(); - builder.serviceConfiguration(s3Config); - } else { - if (s3Credentials.isForcePath()) { - S3Configuration s3Config = S3Configuration.builder() - .pathStyleAccessEnabled(true) - .build(); - builder.serviceConfiguration(s3Config); + throw new IllegalArgumentException("Invalid endpoint URL: " + s3.getCustomEndpoint(), e); } } - return builder.build(); + if (s3.isForcePath() || hasCustomEndpoint) { + builder.serviceConfiguration( + S3Configuration.builder() + .pathStyleAccessEnabled( + s3.isForcePath() + ) + .build() + ); + } } } \ No newline at end of file diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/credential/CredentialsManager.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/credential/CredentialsManager.java index bda65dc2..a6d309b8 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/credential/CredentialsManager.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/credential/CredentialsManager.java @@ -1,15 +1,19 @@ package io.testomat.core.facade.methods.artifact.credential; import static io.testomat.core.constants.ArtifactPropertyNames.ACCESS_KEY_PROPERTY_NAME; +import static io.testomat.core.constants.ArtifactPropertyNames.ASSUME_ROLE_ARN_PROPERTY_NAME; import static io.testomat.core.constants.ArtifactPropertyNames.BUCKET_PROPERTY_NAME; import static io.testomat.core.constants.ArtifactPropertyNames.ENDPOINT_PROPERTY_NAME; +import static io.testomat.core.constants.ArtifactPropertyNames.ASSUME_ROLE_EXTERNAL_ID_PROPERTY_NAME; import static io.testomat.core.constants.ArtifactPropertyNames.FORCE_PATH_PROPERTY_NAME; import static io.testomat.core.constants.ArtifactPropertyNames.PRIVATE_ARTIFACTS_PROPERTY_NAME; import static io.testomat.core.constants.ArtifactPropertyNames.REGION_PROPERTY_NAME; import static io.testomat.core.constants.ArtifactPropertyNames.SECRET_ACCESS_KEY_PROPERTY_NAME; import static io.testomat.core.constants.CredentialConstants.ACCESS_KEY_ID; +import static io.testomat.core.constants.CredentialConstants.ARN; import static io.testomat.core.constants.CredentialConstants.BUCKET; import static io.testomat.core.constants.CredentialConstants.ENDPOINT; +import static io.testomat.core.constants.CredentialConstants.EXTERNAL_ID; import static io.testomat.core.constants.CredentialConstants.FORCE_PATH; import static io.testomat.core.constants.CredentialConstants.IAM; import static io.testomat.core.constants.CredentialConstants.PRESIGN; @@ -77,6 +81,12 @@ public void populateCredentials(Map credsFromServer) { populateCredentialField(REGION_PROPERTY_NAME, REGION, credsFromServer, "Region", value -> credentials.setRegion(getStringValue(value))); + populateCredentialField(ASSUME_ROLE_ARN_PROPERTY_NAME, ARN, credsFromServer, "Arn", + value -> credentials.setRoleArn(getStringValue(value))); + + populateCredentialField(ASSUME_ROLE_EXTERNAL_ID_PROPERTY_NAME, EXTERNAL_ID, credsFromServer, "ExternalId", + value -> credentials.setExternalId(getStringValue(value))); + credentials.setIam(getBooleanValue(credsFromServer.get(IAM))); credentials.setShared(getBooleanValue(credsFromServer.get(SHARED))); diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/credential/S3Credentials.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/credential/S3Credentials.java index aa11127b..b2aa9f4b 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/credential/S3Credentials.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/credential/S3Credentials.java @@ -13,6 +13,8 @@ public class S3Credentials { private String bucket; private String region; private String customEndpoint; + private String roleArn; + private String externalId; private boolean forcePath = false; public boolean isForcePath() { @@ -86,4 +88,20 @@ public String getCustomEndpoint() { public void setCustomEndpoint(String customEndpoint) { this.customEndpoint = customEndpoint; } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public String getRoleArn() { + return roleArn; + } + + public void setRoleArn(String roleArn) { + this.roleArn = roleArn; + } } \ No newline at end of file diff --git a/java-reporter-core/src/test/java/io/testomat/core/artifact/client/S3ClientFactoryTest.java b/java-reporter-core/src/test/java/io/testomat/core/artifact/client/S3ClientFactoryTest.java new file mode 100644 index 00000000..b00c790e --- /dev/null +++ b/java-reporter-core/src/test/java/io/testomat/core/artifact/client/S3ClientFactoryTest.java @@ -0,0 +1,210 @@ +package io.testomat.core.artifact.client; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.testomat.core.facade.methods.artifact.client.S3ClientFactory; +import io.testomat.core.facade.methods.artifact.credential.S3Credentials; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3ClientBuilder; +import software.amazon.awssdk.services.s3.S3Configuration; + +class S3ClientFactoryTest { + + private S3ClientFactory factory; + + @BeforeEach + void setUp() { + factory = new S3ClientFactory(); + } + + @Test + void shouldReturnDefaultRegionWhenRegionIsBlank() throws Exception { + S3Credentials s3 = mock(S3Credentials.class); + + when(s3.getRegion()).thenReturn(" "); + + Region region = invokeResolveRegion(s3); + + assertEquals(Region.US_EAST_1, region); + } + + @Test + void shouldReturnProvidedRegion() throws Exception { + S3Credentials s3 = mock(S3Credentials.class); + + when(s3.getRegion()).thenReturn("eu-central-1"); + + Region region = invokeResolveRegion(s3); + + assertEquals(Region.EU_CENTRAL_1, region); + } + + @Test + void shouldCreateStaticCredentialsProvider() throws Exception { + S3Credentials s3 = mock(S3Credentials.class); + + when(s3.getAccessKeyId()).thenReturn("access-key"); + when(s3.getSecretAccessKey()).thenReturn("secret-key"); + + AwsCredentialsProvider provider = + invokeBuildStaticCredentialsProvider(s3); + + assertNotNull(provider); + assertInstanceOf(StaticCredentialsProvider.class, provider); + } + + @Test + void shouldTrimStaticCredentials() throws Exception { + S3Credentials s3 = mock(S3Credentials.class); + + when(s3.getAccessKeyId()).thenReturn(" access-key "); + when(s3.getSecretAccessKey()).thenReturn(" secret-key "); + + StaticCredentialsProvider provider = + (StaticCredentialsProvider) invokeBuildStaticCredentialsProvider(s3); + + assertEquals( + "access-key", + provider.resolveCredentials().accessKeyId() + ); + + assertEquals( + "secret-key", + provider.resolveCredentials().secretAccessKey() + ); + } + + @Test + void shouldThrowExceptionWhenAccessKeyMissing() { + S3Credentials s3 = mock(S3Credentials.class); + + when(s3.getAccessKeyId()).thenReturn(" "); + when(s3.getSecretAccessKey()).thenReturn("secret"); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> invokeBuildStaticCredentialsProvider(s3) + ); + + assertEquals("AWS access key is missing", exception.getMessage()); + } + + @Test + void shouldThrowExceptionWhenSecretKeyMissing() { + S3Credentials s3 = mock(S3Credentials.class); + + when(s3.getAccessKeyId()).thenReturn("access"); + when(s3.getSecretAccessKey()).thenReturn(" "); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> invokeBuildStaticCredentialsProvider(s3) + ); + + assertEquals("AWS secret key is missing", exception.getMessage()); + } + + @Test + void shouldConfigureCustomEndpoint() throws Exception { + S3ClientBuilder builder = mock(S3ClientBuilder.class, RETURNS_SELF); + S3Credentials s3 = mock(S3Credentials.class); + + when(s3.getCustomEndpoint()) + .thenReturn("http://localhost:9000"); + + when(s3.isForcePath()).thenReturn(false); + + invokeConfigureEndpoint(builder, s3); + + verify(builder).endpointOverride(any()); + verify(builder).serviceConfiguration(any(S3Configuration.class)); + } + + @Test + void shouldConfigurePathStyleWhenForcePathEnabled() throws Exception { + S3ClientBuilder builder = mock(S3ClientBuilder.class, RETURNS_SELF); + S3Credentials s3 = mock(S3Credentials.class); + + when(s3.getCustomEndpoint()).thenReturn(null); + when(s3.isForcePath()).thenReturn(true); + + invokeConfigureEndpoint(builder, s3); + + verify(builder).serviceConfiguration(any(S3Configuration.class)); + } + + @Test + void shouldThrowExceptionForInvalidEndpoint() { + S3ClientBuilder builder = mock(S3ClientBuilder.class, RETURNS_SELF); + S3Credentials s3 = mock(S3Credentials.class); + + when(s3.getCustomEndpoint()).thenReturn("invalid-url%%%"); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> invokeConfigureEndpoint(builder, s3) + ); + + assertTrue(exception.getMessage().contains("Invalid endpoint URL")); + } + + private Region invokeResolveRegion(S3Credentials s3) throws Exception { + Method method = S3ClientFactory.class.getDeclaredMethod( + "resolveRegion", + S3Credentials.class + ); + + method.setAccessible(true); + + try { + return (Region) method.invoke(factory, s3); + } catch (InvocationTargetException e) { + throw (Exception) e.getCause(); + } + } + + private AwsCredentialsProvider invokeBuildStaticCredentialsProvider( + S3Credentials s3 + ) throws Exception { + + Method method = S3ClientFactory.class.getDeclaredMethod( + "buildStaticCredentialsProvider", + S3Credentials.class + ); + + method.setAccessible(true); + + try { + return (AwsCredentialsProvider) method.invoke(factory, s3); + } catch (InvocationTargetException e) { + throw (Exception) e.getCause(); + } + } + + private void invokeConfigureEndpoint( + S3ClientBuilder builder, + S3Credentials s3 + ) throws Exception { + + Method method = S3ClientFactory.class.getDeclaredMethod( + "configureEndpoint", + S3ClientBuilder.class, + S3Credentials.class + ); + + method.setAccessible(true); + + try { + method.invoke(factory, builder, s3); + } catch (InvocationTargetException e) { + throw (Exception) e.getCause(); + } + } +} \ No newline at end of file diff --git a/java-reporter-cucumber/pom.xml b/java-reporter-cucumber/pom.xml index eea88223..9e9ccf80 100644 --- a/java-reporter-cucumber/pom.xml +++ b/java-reporter-cucumber/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-cucumber - 0.7.15 + 0.7.16 jar Testomat.io Java Reporter Cucumber @@ -51,7 +51,7 @@ io.testomat java-reporter-core - 0.12.1 + 0.13.0 org.slf4j diff --git a/java-reporter-junit/pom.xml b/java-reporter-junit/pom.xml index ef7b1b5b..89740315 100644 --- a/java-reporter-junit/pom.xml +++ b/java-reporter-junit/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-junit - 0.8.7 + 0.8.8 jar Testomat.io Java Reporter JUnit @@ -51,7 +51,7 @@ io.testomat java-reporter-core - 0.12.1 + 0.13.0 org.slf4j diff --git a/java-reporter-karate/pom.xml b/java-reporter-karate/pom.xml index 8565a60a..49c5b411 100644 --- a/java-reporter-karate/pom.xml +++ b/java-reporter-karate/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-karate - 0.2.9 + 0.2.10 jar Testomat.io Java Reporter Karate @@ -52,7 +52,7 @@ io.testomat java-reporter-core - 0.12.1 + 0.13.0 io.karatelabs diff --git a/java-reporter-testng/pom.xml b/java-reporter-testng/pom.xml index af5d695f..751a1941 100644 --- a/java-reporter-testng/pom.xml +++ b/java-reporter-testng/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-testng - 0.7.14 + 0.7.15 jar Testomat.io Java Reporter TestNG @@ -47,7 +47,7 @@ io.testomat java-reporter-core - 0.12.1 + 0.13.0 org.slf4j diff --git a/pom.xml b/pom.xml index 31461d0e..bc2dcc23 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter - 0.3.3 + 0.3.4 pom Testomat.io Java Reporter diff --git a/testomat-allure-adapter/pom.xml b/testomat-allure-adapter/pom.xml index 00d5aff3..8defbeb2 100644 --- a/testomat-allure-adapter/pom.xml +++ b/testomat-allure-adapter/pom.xml @@ -6,7 +6,7 @@ io.testomat testomat-allure-adapter - 0.0.5 + 0.0.6 jar Testomat.io Testomat Allure adapter @@ -66,7 +66,7 @@ io.testomat java-reporter-core - 0.12.1 + 0.13.0 io.qameta.allure From 83a70c47a47b22d1c2fefbf9cb888147ff7397fb Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 28 May 2026 23:46:08 +0300 Subject: [PATCH 2/3] Issue-131 feat: add IAM role support for S3 uploads --- .../artifact/client/S3ClientFactory.java | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java index 5e264dd5..0b424965 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java @@ -14,16 +14,14 @@ import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider; /** - * Factory for creating configured S3Client instances with custom endpoint support. - * Handles AWS credentials, regions, and S3-compatible storage configurations. + * Factory for creating configured S3Client instances with custom endpoint support. Handles AWS credentials, regions, + * and S3-compatible storage configurations. */ public class S3ClientFactory { /** - * Creates a configured S3Client based on current credentials and settings. - * Priority: - * 1. IAM Role (if roleArn configured) - * 2. Static access key / secret key + * Creates a configured S3Client based on current credentials and settings. Priority: 1. IAM Role (if roleArn + * configured) 2. Static access key / secret key * * @return configured S3Client instance * @throws IllegalArgumentException if credentials are invalid or missing @@ -61,40 +59,45 @@ private AwsCredentialsProvider buildCredentialsProvider(S3Credentials s3, Region /** * Creates AssumeRole credentials provider. */ - private AwsCredentialsProvider buildIamRoleProvider(S3Credentials s3, Region region) { + private AwsCredentialsProvider buildIamRoleProvider(S3Credentials s3Credentials, Region region) { + AwsCredentialsProvider baseCredentials = buildStaticCredentialsProvider(s3Credentials); + StsClient stsClient = StsClient.builder() .region(region) + .credentialsProvider(baseCredentials) .build(); return StsAssumeRoleCredentialsProvider.builder() .stsClient(stsClient) .refreshRequest(request -> { - request.roleArn(s3.getRoleArn().trim()); + request.roleArn(s3Credentials.getRoleArn().trim()); request.roleSessionName("testomat-s3-upload"); - if (s3.getExternalId() != null && !s3.getExternalId().isBlank()) { - request.externalId(s3.getExternalId().trim()); + if (s3Credentials.getExternalId() != null + && !s3Credentials.getExternalId().isBlank()) { + + request.externalId(s3Credentials.getExternalId().trim()); } - }).build(); + }) + .build(); } /** * Creates static credentials provider. */ - private AwsCredentialsProvider buildStaticCredentialsProvider(S3Credentials s3) { - - if (s3.getAccessKeyId() == null || s3.getAccessKeyId().isBlank()) { + private AwsCredentialsProvider buildStaticCredentialsProvider(S3Credentials s3Credentials) { + if (s3Credentials.getAccessKeyId() == null || s3Credentials.getAccessKeyId().isBlank()) { throw new IllegalArgumentException("AWS access key is missing"); } - if (s3.getSecretAccessKey() == null || s3.getSecretAccessKey().isBlank()) { + if (s3Credentials.getSecretAccessKey() == null || s3Credentials.getSecretAccessKey().isBlank()) { throw new IllegalArgumentException("AWS secret key is missing"); } AwsBasicCredentials credentials = AwsBasicCredentials.create( - s3.getAccessKeyId().trim(), - s3.getSecretAccessKey().trim()); + s3Credentials.getAccessKeyId().trim(), + s3Credentials.getSecretAccessKey().trim()); return StaticCredentialsProvider.create(credentials); } @@ -102,36 +105,37 @@ private AwsCredentialsProvider buildStaticCredentialsProvider(S3Credentials s3) /** * Resolves AWS region. */ - private Region resolveRegion(S3Credentials s3) { + private Region resolveRegion(S3Credentials s3Credentials) { try { - if (s3.getRegion() == null || s3.getRegion().isBlank()) { + if (s3Credentials.getRegion() == null || s3Credentials.getRegion().isBlank()) { return Region.US_EAST_1; } - return Region.of(s3.getRegion().trim()); + return Region.of(s3Credentials.getRegion().trim()); } catch (Exception e) { - throw new IllegalArgumentException("Invalid AWS region: " + s3.getRegion(), e); + throw new IllegalArgumentException("Invalid AWS region: " + s3Credentials.getRegion(), e); } } /** * Configures custom endpoint and path-style access. */ - private void configureEndpoint(S3ClientBuilder builder, S3Credentials s3) { - boolean hasCustomEndpoint = s3.getCustomEndpoint() != null && !s3.getCustomEndpoint().isBlank(); + private void configureEndpoint(S3ClientBuilder builder, S3Credentials s3Credentials) { + boolean hasCustomEndpoint = + s3Credentials.getCustomEndpoint() != null && !s3Credentials.getCustomEndpoint().isBlank(); if (hasCustomEndpoint) { try { - builder.endpointOverride(URI.create(s3.getCustomEndpoint().trim())); + builder.endpointOverride(URI.create(s3Credentials.getCustomEndpoint().trim())); } catch (Exception e) { - throw new IllegalArgumentException("Invalid endpoint URL: " + s3.getCustomEndpoint(), e); + throw new IllegalArgumentException("Invalid endpoint URL: " + s3Credentials.getCustomEndpoint(), e); } } - if (s3.isForcePath() || hasCustomEndpoint) { + if (s3Credentials.isForcePath() || hasCustomEndpoint) { builder.serviceConfiguration( S3Configuration.builder() .pathStyleAccessEnabled( - s3.isForcePath() + s3Credentials.isForcePath() ) .build() ); From b98d0b80f426d9b917dac0c7c68e02e315e9643d Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 29 May 2026 09:04:00 +0300 Subject: [PATCH 3/3] Issue-131 feat: add IAM role support for S3 uploads --- .../facade/methods/artifact/client/S3ClientFactory.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java index 0b424965..9db839ce 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/client/S3ClientFactory.java @@ -20,8 +20,10 @@ public class S3ClientFactory { /** - * Creates a configured S3Client based on current credentials and settings. Priority: 1. IAM Role (if roleArn - * configured) 2. Static access key / secret key + * Creates a configured S3Client based on current credentials and settings. + * Priority: + * 1. IAM Role (if roleArn configured); + * 2. Static access key / secret key. * * @return configured S3Client instance * @throws IllegalArgumentException if credentials are invalid or missing