diff --git a/.github/workflows/post-commit-pipeline.yml b/.github/workflows/post-commit-pipeline.yml index 5ea97eb..00b736b 100644 --- a/.github/workflows/post-commit-pipeline.yml +++ b/.github/workflows/post-commit-pipeline.yml @@ -13,8 +13,7 @@ jobs: build_test_release: runs-on: ubuntu-22.04 env: - SPRING_PROFILES_ACTIVE: prod - BP_SPRING_PROFILES_ACTIVE: prod + SPRING_PROFILES_ACTIVE: local outputs: new-version: ${{ (steps.version_increment.outputs.bump != 'none' && steps.calculate_version.outputs.new_version) || '' }} steps: diff --git a/.github/workflows/pull-request-pipeline.yml b/.github/workflows/pull-request-pipeline.yml index 11f9959..c3a02a7 100644 --- a/.github/workflows/pull-request-pipeline.yml +++ b/.github/workflows/pull-request-pipeline.yml @@ -8,8 +8,6 @@ on: jobs: build-and-test-pipeline: runs-on: ubuntu-22.04 - env: - SPRING_PROFILES_ACTIVE: local steps: - name: Check out the repository uses: actions/checkout@v4 diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/alerts/InMemoryAlertsReadModelRepository.java b/src/main/java/io/autoinvestor/infrastructure/read_models/alerts/InMemoryAlertsReadModelRepository.java index 5f435c6..fb828e4 100644 --- a/src/main/java/io/autoinvestor/infrastructure/read_models/alerts/InMemoryAlertsReadModelRepository.java +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/alerts/InMemoryAlertsReadModelRepository.java @@ -46,4 +46,8 @@ public List get(String userId) { .filter(alert -> alert.userId().equals(userId)) .collect(Collectors.toList()); } + + public void clear() { + alerts.clear(); + } } diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/alerts/MongoAlertsReadModelRepository.java b/src/main/java/io/autoinvestor/infrastructure/read_models/alerts/MongoAlertsReadModelRepository.java index b9183d3..2202c52 100644 --- a/src/main/java/io/autoinvestor/infrastructure/read_models/alerts/MongoAlertsReadModelRepository.java +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/alerts/MongoAlertsReadModelRepository.java @@ -2,6 +2,7 @@ import io.autoinvestor.application.AlertDTO; import io.autoinvestor.application.AlertsReadModelRepository; +import lombok.extern.slf4j.Slf4j; import java.util.List; @@ -13,6 +14,7 @@ @Repository @Profile("prod") +@Slf4j public class MongoAlertsReadModelRepository implements AlertsReadModelRepository { private static final String COLLECTION = "alerts"; @@ -23,11 +25,23 @@ public class MongoAlertsReadModelRepository implements AlertsReadModelRepository public MongoAlertsReadModelRepository(MongoTemplate template, DecisionMapper mapper) { this.template = template; this.mapper = mapper; + log.info("MongoAlertsReadModelRepository initialized."); } @Override public void save(AlertDTO alertDTO) { - template.save(mapper.toDocument(alertDTO), COLLECTION); + try { + template.save(mapper.toDocument(alertDTO), COLLECTION); + log.info("Saved AlertDTO for userId={}", alertDTO.userId()); + } catch (Exception ex) { + log.error( + "Failed to save AlertDTO[userId={}, assetId={}]: {}", + alertDTO.userId(), + alertDTO.assetId(), + ex.getMessage(), + ex); + throw ex; + } } @Override diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/users/InMemoryInboxReadModelRepository.java b/src/main/java/io/autoinvestor/infrastructure/read_models/users/InMemoryInboxReadModelRepository.java index 394aa72..3a51415 100644 --- a/src/main/java/io/autoinvestor/infrastructure/read_models/users/InMemoryInboxReadModelRepository.java +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/users/InMemoryInboxReadModelRepository.java @@ -32,4 +32,8 @@ public Optional getInboxId(UserId userId) { String raw = inbox.get(userId.value()); return raw != null ? Optional.of(InboxId.from(raw)) : Optional.empty(); } + + public void clear() { + inbox.clear(); + } } diff --git a/src/main/java/io/autoinvestor/infrastructure/read_models/users/MongoInboxReadModelRepository.java b/src/main/java/io/autoinvestor/infrastructure/read_models/users/MongoInboxReadModelRepository.java index 979a564..9296208 100644 --- a/src/main/java/io/autoinvestor/infrastructure/read_models/users/MongoInboxReadModelRepository.java +++ b/src/main/java/io/autoinvestor/infrastructure/read_models/users/MongoInboxReadModelRepository.java @@ -3,6 +3,7 @@ import io.autoinvestor.application.InboxReadModelRepository; import io.autoinvestor.domain.model.InboxId; import io.autoinvestor.domain.model.UserId; +import lombok.extern.slf4j.Slf4j; import java.util.Optional; @@ -12,29 +13,55 @@ @Repository @Profile("prod") +@Slf4j public class MongoInboxReadModelRepository implements InboxReadModelRepository { private final MongoTemplate template; public MongoInboxReadModelRepository(MongoTemplate template) { this.template = template; + log.info("MongoInboxReadModelRepository initialized."); } @Override public void save(UserId userId, InboxId inboxId) { - String userIdStr = userId.value(); - String inboxIdStr = inboxId.value(); - DecisionDocument doc = new DecisionDocument(userIdStr, inboxIdStr); - template.save(doc); + DecisionDocument doc = new DecisionDocument(userId.value(), inboxId.value()); + try { + template.save(doc); + log.info( + "Saved DecisionDocument[userId={} -> inboxId={}]", + userId.value(), + inboxId.value()); + } catch (Exception ex) { + log.error( + "Failed to save DecisionDocument[userId={}]: {}", + userId.value(), + ex.getMessage(), + ex); + throw ex; + } } @Override public Optional getInboxId(UserId userId) { String userIdStr = userId.value(); - DecisionDocument doc = template.findById(userIdStr, DecisionDocument.class); + DecisionDocument doc; + try { + doc = template.findById(userIdStr, DecisionDocument.class); + } catch (Exception ex) { + log.error( + "Error fetching DecisionDocument for userId={}: {}", + userIdStr, + ex.getMessage(), + ex); + throw ex; + } + if (doc == null) { + log.warn("No DecisionDocument found for userId={}", userIdStr); return Optional.empty(); } + return Optional.of(InboxId.from(doc.getInboxId())); } } diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/event_store/MongoEventStoreRepository.java b/src/main/java/io/autoinvestor/infrastructure/repositories/event_store/MongoEventStoreRepository.java index 78c87b7..3d02e2d 100644 --- a/src/main/java/io/autoinvestor/infrastructure/repositories/event_store/MongoEventStoreRepository.java +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/event_store/MongoEventStoreRepository.java @@ -4,6 +4,7 @@ import io.autoinvestor.domain.events.EventStoreRepository; import io.autoinvestor.domain.model.Inbox; import io.autoinvestor.domain.model.InboxId; +import lombok.extern.slf4j.Slf4j; import java.util.List; import java.util.Optional; @@ -18,6 +19,7 @@ @Repository @Profile("prod") +@Slf4j public class MongoEventStoreRepository implements EventStoreRepository { private static final String COLLECTION = "events"; @@ -27,15 +29,31 @@ public class MongoEventStoreRepository implements EventStoreRepository { public MongoEventStoreRepository(MongoTemplate template, EventMapper mapper) { this.template = template; this.mapper = mapper; + log.info("MongoEventStoreRepository initialized."); } @Override public void save(Inbox inbox) { - List docs = - inbox.getUncommittedEvents().stream() - .map(mapper::toDocument) - .collect(Collectors.toList()); - template.insertAll(docs); + List> uncommitted = inbox.getUncommittedEvents(); + if (uncommitted.isEmpty()) { + log.debug( + "No uncommitted events to save for inboxId={}", + inbox.getState().getInboxId().value()); + return; + } + + try { + List docs = uncommitted.stream().map(mapper::toDocument).toList(); + template.insertAll(docs); + log.info("Inserted {} event(s) into '{}'", docs.size(), COLLECTION); + } catch (Exception ex) { + log.error( + "Failed to insert events for inboxId={}: {}", + inbox.getState().getInboxId().value(), + ex.getMessage(), + ex); + throw ex; + } } @Override @@ -43,16 +61,32 @@ public Optional get(InboxId inboxId) { Query q = Query.query(Criteria.where("aggregateId").is(inboxId.value())) .with(Sort.by("version")); + log.debug("Querying '{}' for aggregateId={}", COLLECTION, inboxId.value()); - List docs = template.find(q, EventDocument.class, COLLECTION); + List docs; + try { + docs = template.find(q, EventDocument.class, COLLECTION); + } catch (Exception ex) { + log.error( + "Error querying events for inboxId={}: {}", + inboxId.value(), + ex.getMessage(), + ex); + throw ex; + } if (docs.isEmpty()) { + log.warn("No EventDocument found for inboxId={}", inboxId.value()); return Optional.empty(); } List> events = docs.stream().map(mapper::toDomain).collect(Collectors.toList()); if (events.isEmpty()) { + log.warn( + "Mapped {} EventDocument(s) but got 0 domain events for inboxId={}", + docs.size(), + inboxId.value()); return Optional.empty(); } @@ -61,8 +95,21 @@ public Optional get(InboxId inboxId) { @Override public boolean exists(InboxId inboxId) { - Query q = Query.query(Criteria.where("aggregateId").is(inboxId.value())); - - return template.exists(q, EventDocument.class, COLLECTION); + try { + boolean exists = + template.exists( + Query.query(Criteria.where("aggregateId").is(inboxId.value())), + EventDocument.class, + COLLECTION); + log.debug("exists(inboxId={}) = {}", inboxId.value(), exists); + return exists; + } catch (Exception ex) { + log.error( + "Error checking existence of events for inboxId={}: {}", + inboxId.value(), + ex.getMessage(), + ex); + throw ex; + } } } diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/portfolio/InMemoryPortfolioRepository.java b/src/main/java/io/autoinvestor/infrastructure/repositories/portfolio/InMemoryPortfolioRepository.java index 3c4715c..2a47435 100644 --- a/src/main/java/io/autoinvestor/infrastructure/repositories/portfolio/InMemoryPortfolioRepository.java +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/portfolio/InMemoryPortfolioRepository.java @@ -48,4 +48,9 @@ public boolean existsPortfolioAsset(String userId, String assetId) { .map(set -> set.contains(assetId)) .orElse(false); } + + public void clear() { + users.clear(); + portfolio.clear(); + } } diff --git a/src/main/java/io/autoinvestor/infrastructure/repositories/portfolio/MongoPortfolioRepository.java b/src/main/java/io/autoinvestor/infrastructure/repositories/portfolio/MongoPortfolioRepository.java index ddf2107..6d3fd01 100644 --- a/src/main/java/io/autoinvestor/infrastructure/repositories/portfolio/MongoPortfolioRepository.java +++ b/src/main/java/io/autoinvestor/infrastructure/repositories/portfolio/MongoPortfolioRepository.java @@ -6,6 +6,7 @@ import io.autoinvestor.domain.model.UserId; import lombok.Getter; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; import java.util.List; import java.util.stream.Collectors; @@ -20,6 +21,7 @@ @Repository @Profile("prod") +@Slf4j public class MongoPortfolioRepository implements PortfolioRepository { public static final String PORTFOLIO_COLLECTION = "portfolio"; public static final String USERS_COLLECTION = "users"; @@ -28,6 +30,7 @@ public class MongoPortfolioRepository implements PortfolioRepository { public MongoPortfolioRepository(MongoTemplate template) { this.template = template; + log.info("MongoPortfolioRepository initialized."); } @Override @@ -43,9 +46,22 @@ public List getUsersIdByAssetAndRiskLevel(String assetId, int riskLevel) AggregationResults results = template.aggregate(agg, PORTFOLIO_COLLECTION, IdProjection.class); - return results.getMappedResults().stream() - .map(p -> UserId.from(p.getUserId())) - .collect(Collectors.toList()); + List raw = results.getMappedResults(); + + if (raw.isEmpty()) { + log.warn("No users found for assetId='{}' with riskLevel={}", assetId, riskLevel); + return List.of(); + } + + List userIds = + raw.stream().map(p -> UserId.from(p.getUserId())).collect(Collectors.toList()); + + log.info( + "Found {} user(s) for assetId='{}', riskLevel={}", + userIds.size(), + assetId, + riskLevel); + return userIds; } @Setter @@ -56,12 +72,34 @@ private static class IdProjection { @Override public void addUser(String userId, int riskLevel) { - template.save(new UserDocument(null, userId, riskLevel)); + try { + template.save(new UserDocument(null, userId, riskLevel)); + log.info("Saved UserDocument[userId={}]", userId); + } catch (Exception ex) { + log.error( + "Failed to save UserDocument[userId={}, riskLevel={}]: {}", + userId, + riskLevel, + ex.getMessage(), + ex); + throw ex; + } } @Override public void addPortfolioAsset(String userId, String assetId) { - template.save(new PortfolioDocument(null, userId, assetId)); + try { + template.save(new PortfolioDocument(null, userId, assetId)); + log.info("Saved PortfolioDocument[userId={}, assetId={}]", userId, assetId); + } catch (Exception ex) { + log.error( + "Failed to save PortfolioDocument[userId={}, assetId={}]: {}", + userId, + assetId, + ex.getMessage(), + ex); + throw ex; + } } @Override diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index 1ed44a7..ef1eefe 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -1,7 +1,5 @@ spring.autoconfigure.exclude=\ - org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\ - org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration - -spring.autoconfigure.exclude+=,\ - com.google.cloud.spring.autoconfigure.core.GcpContextAutoConfiguration,\ - com.google.cloud.spring.autoconfigure.pubsub.GcpPubSubAutoConfiguration \ No newline at end of file + org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\ + org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\ + com.google.cloud.spring.autoconfigure.core.GcpContextAutoConfiguration,\ + com.google.cloud.spring.autoconfigure.pubsub.GcpPubSubAutoConfiguration diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 283e1d0..3aa94d1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,2 @@ spring.application.name=alerts -spring.profiles.active=local +spring.profiles.active=prod diff --git a/src/test/java/io/autoinvestor/AlertsIntegrationTest.java b/src/test/java/io/autoinvestor/AlertsIntegrationTest.java new file mode 100644 index 0000000..35dde3e --- /dev/null +++ b/src/test/java/io/autoinvestor/AlertsIntegrationTest.java @@ -0,0 +1,197 @@ +package io.autoinvestor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import io.autoinvestor.application.*; +import io.autoinvestor.domain.model.Decision; +import io.autoinvestor.domain.model.UserId; +import io.autoinvestor.infrastructure.read_models.alerts.InMemoryAlertsReadModelRepository; +import io.autoinvestor.infrastructure.read_models.users.InMemoryInboxReadModelRepository; +import io.autoinvestor.infrastructure.repositories.portfolio.InMemoryPortfolioRepository; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("local") +class AlertsIntegrationTest { + + @Autowired private RegisterUserCommandHandler registerUserHandler; + + @Autowired private RegisterPortfolioAssetCommandHandler registerPortfolioAssetHandler; + + @Autowired private EmitAlertsCommandHandler emitAlertsHandler; + + @Autowired private GetAlertsQueryHandler getAlertsQueryHandler; + + @Autowired private InMemoryPortfolioRepository portfolioRepository; + + @Autowired private InMemoryInboxReadModelRepository inboxReadModel; + + @Autowired private InMemoryAlertsReadModelRepository alertsReadModel; + + @Autowired private MockMvc mockMvc; + + @BeforeEach + void resetState() { + inboxReadModel.clear(); + alertsReadModel.clear(); + portfolioRepository.clear(); + } + + @Test + void registerUserHandler_shouldCreateInboxAndPortfolioUser() { + // GIVEN: a brand-new userId that does not exist yet + String userId = "user-1"; + int riskLevel = 10; + + // PRECONDITION: inboxReadModel has no mapping for "user-1" + assertThat(inboxReadModel.getInboxId(UserId.from(userId))).isEmpty(); + + // WHEN: we invoke the RegisterUserCommandHandler + RegisterUserCommand cmd = new RegisterUserCommand(userId, riskLevel); + registerUserHandler.handle(cmd); + + // THEN: the InMemoryInboxReadModelRepository should now contain an inboxId for "user-1" + assertThat(inboxReadModel.getInboxId(UserId.from(userId))).isPresent(); + + // AND: the InMemoryPortfolioRepository should have registered the user internally + // We know that existsPortfolioAsset returns false for any asset because none added yet, + // so simply verify that getUsersIdByAssetAndRiskLevel yields an empty list for an arbitrary + // asset. + List usersForBtcRisk10 = + portfolioRepository.getUsersIdByAssetAndRiskLevel("BTC", 10); + assertThat(usersForBtcRisk10).isEmpty(); + } + + @Test + void registerPortfolioAssetHandler_shouldAttachAssetToExistingUser() { + String userId = "user-2"; + int riskLevel = 5; + String assetId = "AAPL"; + + // First, register the user so that an inbox exists and portfolioRepository has that user + registerUserHandler.handle(new RegisterUserCommand(userId, riskLevel)); + + // PRECONDITION: this user does not yet have "AAPL" in their portfolio + boolean before = portfolioRepository.existsPortfolioAsset(userId, assetId); + assertThat(before).isFalse(); + + // WHEN: we add the portfolio asset + RegisterPortfolioAssetCommand addCmd = new RegisterPortfolioAssetCommand(userId, assetId); + registerPortfolioAssetHandler.handle(addCmd); + + // THEN: the portfolioRepository should reflect that "user-2" now has AAPL + boolean after = portfolioRepository.existsPortfolioAsset(userId, assetId); + assertThat(after).isTrue(); + + // AND: getUsersIdByAssetAndRiskLevel("AAPL", 5) should return exactly user-2 + List matched = + portfolioRepository.getUsersIdByAssetAndRiskLevel(assetId, riskLevel); + assertThat(matched).hasSize(1).allMatch(uid -> uid.value().equals(userId)); + } + + @Test + void emitAlertsHandler_shouldCreateAndStoreAlertForMatchingUsers() { + String userId = "user-3"; + int riskLevel = 3; + String assetId = "GOOG"; + Decision decision = Decision.BUY; + + // 1) register the user: + registerUserHandler.handle(new RegisterUserCommand(userId, riskLevel)); + + // 2) attach "GOOG" to user-3's portfolio: + registerPortfolioAssetHandler.handle(new RegisterPortfolioAssetCommand(userId, assetId)); + + // PRECONDITION: alertsReadModel.get(userId) is empty + List beforeAlerts = alertsReadModel.get(userId); + assertThat(beforeAlerts).isEmpty(); + + // WHEN: we emit an alert for asset "GOOG" at riskLevel=3 with decision=BUY + EmitAlertsCommand emitCmd = new EmitAlertsCommand(assetId, decision.name(), riskLevel); + emitAlertsHandler.handle(emitCmd); + + // THEN: alertsReadModel.get(userId) should contain exactly one AlertDTO + List afterAlerts = alertsReadModel.get(userId); + assertThat(afterAlerts).hasSize(1); + + AlertDTO alertDto = afterAlerts.getFirst(); + assertThat(alertDto.userId()).isEqualTo(userId); + assertThat(alertDto.assetId()).isEqualTo(assetId); + assertThat(alertDto.type()).isEqualTo(decision.name()); + assertThat(alertDto.date()).isNotNull(); + } + + @Test + void getAlertsQueryHandler_shouldReturnAllSavedAlertsForUser() { + String userId = "user-4"; + int riskLevel = 2; + String assetId1 = "MSFT"; + String assetId2 = "TSLA"; + Decision decision1 = Decision.SELL; + Decision decision2 = Decision.HOLD; + + // Setup: register user, attach two assets, emit two alerts + registerUserHandler.handle(new RegisterUserCommand(userId, riskLevel)); + registerPortfolioAssetHandler.handle(new RegisterPortfolioAssetCommand(userId, assetId1)); + registerPortfolioAssetHandler.handle(new RegisterPortfolioAssetCommand(userId, assetId2)); + + // Emit first alert (MSFT, SELL) + emitAlertsHandler.handle(new EmitAlertsCommand(assetId1, decision1.name(), riskLevel)); + + // Emit second alert (TSLA, HOLD) + emitAlertsHandler.handle(new EmitAlertsCommand(assetId2, decision2.name(), riskLevel)); + + // WHEN: we query via GetAlertsQueryHandler + GetDecisionsQuery query = new GetDecisionsQuery(userId); + List responses = getAlertsQueryHandler.handle(query); + + // THEN: We should see exactly two entries, one for MSFT and one for TSLA (in insertion + // order) + assertThat(responses).hasSize(2); + + // Order is not strictly guaranteed, so verify that the set of (assetId, type) matches + assertThat(responses) + .extracting(GetAlertsQueryResponse::assetId, GetAlertsQueryResponse::type) + .containsExactlyInAnyOrder( + org.assertj.core.groups.Tuple.tuple(assetId1, decision1.name()), + org.assertj.core.groups.Tuple.tuple(assetId2, decision2.name())); + } + + @Test + void getAlertsController_endpointShouldReturnJsonListOfAlerts() throws Exception { + String userId = "user-5"; + int riskLevel = 7; + String assetId = "NFLX"; + Decision decision = Decision.SELL; + + // 1) Register user & portfolio & emit one alert (same setup as above) + registerUserHandler.handle(new RegisterUserCommand(userId, riskLevel)); + registerPortfolioAssetHandler.handle(new RegisterPortfolioAssetCommand(userId, assetId)); + emitAlertsHandler.handle(new EmitAlertsCommand(assetId, decision.name(), riskLevel)); + + // 2) Perform an HTTP GET against /alerts with X-User-Id=user-5 + mockMvc.perform( + get("/alerts") + .header("X-User-Id", userId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + // Expect a JSON array of size 1 + .andExpect(jsonPath("$.length()").value(1)) + // The first (and only) element should have "assetId": "NFLX" and "type": "SELL" + .andExpect(jsonPath("$[0].assetId").value(assetId)) + .andExpect(jsonPath("$[0].type").value(decision.name())); + } +} diff --git a/src/test/resources/application-local.properties b/src/test/resources/application-local.properties new file mode 100644 index 0000000..ef1eefe --- /dev/null +++ b/src/test/resources/application-local.properties @@ -0,0 +1,5 @@ +spring.autoconfigure.exclude=\ + org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\ + org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\ + com.google.cloud.spring.autoconfigure.core.GcpContextAutoConfiguration,\ + com.google.cloud.spring.autoconfigure.pubsub.GcpPubSubAutoConfiguration