|
74 | 74 | import org.junit.jupiter.params.ParameterizedTest; |
75 | 75 | import org.junit.jupiter.params.provider.Arguments; |
76 | 76 | import org.junit.jupiter.params.provider.MethodSource; |
| 77 | +import org.junitpioneer.jupiter.ClearSystemProperty; |
77 | 78 | import org.mockito.ArgumentCaptor; |
78 | 79 | import org.mockito.Captor; |
79 | 80 | import org.mockito.Mock; |
@@ -456,6 +457,92 @@ void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) throws IOExce |
456 | 457 | } |
457 | 458 | } |
458 | 459 |
|
| 460 | + @ParameterizedTest |
| 461 | + @MethodSource("provideProjectIdBehaviorTestCases") |
| 462 | + @ClearSystemProperty(key = "google.cloud.project") |
| 463 | + @ClearSystemProperty(key = "google.otel.auth.target.signals") |
| 464 | + @SuppressWarnings("CannotMockMethod") |
| 465 | + void testProjectIdBehavior(ProjectIdTestBehavior testCase) throws IOException { |
| 466 | + |
| 467 | + // configure environment according to test case |
| 468 | + String userSpecifiedProjectId = testCase.getUserSpecifiedProjectId(); |
| 469 | + if (userSpecifiedProjectId != null && !userSpecifiedProjectId.isEmpty()) { |
| 470 | + System.setProperty( |
| 471 | + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), userSpecifiedProjectId); |
| 472 | + } |
| 473 | + System.setProperty( |
| 474 | + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), SIGNAL_TYPE_TRACES); |
| 475 | + |
| 476 | + // prepare request metadata (may or may not be called depending on test scenario) |
| 477 | + AccessToken fakeAccessToken = new AccessToken("fake", Date.from(Instant.now())); |
| 478 | + ImmutableMap<String, List<String>> mockedRequestMetadata = |
| 479 | + ImmutableMap.of( |
| 480 | + "Authorization", |
| 481 | + Collections.singletonList("Bearer " + fakeAccessToken.getTokenValue())); |
| 482 | + Mockito.lenient() |
| 483 | + .when(mockedGoogleCredentials.getRequestMetadata()) |
| 484 | + .thenReturn(mockedRequestMetadata); |
| 485 | + |
| 486 | + // only mock getProjectId() if it will be called (i.e., user didn't specify project ID) |
| 487 | + boolean shouldFallbackToCredentials = |
| 488 | + userSpecifiedProjectId == null || userSpecifiedProjectId.isEmpty(); |
| 489 | + if (shouldFallbackToCredentials) { |
| 490 | + Mockito.when(mockedGoogleCredentials.getProjectId()) |
| 491 | + .thenReturn(testCase.getCredentialsProjectId()); |
| 492 | + } |
| 493 | + |
| 494 | + // prepare mock exporter |
| 495 | + OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class); |
| 496 | + OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder = |
| 497 | + Mockito.spy(OtlpGrpcSpanExporter.builder()); |
| 498 | + List<SpanData> exportedSpans = new ArrayList<>(); |
| 499 | + configureGrpcMockSpanExporter( |
| 500 | + mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans); |
| 501 | + |
| 502 | + try (MockedStatic<GoogleCredentials> googleCredentialsMockedStatic = |
| 503 | + Mockito.mockStatic(GoogleCredentials.class)) { |
| 504 | + googleCredentialsMockedStatic |
| 505 | + .when(GoogleCredentials::getApplicationDefault) |
| 506 | + .thenReturn(mockedGoogleCredentials); |
| 507 | + |
| 508 | + if (testCase.getExpectedToThrow()) { |
| 509 | + // expect exception to be thrown when project ID is not available |
| 510 | + assertThatThrownBy( |
| 511 | + () -> { |
| 512 | + OpenTelemetrySdk sdk = |
| 513 | + buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter); |
| 514 | + generateTestSpan(sdk); |
| 515 | + sdk.shutdown().join(10, TimeUnit.SECONDS); |
| 516 | + }) |
| 517 | + .isInstanceOf(ConfigurationException.class); |
| 518 | + // verify getProjectId() was called to attempt fallback |
| 519 | + Mockito.verify(mockedGoogleCredentials, Mockito.times(1)).getProjectId(); |
| 520 | + } else { |
| 521 | + // export telemetry and verify resource attributes contain expected project ID |
| 522 | + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter); |
| 523 | + generateTestSpan(sdk); |
| 524 | + CompletableResultCode code = sdk.shutdown(); |
| 525 | + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); |
| 526 | + assertThat(joinResult.isSuccess()).isTrue(); |
| 527 | + |
| 528 | + assertThat(exportedSpans).hasSizeGreaterThan(0); |
| 529 | + for (SpanData spanData : exportedSpans) { |
| 530 | + assertThat(spanData.getResource().getAttributes().asMap()) |
| 531 | + .containsEntry( |
| 532 | + AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), |
| 533 | + testCase.getExpectedProjectIdInResource()); |
| 534 | + } |
| 535 | + |
| 536 | + // verify whether getProjectId() was called based on whether fallback was needed |
| 537 | + if (shouldFallbackToCredentials) { |
| 538 | + Mockito.verify(mockedGoogleCredentials, Mockito.times(1)).getProjectId(); |
| 539 | + } else { |
| 540 | + Mockito.verify(mockedGoogleCredentials, Mockito.never()).getProjectId(); |
| 541 | + } |
| 542 | + } |
| 543 | + } |
| 544 | + } |
| 545 | + |
459 | 546 | @ParameterizedTest |
460 | 547 | @MethodSource("provideTargetSignalBehaviorTestCases") |
461 | 548 | void testTargetSignalsBehavior(TargetSignalBehavior testCase) { |
@@ -680,6 +767,34 @@ private static Stream<Arguments> provideTargetSignalBehaviorTestCases() { |
680 | 767 | * indicates that the mocked credentials are configured to provide DUMMY_GCP_QUOTA_PROJECT_ID as |
681 | 768 | * the quota project ID. |
682 | 769 | */ |
| 770 | + private static Stream<Arguments> provideProjectIdBehaviorTestCases() { |
| 771 | + return Stream.of( |
| 772 | + // User specified project ID takes precedence |
| 773 | + Arguments.of( |
| 774 | + ProjectIdTestBehavior.builder() |
| 775 | + .setUserSpecifiedProjectId(DUMMY_GCP_RESOURCE_PROJECT_ID) |
| 776 | + .setCredentialsProjectId("credentials-project-id") |
| 777 | + .setExpectedProjectIdInResource(DUMMY_GCP_RESOURCE_PROJECT_ID) |
| 778 | + .setExpectedToThrow(false) |
| 779 | + .build()), |
| 780 | + // If user doesn't specify project ID, fallback to credentials.getProjectId() |
| 781 | + Arguments.of( |
| 782 | + ProjectIdTestBehavior.builder() |
| 783 | + .setUserSpecifiedProjectId(null) |
| 784 | + .setCredentialsProjectId("credentials-project-id") |
| 785 | + .setExpectedProjectIdInResource("credentials-project-id") |
| 786 | + .setExpectedToThrow(false) |
| 787 | + .build()), |
| 788 | + // If user doesn't specify and credentials.getProjectId() returns null, throw exception |
| 789 | + Arguments.of( |
| 790 | + ProjectIdTestBehavior.builder() |
| 791 | + .setUserSpecifiedProjectId(null) |
| 792 | + .setCredentialsProjectId(null) |
| 793 | + .setExpectedProjectIdInResource(null) |
| 794 | + .setExpectedToThrow(true) |
| 795 | + .build())); |
| 796 | + } |
| 797 | + |
683 | 798 | private static Stream<Arguments> provideQuotaBehaviorTestCases() { |
684 | 799 | return Stream.of( |
685 | 800 | // If quota project present in metadata, it will be used |
@@ -839,6 +954,42 @@ private static void configureGrpcMockMetricExporter( |
839 | 954 | .thenReturn(MemoryMode.IMMUTABLE_DATA); |
840 | 955 | } |
841 | 956 |
|
| 957 | + @AutoValue |
| 958 | + abstract static class ProjectIdTestBehavior { |
| 959 | + // A null user specified project ID represents the use case where user omits specifying it |
| 960 | + @Nullable |
| 961 | + abstract String getUserSpecifiedProjectId(); |
| 962 | + |
| 963 | + // The project ID that credentials.getProjectId() returns (can be null) |
| 964 | + @Nullable |
| 965 | + abstract String getCredentialsProjectId(); |
| 966 | + |
| 967 | + // The expected project ID in the resource attributes (null if exception expected) |
| 968 | + @Nullable |
| 969 | + abstract String getExpectedProjectIdInResource(); |
| 970 | + |
| 971 | + // Whether an exception is expected to be thrown |
| 972 | + abstract boolean getExpectedToThrow(); |
| 973 | + |
| 974 | + static Builder builder() { |
| 975 | + return new AutoValue_GcpAuthAutoConfigurationCustomizerProviderTest_ProjectIdTestBehavior |
| 976 | + .Builder(); |
| 977 | + } |
| 978 | + |
| 979 | + @AutoValue.Builder |
| 980 | + abstract static class Builder { |
| 981 | + abstract Builder setUserSpecifiedProjectId(String projectId); |
| 982 | + |
| 983 | + abstract Builder setCredentialsProjectId(String projectId); |
| 984 | + |
| 985 | + abstract Builder setExpectedProjectIdInResource(String projectId); |
| 986 | + |
| 987 | + abstract Builder setExpectedToThrow(boolean expectedToThrow); |
| 988 | + |
| 989 | + abstract ProjectIdTestBehavior build(); |
| 990 | + } |
| 991 | + } |
| 992 | + |
842 | 993 | @AutoValue |
843 | 994 | abstract static class QuotaProjectIdTestBehavior { |
844 | 995 | // A null user specified quota represents the use case where user omits specifying quota |
|
0 commit comments