From 832b81eb43ede3ce90f182d19db8a4e821d1cd21 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Wed, 15 Jul 2026 11:03:29 +0200 Subject: [PATCH] feat(iamcredentials): add generateAccessToken support Adds the IAM Credentials REST emulator surface for local credential vending, including generateAccessToken validation, service enablement wiring, REST tests, and Java generated-client compatibility coverage. - [x] New feature (`feat:`) Implements the IAM Credentials v1 HTTP/JSON generateAccessToken path used by the Google Java IAM Credentials client. Verified with IamCredentialsServiceTest, IamCredentialsRestIntegrationTest, IamCredentialsDisabledRestIntegrationTest, IamServiceTest, and compatibility-tests/sdk-test-java IamCredentialsTest against a running emulator. - [x] `./mvnw test` passes locally - [x] New or updated integration test added - [x] Commit messages follow Conventional Commits --- compatibility-tests/sdk-test-java/pom.xml | 4 + .../java/io/floci/gcp/test/TestFixtures.java | 10 ++ .../io/floci/gcp/test/IamCredentialsTest.java | 30 ++++ .../io/floci/gcp/config/EmulatorConfig.java | 7 + .../IamCredentialsController.java | 45 ++++++ .../iamcredentials/IamCredentialsService.java | 104 +++++++++++++ src/main/resources/application.yml | 2 + ...redentialsDisabledRestIntegrationTest.java | 37 +++++ .../IamCredentialsRestIntegrationTest.java | 145 ++++++++++++++++++ .../IamCredentialsServiceTest.java | 144 +++++++++++++++++ src/test/resources/application.yml | 2 + 11 files changed, 530 insertions(+) create mode 100644 compatibility-tests/sdk-test-java/src/test/java/io/floci/gcp/test/IamCredentialsTest.java create mode 100644 src/main/java/io/floci/gcp/services/iamcredentials/IamCredentialsController.java create mode 100644 src/main/java/io/floci/gcp/services/iamcredentials/IamCredentialsService.java create mode 100644 src/test/java/io/floci/gcp/services/iamcredentials/IamCredentialsDisabledRestIntegrationTest.java create mode 100644 src/test/java/io/floci/gcp/services/iamcredentials/IamCredentialsRestIntegrationTest.java create mode 100644 src/test/java/io/floci/gcp/services/iamcredentials/IamCredentialsServiceTest.java diff --git a/compatibility-tests/sdk-test-java/pom.xml b/compatibility-tests/sdk-test-java/pom.xml index e22aca3..cca3fee 100644 --- a/compatibility-tests/sdk-test-java/pom.xml +++ b/compatibility-tests/sdk-test-java/pom.xml @@ -105,6 +105,10 @@ com.google.cloud google-cloud-service-usage + + com.google.cloud + google-cloud-iamcredentials + com.google.firebase firebase-admin diff --git a/compatibility-tests/sdk-test-java/src/main/java/io/floci/gcp/test/TestFixtures.java b/compatibility-tests/sdk-test-java/src/main/java/io/floci/gcp/test/TestFixtures.java index c2eca00..8ae981b 100644 --- a/compatibility-tests/sdk-test-java/src/main/java/io/floci/gcp/test/TestFixtures.java +++ b/compatibility-tests/sdk-test-java/src/main/java/io/floci/gcp/test/TestFixtures.java @@ -9,6 +9,8 @@ import com.google.cloud.firestore.FirestoreOptions; import com.google.cloud.functions.v2.FunctionServiceClient; import com.google.cloud.functions.v2.FunctionServiceSettings; +import com.google.cloud.iam.credentials.v1.IamCredentialsClient; +import com.google.cloud.iam.credentials.v1.IamCredentialsSettings; import com.google.cloud.run.v2.RevisionsClient; import com.google.cloud.run.v2.RevisionsSettings; import com.google.cloud.run.v2.ServicesClient; @@ -215,6 +217,14 @@ public static SQLAdmin sqlAdminClient() { .build(); } + public static IamCredentialsClient iamCredentialsClient() throws IOException { + IamCredentialsSettings settings = IamCredentialsSettings.newHttpJsonBuilder() + .setEndpoint(endpoint()) + .setCredentialsProvider(NoCredentialsProvider.create()) + .build(); + return IamCredentialsClient.create(settings); + } + /** * Creates a Cloud Logging client pointing at the emulator. * No standard emulator env var exists; configure explicitly via plaintext gRPC channel. diff --git a/compatibility-tests/sdk-test-java/src/test/java/io/floci/gcp/test/IamCredentialsTest.java b/compatibility-tests/sdk-test-java/src/test/java/io/floci/gcp/test/IamCredentialsTest.java new file mode 100644 index 0000000..aa0df86 --- /dev/null +++ b/compatibility-tests/sdk-test-java/src/test/java/io/floci/gcp/test/IamCredentialsTest.java @@ -0,0 +1,30 @@ +package io.floci.gcp.test; + +import com.google.cloud.iam.credentials.v1.GenerateAccessTokenResponse; +import com.google.cloud.iam.credentials.v1.IamCredentialsClient; +import com.google.protobuf.Duration; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class IamCredentialsTest { + + @Test + void generateAccessTokenWithHttpJsonClient() throws Exception { + String email = "compat@" + TestFixtures.projectId() + ".iam.gserviceaccount.com"; + + try (IamCredentialsClient client = TestFixtures.iamCredentialsClient()) { + GenerateAccessTokenResponse response = client.generateAccessToken( + "projects/-/serviceAccounts/" + email, + List.of(), + List.of("https://www.googleapis.com/auth/cloud-platform"), + Duration.newBuilder().setSeconds(3600).build()); + + assertThat(response.getAccessToken()).startsWith("floci-gcp-impersonated-"); + assertThat(response.getExpireTime().getSeconds()).isGreaterThan(Instant.now().getEpochSecond()); + } + } +} diff --git a/src/main/java/io/floci/gcp/config/EmulatorConfig.java b/src/main/java/io/floci/gcp/config/EmulatorConfig.java index 9bc8647..43f12d0 100644 --- a/src/main/java/io/floci/gcp/config/EmulatorConfig.java +++ b/src/main/java/io/floci/gcp/config/EmulatorConfig.java @@ -83,6 +83,8 @@ interface ServicesConfig { IamServiceConfig iam(); + IamCredentialsServiceConfig iamcredentials(); + SecretManagerServiceConfig secretmanager(); LoggingServiceConfig logging(); @@ -186,6 +188,11 @@ interface IamServiceConfig { boolean enabled(); } + interface IamCredentialsServiceConfig { + @WithDefault("true") + boolean enabled(); + } + interface SecretManagerServiceConfig { @WithDefault("true") boolean enabled(); diff --git a/src/main/java/io/floci/gcp/services/iamcredentials/IamCredentialsController.java b/src/main/java/io/floci/gcp/services/iamcredentials/IamCredentialsController.java new file mode 100644 index 0000000..d941ce1 --- /dev/null +++ b/src/main/java/io/floci/gcp/services/iamcredentials/IamCredentialsController.java @@ -0,0 +1,45 @@ +package io.floci.gcp.services.iamcredentials; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Path("/v1/projects/-/serviceAccounts") +@ApplicationScoped +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class IamCredentialsController { + + private final IamCredentialsService service; + + @Inject + public IamCredentialsController(IamCredentialsService service) { + this.service = service; + } + + @POST + @Path("/{serviceAccount}:generateAccessToken") + public Response generateAccessToken(@PathParam("serviceAccount") String serviceAccount, + Map body) { + List scopes = body != null && body.get("scope") instanceof List scopeList + ? scopeList + : null; + IamCredentialsService.GeneratedAccessToken token = + service.generateAccessToken(serviceAccount, scopes, body != null ? body.get("lifetime") : null); + + Map response = new LinkedHashMap<>(); + response.put("accessToken", token.accessToken()); + response.put("expireTime", token.expireTime().toString()); + return Response.ok(response).build(); + } +} diff --git a/src/main/java/io/floci/gcp/services/iamcredentials/IamCredentialsService.java b/src/main/java/io/floci/gcp/services/iamcredentials/IamCredentialsService.java new file mode 100644 index 0000000..3586f35 --- /dev/null +++ b/src/main/java/io/floci/gcp/services/iamcredentials/IamCredentialsService.java @@ -0,0 +1,104 @@ +package io.floci.gcp.services.iamcredentials; + +import io.floci.gcp.config.EmulatorConfig; +import io.floci.gcp.core.common.GcpException; +import io.floci.gcp.core.common.ServiceDescriptor; +import io.floci.gcp.core.common.ServiceProtocol; +import io.floci.gcp.core.common.ServiceRegistry; +import io.quarkus.runtime.StartupEvent; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import java.time.Clock; +import java.time.Instant; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@ApplicationScoped +public class IamCredentialsService { + + static final String DEVSTORAGE_READ_WRITE_SCOPE = "https://www.googleapis.com/auth/devstorage.read_write"; + static final String CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; + static final String TOKEN_PREFIX = "floci-gcp-impersonated-"; + static final long DEFAULT_LIFETIME_SECONDS = 3600; + static final long MAX_LIFETIME_SECONDS = 3600; + private static final Set SUPPORTED_SCOPES = Set.of(DEVSTORAGE_READ_WRITE_SCOPE, CLOUD_PLATFORM_SCOPE); + + private final ServiceRegistry serviceRegistry; + private final EmulatorConfig config; + private final Clock clock; + + @Inject + public IamCredentialsService(ServiceRegistry serviceRegistry, EmulatorConfig config) { + this(serviceRegistry, config, Clock.systemUTC()); + } + + IamCredentialsService(Clock clock) { + this(null, null, clock); + } + + private IamCredentialsService(ServiceRegistry serviceRegistry, EmulatorConfig config, Clock clock) { + this.serviceRegistry = serviceRegistry; + this.config = config; + this.clock = clock; + } + + void onStart(@Observes StartupEvent ev) { + serviceRegistry.register(ServiceDescriptor.builder("iamcredentials") + .enabled(config.services().iamcredentials().enabled()) + .storageKey("iamcredentials") + .protocol(ServiceProtocol.REST) + .resourceClasses(IamCredentialsController.class) + .build()); + } + + public GeneratedAccessToken generateAccessToken(String serviceAccount, List scopes, Object lifetime) { + if (serviceAccount == null || serviceAccount.isBlank()) { + throw GcpException.invalidArgument("service account is required"); + } + validateScopes(scopes); + + long lifetimeSeconds = parseLifetimeSeconds(lifetime); + Instant expireTime = clock.instant().plusSeconds(lifetimeSeconds); + return new GeneratedAccessToken(TOKEN_PREFIX + UUID.randomUUID(), expireTime); + } + + private static void validateScopes(List scopes) { + if (scopes == null || scopes.isEmpty()) { + throw GcpException.invalidArgument("scope is required"); + } + boolean supported = scopes.stream() + .anyMatch(SUPPORTED_SCOPES::contains); + if (!supported) { + throw GcpException.invalidArgument("unsupported scope"); + } + } + + private static long parseLifetimeSeconds(Object lifetime) { + if (lifetime == null) { + return DEFAULT_LIFETIME_SECONDS; + } + if (!(lifetime instanceof String value)) { + throw GcpException.invalidArgument("lifetime must be a duration string"); + } + if (!value.endsWith("s") || value.length() == 1) { + throw GcpException.invalidArgument("lifetime must be a duration string ending in seconds"); + } + + long requestedSeconds; + try { + requestedSeconds = Long.parseLong(value.substring(0, value.length() - 1)); + } catch (NumberFormatException e) { + throw GcpException.invalidArgument("lifetime must be a duration string ending in seconds"); + } + + if (requestedSeconds <= 0) { + throw GcpException.invalidArgument("lifetime must be positive"); + } + return Math.min(requestedSeconds, MAX_LIFETIME_SECONDS); + } + + public record GeneratedAccessToken(String accessToken, Instant expireTime) {} +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5751f51..2a7c47a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -55,6 +55,8 @@ floci-gcp: enabled: true iam: enabled: true + iamcredentials: + enabled: true secretmanager: enabled: true logging: diff --git a/src/test/java/io/floci/gcp/services/iamcredentials/IamCredentialsDisabledRestIntegrationTest.java b/src/test/java/io/floci/gcp/services/iamcredentials/IamCredentialsDisabledRestIntegrationTest.java new file mode 100644 index 0000000..ff86a39 --- /dev/null +++ b/src/test/java/io/floci/gcp/services/iamcredentials/IamCredentialsDisabledRestIntegrationTest.java @@ -0,0 +1,37 @@ +package io.floci.gcp.services.iamcredentials; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +@QuarkusTest +@TestProfile(IamCredentialsDisabledRestIntegrationTest.DisabledIamCredentialsProfile.class) +class IamCredentialsDisabledRestIntegrationTest { + + @Test + void disabledIamCredentialsReturnsUnavailableWrapper() { + given() + .urlEncodingEnabled(false) + .contentType("application/json") + .body(Map.of("scope", List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE))) + .when().post("/v1/projects/-/serviceAccounts/test@test-project.iam.gserviceaccount.com:generateAccessToken") + .then() + .statusCode(503) + .body("error.status", equalTo("UNAVAILABLE")) + .body("error.message", equalTo("Service iamcredentials is not enabled.")); + } + + public static class DisabledIamCredentialsProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of("floci-gcp.services.iamcredentials.enabled", "false"); + } + } +} diff --git a/src/test/java/io/floci/gcp/services/iamcredentials/IamCredentialsRestIntegrationTest.java b/src/test/java/io/floci/gcp/services/iamcredentials/IamCredentialsRestIntegrationTest.java new file mode 100644 index 0000000..e832aba --- /dev/null +++ b/src/test/java/io/floci/gcp/services/iamcredentials/IamCredentialsRestIntegrationTest.java @@ -0,0 +1,145 @@ +package io.floci.gcp.services.iamcredentials; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@QuarkusTest +class IamCredentialsRestIntegrationTest { + + private static final String PATH = + "/v1/projects/-/serviceAccounts/test@test-project.iam.gserviceaccount.com:generateAccessToken"; + private static final Map VALID_BODY = Map.of( + "delegates", List.of(), + "scope", List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE), + "lifetime", "3600s"); + + @Test + void generateAccessTokenReturnsGoogleCompatibleJson() { + String expireTime = given() + .urlEncodingEnabled(false) + .contentType("application/json") + .body(VALID_BODY) + .when().post(PATH) + .then() + .statusCode(200) + .body("accessToken", startsWith(IamCredentialsService.TOKEN_PREFIX)) + .body("expireTime", notNullValue()) + .extract().path("expireTime"); + + assertTrue(Instant.parse(expireTime).isAfter(Instant.now().minusSeconds(1))); + } + + @Test + void specificRouteDoesNotFallThroughToIamCatchAll() { + given() + .urlEncodingEnabled(false) + .contentType("application/json") + .body(Map.of("scope", List.of("https://www.googleapis.com/auth/userinfo.email"))) + .when().post(PATH) + .then() + .statusCode(400) + .body("error.status", equalTo("INVALID_ARGUMENT")) + .body("error.message", equalTo("unsupported scope")); + } + + @Test + void missingScopeReturnsGcpStyleError() { + given() + .urlEncodingEnabled(false) + .contentType("application/json") + .body(Map.of("lifetime", "60s")) + .when().post(PATH) + .then() + .statusCode(400) + .body("error.status", equalTo("INVALID_ARGUMENT")) + .body("error.errors[0].reason", equalTo("invalid")); + } + + @Test + void emptyScopeReturnsGcpStyleError() { + given() + .urlEncodingEnabled(false) + .contentType("application/json") + .body(Map.of("scope", List.of(), "lifetime", "60s")) + .when().post(PATH) + .then() + .statusCode(400) + .body("error.status", equalTo("INVALID_ARGUMENT")); + } + + @Test + void unsupportedScopeReturnsGcpStyleError() { + given() + .urlEncodingEnabled(false) + .contentType("application/json") + .body(Map.of("scope", List.of("https://www.googleapis.com/auth/userinfo.email"))) + .when().post(PATH) + .then() + .statusCode(400) + .body("error.status", equalTo("INVALID_ARGUMENT")); + } + + @Test + void cloudPlatformScopeReturnsAccessToken() { + given() + .urlEncodingEnabled(false) + .contentType("application/json") + .body(Map.of("scope", List.of(IamCredentialsService.CLOUD_PLATFORM_SCOPE))) + .when().post(PATH) + .then() + .statusCode(200) + .body("accessToken", startsWith(IamCredentialsService.TOKEN_PREFIX)); + } + + @Test + void malformedLifetimeReturnsGcpStyleError() { + given() + .urlEncodingEnabled(false) + .contentType("application/json") + .body(Map.of( + "scope", List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE), + "lifetime", "not-a-duration")) + .when().post(PATH) + .then() + .statusCode(400) + .body("error.status", equalTo("INVALID_ARGUMENT")); + } + + @Test + void zeroLifetimeReturnsGcpStyleError() { + given() + .urlEncodingEnabled(false) + .contentType("application/json") + .body(Map.of( + "scope", List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE), + "lifetime", "0s")) + .when().post(PATH) + .then() + .statusCode(400) + .body("error.status", equalTo("INVALID_ARGUMENT")); + } + + @Test + void negativeLifetimeReturnsGcpStyleError() { + given() + .urlEncodingEnabled(false) + .contentType("application/json") + .body(Map.of( + "scope", List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE), + "lifetime", "-1s")) + .when().post(PATH) + .then() + .statusCode(400) + .body("error.status", equalTo("INVALID_ARGUMENT")); + } +} diff --git a/src/test/java/io/floci/gcp/services/iamcredentials/IamCredentialsServiceTest.java b/src/test/java/io/floci/gcp/services/iamcredentials/IamCredentialsServiceTest.java new file mode 100644 index 0000000..e8c3a31 --- /dev/null +++ b/src/test/java/io/floci/gcp/services/iamcredentials/IamCredentialsServiceTest.java @@ -0,0 +1,144 @@ +package io.floci.gcp.services.iamcredentials; + +import io.floci.gcp.core.common.GcpException; +import org.junit.jupiter.api.Test; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class IamCredentialsServiceTest { + + private static final Instant NOW = Instant.parse("2026-07-15T12:00:00Z"); + private static final String SERVICE_ACCOUNT = "test@test-project.iam.gserviceaccount.com"; + + private final IamCredentialsService service = + new IamCredentialsService(Clock.fixed(NOW, ZoneOffset.UTC)); + + @Test + void generatesTokenAndExpiryForNormalRequest() { + IamCredentialsService.GeneratedAccessToken token = service.generateAccessToken( + SERVICE_ACCOUNT, + List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE), + "1200s"); + + assertTrue(token.accessToken().startsWith(IamCredentialsService.TOKEN_PREFIX)); + assertEquals(NOW.plusSeconds(1200), token.expireTime()); + } + + @Test + void defaultsLifetimeWhenAbsent() { + IamCredentialsService.GeneratedAccessToken token = service.generateAccessToken( + SERVICE_ACCOUNT, + List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE), + null); + + assertEquals(NOW.plusSeconds(IamCredentialsService.DEFAULT_LIFETIME_SECONDS), token.expireTime()); + } + + @Test + void capsLifetimeAtOneHour() { + IamCredentialsService.GeneratedAccessToken token = service.generateAccessToken( + SERVICE_ACCOUNT, + List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE), + "7200s"); + + assertEquals(NOW.plusSeconds(IamCredentialsService.MAX_LIFETIME_SECONDS), token.expireTime()); + } + + @Test + void acceptsArbitraryServiceAccountWithoutPreCreation() { + IamCredentialsService.GeneratedAccessToken token = service.generateAccessToken( + "not-created@example.iam.gserviceaccount.com", + List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE), + "60s"); + + assertTrue(token.accessToken().startsWith(IamCredentialsService.TOKEN_PREFIX)); + } + + @Test + void acceptsCloudPlatformScope() { + IamCredentialsService.GeneratedAccessToken token = service.generateAccessToken( + SERVICE_ACCOUNT, + List.of(IamCredentialsService.CLOUD_PLATFORM_SCOPE), + "60s"); + + assertTrue(token.accessToken().startsWith(IamCredentialsService.TOKEN_PREFIX)); + } + + @Test + void rejectsBlankServiceAccount() { + GcpException ex = assertThrows(GcpException.class, + () -> service.generateAccessToken( + " ", + List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE), + "60s")); + + assertEquals("INVALID_ARGUMENT", ex.getGcpStatus()); + } + + @Test + void rejectsMissingScope() { + GcpException ex = assertThrows(GcpException.class, + () -> service.generateAccessToken(SERVICE_ACCOUNT, null, "60s")); + + assertEquals("INVALID_ARGUMENT", ex.getGcpStatus()); + } + + @Test + void rejectsEmptyScope() { + GcpException ex = assertThrows(GcpException.class, + () -> service.generateAccessToken(SERVICE_ACCOUNT, List.of(), "60s")); + + assertEquals("INVALID_ARGUMENT", ex.getGcpStatus()); + } + + @Test + void rejectsUnsupportedScope() { + GcpException ex = assertThrows(GcpException.class, + () -> service.generateAccessToken( + SERVICE_ACCOUNT, + List.of("https://www.googleapis.com/auth/userinfo.email"), + "60s")); + + assertEquals("INVALID_ARGUMENT", ex.getGcpStatus()); + } + + @Test + void rejectsMalformedLifetime() { + GcpException ex = assertThrows(GcpException.class, + () -> service.generateAccessToken( + SERVICE_ACCOUNT, + List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE), + "one-hour")); + + assertEquals("INVALID_ARGUMENT", ex.getGcpStatus()); + } + + @Test + void rejectsZeroLifetime() { + GcpException ex = assertThrows(GcpException.class, + () -> service.generateAccessToken( + SERVICE_ACCOUNT, + List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE), + "0s")); + + assertEquals("INVALID_ARGUMENT", ex.getGcpStatus()); + } + + @Test + void rejectsNegativeLifetime() { + GcpException ex = assertThrows(GcpException.class, + () -> service.generateAccessToken( + SERVICE_ACCOUNT, + List.of(IamCredentialsService.DEVSTORAGE_READ_WRITE_SCOPE), + "-1s")); + + assertEquals("INVALID_ARGUMENT", ex.getGcpStatus()); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index e8a1463..1906602 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -11,6 +11,8 @@ floci-gcp: storage: mode: memory services: + iamcredentials: + enabled: true datastore: enabled: true kafka: