From 9f8131879b25fca3ebe69794652e5ef084767ec0 Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Thu, 11 Jan 2024 09:55:40 -0500 Subject: [PATCH 1/4] Remove AuthenticationLoggingInterceptor --- .../AuthenticationLoggingInterceptor.java | 93 ---------------- .../credentials/GoogleCredentialsFactory.java | 1 - .../LogAuthenticationFailures.java | 42 ------- .../GoogleCredentialsFactorySpec.groovy | 54 +-------- ...SubscriberAuthenticationFailureSpec.groovy | 105 ------------------ 5 files changed, 6 insertions(+), 289 deletions(-) delete mode 100644 gcp-common/src/main/java/io/micronaut/gcp/credentials/AuthenticationLoggingInterceptor.java delete mode 100644 gcp-common/src/main/java/io/micronaut/gcp/credentials/LogAuthenticationFailures.java delete mode 100644 gcp-pubsub/src/test/groovy/io/micronaut/gcp/pubsub/bind/SubscriberAuthenticationFailureSpec.groovy diff --git a/gcp-common/src/main/java/io/micronaut/gcp/credentials/AuthenticationLoggingInterceptor.java b/gcp-common/src/main/java/io/micronaut/gcp/credentials/AuthenticationLoggingInterceptor.java deleted file mode 100644 index e825a5483..000000000 --- a/gcp-common/src/main/java/io/micronaut/gcp/credentials/AuthenticationLoggingInterceptor.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2017-2023 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.gcp.credentials; - -import com.google.auth.RequestMetadataCallback; -import io.micronaut.aop.MethodInterceptor; -import io.micronaut.aop.MethodInvocationContext; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.type.MutableArgumentValue; -import jakarta.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -/** - * An interceptor for managed instances of {@link com.google.auth.oauth2.GoogleCredentials} that logs certain types of - * authentication errors that the GCP libraries handle silently as infinitely retryable events. - * - * @author Jeremy Grelle - * @since 5.2.0 - */ -@Singleton -public class AuthenticationLoggingInterceptor implements MethodInterceptor { - - private static final Logger LOG = LoggerFactory.getLogger(AuthenticationLoggingInterceptor.class); - private static final String LOGGED_AUTHENTICATION_METHOD = "getRequestMetadata"; - - /** - * Intercepts the "getRequestMetadata" call and logs any retryable errors before allowing the GCP library to continue - * its normal retry algorithm. - * - * @param context The method invocation context - * @return the result of the method invocation - */ - @Override - public @Nullable Object intercept(MethodInvocationContext context) { - if (!context.getExecutableMethod().getMethodName().equals(LOGGED_AUTHENTICATION_METHOD)) { - return context.proceed(); - } - Map> params = context.getParameters(); - params.entrySet().stream().filter(entry -> entry.getValue().getType().equals(RequestMetadataCallback.class)) - .findFirst() - .ifPresent(entry -> { - @SuppressWarnings("unchecked") MutableArgumentValue argValue = (MutableArgumentValue) entry.getValue(); - RequestMetadataCallback callback = argValue.getValue(); - argValue.setValue(new LoggingRequestMetadataCallback(callback)); - }); - return context.proceed(); - } - - /** - * A wrapper {@link RequestMetadataCallback} implementation that logs failures with a warning before proceeding with - * the original callback. - */ - private static final class LoggingRequestMetadataCallback implements RequestMetadataCallback { - - private final RequestMetadataCallback callback; - - private LoggingRequestMetadataCallback(RequestMetadataCallback callback) { - this.callback = callback; - } - - @Override - public void onSuccess(Map> metadata) { - this.callback.onSuccess(metadata); - } - - @Override - public void onFailure(Throwable ex) { - if (ex instanceof IOException) { - LOG.warn("A failure occurred while attempting to build credential metadata for a GCP API request. The GCP libraries treat this as " + - "a retryable error, but misconfigured credentials can keep it from ever succeeding.", ex); - } - this.callback.onFailure(ex); - } - } -} diff --git a/gcp-common/src/main/java/io/micronaut/gcp/credentials/GoogleCredentialsFactory.java b/gcp-common/src/main/java/io/micronaut/gcp/credentials/GoogleCredentialsFactory.java index 5eaad5bab..06a608949 100644 --- a/gcp-common/src/main/java/io/micronaut/gcp/credentials/GoogleCredentialsFactory.java +++ b/gcp-common/src/main/java/io/micronaut/gcp/credentials/GoogleCredentialsFactory.java @@ -87,7 +87,6 @@ public GoogleCredentialsFactory(@NonNull GoogleCredentialsConfiguration configur @Requires(property = GoogleCredentialsConfiguration.PREFIX + ".enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Primary @Singleton - @LogAuthenticationFailures protected GoogleCredentials defaultGoogleCredentials() throws IOException { final List scopes = configuration.getScopes().stream() .map(URI::toString).collect(Collectors.toList()); diff --git a/gcp-common/src/main/java/io/micronaut/gcp/credentials/LogAuthenticationFailures.java b/gcp-common/src/main/java/io/micronaut/gcp/credentials/LogAuthenticationFailures.java deleted file mode 100644 index 266939f97..000000000 --- a/gcp-common/src/main/java/io/micronaut/gcp/credentials/LogAuthenticationFailures.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2017-2023 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.gcp.credentials; - -import com.google.auth.oauth2.GoogleCredentials; -import io.micronaut.aop.Around; -import io.micronaut.context.annotation.Type; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * Annotation for applying authentication failure logging AOP advice to a managed instance of - * {@link GoogleCredentials}. - * - * @author Jeremy Grelle - * @since 5.2.0 - */ -@Documented -@Retention(RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -@Around(proxyTargetMode = Around.ProxyTargetConstructorMode.ALLOW) -@Type(AuthenticationLoggingInterceptor.class) -public @interface LogAuthenticationFailures { -} diff --git a/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy b/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy index b6a49e88f..38806364c 100644 --- a/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy +++ b/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy @@ -1,11 +1,10 @@ package io.micronaut.gcp.credentials -import com.google.auth.RequestMetadataCallback + import com.google.auth.oauth2.GoogleCredentials import com.google.auth.oauth2.ImpersonatedCredentials import com.google.auth.oauth2.ServiceAccountCredentials import com.google.auth.oauth2.UserCredentials -import com.google.common.util.concurrent.MoreExecutors import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Requires import io.micronaut.context.exceptions.BeanInstantiationException @@ -16,7 +15,6 @@ import io.micronaut.http.HttpResponse import io.micronaut.http.MediaType import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post -import io.micronaut.runtime.server.EmbeddedServer import org.spockframework.runtime.IStandardStreamsListener import org.spockframework.runtime.StandardStreamsCapturer import spock.lang.AutoCleanup @@ -191,7 +189,7 @@ class GoogleCredentialsFactorySpec extends Specification { then: gc != null - ImpersonatedCredentials ic = gc.$target + ImpersonatedCredentials ic = (ImpersonatedCredentials) gc UserCredentials uc = (UserCredentials) ic.getSourceCredentials() ic.getAccount() == "sa-test1@micronaut-gcp-testing.iam.gserviceaccount.com" with(uc) { @@ -249,49 +247,9 @@ class GoogleCredentialsFactorySpec extends Specification { matchesJsonServiceAccountCredentials(pk, gc) } - void "invalid credentials cause a warning to be logged when metadata is requested"(){ - given: - PrivateKey pk = generatePrivateKey() - String encodedServiceAccountCredentials = encodeServiceCredentials(pk) - EmbeddedServer gcp = ApplicationContext.run(EmbeddedServer, [ - "spec.name" : "GoogleCredentialsFactorySpec", - "micronaut.server.port" : 8080 - ]) - def ctx = ApplicationContext.run([ - (GoogleCredentialsConfiguration.PREFIX + ".encoded-key"): encodedServiceAccountCredentials - ]) - GoogleCredentials gc = ctx.getBean(GoogleCredentials) - - when: - gc.getRequestMetadata(null, MoreExecutors.directExecutor(), new RequestMetadataCallback() { - @Override - void onSuccess(Map> metadata) { - - } - - @Override - void onFailure(Throwable exception) { - - } - }) - - then: - conditions.eventually { - captured.messages.any { - it.contains("WARN") - it.contains("A failure occurred while attempting to build credential metadata for a GCP API request. The GCP libraries treat this as " + - "a retryable error, but misconfigured credentials can keep it from ever succeeding.") - } - } - - cleanup: - ctx.stop() - gcp.stop() - } - private void matchesJsonUserCredentials(GoogleCredentials gc) { - assert gc != null && gc.$target != null && gc.$target instanceof UserCredentials - UserCredentials uc = (UserCredentials) gc.$target + assert gc != null && gc instanceof UserCredentials + UserCredentials uc = (UserCredentials) gc assert uc.getClientId() == "client-id-1.apps.googleusercontent.com" assert uc.getClientSecret() == "client-secret-1" assert uc.getQuotaProjectId() == "micronaut-gcp-test" @@ -299,8 +257,8 @@ class GoogleCredentialsFactorySpec extends Specification { } private void matchesJsonServiceAccountCredentials(PrivateKey pk, GoogleCredentials gc) { - assert gc != null && gc.$target != null && gc.$target instanceof ServiceAccountCredentials - ServiceAccountCredentials sc = (ServiceAccountCredentials) gc.$target + assert gc != null && gc instanceof ServiceAccountCredentials + ServiceAccountCredentials sc = (ServiceAccountCredentials) gc assert sc.getAccount() == "sa-test1@micronaut-gcp-testing.iam.gserviceaccount.com" assert sc.getClientId() == "client-id-1" assert sc.getProjectId() == "micronaut-gcp-testing" diff --git a/gcp-pubsub/src/test/groovy/io/micronaut/gcp/pubsub/bind/SubscriberAuthenticationFailureSpec.groovy b/gcp-pubsub/src/test/groovy/io/micronaut/gcp/pubsub/bind/SubscriberAuthenticationFailureSpec.groovy deleted file mode 100644 index 7652a284a..000000000 --- a/gcp-pubsub/src/test/groovy/io/micronaut/gcp/pubsub/bind/SubscriberAuthenticationFailureSpec.groovy +++ /dev/null @@ -1,105 +0,0 @@ -package io.micronaut.gcp.pubsub.bind - -import com.google.auth.oauth2.GoogleCredentials -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Requires -import io.micronaut.core.reflect.ReflectionUtils -import io.micronaut.gcp.credentials.GoogleCredentialsConfiguration -import io.micronaut.gcp.pubsub.annotation.PubSubListener -import io.micronaut.gcp.pubsub.annotation.Subscription -import io.micronaut.http.HttpResponse -import io.micronaut.http.MediaType -import io.micronaut.http.annotation.Controller -import io.micronaut.http.annotation.Post -import io.micronaut.runtime.server.EmbeddedServer -import org.spockframework.runtime.IStandardStreamsListener -import org.spockframework.runtime.StandardStreamsCapturer -import spock.lang.AutoCleanup -import spock.lang.Specification -import spock.util.concurrent.PollingConditions - -import java.security.PrivateKey - -import static io.micronaut.gcp.credentials.fixture.ServiceAccountCredentialsTestHelper.encodeServiceCredentials -import static io.micronaut.gcp.credentials.fixture.ServiceAccountCredentialsTestHelper.generatePrivateKey - -class SubscriberAuthenticationFailureSpec extends Specification { - - ServiceAccountTestListener listener - - PollingConditions conditions = new PollingConditions(timeout: 30) - - SimpleStreamsListener captured = new SimpleStreamsListener() - - @AutoCleanup("stop") - StandardStreamsCapturer capturer = new StandardStreamsCapturer() - - void setup() { - capturer.addStandardStreamsListener(captured) - capturer.start() - } - - void "authentication failure on subscription should be logged as a warning"() { - given: - PrivateKey pk = generatePrivateKey() - String encodedServiceAccountCredentials = encodeServiceCredentials(pk) - EmbeddedServer gcp = ApplicationContext.run(EmbeddedServer, [ - "server.name" : "GoogleAuthServerTestFake", - "micronaut.server.port" : 8080 - ]) - def ctx = ApplicationContext.run([ - "spec.name" : "AuthenticationFailureSpec", - "gcp.projectId" : "micronaut-gcp-testing", - (GoogleCredentialsConfiguration.PREFIX + ".encoded-key"): encodedServiceAccountCredentials - ]) - - when: - def gc = ctx.getBean(GoogleCredentials) - listener = ctx.getBean(ServiceAccountTestListener) - - then: - gc != null - listener != null - conditions.eventually { - captured.messages.any { - it.contains("WARN") - it.contains("A failure occurred while attempting to build credential metadata for a GCP API request. The GCP libraries treat this as " + - "a retryable error, but misconfigured credentials can keep it from ever succeeding.") - } - } - - cleanup: - ReflectionUtils.getFieldValue(GoogleCredentials.class, "defaultCredentialsProvider", gc) - .ifPresent { - ReflectionUtils.setField(it.getClass(), "cachedCredentials", it, null) - } - ctx.close() - gcp.close() - } -} - -@PubSubListener -@Requires(property = "spec.name", value = "AuthenticationFailureSpec") -class ServiceAccountTestListener { - - @Subscription("micronaut-gcp-topic1-sub") - void receive(byte[] data) { - - } -} - -@Requires(property = "server.name", value = "GoogleAuthServerTestFake") -@Controller -class GoogleAuth { - - @Post(value="/token", processes = MediaType.APPLICATION_FORM_URLENCODED) - HttpResponse getToken() { - return HttpResponse.unauthorized() - } -} - -class SimpleStreamsListener implements IStandardStreamsListener { - List messages = [] - @Override void standardOut(String m) { messages << m } - @Override void standardErr(String m) { messages << m } -} From f02acb68cf5bd6ee74d0fc70a65eda590fa5f5da Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Thu, 11 Jan 2024 14:42:57 -0500 Subject: [PATCH 2/4] Add test to cover issue 997 --- .../GoogleCredentialsFactorySpec.groovy | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy b/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy index 38806364c..6abda7eba 100644 --- a/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy +++ b/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy @@ -1,6 +1,6 @@ package io.micronaut.gcp.credentials - +import com.google.api.client.util.GenericData import com.google.auth.oauth2.GoogleCredentials import com.google.auth.oauth2.ImpersonatedCredentials import com.google.auth.oauth2.ServiceAccountCredentials @@ -12,9 +12,11 @@ import io.micronaut.context.exceptions.ConfigurationException import io.micronaut.context.exceptions.NoSuchBeanException import io.micronaut.core.reflect.ReflectionUtils import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post +import io.micronaut.runtime.server.EmbeddedServer import org.spockframework.runtime.IStandardStreamsListener import org.spockframework.runtime.StandardStreamsCapturer import spock.lang.AutoCleanup @@ -25,6 +27,7 @@ import uk.org.webcompere.systemstubs.properties.SystemProperties import uk.org.webcompere.systemstubs.resource.Resources import java.security.PrivateKey +import java.util.concurrent.atomic.AtomicInteger import static io.micronaut.gcp.credentials.fixture.ServiceAccountCredentialsTestHelper.* @@ -247,6 +250,31 @@ class GoogleCredentialsFactorySpec extends Specification { matchesJsonServiceAccountCredentials(pk, gc) } + void "an access token should be able to be refreshed and retrieved"() { + given: + PrivateKey pk = generatePrivateKey() + File serviceAccountCredentials = writeServiceCredentialsToTempFile(pk) + + when: + EmbeddedServer gcp = ApplicationContext.run(EmbeddedServer, [ + "spec.name" : "GoogleCredentialsFactorySpec", + "micronaut.server.port" : 8080 + ]) + def ctx = ApplicationContext.run([ + (GoogleCredentialsConfiguration.PREFIX + ".location"): serviceAccountCredentials.getPath() + ]) + GoogleCredentials gc = ctx.getBean(GoogleCredentials) + + then: + matchesJsonServiceAccountCredentials(pk, gc) + + when: + gc.refreshIfExpired() + + then: + gc.getAccessToken() + } + private void matchesJsonUserCredentials(GoogleCredentials gc) { assert gc != null && gc instanceof UserCredentials UserCredentials uc = (UserCredentials) gc @@ -271,9 +299,14 @@ class GoogleCredentialsFactorySpec extends Specification { @Controller class GoogleAuth { - @Post(value="/token", processes = MediaType.APPLICATION_FORM_URLENCODED) - HttpResponse getToken() { - return HttpResponse.unauthorized() + AtomicInteger requestCount = new AtomicInteger(1) + + @Post(value="/token", consumes = MediaType.APPLICATION_FORM_URLENCODED, produces = MediaType.APPLICATION_JSON) + HttpResponse getToken() { + if (requestCount.getAndAdd(1) == 2) { + return HttpResponse.ok(new GenericData().set("access_token", "ThisIsAFreshToken").set("expires_in", 3600)) + } + return HttpResponse.status(HttpStatus.TOO_MANY_REQUESTS) } } From 7c53655900a64f02178d2427a2d60b192ee976ff Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Thu, 11 Jan 2024 14:45:46 -0500 Subject: [PATCH 3/4] Make test expectation more specific --- .../gcp/credentials/GoogleCredentialsFactorySpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy b/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy index 6abda7eba..6ace4bfbe 100644 --- a/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy +++ b/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy @@ -272,7 +272,7 @@ class GoogleCredentialsFactorySpec extends Specification { gc.refreshIfExpired() then: - gc.getAccessToken() + gc.getAccessToken().getTokenValue() == "ThisIsAFreshToken" } private void matchesJsonUserCredentials(GoogleCredentials gc) { From 29dc5984b02865c8d68062e8b35cc5956fbcf01e Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 12 Jan 2024 09:59:15 +0000 Subject: [PATCH 4/4] Close contexts under test and remove unused field --- .../credentials/GoogleCredentialsFactory.java | 2 +- .../GoogleCredentialsFactorySpec.groovy | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/gcp-common/src/main/java/io/micronaut/gcp/credentials/GoogleCredentialsFactory.java b/gcp-common/src/main/java/io/micronaut/gcp/credentials/GoogleCredentialsFactory.java index 06a608949..207b67e40 100644 --- a/gcp-common/src/main/java/io/micronaut/gcp/credentials/GoogleCredentialsFactory.java +++ b/gcp-common/src/main/java/io/micronaut/gcp/credentials/GoogleCredentialsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2024 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy b/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy index 6ace4bfbe..bb1ad0745 100644 --- a/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy +++ b/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy @@ -21,7 +21,6 @@ import org.spockframework.runtime.IStandardStreamsListener import org.spockframework.runtime.StandardStreamsCapturer import spock.lang.AutoCleanup import spock.lang.Specification -import spock.util.concurrent.PollingConditions import uk.org.webcompere.systemstubs.environment.EnvironmentVariables import uk.org.webcompere.systemstubs.properties.SystemProperties import uk.org.webcompere.systemstubs.resource.Resources @@ -38,8 +37,6 @@ class GoogleCredentialsFactorySpec extends Specification { @AutoCleanup("stop") StandardStreamsCapturer capturer = new StandardStreamsCapturer() - PollingConditions conditions = new PollingConditions(timeout: 30) - void setup() { capturer.addStandardStreamsListener(captured) capturer.start() @@ -68,6 +65,9 @@ class GoogleCredentialsFactorySpec extends Specification { then: thrown(NoSuchBeanException) + + cleanup: + ctx.close() } void "configuring both credentials location and encoded-key throws an exception"() { @@ -83,6 +83,9 @@ class GoogleCredentialsFactorySpec extends Specification { then: def ex = thrown(BeanInstantiationException) ex.getCause() instanceof ConfigurationException + + cleanup: + ctx.close() } void "default configuration without GCP SDK installed fails"() { @@ -233,6 +236,9 @@ class GoogleCredentialsFactorySpec extends Specification { then: matchesJsonServiceAccountCredentials(pk, gc) + + cleanup: + ctx.close() } void "service account credentials can be loaded via configured Base64-encoded key"() { @@ -248,6 +254,9 @@ class GoogleCredentialsFactorySpec extends Specification { then: matchesJsonServiceAccountCredentials(pk, gc) + + cleanup: + ctx.close() } void "an access token should be able to be refreshed and retrieved"() { @@ -273,6 +282,10 @@ class GoogleCredentialsFactorySpec extends Specification { then: gc.getAccessToken().getTokenValue() == "ThisIsAFreshToken" + + cleanup: + gcp.close() + ctx.close() } private void matchesJsonUserCredentials(GoogleCredentials gc) {