Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion java-reporter-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<groupId>io.testomat</groupId>
<artifactId>java-reporter-core</artifactId>
<version>0.16.0</version>
<version>0.16.1</version>
<packaging>jar</packaging>

<name>Testomat.io Reporter Core</name>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.testomat.core.client;

import static io.testomat.core.constants.ArtifactPropertyNames.ARTIFACT_DISABLE_PROPERTY_NAME;
import static io.testomat.core.constants.ArtifactPropertyNames.JSONL_PATH_PROPERTY_NAME;
import static io.testomat.core.constants.CommonConstants.RESPONSE_UID_KEY;

Expand All @@ -23,7 +24,6 @@
import io.testomat.core.propertyconfig.impl.PropertyProviderFactoryImpl;
import io.testomat.core.propertyconfig.interf.PropertyProvider;
import io.testomat.core.runmanager.GlobalRunManager;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
Expand Down Expand Up @@ -84,7 +84,7 @@ public String createRun(String title) throws IOException {
throw new RunCreationFailedException(
"Invalid response: missing UID in create test run response");
}
if (responseBody.containsKey("artifacts")) {
if (responseBody.containsKey("artifacts") && !isArtifactDisabled()) {
Map<String, Object> creds = (Map<String, Object>) responseBody.get("artifacts");
credentialsManager.populateCredentials(creds);
credentialsValidationService.areCredentialsValid(CredentialsManager.getCredentials());
Expand Down Expand Up @@ -202,4 +202,12 @@ public void uploadLinksToTestomatio(String uid) {
throw new ArtifactManagementException("Failed to upload artifact links to Testomatio", e);
}
}

private boolean isArtifactDisabled() {
try {
return Boolean.parseBoolean(provider.getProperty(ARTIFACT_DISABLE_PROPERTY_NAME));
} catch (Exception e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public class ArtifactPropertyNames {
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 SESSION_TOKEN_PROPERTY_NAME = "s3.session-token";

public static final String FORCE_PATH_PROPERTY_NAME = "s3.force-path-style";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.testomat.core.constants;

public class CommonConstants {
public static final String REPORTER_VERSION = "0.16.0";
public static final String REPORTER_VERSION = "0.16.1";

public static final String TESTS_STRING = "tests";
public static final String API_KEY_STRING = "api_key";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class CredentialConstants {
public static final String ARN = "ARN";
public static final String ENDPOINT = "ENDPOINT";
public static final String FORCE_PATH = "FORCE_PATH_STYLE";
public static final String SESSION_TOKEN = "SESSION_TOKEN";

private CredentialConstants() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ public class PropertyValuesConstants {
public static final int DEFAULT_FLUSH_INTERVAL_SECONDS = 10;
public static final String DEFAULT_URL = "https://app.testomat.io/";
public static final String DEFAULT_RUN_TITLE = "Default Run Title";
public static final String DEFAULT_JSONL_PATH = "target/testomat/testomat-artifacts.json";
public static final String DEFAULT_JSONL_PATH = "testomat/testomat-artifacts.json";
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import java.net.URI;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
Expand All @@ -14,30 +16,20 @@
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.
*/
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
* Creates configured S3 client.
*/
public S3Client createS3Client() {
S3Credentials s3 = CredentialsManager.getCredentials();

Region region = resolveRegion(s3);

AwsCredentialsProvider provider =
buildCredentialsProvider(s3, region);

S3ClientBuilder builder = S3Client.builder()
.credentialsProvider(provider)
.credentialsProvider(buildCredentialsProvider(s3, region))
.region(region);

configureEndpoint(builder, s3);
Expand All @@ -46,84 +38,89 @@ public S3Client createS3Client() {
}

/**
* Builds AWS credentials provider.
* Creates credentials provider.
*
* Supports:
* - AccessKey + SecretKey
* - AccessKey + SecretKey + SessionToken
* - IAM role assumption via STS (isIam + roleArn)
*/
private AwsCredentialsProvider buildCredentialsProvider(S3Credentials s3, Region region) {
boolean useIamRole = s3.isIam() && s3.getRoleArn() != null && !s3.getRoleArn().isBlank();

if (useIamRole) {
return buildIamRoleProvider(s3, region);
if (!isBlank(s3.getAccessKeyId()) && !isBlank(s3.getSecretAccessKey()) && !isBlank(s3.getSessionToken())) {
return StaticCredentialsProvider.create(
AwsSessionCredentials.create(
s3.getAccessKeyId().trim(),
s3.getSecretAccessKey().trim(),
s3.getSessionToken().trim()
)
);
}

return buildStaticCredentialsProvider(s3);
}
if (s3.isIam() && !isBlank(s3.getRoleArn())) {
AwsCredentialsProvider baseProvider = buildBaseProvider(s3);
StsClient stsClient = StsClient.builder()
.credentialsProvider(baseProvider)
.region(region)
.build();

return StsAssumeRoleCredentialsProvider.builder()
.stsClient(stsClient)
.refreshRequest(r -> {
r.roleArn(s3.getRoleArn().trim());
r.roleSessionName("testomat-s3-session");
if (!isBlank(s3.getExternalId())) {
r.externalId(s3.getExternalId().trim());
}
})
.build();
}

/**
* Creates AssumeRole credentials provider.
*/
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(s3Credentials.getRoleArn().trim());
request.roleSessionName("testomat-s3-upload");

if (s3Credentials.getExternalId() != null
&& !s3Credentials.getExternalId().isBlank()) {

request.externalId(s3Credentials.getExternalId().trim());
}
})
.build();
return buildBaseProvider(s3);
}

/**
* Creates static credentials provider.
*/
private AwsCredentialsProvider buildStaticCredentialsProvider(S3Credentials s3Credentials) {
if (s3Credentials.getAccessKeyId() == null || s3Credentials.getAccessKeyId().isBlank()) {
throw new IllegalArgumentException("AWS access key is missing");
}

if (s3Credentials.getSecretAccessKey() == null || s3Credentials.getSecretAccessKey().isBlank()) {
throw new IllegalArgumentException("AWS secret key is missing");
private AwsCredentialsProvider buildBaseProvider(S3Credentials s3) {
if (!isBlank(s3.getAccessKeyId()) && !isBlank(s3.getSecretAccessKey())) {
if (!isBlank(s3.getSessionToken())) {
return StaticCredentialsProvider.create(
AwsSessionCredentials.create(
s3.getAccessKeyId().trim(),
s3.getSecretAccessKey().trim(),
s3.getSessionToken().trim()
)
);
}
return StaticCredentialsProvider.create(
AwsBasicCredentials.create(
s3.getAccessKeyId().trim(),
s3.getSecretAccessKey().trim()
)
);
}

AwsBasicCredentials credentials =
AwsBasicCredentials.create(
s3Credentials.getAccessKeyId().trim(),
s3Credentials.getSecretAccessKey().trim());

return StaticCredentialsProvider.create(credentials);
return DefaultCredentialsProvider.create();
}

/**
* Resolves AWS region.
*/
private Region resolveRegion(S3Credentials s3Credentials) {
try {
if (s3Credentials.getRegion() == null || s3Credentials.getRegion().isBlank()) {
if (isBlank(s3Credentials.getRegion())) {
return Region.US_EAST_1;
}

return Region.of(s3Credentials.getRegion().trim());

} catch (Exception e) {
throw new IllegalArgumentException("Invalid AWS region: " + s3Credentials.getRegion(), e);
}
}

/**
* Configures custom endpoint and path-style access.
* Configures endpoint and path style access.
*/
private void configureEndpoint(S3ClientBuilder builder, S3Credentials s3Credentials) {
boolean hasCustomEndpoint =
s3Credentials.getCustomEndpoint() != null && !s3Credentials.getCustomEndpoint().isBlank();
boolean hasCustomEndpoint = !isBlank(s3Credentials.getCustomEndpoint());

if (hasCustomEndpoint) {
try {
Expand All @@ -136,11 +133,12 @@ private void configureEndpoint(S3ClientBuilder builder, S3Credentials s3Credenti
if (s3Credentials.isForcePath() || hasCustomEndpoint) {
builder.serviceConfiguration(
S3Configuration.builder()
.pathStyleAccessEnabled(
s3Credentials.isForcePath()
)
.build()
);
.pathStyleAccessEnabled(s3Credentials.isForcePath())
.build());
}
}

private boolean isBlank(String value) {
return value == null || value.isBlank();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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.ArtifactPropertyNames.SESSION_TOKEN_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;
Expand All @@ -19,6 +20,7 @@
import static io.testomat.core.constants.CredentialConstants.PRESIGN;
import static io.testomat.core.constants.CredentialConstants.REGION;
import static io.testomat.core.constants.CredentialConstants.SECRET_ACCESS_KEY;
import static io.testomat.core.constants.CredentialConstants.SESSION_TOKEN;
import static io.testomat.core.constants.CredentialConstants.SHARED;

import io.testomat.core.propertyconfig.impl.PropertyProviderFactoryImpl;
Expand Down Expand Up @@ -75,6 +77,9 @@ public void populateCredentials(Map<String, Object> credsFromServer) {
populateCredentialField(ACCESS_KEY_PROPERTY_NAME, ACCESS_KEY_ID, credsFromServer, "AccessKey",
value -> credentials.setAccessKeyId(getStringValue(value)));

populateCredentialField(SESSION_TOKEN_PROPERTY_NAME, SESSION_TOKEN, credsFromServer, "SessionToken",
value -> credentials.setSessionToken(getStringValue(value)));

populateCredentialField(BUCKET_PROPERTY_NAME, BUCKET, credsFromServer, "Bucket",
value -> credentials.setBucket(getStringValue(value)));

Expand All @@ -90,6 +95,12 @@ public void populateCredentials(Map<String, Object> credsFromServer) {
credentials.setIam(getBooleanValue(credsFromServer.get(IAM)));
credentials.setShared(getBooleanValue(credsFromServer.get(SHARED)));

if (getPropertyFromEnv(ACCESS_KEY_PROPERTY_NAME) != null
|| getPropertyFromEnv(SECRET_ACCESS_KEY_PROPERTY_NAME) != null) {
credentials.setSessionToken(null);
log.debug("SessionToken cleared because access/secret keys were overridden by env");
}

logCredentialsInitializationResult();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class S3Credentials {
private String roleArn;
private String externalId;
private boolean forcePath = false;
private String sessionToken;

public boolean isForcePath() {
return forcePath;
Expand Down Expand Up @@ -104,4 +105,12 @@ public String getRoleArn() {
public void setRoleArn(String roleArn) {
this.roleArn = roleArn;
}

public String getSessionToken() {
return sessionToken;
}

public void setSessionToken(String sessionToken) {
this.sessionToken = sessionToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import static io.testomat.core.constants.PropertyValuesConstants.DEFAULT_RUN_TITLE;
import static io.testomat.core.constants.PropertyValuesConstants.DEFAULT_URL;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;

/**
Expand All @@ -28,10 +30,18 @@ public class DefaultPropertiesStorage {
public static final Map<String, String> DEFAULTS;

static {
String jsonlPath = String.format("target/%s", DEFAULT_JSONL_PATH);
String userDir = System.getProperty("user.dir");
if (userDir != null) {
if (Files.exists(Paths.get(userDir, "build.gradle"))
|| Files.exists(Paths.get(userDir, "build.gradle.kts"))) {
jsonlPath = String.format("build/%s", DEFAULT_JSONL_PATH);
}
}
DEFAULTS = Map.of(
HOST_URL_PROPERTY_NAME, DEFAULT_URL,
RUN_TITLE_PROPERTY_NAME, DEFAULT_RUN_TITLE,
JSONL_PATH_PROPERTY_NAME, DEFAULT_JSONL_PATH
JSONL_PATH_PROPERTY_NAME, jsonlPath
);
}
}
Loading
Loading