Skip to content

Commit ca1c4f4

Browse files
committed
chore: Add tests for serializing ImpersonatedCredentials
1 parent c8cf184 commit ca1c4f4

2 files changed

Lines changed: 108 additions & 4 deletions

File tree

oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,6 @@ public static ImpersonatedCredentials fromStream(
436436
* @return the credentials defined by the JSON
437437
* @throws IOException if the credential cannot be created from the JSON.
438438
*/
439-
@SuppressWarnings("unchecked")
440439
static ImpersonatedCredentials fromJson(
441440
Map<String, Object> json, HttpTransportFactory transportFactory) throws IOException {
442441
checkNotNull(json);
@@ -791,10 +790,7 @@ protected Builder() {}
791790
/**
792791
* @param sourceCredentials The source credentials to use for impersonation.
793792
* @param targetPrincipal The service account to impersonate.
794-
* @deprecated Use {@link #Builder(ImpersonatedCredentials)} instead. This constructor will be
795-
* removed in a future release.
796793
*/
797-
@Deprecated
798794
protected Builder(GoogleCredentials sourceCredentials, String targetPrincipal) {
799795
this.sourceCredentials = sourceCredentials;
800796
this.targetPrincipal = targetPrincipal;

oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import static org.mockito.Mockito.when;
4545

4646
import com.google.api.client.http.HttpStatusCodes;
47+
import com.google.api.client.http.HttpTransport;
4748
import com.google.api.client.json.GenericJson;
4849
import com.google.api.client.json.JsonFactory;
4950
import com.google.api.client.json.JsonGenerator;
@@ -54,6 +55,7 @@
5455
import com.google.auth.Credentials;
5556
import com.google.auth.ServiceAccountSigner.SigningException;
5657
import com.google.auth.TestUtils;
58+
import com.google.auth.http.HttpTransportFactory;
5759
import com.google.common.collect.ImmutableList;
5860
import com.google.common.collect.ImmutableSet;
5961
import java.io.ByteArrayOutputStream;
@@ -1297,6 +1299,112 @@ void serialize() throws IOException, ClassNotFoundException {
12971299
assertSame(deserializedCredentials.clock, Clock.SYSTEM);
12981300
}
12991301

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+
13001408
public static String getDefaultExpireTime() {
13011409
Calendar c = Calendar.getInstance();
13021410
c.add(Calendar.SECOND, VALID_LIFETIME);

0 commit comments

Comments
 (0)