|
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; |
@@ -1297,6 +1299,112 @@ void serialize() throws IOException, ClassNotFoundException { |
1297 | 1299 | assertSame(deserializedCredentials.clock, Clock.SYSTEM); |
1298 | 1300 | } |
1299 | 1301 |
|
| 1302 | + /** |
| 1303 | + * A stateful {@link HttpTransportFactory} that provides a shared {@link |
| 1304 | + * MockIAMCredentialsServiceTransport} instance. |
| 1305 | + * |
| 1306 | + * <p>This is necessary for serialization tests because {@link ImpersonatedCredentials} stores the |
| 1307 | + * factory's class name and re-instantiates it via reflection during deserialization. A standard |
| 1308 | + * factory would create a fresh, unconfigured transport upon re-instantiation, causing refreshed |
| 1309 | + * token requests to fail. Using a static transport ensures the mock configuration persists across |
| 1310 | + * serialization boundaries. |
| 1311 | + */ |
| 1312 | + public static class StatefulMockIAMTransportFactory implements HttpTransportFactory { |
| 1313 | + private static final MockIAMCredentialsServiceTransport TRANSPORT = |
| 1314 | + new MockIAMCredentialsServiceTransport(GoogleCredentials.GOOGLE_DEFAULT_UNIVERSE); |
| 1315 | + |
| 1316 | + @Override |
| 1317 | + public HttpTransport create() { |
| 1318 | + return TRANSPORT; |
| 1319 | + } |
| 1320 | + |
| 1321 | + public static MockIAMCredentialsServiceTransport getTransport() { |
| 1322 | + return TRANSPORT; |
| 1323 | + } |
| 1324 | + } |
| 1325 | + |
| 1326 | + @Test |
| 1327 | + void refreshAccessToken_afterSerialization_success() throws IOException, ClassNotFoundException { |
| 1328 | + // This test ensures that credentials can still refresh after being serialized. |
| 1329 | + // ImpersonatedCredentials only serializes the transport factory's class name. |
| 1330 | + // Upon deserialization, it creates a new instance of that factory via reflection. |
| 1331 | + // StatefulMockIAMTransportFactory uses a static transport instance so that the |
| 1332 | + // configuration we set here (token, expiration) is available to the new factory instance. |
| 1333 | + MockIAMCredentialsServiceTransport transport = StatefulMockIAMTransportFactory.getTransport(); |
| 1334 | + transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); |
| 1335 | + transport.setAccessToken(ACCESS_TOKEN); |
| 1336 | + |
| 1337 | + transport.setExpireTime(getDefaultExpireTime()); |
| 1338 | + transport.addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "", true); |
| 1339 | + |
| 1340 | + // Use a source credential that doesn't need refresh |
| 1341 | + AccessToken sourceToken = |
| 1342 | + new AccessToken("source-token", new Date(System.currentTimeMillis() + 3600000)); |
| 1343 | + GoogleCredentials sourceCredentials = GoogleCredentials.create(sourceToken); |
| 1344 | + |
| 1345 | + ImpersonatedCredentials targetCredentials = |
| 1346 | + ImpersonatedCredentials.create( |
| 1347 | + sourceCredentials, |
| 1348 | + IMPERSONATED_CLIENT_EMAIL, |
| 1349 | + null, |
| 1350 | + IMMUTABLE_SCOPES_LIST, |
| 1351 | + VALID_LIFETIME, |
| 1352 | + new StatefulMockIAMTransportFactory()); |
| 1353 | + |
| 1354 | + ImpersonatedCredentials deserializedCredentials = serializeAndDeserialize(targetCredentials); |
| 1355 | + |
| 1356 | + // This should not throw NPE. The transient 'calendar' field being null after |
| 1357 | + // deserialization is now handled by using java.time.Instant for parsing. |
| 1358 | + AccessToken token = deserializedCredentials.refreshAccessToken(); |
| 1359 | + assertNotNull(token); |
| 1360 | + assertEquals(ACCESS_TOKEN, token.getTokenValue()); |
| 1361 | + } |
| 1362 | + |
| 1363 | + @Test |
| 1364 | + void refreshAccessToken_withCustomCalendar_success() throws IOException { |
| 1365 | + // This test verifies behavioral parity between the new Instant-based logic and |
| 1366 | + // the legacy Calendar-based logic. It ensures that if a user provides a custom |
| 1367 | + // calendar with a specific timezone, that context is correctly respected |
| 1368 | + // during parsing, even though the primary parsing engine has changed. |
| 1369 | + MockIAMCredentialsServiceTransport transport = StatefulMockIAMTransportFactory.getTransport(); |
| 1370 | + transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); |
| 1371 | + transport.setAccessToken(ACCESS_TOKEN); |
| 1372 | + |
| 1373 | + // Create a calendar in a specific timezone (PST/PDT) |
| 1374 | + Calendar c = Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles")); |
| 1375 | + // Set to a fixed point in time: 1:00 PM local wall-clock time |
| 1376 | + c.set(2026, Calendar.MARCH, 24, 13, 0, 0); |
| 1377 | + c.set(Calendar.MILLISECOND, 0); |
| 1378 | + Date expectedDate = c.getTime(); |
| 1379 | + |
| 1380 | + // The IAM API always returns Zulu (UTC) time strings. |
| 1381 | + // 1:00 PM PDT (UTC-7) corresponds to 8:00 PM UTC. |
| 1382 | + String expireTime = "2026-03-24T20:00:00Z"; |
| 1383 | + transport.setExpireTime(expireTime); |
| 1384 | + transport.addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "", true); |
| 1385 | + |
| 1386 | + AccessToken sourceToken = |
| 1387 | + new AccessToken("source-token", new Date(System.currentTimeMillis() + 3600000)); |
| 1388 | + GoogleCredentials sourceCredentials = GoogleCredentials.create(sourceToken); |
| 1389 | + |
| 1390 | + ImpersonatedCredentials targetCredentials = |
| 1391 | + ImpersonatedCredentials.create( |
| 1392 | + sourceCredentials, |
| 1393 | + IMPERSONATED_CLIENT_EMAIL, |
| 1394 | + null, |
| 1395 | + IMMUTABLE_SCOPES_LIST, |
| 1396 | + VALID_LIFETIME, |
| 1397 | + new StatefulMockIAMTransportFactory()) |
| 1398 | + .createWithCustomCalendar(c); |
| 1399 | + |
| 1400 | + // This should work and correctly integrate the custom calendar's timezone configuration. |
| 1401 | + AccessToken token = targetCredentials.refreshAccessToken(); |
| 1402 | + assertNotNull(token); |
| 1403 | + assertEquals(ACCESS_TOKEN, token.getTokenValue()); |
| 1404 | + // Verify that the resulting point-in-time matches our original calendar configuration. |
| 1405 | + assertEquals(expectedDate.getTime(), token.getExpirationTime().getTime()); |
| 1406 | + } |
| 1407 | + |
1300 | 1408 | public static String getDefaultExpireTime() { |
1301 | 1409 | Calendar c = Calendar.getInstance(); |
1302 | 1410 | c.add(Calendar.SECOND, VALID_LIFETIME); |
|
0 commit comments