|
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,86 @@ 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) { |
| 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(() -> buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter)) |
| 511 | + .isInstanceOf(ConfigurationException.class); |
| 512 | + // verify getProjectId() was called to attempt fallback |
| 513 | + Mockito.verify(mockedGoogleCredentials, Mockito.times(1)).getProjectId(); |
| 514 | + } else { |
| 515 | + // export telemetry and verify resource attributes contain expected project ID |
| 516 | + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter); |
| 517 | + generateTestSpan(sdk); |
| 518 | + CompletableResultCode code = sdk.shutdown(); |
| 519 | + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); |
| 520 | + assertThat(joinResult.isSuccess()).isTrue(); |
| 521 | + |
| 522 | + assertThat(exportedSpans).hasSizeGreaterThan(0); |
| 523 | + for (SpanData spanData : exportedSpans) { |
| 524 | + assertThat(spanData.getResource().getAttributes().asMap()) |
| 525 | + .containsEntry( |
| 526 | + AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), |
| 527 | + testCase.getExpectedProjectIdInResource()); |
| 528 | + } |
| 529 | + |
| 530 | + // verify whether getProjectId() was called based on whether fallback was needed |
| 531 | + if (shouldFallbackToCredentials) { |
| 532 | + Mockito.verify(mockedGoogleCredentials, Mockito.times(1)).getProjectId(); |
| 533 | + } else { |
| 534 | + Mockito.verify(mockedGoogleCredentials, Mockito.never()).getProjectId(); |
| 535 | + } |
| 536 | + } |
| 537 | + } |
| 538 | + } |
| 539 | + |
459 | 540 | @ParameterizedTest |
460 | 541 | @MethodSource("provideTargetSignalBehaviorTestCases") |
461 | 542 | void testTargetSignalsBehavior(TargetSignalBehavior testCase) { |
@@ -665,6 +746,72 @@ private static Stream<Arguments> provideTargetSignalBehaviorTestCases() { |
665 | 746 | .build())); |
666 | 747 | } |
667 | 748 |
|
| 749 | + /** |
| 750 | + * Test cases specifying expected value for the project ID in the resource given the user input |
| 751 | + * and the current credentials state. |
| 752 | + * |
| 753 | + * <p>{@code null} for {@link ProjectIdTestBehavior#getUserSpecifiedProjectId()} indicates the |
| 754 | + * case of user not specifying the project ID. |
| 755 | + * |
| 756 | + * <p>{@code null} value for {@link ProjectIdTestBehavior#getCredentialsProjectId()} indicates |
| 757 | + * that the mocked credentials are not providing a project ID. |
| 758 | + * |
| 759 | + * <p>{@code true} for {@link ProjectIdTestBehavior#getExpectedToThrow()} indicates the |
| 760 | + * expectation that an exception should be thrown. |
| 761 | + */ |
| 762 | + private static Stream<Arguments> provideProjectIdBehaviorTestCases() { |
| 763 | + return Stream.of( |
| 764 | + // User specified project ID takes precedence |
| 765 | + Arguments.of( |
| 766 | + ProjectIdTestBehavior.builder() |
| 767 | + .setUserSpecifiedProjectId(DUMMY_GCP_RESOURCE_PROJECT_ID) |
| 768 | + .setCredentialsProjectId("credentials-project-id") |
| 769 | + .setExpectedProjectIdInResource(DUMMY_GCP_RESOURCE_PROJECT_ID) |
| 770 | + .setExpectedToThrow(false) |
| 771 | + .build()), |
| 772 | + // If user specified project ID is empty, fallback to credentials.getProjectId() |
| 773 | + Arguments.of( |
| 774 | + ProjectIdTestBehavior.builder() |
| 775 | + .setUserSpecifiedProjectId("") |
| 776 | + .setCredentialsProjectId("credentials-project-id") |
| 777 | + .setExpectedProjectIdInResource("credentials-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 | + // If user specified project ID is empty and credentials.getProjectId() returns null, throw |
| 797 | + // exception |
| 798 | + Arguments.of( |
| 799 | + ProjectIdTestBehavior.builder() |
| 800 | + .setUserSpecifiedProjectId("") |
| 801 | + .setCredentialsProjectId(null) |
| 802 | + .setExpectedProjectIdInResource(null) |
| 803 | + .setExpectedToThrow(true) |
| 804 | + .build()), |
| 805 | + // If user specifies empty and credentials returns empty (edge case), throw exception |
| 806 | + Arguments.of( |
| 807 | + ProjectIdTestBehavior.builder() |
| 808 | + .setUserSpecifiedProjectId("") |
| 809 | + .setCredentialsProjectId("") |
| 810 | + .setExpectedProjectIdInResource(null) |
| 811 | + .setExpectedToThrow(true) |
| 812 | + .build())); |
| 813 | + } |
| 814 | + |
668 | 815 | /** |
669 | 816 | * Test cases specifying expected value for the user quota project header given the user input and |
670 | 817 | * the current credentials state. |
@@ -839,6 +986,42 @@ private static void configureGrpcMockMetricExporter( |
839 | 986 | .thenReturn(MemoryMode.IMMUTABLE_DATA); |
840 | 987 | } |
841 | 988 |
|
| 989 | + @AutoValue |
| 990 | + abstract static class ProjectIdTestBehavior { |
| 991 | + // A null user specified project ID represents the use case where user omits specifying it |
| 992 | + @Nullable |
| 993 | + abstract String getUserSpecifiedProjectId(); |
| 994 | + |
| 995 | + // The project ID that credentials.getProjectId() returns (can be null) |
| 996 | + @Nullable |
| 997 | + abstract String getCredentialsProjectId(); |
| 998 | + |
| 999 | + // The expected project ID in the resource attributes (null if exception expected) |
| 1000 | + @Nullable |
| 1001 | + abstract String getExpectedProjectIdInResource(); |
| 1002 | + |
| 1003 | + // Whether an exception is expected to be thrown |
| 1004 | + abstract boolean getExpectedToThrow(); |
| 1005 | + |
| 1006 | + static Builder builder() { |
| 1007 | + return new AutoValue_GcpAuthAutoConfigurationCustomizerProviderTest_ProjectIdTestBehavior |
| 1008 | + .Builder(); |
| 1009 | + } |
| 1010 | + |
| 1011 | + @AutoValue.Builder |
| 1012 | + abstract static class Builder { |
| 1013 | + abstract Builder setUserSpecifiedProjectId(String projectId); |
| 1014 | + |
| 1015 | + abstract Builder setCredentialsProjectId(String projectId); |
| 1016 | + |
| 1017 | + abstract Builder setExpectedProjectIdInResource(String projectId); |
| 1018 | + |
| 1019 | + abstract Builder setExpectedToThrow(boolean expectedToThrow); |
| 1020 | + |
| 1021 | + abstract ProjectIdTestBehavior build(); |
| 1022 | + } |
| 1023 | + } |
| 1024 | + |
842 | 1025 | @AutoValue |
843 | 1026 | abstract static class QuotaProjectIdTestBehavior { |
844 | 1027 | // A null user specified quota represents the use case where user omits specifying quota |
|
0 commit comments