|
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; |
|
54 | 55 | import com.google.auth.Credentials; |
55 | 56 | import com.google.auth.ServiceAccountSigner.SigningException; |
56 | 57 | import com.google.auth.TestUtils; |
| 58 | +import com.google.auth.http.HttpTransportFactory; |
57 | 59 | import com.google.common.collect.ImmutableList; |
58 | 60 | import com.google.common.collect.ImmutableSet; |
59 | 61 | import java.io.ByteArrayOutputStream; |
60 | 62 | import java.io.IOException; |
61 | 63 | import java.io.InputStream; |
62 | 64 | import java.nio.charset.Charset; |
63 | 65 | import java.security.PrivateKey; |
64 | | -import java.text.DateFormat; |
65 | | -import java.text.SimpleDateFormat; |
| 66 | +import java.time.Instant; |
66 | 67 | import java.time.temporal.ChronoUnit; |
67 | 68 | import java.util.ArrayList; |
68 | 69 | import java.util.Arrays; |
69 | 70 | import java.util.Calendar; |
70 | 71 | import java.util.Date; |
71 | 72 | import java.util.List; |
72 | 73 | import java.util.Map; |
73 | | -import java.util.TimeZone; |
74 | 74 | import org.junit.jupiter.api.BeforeEach; |
75 | 75 | import org.junit.jupiter.api.Test; |
76 | 76 |
|
@@ -122,8 +122,6 @@ class ImpersonatedCredentialsTest extends BaseSerializationTest { |
122 | 122 | private static final int INVALID_LIFETIME = 43210; |
123 | 123 | private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); |
124 | 124 |
|
125 | | - private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX"; |
126 | | - |
127 | 125 | private static final String TEST_UNIVERSE_DOMAIN = "test.xyz"; |
128 | 126 | private static final String OLD_IMPERSONATION_URL = |
129 | 127 | "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" |
@@ -653,61 +651,9 @@ void refreshAccessToken_delegates_success() throws IOException, IllegalStateExce |
653 | 651 | assertEquals(ACCESS_TOKEN, targetCredentials.refreshAccessToken().getTokenValue()); |
654 | 652 | } |
655 | 653 |
|
656 | | - @Test |
657 | | - void refreshAccessToken_GMT_dateParsedCorrectly() throws IOException, IllegalStateException { |
658 | | - Calendar c = Calendar.getInstance(); |
659 | | - c.add(Calendar.SECOND, VALID_LIFETIME); |
660 | | - |
661 | | - mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); |
662 | | - mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN); |
663 | | - mockTransportFactory.getTransport().setExpireTime(getFormattedTime(c.getTime())); |
664 | | - mockTransportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, ""); |
665 | | - ImpersonatedCredentials targetCredentials = |
666 | | - ImpersonatedCredentials.create( |
667 | | - sourceCredentials, |
668 | | - IMPERSONATED_CLIENT_EMAIL, |
669 | | - null, |
670 | | - IMMUTABLE_SCOPES_LIST, |
671 | | - VALID_LIFETIME, |
672 | | - mockTransportFactory) |
673 | | - .createWithCustomCalendar( |
674 | | - // Set system timezone to GMT |
675 | | - Calendar.getInstance(TimeZone.getTimeZone("GMT"))); |
676 | | - |
677 | | - assertTrue( |
678 | | - c.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli() |
679 | | - == targetCredentials.refreshAccessToken().getExpirationTimeMillis()); |
680 | | - } |
681 | | - |
682 | | - @Test |
683 | | - void refreshAccessToken_nonGMT_dateParsedCorrectly() throws IOException, IllegalStateException { |
684 | | - Calendar c = Calendar.getInstance(); |
685 | | - c.add(Calendar.SECOND, VALID_LIFETIME); |
686 | | - |
687 | | - mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); |
688 | | - mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN); |
689 | | - mockTransportFactory.getTransport().setExpireTime(getFormattedTime(c.getTime())); |
690 | | - mockTransportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, ""); |
691 | | - ImpersonatedCredentials targetCredentials = |
692 | | - ImpersonatedCredentials.create( |
693 | | - sourceCredentials, |
694 | | - IMPERSONATED_CLIENT_EMAIL, |
695 | | - null, |
696 | | - IMMUTABLE_SCOPES_LIST, |
697 | | - VALID_LIFETIME, |
698 | | - mockTransportFactory) |
699 | | - .createWithCustomCalendar( |
700 | | - // Set system timezone to one different than GMT |
701 | | - Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles"))); |
702 | | - |
703 | | - assertTrue( |
704 | | - c.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli() |
705 | | - == targetCredentials.refreshAccessToken().getExpirationTimeMillis()); |
706 | | - } |
707 | | - |
708 | 654 | @Test |
709 | 655 | void refreshAccessToken_invalidDate() throws IllegalStateException { |
710 | | - String expectedMessage = "Unparseable date"; |
| 656 | + String expectedMessage = "Error parsing expireTime: "; |
711 | 657 | mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); |
712 | 658 | mockTransportFactory.getTransport().setAccessToken("foo"); |
713 | 659 | mockTransportFactory.getTransport().setExpireTime("1973-09-29T15:01:23"); |
@@ -1297,22 +1243,69 @@ void serialize() throws IOException, ClassNotFoundException { |
1297 | 1243 | assertSame(deserializedCredentials.clock, Clock.SYSTEM); |
1298 | 1244 | } |
1299 | 1245 |
|
1300 | | - public static String getDefaultExpireTime() { |
1301 | | - Calendar c = Calendar.getInstance(); |
1302 | | - c.add(Calendar.SECOND, VALID_LIFETIME); |
1303 | | - return getFormattedTime(c.getTime()); |
1304 | | - } |
1305 | | - |
1306 | 1246 | /** |
1307 | | - * Given a {@link Date}, it will return a string of the date formatted like |
1308 | | - * <b>yyyy-MM-dd'T'HH:mm:ss'Z'</b> |
| 1247 | + * A stateful {@link HttpTransportFactory} that provides a shared {@link |
| 1248 | + * MockIAMCredentialsServiceTransport} instance. |
| 1249 | + * |
| 1250 | + * <p>This is necessary for serialization tests because {@link ImpersonatedCredentials} stores the |
| 1251 | + * factory's class name and re-instantiates it via reflection during deserialization. A standard |
| 1252 | + * factory would create a fresh, unconfigured transport upon re-instantiation, causing refreshed |
| 1253 | + * token requests to fail. Using a static transport ensures the mock configuration persists across |
| 1254 | + * serialization boundaries. |
1309 | 1255 | */ |
1310 | | - private static String getFormattedTime(final Date date) { |
1311 | | - // Set timezone to GMT since that's the TZ used in the response from the service impersonation |
1312 | | - // token exchange |
1313 | | - final DateFormat formatter = new SimpleDateFormat(RFC3339); |
1314 | | - formatter.setTimeZone(TimeZone.getTimeZone("GMT")); |
1315 | | - return formatter.format(date); |
| 1256 | + public static class StatefulMockIAMTransportFactory implements HttpTransportFactory { |
| 1257 | + private static final MockIAMCredentialsServiceTransport TRANSPORT = |
| 1258 | + new MockIAMCredentialsServiceTransport(GoogleCredentials.GOOGLE_DEFAULT_UNIVERSE); |
| 1259 | + |
| 1260 | + @Override |
| 1261 | + public HttpTransport create() { |
| 1262 | + return TRANSPORT; |
| 1263 | + } |
| 1264 | + |
| 1265 | + public static MockIAMCredentialsServiceTransport getTransport() { |
| 1266 | + return TRANSPORT; |
| 1267 | + } |
| 1268 | + } |
| 1269 | + |
| 1270 | + @Test |
| 1271 | + void refreshAccessToken_afterSerialization_success() throws IOException, ClassNotFoundException { |
| 1272 | + // This test ensures that credentials can still refresh after being serialized. |
| 1273 | + // ImpersonatedCredentials only serializes the transport factory's class name. |
| 1274 | + // Upon deserialization, it creates a new instance of that factory via reflection. |
| 1275 | + // StatefulMockIAMTransportFactory uses a static transport instance so that the |
| 1276 | + // configuration we set here (token, expiration) is available to the new factory instance. |
| 1277 | + MockIAMCredentialsServiceTransport transport = StatefulMockIAMTransportFactory.getTransport(); |
| 1278 | + transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); |
| 1279 | + transport.setAccessToken(ACCESS_TOKEN); |
| 1280 | + |
| 1281 | + transport.setExpireTime(getDefaultExpireTime()); |
| 1282 | + transport.addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "", true); |
| 1283 | + |
| 1284 | + // Use a source credential that doesn't need refresh |
| 1285 | + AccessToken sourceToken = |
| 1286 | + new AccessToken("source-token", new Date(System.currentTimeMillis() + 3600000)); |
| 1287 | + GoogleCredentials sourceCredentials = GoogleCredentials.create(sourceToken); |
| 1288 | + |
| 1289 | + ImpersonatedCredentials targetCredentials = |
| 1290 | + ImpersonatedCredentials.create( |
| 1291 | + sourceCredentials, |
| 1292 | + IMPERSONATED_CLIENT_EMAIL, |
| 1293 | + null, |
| 1294 | + IMMUTABLE_SCOPES_LIST, |
| 1295 | + VALID_LIFETIME, |
| 1296 | + new StatefulMockIAMTransportFactory()); |
| 1297 | + |
| 1298 | + ImpersonatedCredentials deserializedCredentials = serializeAndDeserialize(targetCredentials); |
| 1299 | + |
| 1300 | + // This should not throw NPE. The transient 'calendar' field being null after |
| 1301 | + // deserialization is now handled by using java.time.Instant for parsing. |
| 1302 | + AccessToken token = deserializedCredentials.refreshAccessToken(); |
| 1303 | + assertNotNull(token); |
| 1304 | + assertEquals(ACCESS_TOKEN, token.getTokenValue()); |
| 1305 | + } |
| 1306 | + |
| 1307 | + public static String getDefaultExpireTime() { |
| 1308 | + return Instant.now().plusSeconds(VALID_LIFETIME).truncatedTo(ChronoUnit.SECONDS).toString(); |
1316 | 1309 | } |
1317 | 1310 |
|
1318 | 1311 | private String generateErrorJson( |
|
0 commit comments