Skip to content

Commit 8425b10

Browse files
committed
Add unit tests to verify quota project behavior
1 parent c2f6a6c commit 8425b10

File tree

2 files changed

+196
-12
lines changed

2 files changed

+196
-12
lines changed

gcp-auth-extension/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies {
2929
testCompileOnly("com.google.auto.service:auto-service-annotations")
3030
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
3131
testImplementation("org.junit.jupiter:junit-jupiter-api")
32+
testCompileOnly("org.junit.jupiter:junit-jupiter-params")
3233

3334
testImplementation("io.opentelemetry:opentelemetry-api")
3435
testImplementation("io.opentelemetry:opentelemetry-exporter-otlp")
@@ -45,6 +46,9 @@ dependencies {
4546
testImplementation("org.springframework.boot:spring-boot-starter:2.7.18")
4647
testImplementation("org.springframework.boot:spring-boot-starter-test:2.7.18")
4748

49+
testAnnotationProcessor("com.google.auto.value:auto-value")
50+
testCompileOnly("com.google.auto.value:auto-value-annotations")
51+
4852
agent("io.opentelemetry.javaagent:opentelemetry-javaagent")
4953
}
5054

gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java

Lines changed: 192 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import com.google.auth.oauth2.AccessToken;
1616
import com.google.auth.oauth2.GoogleCredentials;
17+
import com.google.auto.value.AutoValue;
1718
import com.google.common.collect.ImmutableMap;
1819
import io.opentelemetry.api.common.AttributeKey;
1920
import io.opentelemetry.api.trace.Span;
@@ -46,9 +47,14 @@
4647
import java.util.Set;
4748
import java.util.concurrent.TimeUnit;
4849
import java.util.function.Supplier;
50+
import java.util.stream.Stream;
51+
import javax.annotation.Nullable;
4952
import org.junit.jupiter.api.BeforeEach;
5053
import org.junit.jupiter.api.Test;
5154
import org.junit.jupiter.api.extension.ExtendWith;
55+
import org.junit.jupiter.params.ParameterizedTest;
56+
import org.junit.jupiter.params.provider.Arguments;
57+
import org.junit.jupiter.params.provider.MethodSource;
5258
import org.mockito.ArgumentCaptor;
5359
import org.mockito.Captor;
5460
import org.mockito.Mock;
@@ -149,19 +155,11 @@ public void testCustomizerOtlpGrpc() {
149155
// Prepare mocks
150156
prepareMockBehaviorForGoogleCredentials();
151157
OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class);
152-
OtlpGrpcSpanExporterBuilder otlpSpanExporterBuilder = OtlpGrpcSpanExporter.builder();
153158
OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder =
154-
Mockito.spy(otlpSpanExporterBuilder);
155-
Mockito.when(spyOtlpGrpcSpanExporterBuilder.build()).thenReturn(mockOtlpGrpcSpanExporter);
156-
Mockito.when(mockOtlpGrpcSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess());
159+
Mockito.spy(OtlpGrpcSpanExporter.builder());
157160
List<SpanData> exportedSpans = new ArrayList<>();
158-
Mockito.when(mockOtlpGrpcSpanExporter.export(Mockito.anyCollection()))
159-
.thenAnswer(
160-
invocationOnMock -> {
161-
exportedSpans.addAll(invocationOnMock.getArgument(0));
162-
return CompletableResultCode.ofSuccess();
163-
});
164-
Mockito.when(mockOtlpGrpcSpanExporter.toBuilder()).thenReturn(spyOtlpGrpcSpanExporterBuilder);
161+
configureGrpcMockExporters(
162+
mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans);
165163

166164
try (MockedStatic<GoogleCredentials> googleCredentialsMockedStatic =
167165
Mockito.mockStatic(GoogleCredentials.class)) {
@@ -213,6 +211,186 @@ public void testCustomizerFailWithMissingResourceProject() {
213211
}
214212
}
215213

214+
@ParameterizedTest
215+
@MethodSource("provideQuotaBehaviorTestCases")
216+
@SuppressWarnings("CannotMockMethod")
217+
public void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) {
218+
// Set resource project system property
219+
System.setProperty(
220+
ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID);
221+
// Configure mock credentials to return fake access token
222+
Mockito.when(mockedGoogleCredentials.getAccessToken())
223+
.thenReturn(new AccessToken("fake", Date.from(Instant.now())));
224+
225+
// To prevent unncecessary stubbings, mock getQuotaProjectId only when necessary
226+
if (testCase.getUserSpecifiedQuotaProjectId() == null
227+
|| testCase.getUserSpecifiedQuotaProjectId().isEmpty()) {
228+
String quotaProjectFromCredential =
229+
testCase.getIsQuotaProjectPresentInCredentials() ? DUMMY_GCP_QUOTA_PROJECT_ID : null;
230+
Mockito.when(mockedGoogleCredentials.getQuotaProjectId())
231+
.thenReturn(quotaProjectFromCredential);
232+
}
233+
234+
// configure environment according to test case
235+
String quotaProjectId = testCase.getUserSpecifiedQuotaProjectId(); // maybe empty string
236+
if (testCase.getUserSpecifiedQuotaProjectId() != null) {
237+
// user specified a quota project id
238+
System.setProperty(
239+
ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getSystemProperty(), quotaProjectId);
240+
}
241+
242+
// prepare mock exporter
243+
OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class);
244+
OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder =
245+
Mockito.spy(OtlpGrpcSpanExporter.builder());
246+
List<SpanData> exportedSpans = new ArrayList<>();
247+
configureGrpcMockExporters(
248+
mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans);
249+
250+
try (MockedStatic<GoogleCredentials> googleCredentialsMockedStatic =
251+
Mockito.mockStatic(GoogleCredentials.class)) {
252+
googleCredentialsMockedStatic
253+
.when(GoogleCredentials::getApplicationDefault)
254+
.thenReturn(mockedGoogleCredentials);
255+
256+
// Export telemetry to capture headers in the export calls
257+
OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter);
258+
generateTestSpan(sdk);
259+
CompletableResultCode code = sdk.shutdown();
260+
CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS);
261+
assertTrue(joinResult.isSuccess());
262+
Mockito.verify(spyOtlpGrpcSpanExporterBuilder, Mockito.times(1))
263+
.setHeaders(headerSupplierCaptor.capture());
264+
265+
// assert that the Authorization bearer token header is present
266+
Map<String, String> exportHeaders = headerSupplierCaptor.getValue().get();
267+
assertThat(exportHeaders).containsEntry("Authorization", "Bearer fake");
268+
269+
if (testCase.getExpectedQuotaProjectInHeader() == null) {
270+
// there should be no user quota project header
271+
assertThat(exportHeaders).doesNotContainKey(QUOTA_USER_PROJECT_HEADER);
272+
} else {
273+
// there should be user quota project header with expected value
274+
assertThat(exportHeaders)
275+
.containsEntry(QUOTA_USER_PROJECT_HEADER, testCase.getExpectedQuotaProjectInHeader());
276+
}
277+
}
278+
}
279+
280+
/**
281+
* Test cases specifying expected value for the user quota project header given the user input and
282+
* the current credentials state.
283+
*
284+
* <p>{@code null} for {@link QuotaProjectIdTestBehavior#getUserSpecifiedQuotaProjectId()}
285+
* indicates the case of user not specifying the quota project ID.
286+
*
287+
* <p>{@code null} value for {@link QuotaProjectIdTestBehavior#getExpectedQuotaProjectInHeader()}
288+
* indicates the expectation that the QUOTA_USER_PROJECT_HEADER should not be present in the
289+
* export headers.
290+
*
291+
* <p>{@code true} for {@link QuotaProjectIdTestBehavior#getIsQuotaProjectPresentInCredentials()}
292+
* indicates that the mocked credentials are configured to provide DUMMY_GCP_QUOTA_PROJECT_ID as
293+
* the quota project ID.
294+
*/
295+
private static Stream<Arguments> provideQuotaBehaviorTestCases() {
296+
return Stream.of(
297+
Arguments.of(
298+
QuotaProjectIdTestBehavior.builder()
299+
.setUserSpecifiedQuotaProjectId(DUMMY_GCP_QUOTA_PROJECT_ID)
300+
.setIsQuotaProjectPresentInCredentials(true)
301+
.setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
302+
.build()),
303+
Arguments.of(
304+
QuotaProjectIdTestBehavior.builder()
305+
.setUserSpecifiedQuotaProjectId(DUMMY_GCP_QUOTA_PROJECT_ID)
306+
.setIsQuotaProjectPresentInCredentials(false)
307+
.setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
308+
.build()),
309+
Arguments.of(
310+
QuotaProjectIdTestBehavior.builder()
311+
.setUserSpecifiedQuotaProjectId("my-custom-quota-project-id")
312+
.setIsQuotaProjectPresentInCredentials(true)
313+
.setExpectedQuotaProjectInHeader("my-custom-quota-project-id")
314+
.build()),
315+
Arguments.of(
316+
QuotaProjectIdTestBehavior.builder()
317+
.setUserSpecifiedQuotaProjectId("my-custom-quota-project-id")
318+
.setIsQuotaProjectPresentInCredentials(false)
319+
.setExpectedQuotaProjectInHeader("my-custom-quota-project-id")
320+
.build()),
321+
Arguments.of(
322+
QuotaProjectIdTestBehavior.builder()
323+
.setUserSpecifiedQuotaProjectId("") // user explicitly specifies empty
324+
.setIsQuotaProjectPresentInCredentials(true)
325+
.setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
326+
.build()),
327+
Arguments.of(
328+
QuotaProjectIdTestBehavior.builder()
329+
.setUserSpecifiedQuotaProjectId(null) // user omits specifying quota project
330+
.setIsQuotaProjectPresentInCredentials(true)
331+
.setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
332+
.build()),
333+
Arguments.of(
334+
QuotaProjectIdTestBehavior.builder()
335+
.setUserSpecifiedQuotaProjectId("")
336+
.setIsQuotaProjectPresentInCredentials(false)
337+
.setExpectedQuotaProjectInHeader(null)
338+
.build()),
339+
Arguments.of(
340+
QuotaProjectIdTestBehavior.builder()
341+
.setUserSpecifiedQuotaProjectId(null)
342+
.setIsQuotaProjectPresentInCredentials(false)
343+
.setExpectedQuotaProjectInHeader(null)
344+
.build()));
345+
}
346+
347+
// Configure necessary behavior on the Grpc mock exporters to work
348+
// TODO: Potential improvement - make this work for Http exporter as well.
349+
private static void configureGrpcMockExporters(
350+
OtlpGrpcSpanExporter mockGrpcExporter,
351+
OtlpGrpcSpanExporterBuilder spyGrpcExporterBuilder,
352+
List<SpanData> exportedSpanContainer) {
353+
Mockito.when(spyGrpcExporterBuilder.build()).thenReturn(mockGrpcExporter);
354+
Mockito.when(mockGrpcExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess());
355+
Mockito.when(mockGrpcExporter.toBuilder()).thenReturn(spyGrpcExporterBuilder);
356+
Mockito.when(mockGrpcExporter.export(Mockito.anyCollection()))
357+
.thenAnswer(
358+
invocationOnMock -> {
359+
exportedSpanContainer.addAll(invocationOnMock.getArgument(0));
360+
return CompletableResultCode.ofSuccess();
361+
});
362+
}
363+
364+
@AutoValue
365+
abstract static class QuotaProjectIdTestBehavior {
366+
// A null user specified quota represents the use case where user omits specifying quota
367+
@Nullable
368+
abstract String getUserSpecifiedQuotaProjectId();
369+
370+
abstract boolean getIsQuotaProjectPresentInCredentials();
371+
372+
// If expected quota project in header is null, the header entry should not be present in export
373+
@Nullable
374+
abstract String getExpectedQuotaProjectInHeader();
375+
376+
static Builder builder() {
377+
return new AutoValue_GcpAuthAutoConfigurationCustomizerProviderTest_QuotaProjectIdTestBehavior
378+
.Builder();
379+
}
380+
381+
@AutoValue.Builder
382+
abstract static class Builder {
383+
abstract Builder setUserSpecifiedQuotaProjectId(String quotaProjectId);
384+
385+
abstract Builder setIsQuotaProjectPresentInCredentials(
386+
boolean quotaProjectPresentInCredentials);
387+
388+
abstract Builder setExpectedQuotaProjectInHeader(String expectedQuotaProjectInHeader);
389+
390+
abstract QuotaProjectIdTestBehavior build();
391+
}
392+
}
393+
216394
@SuppressWarnings("CannotMockMethod")
217395
private void prepareMockBehaviorForGoogleCredentials() {
218396
Mockito.when(mockedGoogleCredentials.getQuotaProjectId())
@@ -256,7 +434,9 @@ public String getName() {
256434
private static boolean authHeadersQuotaProjectIsPresent(Map<String, String> headers) {
257435
Set<Entry<String, String>> headerEntrySet = headers.entrySet();
258436
return headerEntrySet.contains(
259-
new SimpleEntry<>(QUOTA_USER_PROJECT_HEADER, DUMMY_GCP_QUOTA_PROJECT_ID))
437+
new SimpleEntry<>(
438+
QUOTA_USER_PROJECT_HEADER,
439+
GcpAuthAutoConfigurationCustomizerProviderTest.DUMMY_GCP_QUOTA_PROJECT_ID))
260440
&& headerEntrySet.contains(new SimpleEntry<>("Authorization", "Bearer fake"));
261441
}
262442

0 commit comments

Comments
 (0)