|
44 | 44 | import static org.mockito.Mockito.when; |
45 | 45 |
|
46 | 46 | import com.google.api.client.http.HttpStatusCodes; |
| 47 | +import com.google.api.client.http.HttpTransport; |
47 | 48 | import com.google.api.client.json.GenericJson; |
48 | 49 | import com.google.api.client.json.JsonFactory; |
49 | 50 | import com.google.api.client.json.JsonGenerator; |
|
55 | 56 | import com.google.auth.Credentials; |
56 | 57 | import com.google.auth.ServiceAccountSigner.SigningException; |
57 | 58 | import com.google.auth.TestUtils; |
| 59 | +import com.google.auth.http.HttpTransportFactory; |
58 | 60 | import com.google.common.collect.ImmutableList; |
59 | 61 | import com.google.common.collect.ImmutableSet; |
60 | 62 | import java.io.ByteArrayOutputStream; |
61 | 63 | import java.io.IOException; |
62 | 64 | import java.io.InputStream; |
63 | 65 | import java.nio.charset.Charset; |
64 | 66 | import java.security.PrivateKey; |
65 | | -import java.text.DateFormat; |
66 | | -import java.text.SimpleDateFormat; |
| 67 | +import java.time.Instant; |
67 | 68 | import java.time.temporal.ChronoUnit; |
68 | 69 | import java.util.ArrayList; |
69 | 70 | import java.util.Arrays; |
70 | 71 | import java.util.Calendar; |
71 | 72 | import java.util.Date; |
72 | 73 | import java.util.List; |
73 | 74 | import java.util.Map; |
74 | | -import java.util.TimeZone; |
75 | 75 | import org.junit.jupiter.api.BeforeEach; |
76 | 76 | import org.junit.jupiter.api.Test; |
77 | 77 |
|
@@ -123,8 +123,6 @@ class ImpersonatedCredentialsTest extends BaseSerializationTest { |
123 | 123 | private static final int INVALID_LIFETIME = 43210; |
124 | 124 | private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); |
125 | 125 |
|
126 | | - private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX"; |
127 | | - |
128 | 126 | private static final String TEST_UNIVERSE_DOMAIN = "test.xyz"; |
129 | 127 | public static final String DEFAULT_IMPERSONATION_URL = |
130 | 128 | String.format( |
@@ -631,61 +629,9 @@ void refreshAccessToken_delegates_success() throws IOException, IllegalStateExce |
631 | 629 | assertEquals(ACCESS_TOKEN, targetCredentials.refreshAccessToken().getTokenValue()); |
632 | 630 | } |
633 | 631 |
|
634 | | - @Test |
635 | | - void refreshAccessToken_GMT_dateParsedCorrectly() throws IOException, IllegalStateException { |
636 | | - Calendar c = Calendar.getInstance(); |
637 | | - c.add(Calendar.SECOND, VALID_LIFETIME); |
638 | | - |
639 | | - mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); |
640 | | - mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN); |
641 | | - mockTransportFactory.getTransport().setExpireTime(getFormattedTime(c.getTime())); |
642 | | - mockTransportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, ""); |
643 | | - ImpersonatedCredentials targetCredentials = |
644 | | - ImpersonatedCredentials.create( |
645 | | - sourceCredentials, |
646 | | - IMPERSONATED_CLIENT_EMAIL, |
647 | | - null, |
648 | | - IMMUTABLE_SCOPES_LIST, |
649 | | - VALID_LIFETIME, |
650 | | - mockTransportFactory) |
651 | | - .createWithCustomCalendar( |
652 | | - // Set system timezone to GMT |
653 | | - Calendar.getInstance(TimeZone.getTimeZone("GMT"))); |
654 | | - |
655 | | - assertEquals( |
656 | | - c.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli(), |
657 | | - (long) targetCredentials.refreshAccessToken().getExpirationTimeMillis()); |
658 | | - } |
659 | | - |
660 | | - @Test |
661 | | - void refreshAccessToken_nonGMT_dateParsedCorrectly() throws IOException, IllegalStateException { |
662 | | - Calendar c = Calendar.getInstance(); |
663 | | - c.add(Calendar.SECOND, VALID_LIFETIME); |
664 | | - |
665 | | - mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); |
666 | | - mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN); |
667 | | - mockTransportFactory.getTransport().setExpireTime(getFormattedTime(c.getTime())); |
668 | | - mockTransportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, ""); |
669 | | - ImpersonatedCredentials targetCredentials = |
670 | | - ImpersonatedCredentials.create( |
671 | | - sourceCredentials, |
672 | | - IMPERSONATED_CLIENT_EMAIL, |
673 | | - null, |
674 | | - IMMUTABLE_SCOPES_LIST, |
675 | | - VALID_LIFETIME, |
676 | | - mockTransportFactory) |
677 | | - .createWithCustomCalendar( |
678 | | - // Set system timezone to one different than GMT |
679 | | - Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles"))); |
680 | | - |
681 | | - assertEquals( |
682 | | - c.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli(), |
683 | | - (long) targetCredentials.refreshAccessToken().getExpirationTimeMillis()); |
684 | | - } |
685 | | - |
686 | 632 | @Test |
687 | 633 | void refreshAccessToken_invalidDate() throws IllegalStateException { |
688 | | - String expectedMessage = "Unparseable date"; |
| 634 | + String expectedMessage = "Error parsing expireTime: "; |
689 | 635 | mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); |
690 | 636 | mockTransportFactory.getTransport().setAccessToken("foo"); |
691 | 637 | mockTransportFactory.getTransport().setExpireTime("1973-09-29T15:01:23"); |
@@ -1253,22 +1199,69 @@ void serialize() throws IOException, ClassNotFoundException { |
1253 | 1199 | assertSame(Clock.SYSTEM, deserializedCredentials.clock); |
1254 | 1200 | } |
1255 | 1201 |
|
1256 | | - public static String getDefaultExpireTime() { |
1257 | | - Calendar c = Calendar.getInstance(); |
1258 | | - c.add(Calendar.SECOND, VALID_LIFETIME); |
1259 | | - return getFormattedTime(c.getTime()); |
1260 | | - } |
1261 | | - |
1262 | 1202 | /** |
1263 | | - * Given a {@link Date}, it will return a string of the date formatted like |
1264 | | - * <b>yyyy-MM-dd'T'HH:mm:ss'Z'</b> |
| 1203 | + * A stateful {@link HttpTransportFactory} that provides a shared {@link |
| 1204 | + * MockIAMCredentialsServiceTransport} instance. |
| 1205 | + * |
| 1206 | + * <p>This is necessary for serialization tests because {@link ImpersonatedCredentials} stores the |
| 1207 | + * factory's class name and re-instantiates it via reflection during deserialization. A standard |
| 1208 | + * factory would create a fresh, unconfigured transport upon re-instantiation, causing refreshed |
| 1209 | + * token requests to fail. Using a static transport ensures the mock configuration persists across |
| 1210 | + * serialization boundaries. |
1265 | 1211 | */ |
1266 | | - private static String getFormattedTime(final Date date) { |
1267 | | - // Set timezone to GMT since that's the TZ used in the response from the service impersonation |
1268 | | - // token exchange |
1269 | | - final DateFormat formatter = new SimpleDateFormat(RFC3339); |
1270 | | - formatter.setTimeZone(TimeZone.getTimeZone("GMT")); |
1271 | | - return formatter.format(date); |
| 1212 | + public static class StatefulMockIAMTransportFactory implements HttpTransportFactory { |
| 1213 | + private static final MockIAMCredentialsServiceTransport TRANSPORT = |
| 1214 | + new MockIAMCredentialsServiceTransport(GoogleCredentials.GOOGLE_DEFAULT_UNIVERSE); |
| 1215 | + |
| 1216 | + @Override |
| 1217 | + public HttpTransport create() { |
| 1218 | + return TRANSPORT; |
| 1219 | + } |
| 1220 | + |
| 1221 | + public static MockIAMCredentialsServiceTransport getTransport() { |
| 1222 | + return TRANSPORT; |
| 1223 | + } |
| 1224 | + } |
| 1225 | + |
| 1226 | + @Test |
| 1227 | + void refreshAccessToken_afterSerialization_success() throws IOException, ClassNotFoundException { |
| 1228 | + // This test ensures that credentials can still refresh after being serialized. |
| 1229 | + // ImpersonatedCredentials only serializes the transport factory's class name. |
| 1230 | + // Upon deserialization, it creates a new instance of that factory via reflection. |
| 1231 | + // StatefulMockIAMTransportFactory uses a static transport instance so that the |
| 1232 | + // configuration we set here (token, expiration) is available to the new factory instance. |
| 1233 | + MockIAMCredentialsServiceTransport transport = StatefulMockIAMTransportFactory.getTransport(); |
| 1234 | + transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); |
| 1235 | + transport.setAccessToken(ACCESS_TOKEN); |
| 1236 | + |
| 1237 | + transport.setExpireTime(getDefaultExpireTime()); |
| 1238 | + transport.addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "", true); |
| 1239 | + |
| 1240 | + // Use a source credential that doesn't need refresh |
| 1241 | + AccessToken sourceToken = |
| 1242 | + new AccessToken("source-token", new Date(System.currentTimeMillis() + 3600000)); |
| 1243 | + GoogleCredentials sourceCredentials = GoogleCredentials.create(sourceToken); |
| 1244 | + |
| 1245 | + ImpersonatedCredentials targetCredentials = |
| 1246 | + ImpersonatedCredentials.create( |
| 1247 | + sourceCredentials, |
| 1248 | + IMPERSONATED_CLIENT_EMAIL, |
| 1249 | + null, |
| 1250 | + IMMUTABLE_SCOPES_LIST, |
| 1251 | + VALID_LIFETIME, |
| 1252 | + new StatefulMockIAMTransportFactory()); |
| 1253 | + |
| 1254 | + ImpersonatedCredentials deserializedCredentials = serializeAndDeserialize(targetCredentials); |
| 1255 | + |
| 1256 | + // This should not throw NPE. The transient 'calendar' field being null after |
| 1257 | + // deserialization is now handled by using java.time.Instant for parsing. |
| 1258 | + AccessToken token = deserializedCredentials.refreshAccessToken(); |
| 1259 | + assertNotNull(token); |
| 1260 | + assertEquals(ACCESS_TOKEN, token.getTokenValue()); |
| 1261 | + } |
| 1262 | + |
| 1263 | + public static String getDefaultExpireTime() { |
| 1264 | + return Instant.now().plusSeconds(VALID_LIFETIME).truncatedTo(ChronoUnit.SECONDS).toString(); |
1272 | 1265 | } |
1273 | 1266 |
|
1274 | 1267 | private String generateErrorJson( |
|
0 commit comments