From 003e36fb1f8a7b6aab15cc57d38fc20d1ad21b53 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Tue, 12 Aug 2025 22:23:16 +0300 Subject: [PATCH 01/21] fix for #81 --- .../osl/tmf/pcm620/api/HubApiController.java | 51 +++- .../repo/EventSubscriptionRepository.java | 32 +++ .../EventSubscriptionRepoService.java | 65 +++++ .../osl/tmf/pim637/api/HubApiController.java | 4 + .../api/pcm620/HubApiControllerTest.java | 245 ++++++++++++++++++ .../testPCM620EventSubscriptionInput.json | 4 + 6 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/repo/EventSubscriptionRepository.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java create mode 100644 src/test/resources/testPCM620EventSubscriptionInput.json diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java index 55177431..b15db916 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java @@ -22,21 +22,38 @@ package org.etsi.osl.tmf.pcm620.api; import java.util.Optional; import com.fasterxml.jackson.databind.ObjectMapper; - +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import io.swagger.v3.oas.annotations.Parameter; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; @jakarta.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2019-10-19T00:15:57.249+03:00") @Controller("HubApiController620") @RequestMapping("/productCatalogManagement/v4/") public class HubApiController implements HubApi { + private static final Logger log = LoggerFactory.getLogger(HubApiController.class); + private final ObjectMapper objectMapper; private final HttpServletRequest request; + @Autowired + EventSubscriptionRepoService eventSubscriptionRepoService; + @org.springframework.beans.factory.annotation.Autowired public HubApiController(ObjectMapper objectMapper, HttpServletRequest request) { this.objectMapper = objectMapper; @@ -53,4 +70,36 @@ public class HubApiController implements HubApi { return Optional.ofNullable(request); } + @Override + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + public ResponseEntity registerListener(@Parameter(description = "Data containing the callback endpoint to deliver the information", required = true) @Valid @RequestBody EventSubscriptionInput data) { + try { + EventSubscription eventSubscription = eventSubscriptionRepoService.addEventSubscription(data); + return new ResponseEntity<>(eventSubscription, HttpStatus.CREATED); + } catch (IllegalArgumentException e) { + log.error("Invalid input for listener registration: {}", e.getMessage()); + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } catch (Exception e) { + log.error("Error registering listener", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + public ResponseEntity unregisterListener(@Parameter(description = "The id of the registered listener", required = true) @PathVariable("id") String id) { + try { + EventSubscription existing = eventSubscriptionRepoService.findById(id); + if (existing == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + eventSubscriptionRepoService.deleteById(id); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } catch (Exception e) { + log.error("Error unregistering listener with id: " + id, e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/repo/EventSubscriptionRepository.java b/src/main/java/org/etsi/osl/tmf/pcm620/repo/EventSubscriptionRepository.java new file mode 100644 index 00000000..117892dc --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/repo/EventSubscriptionRepository.java @@ -0,0 +1,32 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.repo; + +import java.util.Optional; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface EventSubscriptionRepository extends CrudRepository, PagingAndSortingRepository { + + Optional findById(String id); +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java new file mode 100644 index 00000000..8fc9caad --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java @@ -0,0 +1,65 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.util.Optional; +import java.util.UUID; + +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput; +import org.etsi.osl.tmf.pcm620.repo.EventSubscriptionRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.validation.Valid; + +@Service +@Transactional +public class EventSubscriptionRepoService { + + @Autowired + EventSubscriptionRepository eventSubscriptionRepo; + + public EventSubscription addEventSubscription(@Valid EventSubscriptionInput eventSubscriptionInput) { + if (eventSubscriptionInput.getCallback() == null || eventSubscriptionInput.getCallback().trim().isEmpty()) { + throw new IllegalArgumentException("Callback URL is required and cannot be empty"); + } + + EventSubscription eventSubscription = new EventSubscription(); + eventSubscription.setId(UUID.randomUUID().toString()); + eventSubscription.setCallback(eventSubscriptionInput.getCallback()); + eventSubscription.setQuery(eventSubscriptionInput.getQuery()); + + return this.eventSubscriptionRepo.save(eventSubscription); + } + + public EventSubscription findById(String id) { + Optional optionalEventSubscription = this.eventSubscriptionRepo.findById(id); + return optionalEventSubscription.orElse(null); + } + + public void deleteById(String id) { + Optional optionalEventSubscription = this.eventSubscriptionRepo.findById(id); + if (optionalEventSubscription.isPresent()) { + this.eventSubscriptionRepo.delete(optionalEventSubscription.get()); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pim637/api/HubApiController.java b/src/main/java/org/etsi/osl/tmf/pim637/api/HubApiController.java index 96129c8d..0b1b6f45 100644 --- a/src/main/java/org/etsi/osl/tmf/pim637/api/HubApiController.java +++ b/src/main/java/org/etsi/osl/tmf/pim637/api/HubApiController.java @@ -1,6 +1,10 @@ package org.etsi.osl.tmf.pim637.api; import java.io.IOException; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + import jakarta.servlet.http.HttpServletRequest; import com.fasterxml.jackson.databind.ObjectMapper; import org.etsi.osl.tmf.pim637.model.EventSubscription; diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java new file mode 100644 index 00000000..072e514f --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java @@ -0,0 +1,245 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; + +import org.apache.commons.io.IOUtils; +import org.etsi.osl.tmf.JsonUtils; +import org.etsi.osl.tmf.OpenAPISpringBoot; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@RunWith(SpringRunner.class) +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class) +@AutoConfigureMockMvc +@ActiveProfiles("testing") +@AutoConfigureTestDatabase +public class HubApiControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Autowired + private ObjectMapper objectMapper; + + @Before + public void setup() { + mvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @Test + public void testRegisterListener() throws Exception { + File resourceSpecFile = new File("src/test/resources/testPCM620EventSubscriptionInput.json"); + InputStream in = new FileInputStream(resourceSpecFile); + String eventSubscriptionInputString = IOUtils.toString(in, "UTF-8"); + EventSubscriptionInput eventSubscriptionInput = JsonUtils.toJsonObj(eventSubscriptionInputString, EventSubscriptionInput.class); + + MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(eventSubscriptionInput))) + .andExpect(status().isCreated()) + .andExpect(content().contentType("application/json;charset=utf-8")) + .andExpect(jsonPath("$.callback").value("http://localhost:8080/callback")) + .andExpect(jsonPath("$.query").value("productOffering.create,productOffering.delete")) + .andExpect(jsonPath("$.id").exists()) + .andReturn(); + + String responseBody = result.getResponse().getContentAsString(); + EventSubscription createdSubscription = objectMapper.readValue(responseBody, EventSubscription.class); + + // Verify the subscription was actually saved to the database + EventSubscription retrievedSubscription = eventSubscriptionRepoService.findById(createdSubscription.getId()); + assert retrievedSubscription != null; + assert retrievedSubscription.getCallback().equals("http://localhost:8080/callback"); + assert retrievedSubscription.getQuery().equals("productOffering.create,productOffering.delete"); + } + + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @Test + public void testRegisterListenerWithoutAcceptHeader() throws Exception { + File resourceSpecFile = new File("src/test/resources/testPCM620EventSubscriptionInput.json"); + InputStream in = new FileInputStream(resourceSpecFile); + String eventSubscriptionInputString = IOUtils.toString(in, "UTF-8"); + EventSubscriptionInput eventSubscriptionInput = JsonUtils.toJsonObj(eventSubscriptionInputString, EventSubscriptionInput.class); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(eventSubscriptionInput))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.callback").value("http://localhost:8080/callback")) + .andExpect(jsonPath("$.query").value("productOffering.create,productOffering.delete")) + .andExpect(jsonPath("$.id").exists()); + } + + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @Test + public void testUnregisterListener() throws Exception { + // First, create a subscription + EventSubscriptionInput input = new EventSubscriptionInput(); + input.setCallback("http://localhost:8080/callback"); + input.setQuery("test.event"); + + EventSubscription created = eventSubscriptionRepoService.addEventSubscription(input); + String subscriptionId = created.getId(); + + // Verify the subscription exists + assert eventSubscriptionRepoService.findById(subscriptionId) != null; + + // Delete the subscription + mvc.perform(MockMvcRequestBuilders.delete("/productCatalogManagement/v4/hub/" + subscriptionId) + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // Verify the subscription was deleted + assert eventSubscriptionRepoService.findById(subscriptionId) == null; + } + + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @Test + public void testUnregisterNonExistentListener() throws Exception { + String nonExistentId = "non-existent-id"; + + mvc.perform(MockMvcRequestBuilders.delete("/productCatalogManagement/v4/hub/" + nonExistentId) + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @Test + public void testRegisterListenerWithInvalidData() throws Exception { + // Test with missing required callback field + EventSubscriptionInput invalidInput = new EventSubscriptionInput(); + invalidInput.setQuery("test.event"); + // callback is missing, which is required + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(invalidInput))) + .andExpect(status().isBadRequest()); + } + + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @Test + public void testRegisterListenerWithEmptyCallback() throws Exception { + EventSubscriptionInput invalidInput = new EventSubscriptionInput(); + invalidInput.setCallback(""); + invalidInput.setQuery("test.event"); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(invalidInput))) + .andExpect(status().isBadRequest()); + } + + @WithMockUser(username = "user", roles = {"USER"}) + @Test + public void testRegisterListenerUnauthorized() throws Exception { + File resourceSpecFile = new File("src/test/resources/testPCM620EventSubscriptionInput.json"); + InputStream in = new FileInputStream(resourceSpecFile); + String eventSubscriptionInputString = IOUtils.toString(in, "UTF-8"); + EventSubscriptionInput eventSubscriptionInput = JsonUtils.toJsonObj(eventSubscriptionInputString, EventSubscriptionInput.class); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(eventSubscriptionInput))) + .andExpect(status().isForbidden()); + } + + @WithMockUser(username = "user", roles = {"USER"}) + @Test + public void testUnregisterListenerUnauthorized() throws Exception { + // First create a subscription as admin + EventSubscriptionInput input = new EventSubscriptionInput(); + input.setCallback("http://localhost:8080/callback"); + input.setQuery("test.event"); + EventSubscription created = eventSubscriptionRepoService.addEventSubscription(input); + String subscriptionId = created.getId(); + + // Try to delete as regular user (should be forbidden) + mvc.perform(MockMvcRequestBuilders.delete("/productCatalogManagement/v4/hub/" + subscriptionId) + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + + // Verify the subscription still exists + assert eventSubscriptionRepoService.findById(subscriptionId) != null; + } + + @Test + public void testRegisterListenerUnauthenticated() throws Exception { + File resourceSpecFile = new File("src/test/resources/testPCM620EventSubscriptionInput.json"); + InputStream in = new FileInputStream(resourceSpecFile); + String eventSubscriptionInputString = IOUtils.toString(in, "UTF-8"); + EventSubscriptionInput eventSubscriptionInput = JsonUtils.toJsonObj(eventSubscriptionInputString, EventSubscriptionInput.class); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(eventSubscriptionInput))) + .andExpect(status().isUnauthorized()); + } + + @Test + public void testUnregisterListenerUnauthenticated() throws Exception { + String testId = "test-subscription-id"; + + mvc.perform(MockMvcRequestBuilders.delete("/productCatalogManagement/v4/hub/" + testId) + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()); + } +} \ No newline at end of file diff --git a/src/test/resources/testPCM620EventSubscriptionInput.json b/src/test/resources/testPCM620EventSubscriptionInput.json new file mode 100644 index 00000000..e6aa9f08 --- /dev/null +++ b/src/test/resources/testPCM620EventSubscriptionInput.json @@ -0,0 +1,4 @@ +{ + "callback": "http://localhost:8080/callback", + "query": "productOffering.create,productOffering.delete" +} \ No newline at end of file -- GitLab From 2355c086d53417ba06a3c814bc01454ea90fd606 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 00:23:18 +0300 Subject: [PATCH 02/21] added notifications and callbacks for catalog create and delete --- .../ProductCatalogApiRouteBuilderEvents.java | 114 ++++++++++++ .../configuration/RestTemplateConfig.java | 39 ++++ .../reposervices/CatalogCallbackService.java | 158 ++++++++++++++++ .../CatalogNotificationService.java | 151 +++++++++++++++ .../EventSubscriptionRepoService.java | 5 + .../ProductCatalogRepoService.java | 28 ++- src/main/resources/application.yml | 3 + .../CatalogCallbackIntegrationTest.java | 173 ++++++++++++++++++ .../pcm620/CatalogCallbackServiceTest.java | 162 ++++++++++++++++ .../CatalogNotificationIntegrationTest.java | 118 ++++++++++++ .../CatalogNotificationServiceTest.java | 80 ++++++++ 11 files changed, 1026 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/configuration/RestTemplateConfig.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackServiceTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationIntegrationTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java new file mode 100644 index 00000000..4dd28107 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -0,0 +1,114 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2020 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.api; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.camel.ProducerTemplate; +import org.apache.camel.builder.RouteBuilder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.etsi.osl.centrallog.client.CLevel; +import org.etsi.osl.centrallog.client.CentralLogger; +import org.etsi.osl.tmf.common.model.Notification; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Configuration +@Component +public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { + + private static final transient Log logger = LogFactory.getLog(ProductCatalogApiRouteBuilderEvents.class.getName()); + + @Value("${EVENT_PRODUCT_CATALOG_CREATE}") + private String EVENT_CATALOG_CREATE = "direct:EVENT_CATALOG_CREATE"; + + @Value("${EVENT_PRODUCT_CATALOG_DELETE}") + private String EVENT_CATALOG_DELETE = "direct:EVENT_CATALOG_DELETE"; + + @Value("${spring.application.name}") + private String compname; + + @Autowired + private ProducerTemplate template; + + @Autowired + private CentralLogger centralLogger; + + @Override + public void configure() throws Exception { + // Configure routes for catalog events + } + + /** + * Publish notification events for catalog operations + * @param n The notification to publish + * @param objId The catalog object ID + */ + @Transactional + public void publishEvent(final Notification n, final String objId) { + n.setEventType(n.getClass().getName()); + logger.info("will send Event for type " + n.getEventType()); + try { + String msgtopic = ""; + + if (n instanceof CatalogCreateNotification) { + msgtopic = EVENT_CATALOG_CREATE; + } else if (n instanceof CatalogDeleteNotification) { + msgtopic = EVENT_CATALOG_DELETE; + } + + Map map = new HashMap<>(); + map.put("eventid", n.getEventId()); + map.put("objId", objId); + + String apayload = toJsonString(n); + template.sendBodyAndHeaders(msgtopic, apayload, map); + + centralLogger.log(CLevel.INFO, apayload, compname); + + } catch (Exception e) { + e.printStackTrace(); + logger.error("Cannot send Event . " + e.getMessage()); + } + } + + static String toJsonString(Object object) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.writeValueAsString(object); + } + + static T toJsonObj(String content, Class valueType) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.readValue(content, valueType); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/configuration/RestTemplateConfig.java b/src/main/java/org/etsi/osl/tmf/pcm620/configuration/RestTemplateConfig.java new file mode 100644 index 00000000..d6240fcf --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/configuration/RestTemplateConfig.java @@ -0,0 +1,39 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.configuration; + +import java.time.Duration; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder + .setConnectTimeout(Duration.ofSeconds(10)) + .setReadTimeout(Duration.ofSeconds(30)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java new file mode 100644 index 00000000..36f7b5a8 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java @@ -0,0 +1,158 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.CatalogCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class CatalogCallbackService { + + private static final Logger logger = LoggerFactory.getLogger(CatalogCallbackService.class); + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Autowired + private RestTemplate restTemplate; + + /** + * Send catalog create event to all registered callback URLs + * @param catalogCreateEvent The catalog create event to send + */ + public void sendCatalogCreateCallback(CatalogCreateEvent catalogCreateEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "catalogCreateEvent")) { + sendCatalogCreateEventToCallback(subscription.getCallback(), catalogCreateEvent); + } + } + } + + /** + * Send catalog delete event to all registered callback URLs + * @param catalogDeleteEvent The catalog delete event to send + */ + public void sendCatalogDeleteCallback(CatalogDeleteEvent catalogDeleteEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "catalogDeleteEvent")) { + sendCatalogDeleteEventToCallback(subscription.getCallback(), catalogDeleteEvent); + } + } + } + + /** + * Send catalog create event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The catalog create event + */ + private void sendCatalogCreateEventToCallback(String callbackUrl, CatalogCreateEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/catalogCreateEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent catalog create event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send catalog create event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send catalog delete event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The catalog delete event + */ + private void sendCatalogDeleteEventToCallback(String callbackUrl, CatalogDeleteEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/catalogDeleteEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent catalog delete event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send catalog delete event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Build the full callback URL with the listener endpoint + * @param baseUrl The base callback URL + * @param listenerPath The listener path to append + * @return The complete callback URL + */ + private String buildCallbackUrl(String baseUrl, String listenerPath) { + if (baseUrl.endsWith("/")) { + return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath; + } else { + return baseUrl + listenerPath; + } + } + + /** + * Check if a subscription should be notified for a specific event type + * @param subscription The event subscription + * @param eventType The event type to check + * @return true if the subscription should be notified + */ + private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) { + // If no query is specified, notify all events + if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) { + return true; + } + + // Check if the query contains the event type + String query = subscription.getQuery().toLowerCase(); + return query.contains("catalog") || + query.contains(eventType.toLowerCase()) || + query.contains("catalog.create") || + query.contains("catalog.delete"); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java new file mode 100644 index 00000000..5cc5000f --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java @@ -0,0 +1,151 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.Catalog; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateEventPayload; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEventPayload; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class CatalogNotificationService { + + private static final Logger logger = LoggerFactory.getLogger(CatalogNotificationService.class); + + @Autowired + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Autowired + private CatalogCallbackService catalogCallbackService; + + /** + * Publish a catalog create notification + * @param catalog The created catalog + */ + public void publishCatalogCreateNotification(Catalog catalog) { + try { + CatalogCreateNotification notification = createCatalogCreateNotification(catalog); + eventPublisher.publishEvent(notification, catalog.getUuid()); + + // Send callbacks to registered subscribers + catalogCallbackService.sendCatalogCreateCallback(notification.getEvent()); + + logger.info("Published catalog create notification for catalog ID: {}", catalog.getUuid()); + } catch (Exception e) { + logger.error("Error publishing catalog create notification for catalog ID: {}", catalog.getUuid(), e); + } + } + + /** + * Publish a catalog delete notification + * @param catalog The deleted catalog + */ + public void publishCatalogDeleteNotification(Catalog catalog) { + try { + CatalogDeleteNotification notification = createCatalogDeleteNotification(catalog); + eventPublisher.publishEvent(notification, catalog.getUuid()); + + // Send callbacks to registered subscribers + catalogCallbackService.sendCatalogDeleteCallback(notification.getEvent()); + + logger.info("Published catalog delete notification for catalog ID: {}", catalog.getUuid()); + } catch (Exception e) { + logger.error("Error publishing catalog delete notification for catalog ID: {}", catalog.getUuid(), e); + } + } + + /** + * Create a catalog create notification + * @param catalog The created catalog + * @return CatalogCreateNotification + */ + private CatalogCreateNotification createCatalogCreateNotification(Catalog catalog) { + CatalogCreateNotification notification = new CatalogCreateNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(CatalogCreateNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/catalog/" + catalog.getUuid()); + + // Create event + CatalogCreateEvent event = new CatalogCreateEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("CatalogCreateEvent"); + event.setTitle("Catalog Create Event"); + event.setDescription("A catalog has been created"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + CatalogCreateEventPayload payload = new CatalogCreateEventPayload(); + payload.setCatalog(catalog); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a catalog delete notification + * @param catalog The deleted catalog + * @return CatalogDeleteNotification + */ + private CatalogDeleteNotification createCatalogDeleteNotification(Catalog catalog) { + CatalogDeleteNotification notification = new CatalogDeleteNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(CatalogDeleteNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/catalog/" + catalog.getUuid()); + + // Create event + CatalogDeleteEvent event = new CatalogDeleteEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("CatalogDeleteEvent"); + event.setTitle("Catalog Delete Event"); + event.setDescription("A catalog has been deleted"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + CatalogDeleteEventPayload payload = new CatalogDeleteEventPayload(); + payload.setCatalog(catalog); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java index 8fc9caad..4f5c8595 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java @@ -19,6 +19,7 @@ */ package org.etsi.osl.tmf.pcm620.reposervices; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -62,4 +63,8 @@ public class EventSubscriptionRepoService { this.eventSubscriptionRepo.delete(optionalEventSubscription.get()); } } + + public List findAll() { + return (List) this.eventSubscriptionRepo.findAll(); + } } \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCatalogRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCatalogRepoService.java index 3f66f5c7..71bfe6fe 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCatalogRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCatalogRepoService.java @@ -53,11 +53,18 @@ public class ProductCatalogRepoService { @Autowired ProductCategoryRepoService categRepoService; + + @Autowired + CatalogNotificationService catalogNotificationService; public Catalog addCatalog(Catalog c) { - - return this.catalogRepo.save(c); + Catalog savedCatalog = this.catalogRepo.save(c); + + // Publish catalog create notification + catalogNotificationService.publishCatalogCreateNotification(savedCatalog); + + return savedCatalog; } public Catalog addCatalog(@Valid CatalogCreate serviceCat) { @@ -65,7 +72,12 @@ public class ProductCatalogRepoService { Catalog sc = new Catalog(); sc = updateCatalogDataFromAPICall(sc, serviceCat); - return this.catalogRepo.save(sc); + Catalog savedCatalog = this.catalogRepo.save(sc); + + // Publish catalog create notification + catalogNotificationService.publishCatalogCreateNotification(savedCatalog); + + return savedCatalog; } public List findAll() { @@ -85,9 +97,15 @@ public class ProductCatalogRepoService { public Void deleteById(String id) { Optional optionalCat = this.catalogRepo.findByUuid(id); - this.catalogRepo.delete(optionalCat.get()); + if (optionalCat.isPresent()) { + Catalog catalogToDelete = optionalCat.get(); + + // Publish catalog delete notification before deletion + catalogNotificationService.publishCatalogDeleteNotification(catalogToDelete); + + this.catalogRepo.delete(catalogToDelete); + } return null; - } public String findByUuidEager(String id) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dc4f0ce8..1ce4f18c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -210,6 +210,9 @@ EVENT_PRODUCT_ORDER_STATE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.STATECHANGED" EVENT_PRODUCT_ORDER_DELETE: "jms:topic:EVENT.PRODUCTORDER.DELETE" EVENT_PRODUCT_ORDER_ATTRIBUTE_VALUE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.ATTRCHANGED" +EVENT_PRODUCT_CATALOG_CREATE: "jms:topic:EVENT.PRODUCTCATALOG.CREATE" +EVENT_PRODUCT_CATALOG_DELETE: "jms:topic:EVENT.PRODUCTCATALOG.DELETE" + #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java new file mode 100644 index 00000000..e08ee16b --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java @@ -0,0 +1,173 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.etsi.osl.tmf.JsonUtils; +import org.etsi.osl.tmf.OpenAPISpringBoot; +import org.etsi.osl.tmf.pcm620.model.Catalog; +import org.etsi.osl.tmf.pcm620.model.CatalogCreate; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput; +import org.etsi.osl.tmf.pcm620.reposervices.CatalogCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductCatalogRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RunWith(SpringRunner.class) +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class) +@AutoConfigureMockMvc +@ActiveProfiles("testing") +@AutoConfigureTestDatabase +public class CatalogCallbackIntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private ProductCatalogRepoService productCatalogRepoService; + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @SpyBean + private CatalogCallbackService catalogCallbackService; + + @SpyBean + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + mvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + + // Mock RestTemplate to avoid actual HTTP calls in tests + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCompleteCallbackFlow() throws Exception { + // Step 1: Register a callback subscription via Hub API + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:8080/test-callback"); + subscriptionInput.setQuery("catalog.create,catalog.delete"); + + MvcResult subscriptionResult = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()) + .andReturn(); + + String subscriptionResponseBody = subscriptionResult.getResponse().getContentAsString(); + EventSubscription createdSubscription = objectMapper.readValue(subscriptionResponseBody, EventSubscription.class); + + // Step 2: Create a catalog (should trigger callback) + CatalogCreate catalogCreate = new CatalogCreate(); + catalogCreate.setName("Test Callback Catalog"); + catalogCreate.setDescription("A catalog to test callback notifications"); + catalogCreate.setVersion("1.0"); + + Catalog createdCatalog = productCatalogRepoService.addCatalog(catalogCreate); + + // Step 3: Verify callback was sent + verify(catalogCallbackService, timeout(2000)).sendCatalogCreateCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/catalogCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Step 4: Delete the catalog (should trigger delete callback) + productCatalogRepoService.deleteById(createdCatalog.getUuid()); + + // Step 5: Verify delete callback was sent + verify(catalogCallbackService, timeout(2000)).sendCatalogDeleteCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/catalogDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCallbackFilteringByQuery() throws Exception { + // Step 1: Register subscription only for create events + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:9090/create-only"); + subscriptionInput.setQuery("catalog.create"); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create and delete a catalog + CatalogCreate catalogCreate = new CatalogCreate(); + catalogCreate.setName("Test Filter Catalog"); + catalogCreate.setDescription("A catalog to test query filtering"); + catalogCreate.setVersion("1.0"); + + Catalog createdCatalog = productCatalogRepoService.addCatalog(catalogCreate); + productCatalogRepoService.deleteById(createdCatalog.getUuid()); + + // Step 3: Verify only create callback was sent (not delete) + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:9090/create-only/listener/catalogCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Note: In a more sophisticated test, we could verify that the delete callback was NOT sent + // by using verify with never(), but this requires more complex mock setup + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackServiceTest.java new file mode 100644 index 00000000..05312420 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackServiceTest.java @@ -0,0 +1,162 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.Catalog; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateEventPayload; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEventPayload; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.reposervices.CatalogCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class CatalogCallbackServiceTest { + + @Mock + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private CatalogCallbackService catalogCallbackService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testSendCatalogCreateCallback() { + // Arrange + EventSubscription subscription1 = createSubscription("1", "http://localhost:8080/callback", "catalog.create"); + EventSubscription subscription2 = createSubscription("2", "http://localhost:9090/webhook", "catalog"); + List subscriptions = Arrays.asList(subscription1, subscription2); + + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + + CatalogCreateEvent event = createCatalogCreateEvent(); + + // Act + catalogCallbackService.sendCatalogCreateCallback(event); + + // Assert + verify(restTemplate, times(2)).exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)); + } + + @Test + public void testSendCatalogDeleteCallback() { + // Arrange + EventSubscription subscription = createSubscription("1", "http://localhost:8080/callback", "catalog.delete"); + List subscriptions = Arrays.asList(subscription); + + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + + CatalogDeleteEvent event = createCatalogDeleteEvent(); + + // Act + catalogCallbackService.sendCatalogDeleteCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)); + } + + @Test + public void testCallbackUrlBuilding() { + // Arrange + EventSubscription subscription1 = createSubscription("1", "http://localhost:8080/callback", "catalog"); + EventSubscription subscription2 = createSubscription("2", "http://localhost:8080/callback/", "catalog"); + List subscriptions = Arrays.asList(subscription1, subscription2); + + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + + CatalogCreateEvent event = createCatalogCreateEvent(); + + // Act + catalogCallbackService.sendCatalogCreateCallback(event); + + // Assert - Both should result in the same URL format + verify(restTemplate, times(2)).exchange(eq("http://localhost:8080/callback/listener/catalogCreateEvent"), + eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)); + } + + @Test + public void testNoSubscriptions() { + // Arrange + when(eventSubscriptionRepoService.findAll()).thenReturn(Arrays.asList()); + CatalogCreateEvent event = createCatalogCreateEvent(); + + // Act + catalogCallbackService.sendCatalogCreateCallback(event); + + // Assert - No calls should be made + verify(restTemplate, times(0)).exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)); + } + + private EventSubscription createSubscription(String id, String callback, String query) { + EventSubscription subscription = new EventSubscription(); + subscription.setId(id); + subscription.setCallback(callback); + subscription.setQuery(query); + return subscription; + } + + private CatalogCreateEvent createCatalogCreateEvent() { + CatalogCreateEvent event = new CatalogCreateEvent(); + event.setEventId("test-event-123"); + event.setEventType("CatalogCreateEvent"); + + CatalogCreateEventPayload payload = new CatalogCreateEventPayload(); + Catalog catalog = new Catalog(); + catalog.setUuid("test-catalog-123"); + catalog.setName("Test Catalog"); + payload.setCatalog(catalog); + + event.setEvent(payload); + return event; + } + + private CatalogDeleteEvent createCatalogDeleteEvent() { + CatalogDeleteEvent event = new CatalogDeleteEvent(); + event.setEventId("test-delete-event-123"); + event.setEventType("CatalogDeleteEvent"); + + CatalogDeleteEventPayload payload = new CatalogDeleteEventPayload(); + Catalog catalog = new Catalog(); + catalog.setUuid("test-catalog-123"); + catalog.setName("Test Catalog"); + payload.setCatalog(catalog); + + event.setEvent(payload); + return event; + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationIntegrationTest.java new file mode 100644 index 00000000..c5fa4406 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationIntegrationTest.java @@ -0,0 +1,118 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.etsi.osl.tmf.OpenAPISpringBoot; +import org.etsi.osl.tmf.pcm620.model.Catalog; +import org.etsi.osl.tmf.pcm620.model.CatalogCreate; +import org.etsi.osl.tmf.pcm620.reposervices.CatalogNotificationService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductCatalogRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +@RunWith(SpringRunner.class) +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class) +@AutoConfigureMockMvc +@ActiveProfiles("testing") +@AutoConfigureTestDatabase +public class CatalogNotificationIntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private ProductCatalogRepoService productCatalogRepoService; + + @SpyBean + private CatalogNotificationService catalogNotificationService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + mvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCatalogCreateNotificationFlow() throws Exception { + // Arrange + CatalogCreate catalogCreate = new CatalogCreate(); + catalogCreate.setName("Test Notification Catalog"); + catalogCreate.setDescription("A catalog to test notifications"); + catalogCreate.setVersion("1.0"); + + // Act - Create catalog through repository service + Catalog createdCatalog = productCatalogRepoService.addCatalog(catalogCreate); + + // Assert - Verify notification was published + verify(catalogNotificationService, timeout(1000)).publishCatalogCreateNotification(any(Catalog.class)); + + // Verify catalog was created + assert createdCatalog != null; + assert createdCatalog.getName().equals("Test Notification Catalog"); + assert createdCatalog.getUuid() != null; + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCatalogDeleteNotificationFlow() throws Exception { + // Arrange - First create a catalog + CatalogCreate catalogCreate = new CatalogCreate(); + catalogCreate.setName("Test Delete Notification Catalog"); + catalogCreate.setDescription("A catalog to test delete notifications"); + catalogCreate.setVersion("1.0"); + + Catalog createdCatalog = productCatalogRepoService.addCatalog(catalogCreate); + String catalogId = createdCatalog.getUuid(); + + // Act - Delete the catalog + productCatalogRepoService.deleteById(catalogId); + + // Assert - Verify both create and delete notifications were published + verify(catalogNotificationService, timeout(1000)).publishCatalogCreateNotification(any(Catalog.class)); + verify(catalogNotificationService, timeout(1000)).publishCatalogDeleteNotification(any(Catalog.class)); + } + + @Test + public void testDirectCatalogOperations() { + // Arrange + Catalog catalog = new Catalog(); + catalog.setName("Direct Test Catalog"); + catalog.setDescription("Direct catalog for testing"); + + // Act - Add catalog directly + Catalog savedCatalog = productCatalogRepoService.addCatalog(catalog); + + // Assert - Verify notification was called + verify(catalogNotificationService, timeout(1000)).publishCatalogCreateNotification(any(Catalog.class)); + + assert savedCatalog != null; + assert savedCatalog.getName().equals("Direct Test Catalog"); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java new file mode 100644 index 00000000..a88ded05 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java @@ -0,0 +1,80 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.Catalog; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification; +import org.etsi.osl.tmf.pcm620.reposervices.CatalogNotificationService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class CatalogNotificationServiceTest { + + @Mock + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @InjectMocks + private CatalogNotificationService catalogNotificationService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testPublishCatalogCreateNotification() { + // Arrange + Catalog catalog = new Catalog(); + catalog.setUuid("test-catalog-123"); + catalog.setName("Test Catalog"); + catalog.setDescription("A test catalog for notifications"); + + // Act + catalogNotificationService.publishCatalogCreateNotification(catalog); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(CatalogCreateNotification.class), eq("test-catalog-123")); + } + + @Test + public void testPublishCatalogDeleteNotification() { + // Arrange + Catalog catalog = new Catalog(); + catalog.setUuid("test-catalog-456"); + catalog.setName("Test Catalog to Delete"); + catalog.setDescription("A test catalog for delete notifications"); + + // Act + catalogNotificationService.publishCatalogDeleteNotification(catalog); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(CatalogDeleteNotification.class), eq("test-catalog-456")); + } + + @Test + public void testCreateNotificationStructure() { + // Arrange + Catalog catalog = new Catalog(); + catalog.setUuid("test-catalog-789"); + catalog.setName("Test Catalog Structure"); + + // Act + catalogNotificationService.publishCatalogCreateNotification(catalog); + + // Assert - verify the notification was published with correct structure + verify(eventPublisher).publishEvent(any(CatalogCreateNotification.class), eq("test-catalog-789")); + } +} \ No newline at end of file -- GitLab From 114e49225d6c28cdce7328f73e82d90c369e8d61 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 01:12:08 +0300 Subject: [PATCH 03/21] adding category create and delete events --- .../ProductCatalogApiRouteBuilderEvents.java | 14 +- .../reposervices/CategoryCallbackService.java | 158 ++++++++++++++ .../CategoryNotificationService.java | 151 +++++++++++++ .../ProductCategoryRepoService.java | 36 ++- src/main/resources/application-testing.yml | 5 + src/main/resources/application.yml | 2 + .../CatalogCallbackIntegrationTest.java | 3 +- .../CategoryCallbackIntegrationTest.java | 205 ++++++++++++++++++ .../pcm620/CategoryCallbackServiceTest.java | 161 ++++++++++++++ .../CategoryNotificationServiceTest.java | 87 ++++++++ 10 files changed, 813 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryCallbackService.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryNotificationService.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackIntegrationTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackServiceTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CategoryNotificationServiceTest.java diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java index 4dd28107..19619394 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -35,6 +35,8 @@ import org.etsi.osl.centrallog.client.CentralLogger; import org.etsi.osl.tmf.common.model.Notification; import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification; import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -52,6 +54,12 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { @Value("${EVENT_PRODUCT_CATALOG_DELETE}") private String EVENT_CATALOG_DELETE = "direct:EVENT_CATALOG_DELETE"; + + @Value("${EVENT_PRODUCT_CATEGORY_CREATE}") + private String EVENT_CATEGORY_CREATE = "direct:EVENT_CATEGORY_CREATE"; + + @Value("${EVENT_PRODUCT_CATEGORY_DELETE}") + private String EVENT_CATEGORY_DELETE = "direct:EVENT_CATEGORY_DELETE"; @Value("${spring.application.name}") private String compname; @@ -82,7 +90,11 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { if (n instanceof CatalogCreateNotification) { msgtopic = EVENT_CATALOG_CREATE; } else if (n instanceof CatalogDeleteNotification) { - msgtopic = EVENT_CATALOG_DELETE; + msgtopic = EVENT_CATALOG_DELETE; + } else if (n instanceof CategoryCreateNotification) { + msgtopic = EVENT_CATEGORY_CREATE; + } else if (n instanceof CategoryDeleteNotification) { + msgtopic = EVENT_CATEGORY_DELETE; } Map map = new HashMap<>(); diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryCallbackService.java new file mode 100644 index 00000000..4eec9f60 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryCallbackService.java @@ -0,0 +1,158 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.CategoryCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class CategoryCallbackService { + + private static final Logger logger = LoggerFactory.getLogger(CategoryCallbackService.class); + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Autowired + private RestTemplate restTemplate; + + /** + * Send category create event to all registered callback URLs + * @param categoryCreateEvent The category create event to send + */ + public void sendCategoryCreateCallback(CategoryCreateEvent categoryCreateEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "categoryCreateEvent")) { + sendCategoryCreateEventToCallback(subscription.getCallback(), categoryCreateEvent); + } + } + } + + /** + * Send category delete event to all registered callback URLs + * @param categoryDeleteEvent The category delete event to send + */ + public void sendCategoryDeleteCallback(CategoryDeleteEvent categoryDeleteEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "categoryDeleteEvent")) { + sendCategoryDeleteEventToCallback(subscription.getCallback(), categoryDeleteEvent); + } + } + } + + /** + * Send category create event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The category create event + */ + private void sendCategoryCreateEventToCallback(String callbackUrl, CategoryCreateEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/categoryCreateEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent category create event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send category create event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send category delete event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The category delete event + */ + private void sendCategoryDeleteEventToCallback(String callbackUrl, CategoryDeleteEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/categoryDeleteEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent category delete event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send category delete event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Build the full callback URL with the listener endpoint + * @param baseUrl The base callback URL + * @param listenerPath The listener path to append + * @return The complete callback URL + */ + private String buildCallbackUrl(String baseUrl, String listenerPath) { + if (baseUrl.endsWith("/")) { + return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath; + } else { + return baseUrl + listenerPath; + } + } + + /** + * Check if a subscription should be notified for a specific event type + * @param subscription The event subscription + * @param eventType The event type to check + * @return true if the subscription should be notified + */ + private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) { + // If no query is specified, notify all events + if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) { + return true; + } + + // Check if the query contains the event type + String query = subscription.getQuery().toLowerCase(); + return query.contains("category") || + query.contains(eventType.toLowerCase()) || + query.contains("category.create") || + query.contains("category.delete"); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryNotificationService.java new file mode 100644 index 00000000..4d200a58 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryNotificationService.java @@ -0,0 +1,151 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.Category; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateEventPayload; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEventPayload; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class CategoryNotificationService { + + private static final Logger logger = LoggerFactory.getLogger(CategoryNotificationService.class); + + @Autowired + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Autowired + private CategoryCallbackService categoryCallbackService; + + /** + * Publish a category create notification + * @param category The created category + */ + public void publishCategoryCreateNotification(Category category) { + try { + CategoryCreateNotification notification = createCategoryCreateNotification(category); + eventPublisher.publishEvent(notification, category.getUuid()); + + // Send callbacks to registered subscribers + categoryCallbackService.sendCategoryCreateCallback(notification.getEvent()); + + logger.info("Published category create notification for category ID: {}", category.getUuid()); + } catch (Exception e) { + logger.error("Error publishing category create notification for category ID: {}", category.getUuid(), e); + } + } + + /** + * Publish a category delete notification + * @param category The deleted category + */ + public void publishCategoryDeleteNotification(Category category) { + try { + CategoryDeleteNotification notification = createCategoryDeleteNotification(category); + eventPublisher.publishEvent(notification, category.getUuid()); + + // Send callbacks to registered subscribers + categoryCallbackService.sendCategoryDeleteCallback(notification.getEvent()); + + logger.info("Published category delete notification for category ID: {}", category.getUuid()); + } catch (Exception e) { + logger.error("Error publishing category delete notification for category ID: {}", category.getUuid(), e); + } + } + + /** + * Create a category create notification + * @param category The created category + * @return CategoryCreateNotification + */ + private CategoryCreateNotification createCategoryCreateNotification(Category category) { + CategoryCreateNotification notification = new CategoryCreateNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(CategoryCreateNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/category/" + category.getUuid()); + + // Create event + CategoryCreateEvent event = new CategoryCreateEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("CategoryCreateEvent"); + event.setTitle("Category Create Event"); + event.setDescription("A category has been created"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + CategoryCreateEventPayload payload = new CategoryCreateEventPayload(); + payload.setCategory(category); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a category delete notification + * @param category The deleted category + * @return CategoryDeleteNotification + */ + private CategoryDeleteNotification createCategoryDeleteNotification(Category category) { + CategoryDeleteNotification notification = new CategoryDeleteNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(CategoryDeleteNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/category/" + category.getUuid()); + + // Create event + CategoryDeleteEvent event = new CategoryDeleteEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("CategoryDeleteEvent"); + event.setTitle("Category Delete Event"); + event.setDescription("A category has been deleted"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + CategoryDeleteEventPayload payload = new CategoryDeleteEventPayload(); + payload.setCategory(category); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java index c7e226c0..be1aae8c 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java @@ -61,6 +61,9 @@ public class ProductCategoryRepoService { private final ProductCategoriesRepository categsRepo; private final ProductOfferingRepository prodsOfferingRepo; + + @Autowired + private CategoryNotificationService categoryNotificationService; /** * from @@ -78,8 +81,14 @@ public class ProductCategoryRepoService { public Category addCategory(Category c) { - - return this.categsRepo.save( c ); + Category savedCategory = this.categsRepo.save( c ); + + // Publish category create notification + if (categoryNotificationService != null) { + categoryNotificationService.publishCategoryCreateNotification(savedCategory); + } + + return savedCategory; } public Category addCategory(@Valid CategoryCreate Category) { @@ -87,7 +96,14 @@ public class ProductCategoryRepoService { Category sc = new Category() ; sc = updateCategoryDataFromAPICall(sc, Category); - return this.categsRepo.save( sc ); + Category savedCategory = this.categsRepo.save( sc ); + + // Publish category create notification + if (categoryNotificationService != null) { + categoryNotificationService.publishCategoryCreateNotification(savedCategory); + } + + return savedCategory; } @@ -138,13 +154,14 @@ public class ProductCategoryRepoService { return false; //has children } + Category categoryToDelete = optionalCat.get(); - if ( optionalCat.get().getParentId() != null ) { - Category parentCat = (this.categsRepo.findByUuid( optionalCat.get().getParentId() )).get(); + if ( categoryToDelete.getParentId() != null ) { + Category parentCat = (this.categsRepo.findByUuid( categoryToDelete.getParentId() )).get(); //remove from parent category for (Category ss : parentCat.getCategoryObj()) { - if ( ss.getId() == optionalCat.get().getId() ) { + if ( ss.getId() == categoryToDelete.getId() ) { parentCat.getCategoryObj().remove(ss); break; } @@ -152,8 +169,13 @@ public class ProductCategoryRepoService { parentCat = this.categsRepo.save(parentCat); } + this.categsRepo.delete( categoryToDelete); + + // Publish category delete notification + if (categoryNotificationService != null) { + categoryNotificationService.publishCategoryDeleteNotification(categoryToDelete); + } - this.categsRepo.delete( optionalCat.get()); return true; } diff --git a/src/main/resources/application-testing.yml b/src/main/resources/application-testing.yml index dc15a9c2..d5506a3b 100644 --- a/src/main/resources/application-testing.yml +++ b/src/main/resources/application-testing.yml @@ -185,6 +185,11 @@ EVENT_PRODUCT_ORDER_STATE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.STATECHANGED" EVENT_PRODUCT_ORDER_DELETE: "jms:topic:EVENT.PRODUCTORDER.DELETE" EVENT_PRODUCT_ORDER_ATTRIBUTE_VALUE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.ATTRCHANGED" +EVENT_PRODUCT_CATALOG_CREATE: "jms:topic:EVENT.PRODUCTCATALOG.CREATE" +EVENT_PRODUCT_CATALOG_DELETE: "jms:topic:EVENT.PRODUCTCATALOG.DELETE" +EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE" +EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE" + #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1ce4f18c..1bfea10e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -212,6 +212,8 @@ EVENT_PRODUCT_ORDER_ATTRIBUTE_VALUE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.ATTRC EVENT_PRODUCT_CATALOG_CREATE: "jms:topic:EVENT.PRODUCTCATALOG.CREATE" EVENT_PRODUCT_CATALOG_DELETE: "jms:topic:EVENT.PRODUCTCATALOG.DELETE" +EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE" +EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java index e08ee16b..0764364c 100644 --- a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java @@ -21,6 +21,7 @@ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabas import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; @@ -70,7 +71,7 @@ public class CatalogCallbackIntegrationTest { @SpyBean private CatalogCallbackService catalogCallbackService; - @SpyBean + @MockBean private RestTemplate restTemplate; @Autowired diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackIntegrationTest.java new file mode 100644 index 00000000..a694ae46 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackIntegrationTest.java @@ -0,0 +1,205 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.etsi.osl.tmf.JsonUtils; +import org.etsi.osl.tmf.OpenAPISpringBoot; +import org.etsi.osl.tmf.pcm620.model.Category; +import org.etsi.osl.tmf.pcm620.model.CategoryCreate; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput; +import org.etsi.osl.tmf.pcm620.reposervices.CategoryCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductCategoryRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RunWith(SpringRunner.class) +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class) +@AutoConfigureMockMvc +@ActiveProfiles("testing") +@AutoConfigureTestDatabase +public class CategoryCallbackIntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private ProductCategoryRepoService productCategoryRepoService; + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @SpyBean + private CategoryCallbackService categoryCallbackService; + + @MockBean + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + mvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + + // Mock RestTemplate to avoid actual HTTP calls in tests + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCompleteCallbackFlow() throws Exception { + // Step 1: Register a callback subscription via Hub API + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:8080/test-callback"); + subscriptionInput.setQuery("category.create,category.delete"); + + MvcResult subscriptionResult = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()) + .andReturn(); + + String subscriptionResponseBody = subscriptionResult.getResponse().getContentAsString(); + EventSubscription createdSubscription = objectMapper.readValue(subscriptionResponseBody, EventSubscription.class); + + // Step 2: Create a category (should trigger callback) + CategoryCreate categoryCreate = new CategoryCreate(); + categoryCreate.setName("Test Callback Category"); + categoryCreate.setDescription("A category to test callback notifications"); + categoryCreate.setVersion("1.0"); + + Category createdCategory = productCategoryRepoService.addCategory(categoryCreate); + + // Step 3: Verify callback was sent + verify(categoryCallbackService, timeout(2000)).sendCategoryCreateCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/categoryCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Step 4: Delete the category (should trigger delete callback) + productCategoryRepoService.deleteById(createdCategory.getUuid()); + + // Step 5: Verify delete callback was sent + verify(categoryCallbackService, timeout(2000)).sendCategoryDeleteCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/categoryDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCallbackFilteringByQuery() throws Exception { + // Step 1: Register subscription only for create events + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:9090/create-only"); + subscriptionInput.setQuery("category.create"); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create and delete a category + CategoryCreate categoryCreate = new CategoryCreate(); + categoryCreate.setName("Test Filter Category"); + categoryCreate.setDescription("A category to test query filtering"); + categoryCreate.setVersion("1.0"); + + Category createdCategory = productCategoryRepoService.addCategory(categoryCreate); + productCategoryRepoService.deleteById(createdCategory.getUuid()); + + // Step 3: Verify only create callback was sent (not delete) + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:9090/create-only/listener/categoryCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Note: In a more sophisticated test, we could verify that the delete callback was NOT sent + // by using verify with never(), but this requires more complex mock setup + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCategoryCallbackWithAllEventsQuery() throws Exception { + // Step 1: Register subscription for all events (empty query) + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:7070/all-events"); + subscriptionInput.setQuery(""); // Empty query should receive all events + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create a category + CategoryCreate categoryCreate = new CategoryCreate(); + categoryCreate.setName("Test All Events Category"); + categoryCreate.setDescription("A category to test all events subscription"); + categoryCreate.setVersion("1.0"); + + Category createdCategory = productCategoryRepoService.addCategory(categoryCreate); + + // Step 3: Verify callback was sent even with empty query + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:7070/all-events/listener/categoryCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackServiceTest.java new file mode 100644 index 00000000..df70e632 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackServiceTest.java @@ -0,0 +1,161 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.Category; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.reposervices.CategoryCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class CategoryCallbackServiceTest { + + @Mock + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private CategoryCallbackService categoryCallbackService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testSendCategoryCreateCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("category"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + CategoryCreateEvent event = new CategoryCreateEvent(); + event.setEventId("test-event-123"); + + // Act + categoryCallbackService.sendCategoryCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/categoryCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendCategoryDeleteCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("category"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + CategoryDeleteEvent event = new CategoryDeleteEvent(); + event.setEventId("test-event-456"); + + // Act + categoryCallbackService.sendCategoryDeleteCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/categoryDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testCallbackWithTrailingSlash() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback/"); + subscription.setQuery("category"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + CategoryCreateEvent event = new CategoryCreateEvent(); + event.setEventId("test-event-789"); + + // Act + categoryCallbackService.sendCategoryCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/categoryCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testFilterSubscriptionsByQuery() { + // Arrange + EventSubscription categorySubscription = new EventSubscription(); + categorySubscription.setCallback("http://localhost:8080/category-callback"); + categorySubscription.setQuery("category"); + + EventSubscription otherSubscription = new EventSubscription(); + otherSubscription.setCallback("http://localhost:8080/other-callback"); + otherSubscription.setQuery("catalog"); + + List subscriptions = Arrays.asList(categorySubscription, otherSubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + CategoryCreateEvent event = new CategoryCreateEvent(); + event.setEventId("test-event-filter"); + + // Act + categoryCallbackService.sendCategoryCreateCallback(event); + + // Assert - only category subscription should receive callback + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/category-callback/listener/categoryCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CategoryNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryNotificationServiceTest.java new file mode 100644 index 00000000..9de497fc --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryNotificationServiceTest.java @@ -0,0 +1,87 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.Category; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification; +import org.etsi.osl.tmf.pcm620.reposervices.CategoryNotificationService; +import org.etsi.osl.tmf.pcm620.reposervices.CategoryCallbackService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class CategoryNotificationServiceTest { + + @Mock + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Mock + private CategoryCallbackService categoryCallbackService; + + @InjectMocks + private CategoryNotificationService categoryNotificationService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testPublishCategoryCreateNotification() { + // Arrange + Category category = new Category(); + category.setUuid("test-category-123"); + category.setName("Test Category"); + category.setDescription("A test category for notifications"); + + // Act + categoryNotificationService.publishCategoryCreateNotification(category); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(CategoryCreateNotification.class), eq("test-category-123")); + verify(categoryCallbackService, times(1)).sendCategoryCreateCallback(any()); + } + + @Test + public void testPublishCategoryDeleteNotification() { + // Arrange + Category category = new Category(); + category.setUuid("test-category-456"); + category.setName("Test Category to Delete"); + category.setDescription("A test category for delete notifications"); + + // Act + categoryNotificationService.publishCategoryDeleteNotification(category); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(CategoryDeleteNotification.class), eq("test-category-456")); + verify(categoryCallbackService, times(1)).sendCategoryDeleteCallback(any()); + } + + @Test + public void testCreateNotificationStructure() { + // Arrange + Category category = new Category(); + category.setUuid("test-category-789"); + category.setName("Test Category Structure"); + + // Act + categoryNotificationService.publishCategoryCreateNotification(category); + + // Assert - verify the notification was published with correct structure + verify(eventPublisher).publishEvent(any(CategoryCreateNotification.class), eq("test-category-789")); + verify(categoryCallbackService).sendCategoryCreateCallback(any()); + } +} \ No newline at end of file -- GitLab From cd43309d59bda6b9ef9c562c1200e46bd35c1502 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 01:43:18 +0300 Subject: [PATCH 04/21] adding product spec notifications and events --- .../ProductCatalogApiRouteBuilderEvents.java | 14 +- .../ProductSpecificationCallbackService.java | 158 ++++++++++++++ ...oductSpecificationNotificationService.java | 151 +++++++++++++ .../ProductSpecificationRepoService.java | 15 +- src/main/resources/application-testing.yml | 2 + src/main/resources/application.yml | 2 + ...tSpecificationCallbackIntegrationTest.java | 205 ++++++++++++++++++ ...oductSpecificationCallbackServiceTest.java | 188 ++++++++++++++++ ...tSpecificationNotificationServiceTest.java | 87 ++++++++ 9 files changed, 820 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationCallbackService.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationNotificationService.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackIntegrationTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackServiceTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationNotificationServiceTest.java diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java index 19619394..674c0cfb 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -37,6 +37,8 @@ import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification; import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification; import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification; import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteNotification; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -60,6 +62,12 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { @Value("${EVENT_PRODUCT_CATEGORY_DELETE}") private String EVENT_CATEGORY_DELETE = "direct:EVENT_CATEGORY_DELETE"; + + @Value("${EVENT_PRODUCT_SPECIFICATION_CREATE}") + private String EVENT_PRODUCT_SPECIFICATION_CREATE = "direct:EVENT_PRODUCT_SPECIFICATION_CREATE"; + + @Value("${EVENT_PRODUCT_SPECIFICATION_DELETE}") + private String EVENT_PRODUCT_SPECIFICATION_DELETE = "direct:EVENT_PRODUCT_SPECIFICATION_DELETE"; @Value("${spring.application.name}") private String compname; @@ -94,7 +102,11 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { } else if (n instanceof CategoryCreateNotification) { msgtopic = EVENT_CATEGORY_CREATE; } else if (n instanceof CategoryDeleteNotification) { - msgtopic = EVENT_CATEGORY_DELETE; + msgtopic = EVENT_CATEGORY_DELETE; + } else if (n instanceof ProductSpecificationCreateNotification) { + msgtopic = EVENT_PRODUCT_SPECIFICATION_CREATE; + } else if (n instanceof ProductSpecificationDeleteNotification) { + msgtopic = EVENT_PRODUCT_SPECIFICATION_DELETE; } Map map = new HashMap<>(); diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationCallbackService.java new file mode 100644 index 00000000..ae627191 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationCallbackService.java @@ -0,0 +1,158 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class ProductSpecificationCallbackService { + + private static final Logger logger = LoggerFactory.getLogger(ProductSpecificationCallbackService.class); + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Autowired + private RestTemplate restTemplate; + + /** + * Send product specification create event to all registered callback URLs + * @param productSpecificationCreateEvent The product specification create event to send + */ + public void sendProductSpecificationCreateCallback(ProductSpecificationCreateEvent productSpecificationCreateEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productSpecificationCreateEvent")) { + sendProductSpecificationCreateEventToCallback(subscription.getCallback(), productSpecificationCreateEvent); + } + } + } + + /** + * Send product specification delete event to all registered callback URLs + * @param productSpecificationDeleteEvent The product specification delete event to send + */ + public void sendProductSpecificationDeleteCallback(ProductSpecificationDeleteEvent productSpecificationDeleteEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productSpecificationDeleteEvent")) { + sendProductSpecificationDeleteEventToCallback(subscription.getCallback(), productSpecificationDeleteEvent); + } + } + } + + /** + * Send product specification create event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product specification create event + */ + private void sendProductSpecificationCreateEventToCallback(String callbackUrl, ProductSpecificationCreateEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productSpecificationCreateEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product specification create event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product specification create event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product specification delete event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product specification delete event + */ + private void sendProductSpecificationDeleteEventToCallback(String callbackUrl, ProductSpecificationDeleteEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productSpecificationDeleteEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product specification delete event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product specification delete event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Build the full callback URL with the listener endpoint + * @param baseUrl The base callback URL + * @param listenerPath The listener path to append + * @return The complete callback URL + */ + private String buildCallbackUrl(String baseUrl, String listenerPath) { + if (baseUrl.endsWith("/")) { + return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath; + } else { + return baseUrl + listenerPath; + } + } + + /** + * Check if a subscription should be notified for a specific event type + * @param subscription The event subscription + * @param eventType The event type to check + * @return true if the subscription should be notified + */ + private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) { + // If no query is specified, notify all events + if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) { + return true; + } + + // Check if the query contains the event type + String query = subscription.getQuery().toLowerCase(); + return query.contains("productspecification") || + query.contains(eventType.toLowerCase()) || + query.contains("productspecification.create") || + query.contains("productspecification.delete"); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationNotificationService.java new file mode 100644 index 00000000..0818539a --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationNotificationService.java @@ -0,0 +1,151 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.ProductSpecification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteNotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class ProductSpecificationNotificationService { + + private static final Logger logger = LoggerFactory.getLogger(ProductSpecificationNotificationService.class); + + @Autowired + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Autowired + private ProductSpecificationCallbackService productSpecificationCallbackService; + + /** + * Publish a product specification create notification + * @param productSpecification The created product specification + */ + public void publishProductSpecificationCreateNotification(ProductSpecification productSpecification) { + try { + ProductSpecificationCreateNotification notification = createProductSpecificationCreateNotification(productSpecification); + eventPublisher.publishEvent(notification, productSpecification.getUuid()); + + // Send callbacks to registered subscribers + productSpecificationCallbackService.sendProductSpecificationCreateCallback(notification.getEvent()); + + logger.info("Published product specification create notification for product spec ID: {}", productSpecification.getUuid()); + } catch (Exception e) { + logger.error("Error publishing product specification create notification for product spec ID: {}", productSpecification.getUuid(), e); + } + } + + /** + * Publish a product specification delete notification + * @param productSpecification The deleted product specification + */ + public void publishProductSpecificationDeleteNotification(ProductSpecification productSpecification) { + try { + ProductSpecificationDeleteNotification notification = createProductSpecificationDeleteNotification(productSpecification); + eventPublisher.publishEvent(notification, productSpecification.getUuid()); + + // Send callbacks to registered subscribers + productSpecificationCallbackService.sendProductSpecificationDeleteCallback(notification.getEvent()); + + logger.info("Published product specification delete notification for product spec ID: {}", productSpecification.getUuid()); + } catch (Exception e) { + logger.error("Error publishing product specification delete notification for product spec ID: {}", productSpecification.getUuid(), e); + } + } + + /** + * Create a product specification create notification + * @param productSpecification The created product specification + * @return ProductSpecificationCreateNotification + */ + private ProductSpecificationCreateNotification createProductSpecificationCreateNotification(ProductSpecification productSpecification) { + ProductSpecificationCreateNotification notification = new ProductSpecificationCreateNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductSpecificationCreateNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productSpecification/" + productSpecification.getUuid()); + + // Create event + ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductSpecificationCreateEvent"); + event.setTitle("Product Specification Create Event"); + event.setDescription("A product specification has been created"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductSpecificationCreateEventPayload payload = new ProductSpecificationCreateEventPayload(); + payload.setProductSpecification(productSpecification); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product specification delete notification + * @param productSpecification The deleted product specification + * @return ProductSpecificationDeleteNotification + */ + private ProductSpecificationDeleteNotification createProductSpecificationDeleteNotification(ProductSpecification productSpecification) { + ProductSpecificationDeleteNotification notification = new ProductSpecificationDeleteNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductSpecificationDeleteNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productSpecification/" + productSpecification.getUuid()); + + // Create event + ProductSpecificationDeleteEvent event = new ProductSpecificationDeleteEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductSpecificationDeleteEvent"); + event.setTitle("Product Specification Delete Event"); + event.setDescription("A product specification has been deleted"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductSpecificationDeleteEventPayload payload = new ProductSpecificationDeleteEventPayload(); + payload.setProductSpecification(productSpecification); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java index deec0a66..8b8fdfc7 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java @@ -70,6 +70,9 @@ public class ProductSpecificationRepoService { @Autowired ServiceSpecificationRepoService serviceSpecificationRepoService; + + @Autowired + private ProductSpecificationNotificationService productSpecificationNotificationService; private SessionFactory sessionFactory; @@ -94,8 +97,12 @@ public class ProductSpecificationRepoService { serviceSpec = this.updateProductSpecificationDataFromAPIcall(serviceSpec, serviceProductSpecification); serviceSpec = this.prodsOfferingRepo.save(serviceSpec); + // Publish product specification create notification + if (productSpecificationNotificationService != null) { + productSpecificationNotificationService.publishProductSpecificationCreateNotification(serviceSpec); + } - return this.prodsOfferingRepo.save(serviceSpec); + return serviceSpec; } public List findAll() { @@ -252,6 +259,12 @@ public class ProductSpecificationRepoService { */ this.prodsOfferingRepo.delete(s); + + // Publish product specification delete notification + if (productSpecificationNotificationService != null) { + productSpecificationNotificationService.publishProductSpecificationDeleteNotification(s); + } + return null; } diff --git a/src/main/resources/application-testing.yml b/src/main/resources/application-testing.yml index d5506a3b..decd660c 100644 --- a/src/main/resources/application-testing.yml +++ b/src/main/resources/application-testing.yml @@ -189,6 +189,8 @@ EVENT_PRODUCT_CATALOG_CREATE: "jms:topic:EVENT.PRODUCTCATALOG.CREATE" EVENT_PRODUCT_CATALOG_DELETE: "jms:topic:EVENT.PRODUCTCATALOG.DELETE" EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE" EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE" +EVENT_PRODUCT_SPECIFICATION_CREATE: "jms:topic:EVENT.PRODUCTSPECIFICATION.CREATE" +EVENT_PRODUCT_SPECIFICATION_DELETE: "jms:topic:EVENT.PRODUCTSPECIFICATION.DELETE" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1bfea10e..25d47524 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -214,6 +214,8 @@ EVENT_PRODUCT_CATALOG_CREATE: "jms:topic:EVENT.PRODUCTCATALOG.CREATE" EVENT_PRODUCT_CATALOG_DELETE: "jms:topic:EVENT.PRODUCTCATALOG.DELETE" EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE" EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE" +EVENT_PRODUCT_SPECIFICATION_CREATE: "jms:topic:EVENT.PRODUCTSPECIFICATION.CREATE" +EVENT_PRODUCT_SPECIFICATION_DELETE: "jms:topic:EVENT.PRODUCTSPECIFICATION.DELETE" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackIntegrationTest.java new file mode 100644 index 00000000..f85b8671 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackIntegrationTest.java @@ -0,0 +1,205 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.etsi.osl.tmf.JsonUtils; +import org.etsi.osl.tmf.OpenAPISpringBoot; +import org.etsi.osl.tmf.pcm620.model.ProductSpecification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreate; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput; +import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RunWith(SpringRunner.class) +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class) +@AutoConfigureMockMvc +@ActiveProfiles("testing") +@AutoConfigureTestDatabase +public class ProductSpecificationCallbackIntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private ProductSpecificationRepoService productSpecificationRepoService; + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @SpyBean + private ProductSpecificationCallbackService productSpecificationCallbackService; + + @MockBean + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + mvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + + // Mock RestTemplate to avoid actual HTTP calls in tests + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCompleteCallbackFlow() throws Exception { + // Step 1: Register a callback subscription via Hub API + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:8080/test-callback"); + subscriptionInput.setQuery("productspecification.create,productspecification.delete"); + + MvcResult subscriptionResult = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()) + .andReturn(); + + String subscriptionResponseBody = subscriptionResult.getResponse().getContentAsString(); + EventSubscription createdSubscription = objectMapper.readValue(subscriptionResponseBody, EventSubscription.class); + + // Step 2: Create a product specification (should trigger callback) + ProductSpecificationCreate productSpecificationCreate = new ProductSpecificationCreate(); + productSpecificationCreate.setName("Test Callback Product Specification"); + productSpecificationCreate.setDescription("A product specification to test callback notifications"); + productSpecificationCreate.setVersion("1.0"); + + ProductSpecification createdProductSpecification = productSpecificationRepoService.addProductSpecification(productSpecificationCreate); + + // Step 3: Verify callback was sent + verify(productSpecificationCallbackService, timeout(2000)).sendProductSpecificationCreateCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Step 4: Delete the product specification (should trigger delete callback) + productSpecificationRepoService.deleteByUuid(createdProductSpecification.getUuid()); + + // Step 5: Verify delete callback was sent + verify(productSpecificationCallbackService, timeout(2000)).sendProductSpecificationDeleteCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/productSpecificationDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCallbackFilteringByQuery() throws Exception { + // Step 1: Register subscription only for create events + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:9090/create-only"); + subscriptionInput.setQuery("productspecification.create"); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create and delete a product specification + ProductSpecificationCreate productSpecificationCreate = new ProductSpecificationCreate(); + productSpecificationCreate.setName("Test Filter Product Specification"); + productSpecificationCreate.setDescription("A product specification to test query filtering"); + productSpecificationCreate.setVersion("1.0"); + + ProductSpecification createdProductSpecification = productSpecificationRepoService.addProductSpecification(productSpecificationCreate); + productSpecificationRepoService.deleteByUuid(createdProductSpecification.getUuid()); + + // Step 3: Verify only create callback was sent (not delete) + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:9090/create-only/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Note: In a more sophisticated test, we could verify that the delete callback was NOT sent + // by using verify with never(), but this requires more complex mock setup + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testProductSpecificationCallbackWithAllEventsQuery() throws Exception { + // Step 1: Register subscription for all events (empty query) + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:7070/all-events"); + subscriptionInput.setQuery(""); // Empty query should receive all events + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create a product specification + ProductSpecificationCreate productSpecificationCreate = new ProductSpecificationCreate(); + productSpecificationCreate.setName("Test All Events Product Specification"); + productSpecificationCreate.setDescription("A product specification to test all events subscription"); + productSpecificationCreate.setVersion("1.0"); + + ProductSpecification createdProductSpecification = productSpecificationRepoService.addProductSpecification(productSpecificationCreate); + + // Step 3: Verify callback was sent even with empty query + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:7070/all-events/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackServiceTest.java new file mode 100644 index 00000000..1d11a49d --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackServiceTest.java @@ -0,0 +1,188 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.ProductSpecification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class ProductSpecificationCallbackServiceTest { + + @Mock + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private ProductSpecificationCallbackService productSpecificationCallbackService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testSendProductSpecificationCreateCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productspecification"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent(); + event.setEventId("test-event-123"); + + // Act + productSpecificationCallbackService.sendProductSpecificationCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductSpecificationDeleteCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productspecification"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductSpecificationDeleteEvent event = new ProductSpecificationDeleteEvent(); + event.setEventId("test-event-456"); + + // Act + productSpecificationCallbackService.sendProductSpecificationDeleteCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productSpecificationDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testCallbackWithTrailingSlash() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback/"); + subscription.setQuery("productspecification"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent(); + event.setEventId("test-event-789"); + + // Act + productSpecificationCallbackService.sendProductSpecificationCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testFilterSubscriptionsByQuery() { + // Arrange + EventSubscription productSpecSubscription = new EventSubscription(); + productSpecSubscription.setCallback("http://localhost:8080/productspec-callback"); + productSpecSubscription.setQuery("productspecification"); + + EventSubscription otherSubscription = new EventSubscription(); + otherSubscription.setCallback("http://localhost:8080/other-callback"); + otherSubscription.setQuery("catalog"); + + List subscriptions = Arrays.asList(productSpecSubscription, otherSubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent(); + event.setEventId("test-event-filter"); + + // Act + productSpecificationCallbackService.sendProductSpecificationCreateCallback(event); + + // Assert - only product specification subscription should receive callback + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/productspec-callback/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testProductSpecificationSpecificQueries() { + // Arrange + EventSubscription createOnlySubscription = new EventSubscription(); + createOnlySubscription.setCallback("http://localhost:9090/create-only"); + createOnlySubscription.setQuery("productspecification.create"); + + List subscriptions = Arrays.asList(createOnlySubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent(); + event.setEventId("test-event-specific-query"); + + // Act + productSpecificationCallbackService.sendProductSpecificationCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:9090/create-only/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationNotificationServiceTest.java new file mode 100644 index 00000000..b251a053 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationNotificationServiceTest.java @@ -0,0 +1,87 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.ProductSpecification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteNotification; +import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationNotificationService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationCallbackService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class ProductSpecificationNotificationServiceTest { + + @Mock + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Mock + private ProductSpecificationCallbackService productSpecificationCallbackService; + + @InjectMocks + private ProductSpecificationNotificationService productSpecificationNotificationService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testPublishProductSpecificationCreateNotification() { + // Arrange + ProductSpecification productSpecification = new ProductSpecification(); + productSpecification.setUuid("test-productspec-123"); + productSpecification.setName("Test Product Specification"); + productSpecification.setDescription("A test product specification for notifications"); + + // Act + productSpecificationNotificationService.publishProductSpecificationCreateNotification(productSpecification); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductSpecificationCreateNotification.class), eq("test-productspec-123")); + verify(productSpecificationCallbackService, times(1)).sendProductSpecificationCreateCallback(any()); + } + + @Test + public void testPublishProductSpecificationDeleteNotification() { + // Arrange + ProductSpecification productSpecification = new ProductSpecification(); + productSpecification.setUuid("test-productspec-456"); + productSpecification.setName("Test Product Specification to Delete"); + productSpecification.setDescription("A test product specification for delete notifications"); + + // Act + productSpecificationNotificationService.publishProductSpecificationDeleteNotification(productSpecification); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductSpecificationDeleteNotification.class), eq("test-productspec-456")); + verify(productSpecificationCallbackService, times(1)).sendProductSpecificationDeleteCallback(any()); + } + + @Test + public void testCreateNotificationStructure() { + // Arrange + ProductSpecification productSpecification = new ProductSpecification(); + productSpecification.setUuid("test-productspec-789"); + productSpecification.setName("Test Product Specification Structure"); + + // Act + productSpecificationNotificationService.publishProductSpecificationCreateNotification(productSpecification); + + // Assert - verify the notification was published with correct structure + verify(eventPublisher).publishEvent(any(ProductSpecificationCreateNotification.class), eq("test-productspec-789")); + verify(productSpecificationCallbackService).sendProductSpecificationCreateCallback(any()); + } +} \ No newline at end of file -- GitLab From acabc2b42babbab10437b569469abeae032ab0a8 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 02:04:22 +0300 Subject: [PATCH 05/21] adding ProductOffering events --- .../ProductCatalogApiRouteBuilderEvents.java | 26 +- .../ProductOfferingCallbackService.java | 238 ++++++++++++++++ .../ProductOfferingNotificationService.java | 257 +++++++++++++++++ .../ProductOfferingRepoService.java | 33 ++- src/main/resources/application-testing.yml | 4 + src/main/resources/application.yml | 4 + ...roductOfferingCallbackIntegrationTest.java | 254 +++++++++++++++++ .../ProductOfferingCallbackServiceTest.java | 258 ++++++++++++++++++ ...roductOfferingNotificationServiceTest.java | 121 ++++++++ 9 files changed, 1191 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingCallbackService.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingNotificationService.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackServiceTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingNotificationServiceTest.java diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java index 674c0cfb..c0828283 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -39,6 +39,10 @@ import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification; import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification; import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateNotification; import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeNotification; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -68,6 +72,18 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { @Value("${EVENT_PRODUCT_SPECIFICATION_DELETE}") private String EVENT_PRODUCT_SPECIFICATION_DELETE = "direct:EVENT_PRODUCT_SPECIFICATION_DELETE"; + + @Value("${EVENT_PRODUCT_OFFERING_CREATE}") + private String EVENT_PRODUCT_OFFERING_CREATE = "direct:EVENT_PRODUCT_OFFERING_CREATE"; + + @Value("${EVENT_PRODUCT_OFFERING_DELETE}") + private String EVENT_PRODUCT_OFFERING_DELETE = "direct:EVENT_PRODUCT_OFFERING_DELETE"; + + @Value("${EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE}") + private String EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE = "direct:EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE"; + + @Value("${EVENT_PRODUCT_OFFERING_STATE_CHANGE}") + private String EVENT_PRODUCT_OFFERING_STATE_CHANGE = "direct:EVENT_PRODUCT_OFFERING_STATE_CHANGE"; @Value("${spring.application.name}") private String compname; @@ -106,7 +122,15 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { } else if (n instanceof ProductSpecificationCreateNotification) { msgtopic = EVENT_PRODUCT_SPECIFICATION_CREATE; } else if (n instanceof ProductSpecificationDeleteNotification) { - msgtopic = EVENT_PRODUCT_SPECIFICATION_DELETE; + msgtopic = EVENT_PRODUCT_SPECIFICATION_DELETE; + } else if (n instanceof ProductOfferingCreateNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_CREATE; + } else if (n instanceof ProductOfferingDeleteNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_DELETE; + } else if (n instanceof ProductOfferingAttributeValueChangeNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE; + } else if (n instanceof ProductOfferingStateChangeNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_STATE_CHANGE; } Map map = new HashMap<>(); diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingCallbackService.java new file mode 100644 index 00000000..f6008a08 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingCallbackService.java @@ -0,0 +1,238 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class ProductOfferingCallbackService { + + private static final Logger logger = LoggerFactory.getLogger(ProductOfferingCallbackService.class); + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Autowired + private RestTemplate restTemplate; + + /** + * Send product offering create event to all registered callback URLs + * @param productOfferingCreateEvent The product offering create event to send + */ + public void sendProductOfferingCreateCallback(ProductOfferingCreateEvent productOfferingCreateEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingCreateEvent")) { + sendProductOfferingCreateEventToCallback(subscription.getCallback(), productOfferingCreateEvent); + } + } + } + + /** + * Send product offering delete event to all registered callback URLs + * @param productOfferingDeleteEvent The product offering delete event to send + */ + public void sendProductOfferingDeleteCallback(ProductOfferingDeleteEvent productOfferingDeleteEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingDeleteEvent")) { + sendProductOfferingDeleteEventToCallback(subscription.getCallback(), productOfferingDeleteEvent); + } + } + } + + /** + * Send product offering attribute value change event to all registered callback URLs + * @param productOfferingAttributeValueChangeEvent The product offering attribute value change event to send + */ + public void sendProductOfferingAttributeValueChangeCallback(ProductOfferingAttributeValueChangeEvent productOfferingAttributeValueChangeEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingAttributeValueChangeEvent")) { + sendProductOfferingAttributeValueChangeEventToCallback(subscription.getCallback(), productOfferingAttributeValueChangeEvent); + } + } + } + + /** + * Send product offering state change event to all registered callback URLs + * @param productOfferingStateChangeEvent The product offering state change event to send + */ + public void sendProductOfferingStateChangeCallback(ProductOfferingStateChangeEvent productOfferingStateChangeEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingStateChangeEvent")) { + sendProductOfferingStateChangeEventToCallback(subscription.getCallback(), productOfferingStateChangeEvent); + } + } + } + + /** + * Send product offering create event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering create event + */ + private void sendProductOfferingCreateEventToCallback(String callbackUrl, ProductOfferingCreateEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingCreateEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering create event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering create event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product offering delete event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering delete event + */ + private void sendProductOfferingDeleteEventToCallback(String callbackUrl, ProductOfferingDeleteEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingDeleteEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering delete event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering delete event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product offering attribute value change event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering attribute value change event + */ + private void sendProductOfferingAttributeValueChangeEventToCallback(String callbackUrl, ProductOfferingAttributeValueChangeEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingAttributeValueChangeEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering attribute value change event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering attribute value change event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product offering state change event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering state change event + */ + private void sendProductOfferingStateChangeEventToCallback(String callbackUrl, ProductOfferingStateChangeEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingStateChangeEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering state change event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering state change event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Build the full callback URL with the listener endpoint + * @param baseUrl The base callback URL + * @param listenerPath The listener path to append + * @return The complete callback URL + */ + private String buildCallbackUrl(String baseUrl, String listenerPath) { + if (baseUrl.endsWith("/")) { + return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath; + } else { + return baseUrl + listenerPath; + } + } + + /** + * Check if a subscription should be notified for a specific event type + * @param subscription The event subscription + * @param eventType The event type to check + * @return true if the subscription should be notified + */ + private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) { + // If no query is specified, notify all events + if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) { + return true; + } + + // Check if the query contains the event type + String query = subscription.getQuery().toLowerCase(); + return query.contains("productoffering") || + query.contains(eventType.toLowerCase()) || + query.contains("productoffering.create") || + query.contains("productoffering.delete") || + query.contains("productoffering.attributevaluechange") || + query.contains("productoffering.statechange"); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingNotificationService.java new file mode 100644 index 00000000..e10e25f0 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingNotificationService.java @@ -0,0 +1,257 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.ProductOffering; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeNotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class ProductOfferingNotificationService { + + private static final Logger logger = LoggerFactory.getLogger(ProductOfferingNotificationService.class); + + @Autowired + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Autowired + private ProductOfferingCallbackService productOfferingCallbackService; + + /** + * Publish a product offering create notification + * @param productOffering The created product offering + */ + public void publishProductOfferingCreateNotification(ProductOffering productOffering) { + try { + ProductOfferingCreateNotification notification = createProductOfferingCreateNotification(productOffering); + eventPublisher.publishEvent(notification, productOffering.getUuid()); + + // Send callbacks to registered subscribers + productOfferingCallbackService.sendProductOfferingCreateCallback(notification.getEvent()); + + logger.info("Published product offering create notification for product offering ID: {}", productOffering.getUuid()); + } catch (Exception e) { + logger.error("Error publishing product offering create notification for product offering ID: {}", productOffering.getUuid(), e); + } + } + + /** + * Publish a product offering delete notification + * @param productOffering The deleted product offering + */ + public void publishProductOfferingDeleteNotification(ProductOffering productOffering) { + try { + ProductOfferingDeleteNotification notification = createProductOfferingDeleteNotification(productOffering); + eventPublisher.publishEvent(notification, productOffering.getUuid()); + + // Send callbacks to registered subscribers + productOfferingCallbackService.sendProductOfferingDeleteCallback(notification.getEvent()); + + logger.info("Published product offering delete notification for product offering ID: {}", productOffering.getUuid()); + } catch (Exception e) { + logger.error("Error publishing product offering delete notification for product offering ID: {}", productOffering.getUuid(), e); + } + } + + /** + * Publish a product offering attribute value change notification + * @param productOffering The product offering with changed attributes + */ + public void publishProductOfferingAttributeValueChangeNotification(ProductOffering productOffering) { + try { + ProductOfferingAttributeValueChangeNotification notification = createProductOfferingAttributeValueChangeNotification(productOffering); + eventPublisher.publishEvent(notification, productOffering.getUuid()); + + // Send callbacks to registered subscribers + productOfferingCallbackService.sendProductOfferingAttributeValueChangeCallback(notification.getEvent()); + + logger.info("Published product offering attribute value change notification for product offering ID: {}", productOffering.getUuid()); + } catch (Exception e) { + logger.error("Error publishing product offering attribute value change notification for product offering ID: {}", productOffering.getUuid(), e); + } + } + + /** + * Publish a product offering state change notification + * @param productOffering The product offering with changed state + */ + public void publishProductOfferingStateChangeNotification(ProductOffering productOffering) { + try { + ProductOfferingStateChangeNotification notification = createProductOfferingStateChangeNotification(productOffering); + eventPublisher.publishEvent(notification, productOffering.getUuid()); + + // Send callbacks to registered subscribers + productOfferingCallbackService.sendProductOfferingStateChangeCallback(notification.getEvent()); + + logger.info("Published product offering state change notification for product offering ID: {}", productOffering.getUuid()); + } catch (Exception e) { + logger.error("Error publishing product offering state change notification for product offering ID: {}", productOffering.getUuid(), e); + } + } + + /** + * Create a product offering create notification + * @param productOffering The created product offering + * @return ProductOfferingCreateNotification + */ + private ProductOfferingCreateNotification createProductOfferingCreateNotification(ProductOffering productOffering) { + ProductOfferingCreateNotification notification = new ProductOfferingCreateNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingCreateNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOffering/" + productOffering.getUuid()); + + // Create event + ProductOfferingCreateEvent event = new ProductOfferingCreateEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingCreateEvent"); + event.setTitle("Product Offering Create Event"); + event.setDescription("A product offering has been created"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingCreateEventPayload payload = new ProductOfferingCreateEventPayload(); + payload.setProductOffering(productOffering); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product offering delete notification + * @param productOffering The deleted product offering + * @return ProductOfferingDeleteNotification + */ + private ProductOfferingDeleteNotification createProductOfferingDeleteNotification(ProductOffering productOffering) { + ProductOfferingDeleteNotification notification = new ProductOfferingDeleteNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingDeleteNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOffering/" + productOffering.getUuid()); + + // Create event + ProductOfferingDeleteEvent event = new ProductOfferingDeleteEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingDeleteEvent"); + event.setTitle("Product Offering Delete Event"); + event.setDescription("A product offering has been deleted"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingDeleteEventPayload payload = new ProductOfferingDeleteEventPayload(); + payload.setProductOffering(productOffering); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product offering attribute value change notification + * @param productOffering The product offering with changed attributes + * @return ProductOfferingAttributeValueChangeNotification + */ + private ProductOfferingAttributeValueChangeNotification createProductOfferingAttributeValueChangeNotification(ProductOffering productOffering) { + ProductOfferingAttributeValueChangeNotification notification = new ProductOfferingAttributeValueChangeNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingAttributeValueChangeNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOffering/" + productOffering.getUuid()); + + // Create event + ProductOfferingAttributeValueChangeEvent event = new ProductOfferingAttributeValueChangeEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingAttributeValueChangeEvent"); + event.setTitle("Product Offering Attribute Value Change Event"); + event.setDescription("A product offering attribute value has been changed"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingAttributeValueChangeEventPayload payload = new ProductOfferingAttributeValueChangeEventPayload(); + payload.setProductOffering(productOffering); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product offering state change notification + * @param productOffering The product offering with changed state + * @return ProductOfferingStateChangeNotification + */ + private ProductOfferingStateChangeNotification createProductOfferingStateChangeNotification(ProductOffering productOffering) { + ProductOfferingStateChangeNotification notification = new ProductOfferingStateChangeNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingStateChangeNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOffering/" + productOffering.getUuid()); + + // Create event + ProductOfferingStateChangeEvent event = new ProductOfferingStateChangeEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingStateChangeEvent"); + event.setTitle("Product Offering State Change Event"); + event.setDescription("A product offering state has been changed"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingStateChangeEventPayload payload = new ProductOfferingStateChangeEventPayload(); + payload.setProductOffering(productOffering); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java index 942d0e7d..55d6c1b0 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java @@ -77,6 +77,9 @@ public class ProductOfferingRepoService { @Autowired ServiceSpecificationRepoService serviceSpecificationRepoService; + + @Autowired + private ProductOfferingNotificationService productOfferingNotificationService; private SessionFactory sessionFactory; @@ -101,8 +104,12 @@ public class ProductOfferingRepoService { serviceSpec = this.updateProductOfferingDataFromAPIcall(serviceSpec, serviceProductOffering); serviceSpec = this.prodsOfferingRepo.save(serviceSpec); + // Publish product offering create notification + if (productOfferingNotificationService != null) { + productOfferingNotificationService.publishProductOfferingCreateNotification(serviceSpec); + } - return this.prodsOfferingRepo.save(serviceSpec); + return serviceSpec; } public List findAll() { @@ -261,6 +268,12 @@ public class ProductOfferingRepoService { */ this.prodsOfferingRepo.delete(s); + + // Publish product offering delete notification + if (productOfferingNotificationService != null) { + productOfferingNotificationService.publishProductOfferingDeleteNotification(s); + } + return null; } @@ -273,14 +286,28 @@ public class ProductOfferingRepoService { if (s == null) { return null; } + + // Store original state for comparison + String originalLifecycleStatus = s.getLifecycleStatus(); + ProductOffering prodOff = s; prodOff = this.updateProductOfferingDataFromAPIcall(prodOff, aProductOffering); prodOff = this.prodsOfferingRepo.save(prodOff); + // Publish notifications + if (productOfferingNotificationService != null) { + // Always publish attribute value change notification on update + productOfferingNotificationService.publishProductOfferingAttributeValueChangeNotification(prodOff); + + // Publish state change notification if lifecycle status changed + if (originalLifecycleStatus != null && prodOff.getLifecycleStatus() != null + && !originalLifecycleStatus.equals(prodOff.getLifecycleStatus())) { + productOfferingNotificationService.publishProductOfferingStateChangeNotification(prodOff); + } + } - - return this.prodsOfferingRepo.save(prodOff); + return prodOff; } diff --git a/src/main/resources/application-testing.yml b/src/main/resources/application-testing.yml index decd660c..19366def 100644 --- a/src/main/resources/application-testing.yml +++ b/src/main/resources/application-testing.yml @@ -191,6 +191,10 @@ EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE" EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE" EVENT_PRODUCT_SPECIFICATION_CREATE: "jms:topic:EVENT.PRODUCTSPECIFICATION.CREATE" EVENT_PRODUCT_SPECIFICATION_DELETE: "jms:topic:EVENT.PRODUCTSPECIFICATION.DELETE" +EVENT_PRODUCT_OFFERING_CREATE: "jms:topic:EVENT.PRODUCTOFFERING.CREATE" +EVENT_PRODUCT_OFFERING_DELETE: "jms:topic:EVENT.PRODUCTOFFERING.DELETE" +EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.ATTRCHANGED" +EVENT_PRODUCT_OFFERING_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.STATECHANGED" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 25d47524..66995d33 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -216,6 +216,10 @@ EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE" EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE" EVENT_PRODUCT_SPECIFICATION_CREATE: "jms:topic:EVENT.PRODUCTSPECIFICATION.CREATE" EVENT_PRODUCT_SPECIFICATION_DELETE: "jms:topic:EVENT.PRODUCTSPECIFICATION.DELETE" +EVENT_PRODUCT_OFFERING_CREATE: "jms:topic:EVENT.PRODUCTOFFERING.CREATE" +EVENT_PRODUCT_OFFERING_DELETE: "jms:topic:EVENT.PRODUCTOFFERING.DELETE" +EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.ATTRCHANGED" +EVENT_PRODUCT_OFFERING_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.STATECHANGED" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java new file mode 100644 index 00000000..1755896d --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java @@ -0,0 +1,254 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.etsi.osl.tmf.JsonUtils; +import org.etsi.osl.tmf.OpenAPISpringBoot; +import org.etsi.osl.tmf.pcm620.model.ProductOffering; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreate; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingUpdate; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RunWith(SpringRunner.class) +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class) +@AutoConfigureMockMvc +@ActiveProfiles("testing") +@AutoConfigureTestDatabase +public class ProductOfferingCallbackIntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private ProductOfferingRepoService productOfferingRepoService; + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @SpyBean + private ProductOfferingCallbackService productOfferingCallbackService; + + @MockBean + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + mvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + + // Mock RestTemplate to avoid actual HTTP calls in tests + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCompleteCallbackFlow() throws Exception { + // Step 1: Register a callback subscription via Hub API + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:8080/test-callback"); + subscriptionInput.setQuery("productoffering.create,productoffering.delete"); + + MvcResult subscriptionResult = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()) + .andReturn(); + + String subscriptionResponseBody = subscriptionResult.getResponse().getContentAsString(); + EventSubscription createdSubscription = objectMapper.readValue(subscriptionResponseBody, EventSubscription.class); + + // Step 2: Create a product offering (should trigger callback) + ProductOfferingCreate productOfferingCreate = new ProductOfferingCreate(); + productOfferingCreate.setName("Test Callback Product Offering"); + productOfferingCreate.setDescription("A product offering to test callback notifications"); + productOfferingCreate.setVersion("1.0"); + + ProductOffering createdProductOffering = productOfferingRepoService.addProductOffering(productOfferingCreate); + + // Step 3: Verify callback was sent + verify(productOfferingCallbackService, timeout(2000)).sendProductOfferingCreateCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Step 4: Delete the product offering (should trigger delete callback) + productOfferingRepoService.deleteByUuid(createdProductOffering.getUuid()); + + // Step 5: Verify delete callback was sent + verify(productOfferingCallbackService, timeout(2000)).sendProductOfferingDeleteCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/productOfferingDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testAttributeValueChangeAndStateChangeCallbacks() throws Exception { + // Step 1: Register subscription for attribute and state change events + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:8080/change-callback"); + subscriptionInput.setQuery("productoffering.attributevaluechange,productoffering.statechange"); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create a product offering + ProductOfferingCreate productOfferingCreate = new ProductOfferingCreate(); + productOfferingCreate.setName("Test Change Product Offering"); + productOfferingCreate.setDescription("A product offering to test change notifications"); + productOfferingCreate.setVersion("1.0"); + productOfferingCreate.setLifecycleStatus("Active"); + + ProductOffering createdProductOffering = productOfferingRepoService.addProductOffering(productOfferingCreate); + + // Step 3: Update the product offering (should trigger attribute value change callback) + ProductOfferingUpdate productOfferingUpdate = new ProductOfferingUpdate(); + productOfferingUpdate.setDescription("Updated description for testing"); + productOfferingUpdate.setLifecycleStatus("Retired"); + + productOfferingRepoService.updateProductOffering(createdProductOffering.getUuid(), productOfferingUpdate); + + // Step 4: Verify both attribute value change and state change callbacks were sent + verify(productOfferingCallbackService, timeout(2000)).sendProductOfferingAttributeValueChangeCallback(any()); + verify(productOfferingCallbackService, timeout(2000)).sendProductOfferingStateChangeCallback(any()); + + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/change-callback/listener/productOfferingAttributeValueChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/change-callback/listener/productOfferingStateChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCallbackFilteringByEventType() throws Exception { + // Step 1: Register subscription only for create events + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:9090/create-only"); + subscriptionInput.setQuery("productoffering.create"); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create and delete a product offering + ProductOfferingCreate productOfferingCreate = new ProductOfferingCreate(); + productOfferingCreate.setName("Test Filter Product Offering"); + productOfferingCreate.setDescription("A product offering to test query filtering"); + productOfferingCreate.setVersion("1.0"); + + ProductOffering createdProductOffering = productOfferingRepoService.addProductOffering(productOfferingCreate); + productOfferingRepoService.deleteByUuid(createdProductOffering.getUuid()); + + // Step 3: Verify only create callback was sent (not delete) + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:9090/create-only/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Note: In a more sophisticated test, we could verify that the delete callback was NOT sent + // by using verify with never(), but this requires more complex mock setup + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testProductOfferingCallbackWithAllEventsQuery() throws Exception { + // Step 1: Register subscription for all events (empty query) + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:7070/all-events"); + subscriptionInput.setQuery(""); // Empty query should receive all events + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create a product offering + ProductOfferingCreate productOfferingCreate = new ProductOfferingCreate(); + productOfferingCreate.setName("Test All Events Product Offering"); + productOfferingCreate.setDescription("A product offering to test all events subscription"); + productOfferingCreate.setVersion("1.0"); + + ProductOffering createdProductOffering = productOfferingRepoService.addProductOffering(productOfferingCreate); + + // Step 3: Verify callback was sent even with empty query + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:7070/all-events/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackServiceTest.java new file mode 100644 index 00000000..40875e4a --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackServiceTest.java @@ -0,0 +1,258 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.ProductOffering; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class ProductOfferingCallbackServiceTest { + + @Mock + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private ProductOfferingCallbackService productOfferingCallbackService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testSendProductOfferingCreateCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productoffering"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingCreateEvent event = new ProductOfferingCreateEvent(); + event.setEventId("test-event-123"); + + // Act + productOfferingCallbackService.sendProductOfferingCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductOfferingDeleteCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productoffering"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingDeleteEvent event = new ProductOfferingDeleteEvent(); + event.setEventId("test-event-456"); + + // Act + productOfferingCallbackService.sendProductOfferingDeleteCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductOfferingAttributeValueChangeCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productoffering.attributevaluechange"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingAttributeValueChangeEvent event = new ProductOfferingAttributeValueChangeEvent(); + event.setEventId("test-event-789"); + + // Act + productOfferingCallbackService.sendProductOfferingAttributeValueChangeCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingAttributeValueChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductOfferingStateChangeCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productoffering.statechange"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingStateChangeEvent event = new ProductOfferingStateChangeEvent(); + event.setEventId("test-event-101"); + + // Act + productOfferingCallbackService.sendProductOfferingStateChangeCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingStateChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testCallbackWithTrailingSlash() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback/"); + subscription.setQuery("productoffering"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingCreateEvent event = new ProductOfferingCreateEvent(); + event.setEventId("test-event-trailing-slash"); + + // Act + productOfferingCallbackService.sendProductOfferingCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testFilterSubscriptionsByQuery() { + // Arrange + EventSubscription productOfferingSubscription = new EventSubscription(); + productOfferingSubscription.setCallback("http://localhost:8080/productoffering-callback"); + productOfferingSubscription.setQuery("productoffering"); + + EventSubscription otherSubscription = new EventSubscription(); + otherSubscription.setCallback("http://localhost:8080/other-callback"); + otherSubscription.setQuery("catalog"); + + List subscriptions = Arrays.asList(productOfferingSubscription, otherSubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingCreateEvent event = new ProductOfferingCreateEvent(); + event.setEventId("test-event-filter"); + + // Act + productOfferingCallbackService.sendProductOfferingCreateCallback(event); + + // Assert - only product offering subscription should receive callback + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/productoffering-callback/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSpecificEventTypeQueries() { + // Arrange + EventSubscription createOnlySubscription = new EventSubscription(); + createOnlySubscription.setCallback("http://localhost:9090/create-only"); + createOnlySubscription.setQuery("productoffering.create"); + + EventSubscription stateChangeOnlySubscription = new EventSubscription(); + stateChangeOnlySubscription.setCallback("http://localhost:9091/state-change-only"); + stateChangeOnlySubscription.setQuery("productoffering.statechange"); + + List subscriptions = Arrays.asList(createOnlySubscription, stateChangeOnlySubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingCreateEvent createEvent = new ProductOfferingCreateEvent(); + createEvent.setEventId("test-create-event"); + + ProductOfferingStateChangeEvent stateChangeEvent = new ProductOfferingStateChangeEvent(); + stateChangeEvent.setEventId("test-state-change-event"); + + // Act + productOfferingCallbackService.sendProductOfferingCreateCallback(createEvent); + productOfferingCallbackService.sendProductOfferingStateChangeCallback(stateChangeEvent); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:9090/create-only/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + verify(restTemplate, times(1)).exchange( + eq("http://localhost:9091/state-change-only/listener/productOfferingStateChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingNotificationServiceTest.java new file mode 100644 index 00000000..037f60ce --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingNotificationServiceTest.java @@ -0,0 +1,121 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.ProductOffering; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeNotification; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingNotificationService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingCallbackService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class ProductOfferingNotificationServiceTest { + + @Mock + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Mock + private ProductOfferingCallbackService productOfferingCallbackService; + + @InjectMocks + private ProductOfferingNotificationService productOfferingNotificationService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testPublishProductOfferingCreateNotification() { + // Arrange + ProductOffering productOffering = new ProductOffering(); + productOffering.setUuid("test-productoffering-123"); + productOffering.setName("Test Product Offering"); + productOffering.setDescription("A test product offering for notifications"); + + // Act + productOfferingNotificationService.publishProductOfferingCreateNotification(productOffering); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingCreateNotification.class), eq("test-productoffering-123")); + verify(productOfferingCallbackService, times(1)).sendProductOfferingCreateCallback(any()); + } + + @Test + public void testPublishProductOfferingDeleteNotification() { + // Arrange + ProductOffering productOffering = new ProductOffering(); + productOffering.setUuid("test-productoffering-456"); + productOffering.setName("Test Product Offering to Delete"); + productOffering.setDescription("A test product offering for delete notifications"); + + // Act + productOfferingNotificationService.publishProductOfferingDeleteNotification(productOffering); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingDeleteNotification.class), eq("test-productoffering-456")); + verify(productOfferingCallbackService, times(1)).sendProductOfferingDeleteCallback(any()); + } + + @Test + public void testPublishProductOfferingAttributeValueChangeNotification() { + // Arrange + ProductOffering productOffering = new ProductOffering(); + productOffering.setUuid("test-productoffering-789"); + productOffering.setName("Test Product Offering Attribute Change"); + productOffering.setDescription("A test product offering for attribute change notifications"); + + // Act + productOfferingNotificationService.publishProductOfferingAttributeValueChangeNotification(productOffering); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingAttributeValueChangeNotification.class), eq("test-productoffering-789")); + verify(productOfferingCallbackService, times(1)).sendProductOfferingAttributeValueChangeCallback(any()); + } + + @Test + public void testPublishProductOfferingStateChangeNotification() { + // Arrange + ProductOffering productOffering = new ProductOffering(); + productOffering.setUuid("test-productoffering-101"); + productOffering.setName("Test Product Offering State Change"); + productOffering.setDescription("A test product offering for state change notifications"); + + // Act + productOfferingNotificationService.publishProductOfferingStateChangeNotification(productOffering); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingStateChangeNotification.class), eq("test-productoffering-101")); + verify(productOfferingCallbackService, times(1)).sendProductOfferingStateChangeCallback(any()); + } + + @Test + public void testCreateNotificationStructure() { + // Arrange + ProductOffering productOffering = new ProductOffering(); + productOffering.setUuid("test-productoffering-structure"); + productOffering.setName("Test Product Offering Structure"); + + // Act + productOfferingNotificationService.publishProductOfferingCreateNotification(productOffering); + + // Assert - verify the notification was published with correct structure + verify(eventPublisher).publishEvent(any(ProductOfferingCreateNotification.class), eq("test-productoffering-structure")); + verify(productOfferingCallbackService).sendProductOfferingCreateCallback(any()); + } +} \ No newline at end of file -- GitLab From 95ce8537345a1a9bea9857f8d059ecbf8a4ec155 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 10:44:39 +0300 Subject: [PATCH 06/21] adding productofferringPrice events --- .../ProductCatalogApiRouteBuilderEvents.java | 46 +++- .../ProductOfferingPriceCallbackService.java | 238 ++++++++++++++++ ...oductOfferingPriceNotificationService.java | 257 +++++++++++++++++ .../ProductOfferingPriceRepoService.java | 27 +- src/main/resources/application-testing.yml | 4 + src/main/resources/application.yml | 4 + ...oductOfferingPriceCallbackServiceTest.java | 258 ++++++++++++++++++ ...tOfferingPriceNotificationServiceTest.java | 121 ++++++++ 8 files changed, 943 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceCallbackService.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceNotificationService.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceCallbackServiceTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceNotificationServiceTest.java diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java index c0828283..746ac4f7 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -43,6 +43,10 @@ import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateNotification; import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteNotification; import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeNotification; import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeNotification; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -56,34 +60,46 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { private static final transient Log logger = LogFactory.getLog(ProductCatalogApiRouteBuilderEvents.class.getName()); @Value("${EVENT_PRODUCT_CATALOG_CREATE}") - private String EVENT_CATALOG_CREATE = "direct:EVENT_CATALOG_CREATE"; + private String EVENT_CATALOG_CREATE = ""; @Value("${EVENT_PRODUCT_CATALOG_DELETE}") - private String EVENT_CATALOG_DELETE = "direct:EVENT_CATALOG_DELETE"; + private String EVENT_CATALOG_DELETE = ""; @Value("${EVENT_PRODUCT_CATEGORY_CREATE}") - private String EVENT_CATEGORY_CREATE = "direct:EVENT_CATEGORY_CREATE"; + private String EVENT_CATEGORY_CREATE = ""; @Value("${EVENT_PRODUCT_CATEGORY_DELETE}") - private String EVENT_CATEGORY_DELETE = "direct:EVENT_CATEGORY_DELETE"; + private String EVENT_CATEGORY_DELETE = ""; @Value("${EVENT_PRODUCT_SPECIFICATION_CREATE}") - private String EVENT_PRODUCT_SPECIFICATION_CREATE = "direct:EVENT_PRODUCT_SPECIFICATION_CREATE"; + private String EVENT_PRODUCT_SPECIFICATION_CREATE = ""; @Value("${EVENT_PRODUCT_SPECIFICATION_DELETE}") - private String EVENT_PRODUCT_SPECIFICATION_DELETE = "direct:EVENT_PRODUCT_SPECIFICATION_DELETE"; + private String EVENT_PRODUCT_SPECIFICATION_DELETE = ""; @Value("${EVENT_PRODUCT_OFFERING_CREATE}") - private String EVENT_PRODUCT_OFFERING_CREATE = "direct:EVENT_PRODUCT_OFFERING_CREATE"; + private String EVENT_PRODUCT_OFFERING_CREATE = ""; @Value("${EVENT_PRODUCT_OFFERING_DELETE}") - private String EVENT_PRODUCT_OFFERING_DELETE = "direct:EVENT_PRODUCT_OFFERING_DELETE"; + private String EVENT_PRODUCT_OFFERING_DELETE = ""; @Value("${EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE}") - private String EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE = "direct:EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE"; + private String EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE = ""; @Value("${EVENT_PRODUCT_OFFERING_STATE_CHANGE}") - private String EVENT_PRODUCT_OFFERING_STATE_CHANGE = "direct:EVENT_PRODUCT_OFFERING_STATE_CHANGE"; + private String EVENT_PRODUCT_OFFERING_STATE_CHANGE = ""; + + @Value("${EVENT_PRODUCT_OFFERING_PRICE_CREATE}") + private String EVENT_PRODUCT_OFFERING_PRICE_CREATE = ""; + + @Value("${EVENT_PRODUCT_OFFERING_PRICE_DELETE}") + private String EVENT_PRODUCT_OFFERING_PRICE_DELETE = ""; + + @Value("${EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE}") + private String EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE = ""; + + @Value("${EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE}") + private String EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE = ""; @Value("${spring.application.name}") private String compname; @@ -130,7 +146,15 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { } else if (n instanceof ProductOfferingAttributeValueChangeNotification) { msgtopic = EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE; } else if (n instanceof ProductOfferingStateChangeNotification) { - msgtopic = EVENT_PRODUCT_OFFERING_STATE_CHANGE; + msgtopic = EVENT_PRODUCT_OFFERING_STATE_CHANGE; + } else if (n instanceof ProductOfferingPriceCreateNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_PRICE_CREATE; + } else if (n instanceof ProductOfferingPriceDeleteNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_PRICE_DELETE; + } else if (n instanceof ProductOfferingPriceAttributeValueChangeNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE; + } else if (n instanceof ProductOfferingPriceStateChangeNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE; } Map map = new HashMap<>(); diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceCallbackService.java new file mode 100644 index 00000000..f1533850 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceCallbackService.java @@ -0,0 +1,238 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class ProductOfferingPriceCallbackService { + + private static final Logger logger = LoggerFactory.getLogger(ProductOfferingPriceCallbackService.class); + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Autowired + private RestTemplate restTemplate; + + /** + * Send product offering price create event to all registered callback URLs + * @param productOfferingPriceCreateEvent The product offering price create event to send + */ + public void sendProductOfferingPriceCreateCallback(ProductOfferingPriceCreateEvent productOfferingPriceCreateEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingPriceCreateEvent")) { + sendProductOfferingPriceCreateEventToCallback(subscription.getCallback(), productOfferingPriceCreateEvent); + } + } + } + + /** + * Send product offering price delete event to all registered callback URLs + * @param productOfferingPriceDeleteEvent The product offering price delete event to send + */ + public void sendProductOfferingPriceDeleteCallback(ProductOfferingPriceDeleteEvent productOfferingPriceDeleteEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingPriceDeleteEvent")) { + sendProductOfferingPriceDeleteEventToCallback(subscription.getCallback(), productOfferingPriceDeleteEvent); + } + } + } + + /** + * Send product offering price attribute value change event to all registered callback URLs + * @param productOfferingPriceAttributeValueChangeEvent The product offering price attribute value change event to send + */ + public void sendProductOfferingPriceAttributeValueChangeCallback(ProductOfferingPriceAttributeValueChangeEvent productOfferingPriceAttributeValueChangeEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingPriceAttributeValueChangeEvent")) { + sendProductOfferingPriceAttributeValueChangeEventToCallback(subscription.getCallback(), productOfferingPriceAttributeValueChangeEvent); + } + } + } + + /** + * Send product offering price state change event to all registered callback URLs + * @param productOfferingPriceStateChangeEvent The product offering price state change event to send + */ + public void sendProductOfferingPriceStateChangeCallback(ProductOfferingPriceStateChangeEvent productOfferingPriceStateChangeEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingPriceStateChangeEvent")) { + sendProductOfferingPriceStateChangeEventToCallback(subscription.getCallback(), productOfferingPriceStateChangeEvent); + } + } + } + + /** + * Send product offering price create event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering price create event + */ + private void sendProductOfferingPriceCreateEventToCallback(String callbackUrl, ProductOfferingPriceCreateEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingPriceCreateEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering price create event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering price create event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product offering price delete event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering price delete event + */ + private void sendProductOfferingPriceDeleteEventToCallback(String callbackUrl, ProductOfferingPriceDeleteEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingPriceDeleteEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering price delete event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering price delete event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product offering price attribute value change event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering price attribute value change event + */ + private void sendProductOfferingPriceAttributeValueChangeEventToCallback(String callbackUrl, ProductOfferingPriceAttributeValueChangeEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingPriceAttributeValueChangeEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering price attribute value change event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering price attribute value change event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product offering price state change event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering price state change event + */ + private void sendProductOfferingPriceStateChangeEventToCallback(String callbackUrl, ProductOfferingPriceStateChangeEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingPriceStateChangeEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering price state change event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering price state change event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Build the full callback URL with the listener endpoint + * @param baseUrl The base callback URL + * @param listenerPath The listener path to append + * @return The complete callback URL + */ + private String buildCallbackUrl(String baseUrl, String listenerPath) { + if (baseUrl.endsWith("/")) { + return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath; + } else { + return baseUrl + listenerPath; + } + } + + /** + * Check if a subscription should be notified for a specific event type + * @param subscription The event subscription + * @param eventType The event type to check + * @return true if the subscription should be notified + */ + private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) { + // If no query is specified, notify all events + if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) { + return true; + } + + // Check if the query contains the event type + String query = subscription.getQuery().toLowerCase(); + return query.contains("productofferingprice") || + query.contains(eventType.toLowerCase()) || + query.contains("productofferingprice.create") || + query.contains("productofferingprice.delete") || + query.contains("productofferingprice.attributevaluechange") || + query.contains("productofferingprice.statechange"); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceNotificationService.java new file mode 100644 index 00000000..336c51e6 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceNotificationService.java @@ -0,0 +1,257 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPrice; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeNotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class ProductOfferingPriceNotificationService { + + private static final Logger logger = LoggerFactory.getLogger(ProductOfferingPriceNotificationService.class); + + @Autowired + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Autowired + private ProductOfferingPriceCallbackService productOfferingPriceCallbackService; + + /** + * Publish a product offering price create notification + * @param productOfferingPrice The created product offering price + */ + public void publishProductOfferingPriceCreateNotification(ProductOfferingPrice productOfferingPrice) { + try { + ProductOfferingPriceCreateNotification notification = createProductOfferingPriceCreateNotification(productOfferingPrice); + eventPublisher.publishEvent(notification, productOfferingPrice.getId()); + + // Send callbacks to registered subscribers + productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(notification.getEvent()); + + logger.info("Published product offering price create notification for product offering price ID: {}", productOfferingPrice.getId()); + } catch (Exception e) { + logger.error("Error publishing product offering price create notification for product offering price ID: {}", productOfferingPrice.getId(), e); + } + } + + /** + * Publish a product offering price delete notification + * @param productOfferingPrice The deleted product offering price + */ + public void publishProductOfferingPriceDeleteNotification(ProductOfferingPrice productOfferingPrice) { + try { + ProductOfferingPriceDeleteNotification notification = createProductOfferingPriceDeleteNotification(productOfferingPrice); + eventPublisher.publishEvent(notification, productOfferingPrice.getId()); + + // Send callbacks to registered subscribers + productOfferingPriceCallbackService.sendProductOfferingPriceDeleteCallback(notification.getEvent()); + + logger.info("Published product offering price delete notification for product offering price ID: {}", productOfferingPrice.getId()); + } catch (Exception e) { + logger.error("Error publishing product offering price delete notification for product offering price ID: {}", productOfferingPrice.getId(), e); + } + } + + /** + * Publish a product offering price attribute value change notification + * @param productOfferingPrice The product offering price with changed attributes + */ + public void publishProductOfferingPriceAttributeValueChangeNotification(ProductOfferingPrice productOfferingPrice) { + try { + ProductOfferingPriceAttributeValueChangeNotification notification = createProductOfferingPriceAttributeValueChangeNotification(productOfferingPrice); + eventPublisher.publishEvent(notification, productOfferingPrice.getId()); + + // Send callbacks to registered subscribers + productOfferingPriceCallbackService.sendProductOfferingPriceAttributeValueChangeCallback(notification.getEvent()); + + logger.info("Published product offering price attribute value change notification for product offering price ID: {}", productOfferingPrice.getId()); + } catch (Exception e) { + logger.error("Error publishing product offering price attribute value change notification for product offering price ID: {}", productOfferingPrice.getId(), e); + } + } + + /** + * Publish a product offering price state change notification + * @param productOfferingPrice The product offering price with changed state + */ + public void publishProductOfferingPriceStateChangeNotification(ProductOfferingPrice productOfferingPrice) { + try { + ProductOfferingPriceStateChangeNotification notification = createProductOfferingPriceStateChangeNotification(productOfferingPrice); + eventPublisher.publishEvent(notification, productOfferingPrice.getId()); + + // Send callbacks to registered subscribers + productOfferingPriceCallbackService.sendProductOfferingPriceStateChangeCallback(notification.getEvent()); + + logger.info("Published product offering price state change notification for product offering price ID: {}", productOfferingPrice.getId()); + } catch (Exception e) { + logger.error("Error publishing product offering price state change notification for product offering price ID: {}", productOfferingPrice.getId(), e); + } + } + + /** + * Create a product offering price create notification + * @param productOfferingPrice The created product offering price + * @return ProductOfferingPriceCreateNotification + */ + private ProductOfferingPriceCreateNotification createProductOfferingPriceCreateNotification(ProductOfferingPrice productOfferingPrice) { + ProductOfferingPriceCreateNotification notification = new ProductOfferingPriceCreateNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingPriceCreateNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOfferingPrice/" + productOfferingPrice.getId()); + + // Create event + ProductOfferingPriceCreateEvent event = new ProductOfferingPriceCreateEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingPriceCreateEvent"); + event.setTitle("Product Offering Price Create Event"); + event.setDescription("A product offering price has been created"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingPriceCreateEventPayload payload = new ProductOfferingPriceCreateEventPayload(); + payload.setProductOfferingPrice(productOfferingPrice); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product offering price delete notification + * @param productOfferingPrice The deleted product offering price + * @return ProductOfferingPriceDeleteNotification + */ + private ProductOfferingPriceDeleteNotification createProductOfferingPriceDeleteNotification(ProductOfferingPrice productOfferingPrice) { + ProductOfferingPriceDeleteNotification notification = new ProductOfferingPriceDeleteNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingPriceDeleteNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOfferingPrice/" + productOfferingPrice.getId()); + + // Create event + ProductOfferingPriceDeleteEvent event = new ProductOfferingPriceDeleteEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingPriceDeleteEvent"); + event.setTitle("Product Offering Price Delete Event"); + event.setDescription("A product offering price has been deleted"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingPriceDeleteEventPayload payload = new ProductOfferingPriceDeleteEventPayload(); + payload.setProductOfferingPrice(productOfferingPrice); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product offering price attribute value change notification + * @param productOfferingPrice The product offering price with changed attributes + * @return ProductOfferingPriceAttributeValueChangeNotification + */ + private ProductOfferingPriceAttributeValueChangeNotification createProductOfferingPriceAttributeValueChangeNotification(ProductOfferingPrice productOfferingPrice) { + ProductOfferingPriceAttributeValueChangeNotification notification = new ProductOfferingPriceAttributeValueChangeNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingPriceAttributeValueChangeNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOfferingPrice/" + productOfferingPrice.getId()); + + // Create event + ProductOfferingPriceAttributeValueChangeEvent event = new ProductOfferingPriceAttributeValueChangeEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingPriceAttributeValueChangeEvent"); + event.setTitle("Product Offering Price Attribute Value Change Event"); + event.setDescription("A product offering price attribute value has been changed"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingPriceAttributeValueChangeEventPayload payload = new ProductOfferingPriceAttributeValueChangeEventPayload(); + payload.setProductOfferingPrice(productOfferingPrice); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product offering price state change notification + * @param productOfferingPrice The product offering price with changed state + * @return ProductOfferingPriceStateChangeNotification + */ + private ProductOfferingPriceStateChangeNotification createProductOfferingPriceStateChangeNotification(ProductOfferingPrice productOfferingPrice) { + ProductOfferingPriceStateChangeNotification notification = new ProductOfferingPriceStateChangeNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingPriceStateChangeNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOfferingPrice/" + productOfferingPrice.getId()); + + // Create event + ProductOfferingPriceStateChangeEvent event = new ProductOfferingPriceStateChangeEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingPriceStateChangeEvent"); + event.setTitle("Product Offering Price State Change Event"); + event.setDescription("A product offering price state has been changed"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingPriceStateChangeEventPayload payload = new ProductOfferingPriceStateChangeEventPayload(); + payload.setProductOfferingPrice(productOfferingPrice); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceRepoService.java index bffff81e..3ad54021 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceRepoService.java @@ -55,6 +55,9 @@ public class ProductOfferingPriceRepoService { @Autowired ProductOfferingPriceRepository prodsOfferingRepo; + @Autowired + ProductOfferingPriceNotificationService productOfferingPriceNotificationService; + private SessionFactory sessionFactory; @@ -80,6 +83,10 @@ public class ProductOfferingPriceRepoService { serviceSpec = this.updateProductOfferingPriceDataFromAPIcall(serviceSpec, serviceProductOfferingPrice); serviceSpec = this.prodsOfferingRepo.save(serviceSpec); + // Publish create notification + if (productOfferingPriceNotificationService != null) { + productOfferingPriceNotificationService.publishProductOfferingPriceCreateNotification(serviceSpec); + } return this.prodsOfferingRepo.save(serviceSpec); } @@ -231,6 +238,11 @@ public class ProductOfferingPriceRepoService { * prior deleting we need to delete other dependency objects */ + // Publish delete notification before actual deletion + if (productOfferingPriceNotificationService != null) { + productOfferingPriceNotificationService.publishProductOfferingPriceDeleteNotification(s); + } + this.prodsOfferingRepo.delete(s); return null; } @@ -244,12 +256,25 @@ public class ProductOfferingPriceRepoService { if (s == null) { return null; } + + // Store original state for comparison + String originalLifecycleStatus = s.getLifecycleStatus(); + ProductOfferingPrice prodOff = s; prodOff = this.updateProductOfferingPriceDataFromAPIcall(prodOff, aProductOfferingPrice); prodOff = this.prodsOfferingRepo.save(prodOff); - + // Publish notifications + if (productOfferingPriceNotificationService != null) { + // Always publish attribute value change notification for updates + productOfferingPriceNotificationService.publishProductOfferingPriceAttributeValueChangeNotification(prodOff); + + // Check for state change and publish state change notification if needed + if (originalLifecycleStatus != null && !originalLifecycleStatus.equals(prodOff.getLifecycleStatus())) { + productOfferingPriceNotificationService.publishProductOfferingPriceStateChangeNotification(prodOff); + } + } return this.prodsOfferingRepo.save(prodOff); diff --git a/src/main/resources/application-testing.yml b/src/main/resources/application-testing.yml index 19366def..1c08e30a 100644 --- a/src/main/resources/application-testing.yml +++ b/src/main/resources/application-testing.yml @@ -195,6 +195,10 @@ EVENT_PRODUCT_OFFERING_CREATE: "jms:topic:EVENT.PRODUCTOFFERING.CREATE" EVENT_PRODUCT_OFFERING_DELETE: "jms:topic:EVENT.PRODUCTOFFERING.DELETE" EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.ATTRCHANGED" EVENT_PRODUCT_OFFERING_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.STATECHANGED" +EVENT_PRODUCT_OFFERING_PRICE_CREATE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.CREATE" +EVENT_PRODUCT_OFFERING_PRICE_DELETE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.DELETE" +EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.ATTRCHANGED" +EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.STATECHANGED" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 66995d33..2cf3438a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -220,6 +220,10 @@ EVENT_PRODUCT_OFFERING_CREATE: "jms:topic:EVENT.PRODUCTOFFERING.CREATE" EVENT_PRODUCT_OFFERING_DELETE: "jms:topic:EVENT.PRODUCTOFFERING.DELETE" EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.ATTRCHANGED" EVENT_PRODUCT_OFFERING_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.STATECHANGED" +EVENT_PRODUCT_OFFERING_PRICE_CREATE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.CREATE" +EVENT_PRODUCT_OFFERING_PRICE_DELETE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.DELETE" +EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.ATTRCHANGED" +EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.STATECHANGED" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceCallbackServiceTest.java new file mode 100644 index 00000000..8e8b04a2 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceCallbackServiceTest.java @@ -0,0 +1,258 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPrice; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingPriceCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class ProductOfferingPriceCallbackServiceTest { + + @Mock + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private ProductOfferingPriceCallbackService productOfferingPriceCallbackService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testSendProductOfferingPriceCreateCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productofferingprice"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceCreateEvent event = new ProductOfferingPriceCreateEvent(); + event.setEventId("test-event-123"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingPriceCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductOfferingPriceDeleteCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productofferingprice"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceDeleteEvent event = new ProductOfferingPriceDeleteEvent(); + event.setEventId("test-event-456"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceDeleteCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingPriceDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductOfferingPriceAttributeValueChangeCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productofferingprice.attributevaluechange"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceAttributeValueChangeEvent event = new ProductOfferingPriceAttributeValueChangeEvent(); + event.setEventId("test-event-789"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceAttributeValueChangeCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingPriceAttributeValueChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductOfferingPriceStateChangeCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productofferingprice.statechange"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceStateChangeEvent event = new ProductOfferingPriceStateChangeEvent(); + event.setEventId("test-event-101"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceStateChangeCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingPriceStateChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testCallbackWithTrailingSlash() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback/"); + subscription.setQuery("productofferingprice"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceCreateEvent event = new ProductOfferingPriceCreateEvent(); + event.setEventId("test-event-trailing-slash"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingPriceCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testFilterSubscriptionsByQuery() { + // Arrange + EventSubscription productOfferingPriceSubscription = new EventSubscription(); + productOfferingPriceSubscription.setCallback("http://localhost:8080/productofferingprice-callback"); + productOfferingPriceSubscription.setQuery("productofferingprice"); + + EventSubscription otherSubscription = new EventSubscription(); + otherSubscription.setCallback("http://localhost:8080/other-callback"); + otherSubscription.setQuery("catalog"); + + List subscriptions = Arrays.asList(productOfferingPriceSubscription, otherSubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceCreateEvent event = new ProductOfferingPriceCreateEvent(); + event.setEventId("test-event-filter"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(event); + + // Assert - only product offering price subscription should receive callback + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/productofferingprice-callback/listener/productOfferingPriceCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSpecificEventTypeQueries() { + // Arrange + EventSubscription createOnlySubscription = new EventSubscription(); + createOnlySubscription.setCallback("http://localhost:9090/create-only"); + createOnlySubscription.setQuery("productofferingprice.create"); + + EventSubscription stateChangeOnlySubscription = new EventSubscription(); + stateChangeOnlySubscription.setCallback("http://localhost:9091/state-change-only"); + stateChangeOnlySubscription.setQuery("productofferingprice.statechange"); + + List subscriptions = Arrays.asList(createOnlySubscription, stateChangeOnlySubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceCreateEvent createEvent = new ProductOfferingPriceCreateEvent(); + createEvent.setEventId("test-create-event"); + + ProductOfferingPriceStateChangeEvent stateChangeEvent = new ProductOfferingPriceStateChangeEvent(); + stateChangeEvent.setEventId("test-state-change-event"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(createEvent); + productOfferingPriceCallbackService.sendProductOfferingPriceStateChangeCallback(stateChangeEvent); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:9090/create-only/listener/productOfferingPriceCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + verify(restTemplate, times(1)).exchange( + eq("http://localhost:9091/state-change-only/listener/productOfferingPriceStateChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceNotificationServiceTest.java new file mode 100644 index 00000000..e31a0b8c --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceNotificationServiceTest.java @@ -0,0 +1,121 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPrice; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeNotification; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingPriceNotificationService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingPriceCallbackService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class ProductOfferingPriceNotificationServiceTest { + + @Mock + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Mock + private ProductOfferingPriceCallbackService productOfferingPriceCallbackService; + + @InjectMocks + private ProductOfferingPriceNotificationService productOfferingPriceNotificationService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testPublishProductOfferingPriceCreateNotification() { + // Arrange + ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice(); + productOfferingPrice.setUuid("test-productofferingprice-123"); + productOfferingPrice.setName("Test Product Offering Price"); + productOfferingPrice.setDescription("A test product offering price for notifications"); + + // Act + productOfferingPriceNotificationService.publishProductOfferingPriceCreateNotification(productOfferingPrice); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingPriceCreateNotification.class), eq("test-productofferingprice-123")); + verify(productOfferingPriceCallbackService, times(1)).sendProductOfferingPriceCreateCallback(any()); + } + + @Test + public void testPublishProductOfferingPriceDeleteNotification() { + // Arrange + ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice(); + productOfferingPrice.setUuid("test-productofferingprice-456"); + productOfferingPrice.setName("Test Product Offering Price to Delete"); + productOfferingPrice.setDescription("A test product offering price for delete notifications"); + + // Act + productOfferingPriceNotificationService.publishProductOfferingPriceDeleteNotification(productOfferingPrice); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingPriceDeleteNotification.class), eq("test-productofferingprice-456")); + verify(productOfferingPriceCallbackService, times(1)).sendProductOfferingPriceDeleteCallback(any()); + } + + @Test + public void testPublishProductOfferingPriceAttributeValueChangeNotification() { + // Arrange + ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice(); + productOfferingPrice.setUuid("test-productofferingprice-789"); + productOfferingPrice.setName("Test Product Offering Price Attribute Change"); + productOfferingPrice.setDescription("A test product offering price for attribute change notifications"); + + // Act + productOfferingPriceNotificationService.publishProductOfferingPriceAttributeValueChangeNotification(productOfferingPrice); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingPriceAttributeValueChangeNotification.class), eq("test-productofferingprice-789")); + verify(productOfferingPriceCallbackService, times(1)).sendProductOfferingPriceAttributeValueChangeCallback(any()); + } + + @Test + public void testPublishProductOfferingPriceStateChangeNotification() { + // Arrange + ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice(); + productOfferingPrice.setUuid("test-productofferingprice-101"); + productOfferingPrice.setName("Test Product Offering Price State Change"); + productOfferingPrice.setDescription("A test product offering price for state change notifications"); + + // Act + productOfferingPriceNotificationService.publishProductOfferingPriceStateChangeNotification(productOfferingPrice); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingPriceStateChangeNotification.class), eq("test-productofferingprice-101")); + verify(productOfferingPriceCallbackService, times(1)).sendProductOfferingPriceStateChangeCallback(any()); + } + + @Test + public void testCreateNotificationStructure() { + // Arrange + ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice(); + productOfferingPrice.setUuid("test-productofferingprice-structure"); + productOfferingPrice.setName("Test Product Offering Price Structure"); + + // Act + productOfferingPriceNotificationService.publishProductOfferingPriceCreateNotification(productOfferingPrice); + + // Assert - verify the notification was published with correct structure + verify(eventPublisher).publishEvent(any(ProductOfferingPriceCreateNotification.class), eq("test-productofferingprice-structure")); + verify(productOfferingPriceCallbackService).sendProductOfferingPriceCreateCallback(any()); + } +} \ No newline at end of file -- GitLab From 4f5ab1c8e4072835e66d21385ecf6a7ca26679ca Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 11:02:39 +0300 Subject: [PATCH 07/21] allow USER role to register callback --- .../org/etsi/osl/tmf/pcm620/api/HubApiController.java | 4 ++-- .../osl/services/api/pcm620/HubApiControllerTest.java | 4 ++-- .../pcm620/ProductOfferingCallbackIntegrationTest.java | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java index b15db916..dbf51fb2 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java @@ -71,7 +71,7 @@ public class HubApiController implements HubApi { } @Override - @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')") public ResponseEntity registerListener(@Parameter(description = "Data containing the callback endpoint to deliver the information", required = true) @Valid @RequestBody EventSubscriptionInput data) { try { EventSubscription eventSubscription = eventSubscriptionRepoService.addEventSubscription(data); @@ -86,7 +86,7 @@ public class HubApiController implements HubApi { } @Override - @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')") public ResponseEntity unregisterListener(@Parameter(description = "The id of the registered listener", required = true) @PathVariable("id") String id) { try { EventSubscription existing = eventSubscriptionRepoService.findById(id); diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java index 072e514f..c3d4d0d7 100644 --- a/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java +++ b/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java @@ -180,7 +180,7 @@ public class HubApiControllerTest { .andExpect(status().isBadRequest()); } - @WithMockUser(username = "user", roles = {"USER"}) + @WithMockUser(username = "user", roles = {"OTHER"}) @Test public void testRegisterListenerUnauthorized() throws Exception { File resourceSpecFile = new File("src/test/resources/testPCM620EventSubscriptionInput.json"); @@ -196,7 +196,7 @@ public class HubApiControllerTest { .andExpect(status().isForbidden()); } - @WithMockUser(username = "user", roles = {"USER"}) + @WithMockUser(username = "user", roles = {"OTHER"}) @Test public void testUnregisterListenerUnauthorized() throws Exception { // First create a subscription as admin diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java index 1755896d..0974606f 100644 --- a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java @@ -92,7 +92,7 @@ public class ProductOfferingCallbackIntegrationTest { } @Test - @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @WithMockUser(username = "osadmin", roles = {"USER"}) public void testCompleteCallbackFlow() throws Exception { // Step 1: Register a callback subscription via Hub API EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); @@ -139,7 +139,7 @@ public class ProductOfferingCallbackIntegrationTest { } @Test - @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @WithMockUser(username = "osadmin", roles = {"USER"}) public void testAttributeValueChangeAndStateChangeCallbacks() throws Exception { // Step 1: Register subscription for attribute and state change events EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); @@ -187,7 +187,7 @@ public class ProductOfferingCallbackIntegrationTest { } @Test - @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @WithMockUser(username = "osadmin", roles = {"USER"}) public void testCallbackFilteringByEventType() throws Exception { // Step 1: Register subscription only for create events EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); @@ -222,7 +222,7 @@ public class ProductOfferingCallbackIntegrationTest { } @Test - @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @WithMockUser(username = "osadmin", roles = {"USER"}) public void testProductOfferingCallbackWithAllEventsQuery() throws Exception { // Step 1: Register subscription for all events (empty query) EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); -- GitLab From 9937a3f7125fe5ee359e2356e9c6b12d98358983 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 11:15:58 +0300 Subject: [PATCH 08/21] Closes #83 --- .../tmf/pcm620/api/ListenerApiController.java | 206 +++++++++++++++++- 1 file changed, 205 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java index 77f66dba..ddd56172 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java @@ -22,17 +22,41 @@ package org.etsi.osl.tmf.pcm620.api; import java.util.Optional; import com.fasterxml.jackson.databind.ObjectMapper; - +import org.etsi.osl.tmf.pcm620.model.CatalogBatchEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import io.swagger.v3.oas.annotations.Parameter; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; @jakarta.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2019-10-19T00:15:57.249+03:00") @Controller("ListenerApiController620") @RequestMapping("/productCatalogManagement/v4/") public class ListenerApiController implements ListenerApi { + private static final Logger log = LoggerFactory.getLogger(ListenerApiController.class); + private final ObjectMapper objectMapper; private final HttpServletRequest request; @@ -53,4 +77,184 @@ public class ListenerApiController implements ListenerApi { return Optional.ofNullable(request); } + @Override + public ResponseEntity listenToCatalogBatchEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CatalogBatchEvent data) { + try { + log.info("Received CatalogBatchEvent: {}", data.getEventId()); + log.debug("CatalogBatchEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing CatalogBatchEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToCatalogCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CatalogCreateEvent data) { + try { + log.info("Received CatalogCreateEvent: {}", data.getEventId()); + log.debug("CatalogCreateEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing CatalogCreateEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToCatalogDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CatalogDeleteEvent data) { + try { + log.info("Received CatalogDeleteEvent: {}", data.getEventId()); + log.debug("CatalogDeleteEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing CatalogDeleteEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToCategoryCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CategoryCreateEvent data) { + try { + log.info("Received CategoryCreateEvent: {}", data.getEventId()); + log.debug("CategoryCreateEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing CategoryCreateEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToCategoryDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CategoryDeleteEvent data) { + try { + log.info("Received CategoryDeleteEvent: {}", data.getEventId()); + log.debug("CategoryDeleteEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing CategoryDeleteEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingAttributeValueChangeEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingAttributeValueChangeEvent data) { + try { + log.info("Received ProductOfferingAttributeValueChangeEvent: {}", data.getEventId()); + log.debug("ProductOfferingAttributeValueChangeEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingAttributeValueChangeEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingCreateEvent data) { + try { + log.info("Received ProductOfferingCreateEvent: {}", data.getEventId()); + log.debug("ProductOfferingCreateEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingCreateEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingDeleteEvent data) { + try { + log.info("Received ProductOfferingDeleteEvent: {}", data.getEventId()); + log.debug("ProductOfferingDeleteEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingDeleteEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingPriceAttributeValueChangeEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingPriceAttributeValueChangeEvent data) { + try { + log.info("Received ProductOfferingPriceAttributeValueChangeEvent: {}", data.getEventId()); + log.debug("ProductOfferingPriceAttributeValueChangeEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingPriceAttributeValueChangeEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingPriceCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingPriceCreateEvent data) { + try { + log.info("Received ProductOfferingPriceCreateEvent: {}", data.getEventId()); + log.debug("ProductOfferingPriceCreateEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingPriceCreateEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingPriceDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingPriceDeleteEvent data) { + try { + log.info("Received ProductOfferingPriceDeleteEvent: {}", data.getEventId()); + log.debug("ProductOfferingPriceDeleteEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingPriceDeleteEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingPriceStateChangeEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingPriceStateChangeEvent data) { + try { + log.info("Received ProductOfferingPriceStateChangeEvent: {}", data.getEventId()); + log.debug("ProductOfferingPriceStateChangeEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingPriceStateChangeEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingStateChangeEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingStateChangeEvent data) { + try { + log.info("Received ProductOfferingStateChangeEvent: {}", data.getEventId()); + log.debug("ProductOfferingStateChangeEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingStateChangeEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductSpecificationCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductSpecificationCreateEvent data) { + try { + log.info("Received ProductSpecificationCreateEvent: {}", data.getEventId()); + log.debug("ProductSpecificationCreateEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductSpecificationCreateEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductSpecificationDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductSpecificationDeleteEvent data) { + try { + log.info("Received ProductSpecificationDeleteEvent: {}", data.getEventId()); + log.debug("ProductSpecificationDeleteEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductSpecificationDeleteEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + } -- GitLab From f9573d2cc626b571c51aa53687de5ebf3680f1ce Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 13:16:52 +0300 Subject: [PATCH 09/21] fix lazy init errors and add hub GET --- .../org/etsi/osl/tmf/pcm620/api/HubApi.java | 27 +++++++++++++++++++ .../osl/tmf/pcm620/api/HubApiController.java | 18 +++++++++++++ .../ProductCatalogApiRouteBuilderEvents.java | 3 +++ .../reposervices/CatalogCallbackService.java | 9 ++++--- .../ProductCategoryRepoService.java | 14 ++++++---- .../ProductOfferingRepoService.java | 6 ++--- .../ProductSpecificationRepoService.java | 6 ++--- 7 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApi.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApi.java index 2de26104..c6a77416 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApi.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApi.java @@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import java.util.List; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -116,4 +117,30 @@ public interface HubApi { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } + @Operation(summary = "Get all registered listeners", operationId = "getListeners", description = "Retrieves all registered event subscriptions", tags={ "events subscription", }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Success" ), + @ApiResponse(responseCode = "400", description = "Bad Request" ), + @ApiResponse(responseCode = "401", description = "Unauthorized" ), + @ApiResponse(responseCode = "403", description = "Forbidden" ), + @ApiResponse(responseCode = "500", description = "Internal Server Error" ) }) + @RequestMapping(value = "/hub", + produces = { "application/json;charset=utf-8" }, + method = RequestMethod.GET) + default ResponseEntity> getListeners() { + if(getObjectMapper().isPresent() && getAcceptHeader().isPresent()) { + if (getAcceptHeader().get().contains("application/json")) { + try { + return new ResponseEntity<>(getObjectMapper().get().readValue("[ { \"query\" : \"query\", \"callback\" : \"callback\", \"id\" : \"id\"} ]", List.class), HttpStatus.NOT_IMPLEMENTED); + } catch (IOException e) { + log.error("Couldn't serialize response for content type application/json", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + } else { + log.warn("ObjectMapper or HttpServletRequest not configured in default HubApi interface so no example is generated"); + } + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java index dbf51fb2..649fa957 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java @@ -19,6 +19,7 @@ */ package org.etsi.osl.tmf.pcm620.api; +import java.util.List; import java.util.Optional; import com.fasterxml.jackson.databind.ObjectMapper; @@ -35,6 +36,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import io.swagger.v3.oas.annotations.Parameter; import jakarta.servlet.http.HttpServletRequest; @@ -70,6 +72,9 @@ public class HubApiController implements HubApi { return Optional.ofNullable(request); } + /* + * to register another OSL for example use "callback": "http://localhost:13082/tmf-api/productCatalogManagement/v4/" + */ @Override @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')") public ResponseEntity registerListener(@Parameter(description = "Data containing the callback endpoint to deliver the information", required = true) @Valid @RequestBody EventSubscriptionInput data) { @@ -87,6 +92,7 @@ public class HubApiController implements HubApi { @Override @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')") + @RequestMapping(value = "/hub/{id}", method = RequestMethod.DELETE, produces = { "application/json;charset=utf-8" }) public ResponseEntity unregisterListener(@Parameter(description = "The id of the registered listener", required = true) @PathVariable("id") String id) { try { EventSubscription existing = eventSubscriptionRepoService.findById(id); @@ -102,4 +108,16 @@ public class HubApiController implements HubApi { } } + @Override + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')") + public ResponseEntity> getListeners() { + try { + List eventSubscriptions = eventSubscriptionRepoService.findAll(); + return new ResponseEntity<>(eventSubscriptions, HttpStatus.OK); + } catch (Exception e) { + log.error("Error retrieving listeners", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java index 746ac4f7..f6c7b6e7 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -25,6 +25,7 @@ import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.apache.camel.ProducerTemplate; import org.apache.camel.builder.RouteBuilder; @@ -174,12 +175,14 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { static String toJsonString(Object object) throws IOException { ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper.writeValueAsString(object); } static T toJsonObj(String content, Class valueType) throws IOException { ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper.readValue(content, valueType); } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java index 36f7b5a8..27e0e6cc 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java @@ -80,8 +80,9 @@ public class CatalogCallbackService { * @param event The catalog create event */ private void sendCatalogCreateEventToCallback(String callbackUrl, CatalogCreateEvent event) { + + String url = buildCallbackUrl(callbackUrl, "/listener/catalogCreateEvent"); try { - String url = buildCallbackUrl(callbackUrl, "/listener/catalogCreateEvent"); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -94,7 +95,7 @@ public class CatalogCallbackService { url, response.getStatusCode()); } catch (Exception e) { - logger.error("Failed to send catalog create event to callback URL: {}", callbackUrl, e); + logger.error("Failed to send catalog create event to callback URL: {}", url, e); } } @@ -104,8 +105,8 @@ public class CatalogCallbackService { * @param event The catalog delete event */ private void sendCatalogDeleteEventToCallback(String callbackUrl, CatalogDeleteEvent event) { + String url = buildCallbackUrl(callbackUrl, "/listener/catalogDeleteEvent"); try { - String url = buildCallbackUrl(callbackUrl, "/listener/catalogDeleteEvent"); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -118,7 +119,7 @@ public class CatalogCallbackService { url, response.getStatusCode()); } catch (Exception e) { - logger.error("Failed to send catalog delete event to callback URL: {}", callbackUrl, e); + logger.error("Failed to send catalog delete event to callback URL: {}", url, e); } } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java index be1aae8c..ca2355d3 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java @@ -156,6 +156,15 @@ public class ProductCategoryRepoService { Category categoryToDelete = optionalCat.get(); + // Trigger lazy loading of associations before deletion to avoid lazy initialization exception + categoryToDelete.getProductOfferingObj().size(); // This will initialize the lazy collection + categoryToDelete.getCategoryObj().size(); // This will initialize the lazy collection + + // Publish category delete notification BEFORE deletion to ensure session is still active + if (categoryNotificationService != null) { + categoryNotificationService.publishCategoryDeleteNotification(categoryToDelete); + } + if ( categoryToDelete.getParentId() != null ) { Category parentCat = (this.categsRepo.findByUuid( categoryToDelete.getParentId() )).get(); @@ -171,11 +180,6 @@ public class ProductCategoryRepoService { this.categsRepo.delete( categoryToDelete); - // Publish category delete notification - if (categoryNotificationService != null) { - categoryNotificationService.publishCategoryDeleteNotification(categoryToDelete); - } - return true; } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java index 55d6c1b0..02b0d19f 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java @@ -266,13 +266,13 @@ public class ProductOfferingRepoService { /** * prior deleting we need to delete other dependency objects */ - - this.prodsOfferingRepo.delete(s); - // Publish product offering delete notification + // Publish product offering delete notification BEFORE deletion to ensure session is still active if (productOfferingNotificationService != null) { productOfferingNotificationService.publishProductOfferingDeleteNotification(s); } + + this.prodsOfferingRepo.delete(s); return null; } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java index 8b8fdfc7..ab2e3ee3 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java @@ -257,13 +257,13 @@ public class ProductSpecificationRepoService { /** * prior deleting we need to delete other dependency objects */ - - this.prodsOfferingRepo.delete(s); - // Publish product specification delete notification + // Publish product specification delete notification BEFORE deletion to ensure session is still active if (productSpecificationNotificationService != null) { productSpecificationNotificationService.publishProductSpecificationDeleteNotification(s); } + + this.prodsOfferingRepo.delete(s); return null; } -- GitLab From cb86ef93949d90341d5e6c0a89a623b45e29545f Mon Sep 17 00:00:00 2001 From: trantzas Date: Mon, 18 Aug 2025 17:43:06 +0000 Subject: [PATCH 10/21] Preparing the develop branch for 2025Q4 Release Cycle --- Dockerfile | 4 ++-- pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 945df88d..bb646ab8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM ibm-semeru-runtimes:open-17.0.7_7-jdk # RUN mkdir /opt/shareclasses RUN mkdir -p /opt/openslice/lib/ -COPY target/org.etsi.osl.tmf.api-1.2.0-exec.jar /opt/openslice/lib/ -CMD ["java", "-Xshareclasses:cacheDir=/opt/shareclasses", "-jar", "/opt/openslice/lib/org.etsi.osl.tmf.api-1.2.0-exec.jar"] +COPY target/org.etsi.osl.tmf.api-1.3.0-SNAPSHOT-exec.jar /opt/openslice/lib/ +CMD ["java", "-Xshareclasses:cacheDir=/opt/shareclasses", "-jar", "/opt/openslice/lib/org.etsi.osl.tmf.api-1.3.0-SNAPSHOT-exec.jar"] EXPOSE 13082 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9773329a..a39c8a0f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.etsi.osl org.etsi.osl.main - 2025Q2 + 2025Q4-SNAPSHOT ../org.etsi.osl.main -- GitLab From d87dc3917de4a3557dff0f50ef0fc800962ef415 Mon Sep 17 00:00:00 2001 From: trantzas Date: Mon, 18 Aug 2025 17:43:06 +0000 Subject: [PATCH 11/21] Preparing the develop branch for 2025Q4 Release Cycle -- GitLab From fec522e2c8e1b093595e2c574993b6c3483d04c0 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 11:15:58 +0300 Subject: [PATCH 12/21] Closes #83 --- .../tmf/pcm620/api/ListenerApiController.java | 206 +++++++++++++++++- 1 file changed, 205 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java index 77f66dba..ddd56172 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java @@ -22,17 +22,41 @@ package org.etsi.osl.tmf.pcm620.api; import java.util.Optional; import com.fasterxml.jackson.databind.ObjectMapper; - +import org.etsi.osl.tmf.pcm620.model.CatalogBatchEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import io.swagger.v3.oas.annotations.Parameter; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; @jakarta.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2019-10-19T00:15:57.249+03:00") @Controller("ListenerApiController620") @RequestMapping("/productCatalogManagement/v4/") public class ListenerApiController implements ListenerApi { + private static final Logger log = LoggerFactory.getLogger(ListenerApiController.class); + private final ObjectMapper objectMapper; private final HttpServletRequest request; @@ -53,4 +77,184 @@ public class ListenerApiController implements ListenerApi { return Optional.ofNullable(request); } + @Override + public ResponseEntity listenToCatalogBatchEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CatalogBatchEvent data) { + try { + log.info("Received CatalogBatchEvent: {}", data.getEventId()); + log.debug("CatalogBatchEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing CatalogBatchEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToCatalogCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CatalogCreateEvent data) { + try { + log.info("Received CatalogCreateEvent: {}", data.getEventId()); + log.debug("CatalogCreateEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing CatalogCreateEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToCatalogDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CatalogDeleteEvent data) { + try { + log.info("Received CatalogDeleteEvent: {}", data.getEventId()); + log.debug("CatalogDeleteEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing CatalogDeleteEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToCategoryCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CategoryCreateEvent data) { + try { + log.info("Received CategoryCreateEvent: {}", data.getEventId()); + log.debug("CategoryCreateEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing CategoryCreateEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToCategoryDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody CategoryDeleteEvent data) { + try { + log.info("Received CategoryDeleteEvent: {}", data.getEventId()); + log.debug("CategoryDeleteEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing CategoryDeleteEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingAttributeValueChangeEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingAttributeValueChangeEvent data) { + try { + log.info("Received ProductOfferingAttributeValueChangeEvent: {}", data.getEventId()); + log.debug("ProductOfferingAttributeValueChangeEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingAttributeValueChangeEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingCreateEvent data) { + try { + log.info("Received ProductOfferingCreateEvent: {}", data.getEventId()); + log.debug("ProductOfferingCreateEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingCreateEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingDeleteEvent data) { + try { + log.info("Received ProductOfferingDeleteEvent: {}", data.getEventId()); + log.debug("ProductOfferingDeleteEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingDeleteEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingPriceAttributeValueChangeEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingPriceAttributeValueChangeEvent data) { + try { + log.info("Received ProductOfferingPriceAttributeValueChangeEvent: {}", data.getEventId()); + log.debug("ProductOfferingPriceAttributeValueChangeEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingPriceAttributeValueChangeEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingPriceCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingPriceCreateEvent data) { + try { + log.info("Received ProductOfferingPriceCreateEvent: {}", data.getEventId()); + log.debug("ProductOfferingPriceCreateEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingPriceCreateEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingPriceDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingPriceDeleteEvent data) { + try { + log.info("Received ProductOfferingPriceDeleteEvent: {}", data.getEventId()); + log.debug("ProductOfferingPriceDeleteEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingPriceDeleteEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingPriceStateChangeEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingPriceStateChangeEvent data) { + try { + log.info("Received ProductOfferingPriceStateChangeEvent: {}", data.getEventId()); + log.debug("ProductOfferingPriceStateChangeEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingPriceStateChangeEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductOfferingStateChangeEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductOfferingStateChangeEvent data) { + try { + log.info("Received ProductOfferingStateChangeEvent: {}", data.getEventId()); + log.debug("ProductOfferingStateChangeEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductOfferingStateChangeEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductSpecificationCreateEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductSpecificationCreateEvent data) { + try { + log.info("Received ProductSpecificationCreateEvent: {}", data.getEventId()); + log.debug("ProductSpecificationCreateEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductSpecificationCreateEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Override + public ResponseEntity listenToProductSpecificationDeleteEvent(@Parameter(description = "The event data", required = true) @Valid @RequestBody ProductSpecificationDeleteEvent data) { + try { + log.info("Received ProductSpecificationDeleteEvent: {}", data.getEventId()); + log.debug("ProductSpecificationDeleteEvent details: {}", data); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + log.error("Error processing ProductSpecificationDeleteEvent", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + } -- GitLab From 9824b7e6cf761f717feb464b8470e65da5b2dd38 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 00:23:18 +0300 Subject: [PATCH 13/21] added notifications and callbacks for catalog create and delete --- .../ProductCatalogApiRouteBuilderEvents.java | 114 ++++++++++++ .../configuration/RestTemplateConfig.java | 39 ++++ .../reposervices/CatalogCallbackService.java | 158 ++++++++++++++++ .../CatalogNotificationService.java | 151 +++++++++++++++ .../EventSubscriptionRepoService.java | 5 + .../ProductCatalogRepoService.java | 28 ++- src/main/resources/application.yml | 3 + .../CatalogCallbackIntegrationTest.java | 173 ++++++++++++++++++ .../pcm620/CatalogCallbackServiceTest.java | 162 ++++++++++++++++ .../CatalogNotificationIntegrationTest.java | 118 ++++++++++++ .../CatalogNotificationServiceTest.java | 80 ++++++++ 11 files changed, 1026 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/configuration/RestTemplateConfig.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackServiceTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationIntegrationTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java new file mode 100644 index 00000000..4dd28107 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -0,0 +1,114 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2020 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.api; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.camel.ProducerTemplate; +import org.apache.camel.builder.RouteBuilder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.etsi.osl.centrallog.client.CLevel; +import org.etsi.osl.centrallog.client.CentralLogger; +import org.etsi.osl.tmf.common.model.Notification; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Configuration +@Component +public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { + + private static final transient Log logger = LogFactory.getLog(ProductCatalogApiRouteBuilderEvents.class.getName()); + + @Value("${EVENT_PRODUCT_CATALOG_CREATE}") + private String EVENT_CATALOG_CREATE = "direct:EVENT_CATALOG_CREATE"; + + @Value("${EVENT_PRODUCT_CATALOG_DELETE}") + private String EVENT_CATALOG_DELETE = "direct:EVENT_CATALOG_DELETE"; + + @Value("${spring.application.name}") + private String compname; + + @Autowired + private ProducerTemplate template; + + @Autowired + private CentralLogger centralLogger; + + @Override + public void configure() throws Exception { + // Configure routes for catalog events + } + + /** + * Publish notification events for catalog operations + * @param n The notification to publish + * @param objId The catalog object ID + */ + @Transactional + public void publishEvent(final Notification n, final String objId) { + n.setEventType(n.getClass().getName()); + logger.info("will send Event for type " + n.getEventType()); + try { + String msgtopic = ""; + + if (n instanceof CatalogCreateNotification) { + msgtopic = EVENT_CATALOG_CREATE; + } else if (n instanceof CatalogDeleteNotification) { + msgtopic = EVENT_CATALOG_DELETE; + } + + Map map = new HashMap<>(); + map.put("eventid", n.getEventId()); + map.put("objId", objId); + + String apayload = toJsonString(n); + template.sendBodyAndHeaders(msgtopic, apayload, map); + + centralLogger.log(CLevel.INFO, apayload, compname); + + } catch (Exception e) { + e.printStackTrace(); + logger.error("Cannot send Event . " + e.getMessage()); + } + } + + static String toJsonString(Object object) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.writeValueAsString(object); + } + + static T toJsonObj(String content, Class valueType) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.readValue(content, valueType); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/configuration/RestTemplateConfig.java b/src/main/java/org/etsi/osl/tmf/pcm620/configuration/RestTemplateConfig.java new file mode 100644 index 00000000..d6240fcf --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/configuration/RestTemplateConfig.java @@ -0,0 +1,39 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.configuration; + +import java.time.Duration; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder + .setConnectTimeout(Duration.ofSeconds(10)) + .setReadTimeout(Duration.ofSeconds(30)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java new file mode 100644 index 00000000..36f7b5a8 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java @@ -0,0 +1,158 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.CatalogCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class CatalogCallbackService { + + private static final Logger logger = LoggerFactory.getLogger(CatalogCallbackService.class); + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Autowired + private RestTemplate restTemplate; + + /** + * Send catalog create event to all registered callback URLs + * @param catalogCreateEvent The catalog create event to send + */ + public void sendCatalogCreateCallback(CatalogCreateEvent catalogCreateEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "catalogCreateEvent")) { + sendCatalogCreateEventToCallback(subscription.getCallback(), catalogCreateEvent); + } + } + } + + /** + * Send catalog delete event to all registered callback URLs + * @param catalogDeleteEvent The catalog delete event to send + */ + public void sendCatalogDeleteCallback(CatalogDeleteEvent catalogDeleteEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "catalogDeleteEvent")) { + sendCatalogDeleteEventToCallback(subscription.getCallback(), catalogDeleteEvent); + } + } + } + + /** + * Send catalog create event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The catalog create event + */ + private void sendCatalogCreateEventToCallback(String callbackUrl, CatalogCreateEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/catalogCreateEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent catalog create event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send catalog create event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send catalog delete event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The catalog delete event + */ + private void sendCatalogDeleteEventToCallback(String callbackUrl, CatalogDeleteEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/catalogDeleteEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent catalog delete event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send catalog delete event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Build the full callback URL with the listener endpoint + * @param baseUrl The base callback URL + * @param listenerPath The listener path to append + * @return The complete callback URL + */ + private String buildCallbackUrl(String baseUrl, String listenerPath) { + if (baseUrl.endsWith("/")) { + return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath; + } else { + return baseUrl + listenerPath; + } + } + + /** + * Check if a subscription should be notified for a specific event type + * @param subscription The event subscription + * @param eventType The event type to check + * @return true if the subscription should be notified + */ + private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) { + // If no query is specified, notify all events + if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) { + return true; + } + + // Check if the query contains the event type + String query = subscription.getQuery().toLowerCase(); + return query.contains("catalog") || + query.contains(eventType.toLowerCase()) || + query.contains("catalog.create") || + query.contains("catalog.delete"); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java new file mode 100644 index 00000000..5cc5000f --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java @@ -0,0 +1,151 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.Catalog; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateEventPayload; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEventPayload; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class CatalogNotificationService { + + private static final Logger logger = LoggerFactory.getLogger(CatalogNotificationService.class); + + @Autowired + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Autowired + private CatalogCallbackService catalogCallbackService; + + /** + * Publish a catalog create notification + * @param catalog The created catalog + */ + public void publishCatalogCreateNotification(Catalog catalog) { + try { + CatalogCreateNotification notification = createCatalogCreateNotification(catalog); + eventPublisher.publishEvent(notification, catalog.getUuid()); + + // Send callbacks to registered subscribers + catalogCallbackService.sendCatalogCreateCallback(notification.getEvent()); + + logger.info("Published catalog create notification for catalog ID: {}", catalog.getUuid()); + } catch (Exception e) { + logger.error("Error publishing catalog create notification for catalog ID: {}", catalog.getUuid(), e); + } + } + + /** + * Publish a catalog delete notification + * @param catalog The deleted catalog + */ + public void publishCatalogDeleteNotification(Catalog catalog) { + try { + CatalogDeleteNotification notification = createCatalogDeleteNotification(catalog); + eventPublisher.publishEvent(notification, catalog.getUuid()); + + // Send callbacks to registered subscribers + catalogCallbackService.sendCatalogDeleteCallback(notification.getEvent()); + + logger.info("Published catalog delete notification for catalog ID: {}", catalog.getUuid()); + } catch (Exception e) { + logger.error("Error publishing catalog delete notification for catalog ID: {}", catalog.getUuid(), e); + } + } + + /** + * Create a catalog create notification + * @param catalog The created catalog + * @return CatalogCreateNotification + */ + private CatalogCreateNotification createCatalogCreateNotification(Catalog catalog) { + CatalogCreateNotification notification = new CatalogCreateNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(CatalogCreateNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/catalog/" + catalog.getUuid()); + + // Create event + CatalogCreateEvent event = new CatalogCreateEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("CatalogCreateEvent"); + event.setTitle("Catalog Create Event"); + event.setDescription("A catalog has been created"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + CatalogCreateEventPayload payload = new CatalogCreateEventPayload(); + payload.setCatalog(catalog); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a catalog delete notification + * @param catalog The deleted catalog + * @return CatalogDeleteNotification + */ + private CatalogDeleteNotification createCatalogDeleteNotification(Catalog catalog) { + CatalogDeleteNotification notification = new CatalogDeleteNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(CatalogDeleteNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/catalog/" + catalog.getUuid()); + + // Create event + CatalogDeleteEvent event = new CatalogDeleteEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("CatalogDeleteEvent"); + event.setTitle("Catalog Delete Event"); + event.setDescription("A catalog has been deleted"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + CatalogDeleteEventPayload payload = new CatalogDeleteEventPayload(); + payload.setCatalog(catalog); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java index 8fc9caad..4f5c8595 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java @@ -19,6 +19,7 @@ */ package org.etsi.osl.tmf.pcm620.reposervices; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -62,4 +63,8 @@ public class EventSubscriptionRepoService { this.eventSubscriptionRepo.delete(optionalEventSubscription.get()); } } + + public List findAll() { + return (List) this.eventSubscriptionRepo.findAll(); + } } \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCatalogRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCatalogRepoService.java index 3f66f5c7..71bfe6fe 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCatalogRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCatalogRepoService.java @@ -53,11 +53,18 @@ public class ProductCatalogRepoService { @Autowired ProductCategoryRepoService categRepoService; + + @Autowired + CatalogNotificationService catalogNotificationService; public Catalog addCatalog(Catalog c) { - - return this.catalogRepo.save(c); + Catalog savedCatalog = this.catalogRepo.save(c); + + // Publish catalog create notification + catalogNotificationService.publishCatalogCreateNotification(savedCatalog); + + return savedCatalog; } public Catalog addCatalog(@Valid CatalogCreate serviceCat) { @@ -65,7 +72,12 @@ public class ProductCatalogRepoService { Catalog sc = new Catalog(); sc = updateCatalogDataFromAPICall(sc, serviceCat); - return this.catalogRepo.save(sc); + Catalog savedCatalog = this.catalogRepo.save(sc); + + // Publish catalog create notification + catalogNotificationService.publishCatalogCreateNotification(savedCatalog); + + return savedCatalog; } public List findAll() { @@ -85,9 +97,15 @@ public class ProductCatalogRepoService { public Void deleteById(String id) { Optional optionalCat = this.catalogRepo.findByUuid(id); - this.catalogRepo.delete(optionalCat.get()); + if (optionalCat.isPresent()) { + Catalog catalogToDelete = optionalCat.get(); + + // Publish catalog delete notification before deletion + catalogNotificationService.publishCatalogDeleteNotification(catalogToDelete); + + this.catalogRepo.delete(catalogToDelete); + } return null; - } public String findByUuidEager(String id) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dc4f0ce8..1ce4f18c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -210,6 +210,9 @@ EVENT_PRODUCT_ORDER_STATE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.STATECHANGED" EVENT_PRODUCT_ORDER_DELETE: "jms:topic:EVENT.PRODUCTORDER.DELETE" EVENT_PRODUCT_ORDER_ATTRIBUTE_VALUE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.ATTRCHANGED" +EVENT_PRODUCT_CATALOG_CREATE: "jms:topic:EVENT.PRODUCTCATALOG.CREATE" +EVENT_PRODUCT_CATALOG_DELETE: "jms:topic:EVENT.PRODUCTCATALOG.DELETE" + #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java new file mode 100644 index 00000000..e08ee16b --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java @@ -0,0 +1,173 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.etsi.osl.tmf.JsonUtils; +import org.etsi.osl.tmf.OpenAPISpringBoot; +import org.etsi.osl.tmf.pcm620.model.Catalog; +import org.etsi.osl.tmf.pcm620.model.CatalogCreate; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput; +import org.etsi.osl.tmf.pcm620.reposervices.CatalogCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductCatalogRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RunWith(SpringRunner.class) +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class) +@AutoConfigureMockMvc +@ActiveProfiles("testing") +@AutoConfigureTestDatabase +public class CatalogCallbackIntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private ProductCatalogRepoService productCatalogRepoService; + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @SpyBean + private CatalogCallbackService catalogCallbackService; + + @SpyBean + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + mvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + + // Mock RestTemplate to avoid actual HTTP calls in tests + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCompleteCallbackFlow() throws Exception { + // Step 1: Register a callback subscription via Hub API + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:8080/test-callback"); + subscriptionInput.setQuery("catalog.create,catalog.delete"); + + MvcResult subscriptionResult = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()) + .andReturn(); + + String subscriptionResponseBody = subscriptionResult.getResponse().getContentAsString(); + EventSubscription createdSubscription = objectMapper.readValue(subscriptionResponseBody, EventSubscription.class); + + // Step 2: Create a catalog (should trigger callback) + CatalogCreate catalogCreate = new CatalogCreate(); + catalogCreate.setName("Test Callback Catalog"); + catalogCreate.setDescription("A catalog to test callback notifications"); + catalogCreate.setVersion("1.0"); + + Catalog createdCatalog = productCatalogRepoService.addCatalog(catalogCreate); + + // Step 3: Verify callback was sent + verify(catalogCallbackService, timeout(2000)).sendCatalogCreateCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/catalogCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Step 4: Delete the catalog (should trigger delete callback) + productCatalogRepoService.deleteById(createdCatalog.getUuid()); + + // Step 5: Verify delete callback was sent + verify(catalogCallbackService, timeout(2000)).sendCatalogDeleteCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/catalogDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCallbackFilteringByQuery() throws Exception { + // Step 1: Register subscription only for create events + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:9090/create-only"); + subscriptionInput.setQuery("catalog.create"); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create and delete a catalog + CatalogCreate catalogCreate = new CatalogCreate(); + catalogCreate.setName("Test Filter Catalog"); + catalogCreate.setDescription("A catalog to test query filtering"); + catalogCreate.setVersion("1.0"); + + Catalog createdCatalog = productCatalogRepoService.addCatalog(catalogCreate); + productCatalogRepoService.deleteById(createdCatalog.getUuid()); + + // Step 3: Verify only create callback was sent (not delete) + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:9090/create-only/listener/catalogCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Note: In a more sophisticated test, we could verify that the delete callback was NOT sent + // by using verify with never(), but this requires more complex mock setup + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackServiceTest.java new file mode 100644 index 00000000..05312420 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackServiceTest.java @@ -0,0 +1,162 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.Catalog; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateEventPayload; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteEventPayload; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.reposervices.CatalogCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class CatalogCallbackServiceTest { + + @Mock + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private CatalogCallbackService catalogCallbackService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testSendCatalogCreateCallback() { + // Arrange + EventSubscription subscription1 = createSubscription("1", "http://localhost:8080/callback", "catalog.create"); + EventSubscription subscription2 = createSubscription("2", "http://localhost:9090/webhook", "catalog"); + List subscriptions = Arrays.asList(subscription1, subscription2); + + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + + CatalogCreateEvent event = createCatalogCreateEvent(); + + // Act + catalogCallbackService.sendCatalogCreateCallback(event); + + // Assert + verify(restTemplate, times(2)).exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)); + } + + @Test + public void testSendCatalogDeleteCallback() { + // Arrange + EventSubscription subscription = createSubscription("1", "http://localhost:8080/callback", "catalog.delete"); + List subscriptions = Arrays.asList(subscription); + + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + + CatalogDeleteEvent event = createCatalogDeleteEvent(); + + // Act + catalogCallbackService.sendCatalogDeleteCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)); + } + + @Test + public void testCallbackUrlBuilding() { + // Arrange + EventSubscription subscription1 = createSubscription("1", "http://localhost:8080/callback", "catalog"); + EventSubscription subscription2 = createSubscription("2", "http://localhost:8080/callback/", "catalog"); + List subscriptions = Arrays.asList(subscription1, subscription2); + + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + + CatalogCreateEvent event = createCatalogCreateEvent(); + + // Act + catalogCallbackService.sendCatalogCreateCallback(event); + + // Assert - Both should result in the same URL format + verify(restTemplate, times(2)).exchange(eq("http://localhost:8080/callback/listener/catalogCreateEvent"), + eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)); + } + + @Test + public void testNoSubscriptions() { + // Arrange + when(eventSubscriptionRepoService.findAll()).thenReturn(Arrays.asList()); + CatalogCreateEvent event = createCatalogCreateEvent(); + + // Act + catalogCallbackService.sendCatalogCreateCallback(event); + + // Assert - No calls should be made + verify(restTemplate, times(0)).exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class)); + } + + private EventSubscription createSubscription(String id, String callback, String query) { + EventSubscription subscription = new EventSubscription(); + subscription.setId(id); + subscription.setCallback(callback); + subscription.setQuery(query); + return subscription; + } + + private CatalogCreateEvent createCatalogCreateEvent() { + CatalogCreateEvent event = new CatalogCreateEvent(); + event.setEventId("test-event-123"); + event.setEventType("CatalogCreateEvent"); + + CatalogCreateEventPayload payload = new CatalogCreateEventPayload(); + Catalog catalog = new Catalog(); + catalog.setUuid("test-catalog-123"); + catalog.setName("Test Catalog"); + payload.setCatalog(catalog); + + event.setEvent(payload); + return event; + } + + private CatalogDeleteEvent createCatalogDeleteEvent() { + CatalogDeleteEvent event = new CatalogDeleteEvent(); + event.setEventId("test-delete-event-123"); + event.setEventType("CatalogDeleteEvent"); + + CatalogDeleteEventPayload payload = new CatalogDeleteEventPayload(); + Catalog catalog = new Catalog(); + catalog.setUuid("test-catalog-123"); + catalog.setName("Test Catalog"); + payload.setCatalog(catalog); + + event.setEvent(payload); + return event; + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationIntegrationTest.java new file mode 100644 index 00000000..c5fa4406 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationIntegrationTest.java @@ -0,0 +1,118 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.etsi.osl.tmf.OpenAPISpringBoot; +import org.etsi.osl.tmf.pcm620.model.Catalog; +import org.etsi.osl.tmf.pcm620.model.CatalogCreate; +import org.etsi.osl.tmf.pcm620.reposervices.CatalogNotificationService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductCatalogRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +@RunWith(SpringRunner.class) +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class) +@AutoConfigureMockMvc +@ActiveProfiles("testing") +@AutoConfigureTestDatabase +public class CatalogNotificationIntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private ProductCatalogRepoService productCatalogRepoService; + + @SpyBean + private CatalogNotificationService catalogNotificationService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + mvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCatalogCreateNotificationFlow() throws Exception { + // Arrange + CatalogCreate catalogCreate = new CatalogCreate(); + catalogCreate.setName("Test Notification Catalog"); + catalogCreate.setDescription("A catalog to test notifications"); + catalogCreate.setVersion("1.0"); + + // Act - Create catalog through repository service + Catalog createdCatalog = productCatalogRepoService.addCatalog(catalogCreate); + + // Assert - Verify notification was published + verify(catalogNotificationService, timeout(1000)).publishCatalogCreateNotification(any(Catalog.class)); + + // Verify catalog was created + assert createdCatalog != null; + assert createdCatalog.getName().equals("Test Notification Catalog"); + assert createdCatalog.getUuid() != null; + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCatalogDeleteNotificationFlow() throws Exception { + // Arrange - First create a catalog + CatalogCreate catalogCreate = new CatalogCreate(); + catalogCreate.setName("Test Delete Notification Catalog"); + catalogCreate.setDescription("A catalog to test delete notifications"); + catalogCreate.setVersion("1.0"); + + Catalog createdCatalog = productCatalogRepoService.addCatalog(catalogCreate); + String catalogId = createdCatalog.getUuid(); + + // Act - Delete the catalog + productCatalogRepoService.deleteById(catalogId); + + // Assert - Verify both create and delete notifications were published + verify(catalogNotificationService, timeout(1000)).publishCatalogCreateNotification(any(Catalog.class)); + verify(catalogNotificationService, timeout(1000)).publishCatalogDeleteNotification(any(Catalog.class)); + } + + @Test + public void testDirectCatalogOperations() { + // Arrange + Catalog catalog = new Catalog(); + catalog.setName("Direct Test Catalog"); + catalog.setDescription("Direct catalog for testing"); + + // Act - Add catalog directly + Catalog savedCatalog = productCatalogRepoService.addCatalog(catalog); + + // Assert - Verify notification was called + verify(catalogNotificationService, timeout(1000)).publishCatalogCreateNotification(any(Catalog.class)); + + assert savedCatalog != null; + assert savedCatalog.getName().equals("Direct Test Catalog"); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java new file mode 100644 index 00000000..a88ded05 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java @@ -0,0 +1,80 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.Catalog; +import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification; +import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification; +import org.etsi.osl.tmf.pcm620.reposervices.CatalogNotificationService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class CatalogNotificationServiceTest { + + @Mock + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @InjectMocks + private CatalogNotificationService catalogNotificationService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testPublishCatalogCreateNotification() { + // Arrange + Catalog catalog = new Catalog(); + catalog.setUuid("test-catalog-123"); + catalog.setName("Test Catalog"); + catalog.setDescription("A test catalog for notifications"); + + // Act + catalogNotificationService.publishCatalogCreateNotification(catalog); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(CatalogCreateNotification.class), eq("test-catalog-123")); + } + + @Test + public void testPublishCatalogDeleteNotification() { + // Arrange + Catalog catalog = new Catalog(); + catalog.setUuid("test-catalog-456"); + catalog.setName("Test Catalog to Delete"); + catalog.setDescription("A test catalog for delete notifications"); + + // Act + catalogNotificationService.publishCatalogDeleteNotification(catalog); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(CatalogDeleteNotification.class), eq("test-catalog-456")); + } + + @Test + public void testCreateNotificationStructure() { + // Arrange + Catalog catalog = new Catalog(); + catalog.setUuid("test-catalog-789"); + catalog.setName("Test Catalog Structure"); + + // Act + catalogNotificationService.publishCatalogCreateNotification(catalog); + + // Assert - verify the notification was published with correct structure + verify(eventPublisher).publishEvent(any(CatalogCreateNotification.class), eq("test-catalog-789")); + } +} \ No newline at end of file -- GitLab From cb0186451c3575969178b7d7d7a632acbb36692f Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 01:12:08 +0300 Subject: [PATCH 14/21] adding category create and delete events --- .../ProductCatalogApiRouteBuilderEvents.java | 14 +- .../reposervices/CategoryCallbackService.java | 158 ++++++++++++++ .../CategoryNotificationService.java | 151 +++++++++++++ .../ProductCategoryRepoService.java | 36 ++- src/main/resources/application-testing.yml | 5 + src/main/resources/application.yml | 2 + .../CatalogCallbackIntegrationTest.java | 3 +- .../CategoryCallbackIntegrationTest.java | 205 ++++++++++++++++++ .../pcm620/CategoryCallbackServiceTest.java | 161 ++++++++++++++ .../CategoryNotificationServiceTest.java | 87 ++++++++ 10 files changed, 813 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryCallbackService.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryNotificationService.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackIntegrationTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackServiceTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/CategoryNotificationServiceTest.java diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java index 4dd28107..19619394 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -35,6 +35,8 @@ import org.etsi.osl.centrallog.client.CentralLogger; import org.etsi.osl.tmf.common.model.Notification; import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification; import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -52,6 +54,12 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { @Value("${EVENT_PRODUCT_CATALOG_DELETE}") private String EVENT_CATALOG_DELETE = "direct:EVENT_CATALOG_DELETE"; + + @Value("${EVENT_PRODUCT_CATEGORY_CREATE}") + private String EVENT_CATEGORY_CREATE = "direct:EVENT_CATEGORY_CREATE"; + + @Value("${EVENT_PRODUCT_CATEGORY_DELETE}") + private String EVENT_CATEGORY_DELETE = "direct:EVENT_CATEGORY_DELETE"; @Value("${spring.application.name}") private String compname; @@ -82,7 +90,11 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { if (n instanceof CatalogCreateNotification) { msgtopic = EVENT_CATALOG_CREATE; } else if (n instanceof CatalogDeleteNotification) { - msgtopic = EVENT_CATALOG_DELETE; + msgtopic = EVENT_CATALOG_DELETE; + } else if (n instanceof CategoryCreateNotification) { + msgtopic = EVENT_CATEGORY_CREATE; + } else if (n instanceof CategoryDeleteNotification) { + msgtopic = EVENT_CATEGORY_DELETE; } Map map = new HashMap<>(); diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryCallbackService.java new file mode 100644 index 00000000..4eec9f60 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryCallbackService.java @@ -0,0 +1,158 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.CategoryCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class CategoryCallbackService { + + private static final Logger logger = LoggerFactory.getLogger(CategoryCallbackService.class); + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Autowired + private RestTemplate restTemplate; + + /** + * Send category create event to all registered callback URLs + * @param categoryCreateEvent The category create event to send + */ + public void sendCategoryCreateCallback(CategoryCreateEvent categoryCreateEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "categoryCreateEvent")) { + sendCategoryCreateEventToCallback(subscription.getCallback(), categoryCreateEvent); + } + } + } + + /** + * Send category delete event to all registered callback URLs + * @param categoryDeleteEvent The category delete event to send + */ + public void sendCategoryDeleteCallback(CategoryDeleteEvent categoryDeleteEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "categoryDeleteEvent")) { + sendCategoryDeleteEventToCallback(subscription.getCallback(), categoryDeleteEvent); + } + } + } + + /** + * Send category create event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The category create event + */ + private void sendCategoryCreateEventToCallback(String callbackUrl, CategoryCreateEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/categoryCreateEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent category create event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send category create event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send category delete event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The category delete event + */ + private void sendCategoryDeleteEventToCallback(String callbackUrl, CategoryDeleteEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/categoryDeleteEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent category delete event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send category delete event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Build the full callback URL with the listener endpoint + * @param baseUrl The base callback URL + * @param listenerPath The listener path to append + * @return The complete callback URL + */ + private String buildCallbackUrl(String baseUrl, String listenerPath) { + if (baseUrl.endsWith("/")) { + return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath; + } else { + return baseUrl + listenerPath; + } + } + + /** + * Check if a subscription should be notified for a specific event type + * @param subscription The event subscription + * @param eventType The event type to check + * @return true if the subscription should be notified + */ + private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) { + // If no query is specified, notify all events + if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) { + return true; + } + + // Check if the query contains the event type + String query = subscription.getQuery().toLowerCase(); + return query.contains("category") || + query.contains(eventType.toLowerCase()) || + query.contains("category.create") || + query.contains("category.delete"); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryNotificationService.java new file mode 100644 index 00000000..4d200a58 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryNotificationService.java @@ -0,0 +1,151 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.Category; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateEventPayload; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEventPayload; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class CategoryNotificationService { + + private static final Logger logger = LoggerFactory.getLogger(CategoryNotificationService.class); + + @Autowired + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Autowired + private CategoryCallbackService categoryCallbackService; + + /** + * Publish a category create notification + * @param category The created category + */ + public void publishCategoryCreateNotification(Category category) { + try { + CategoryCreateNotification notification = createCategoryCreateNotification(category); + eventPublisher.publishEvent(notification, category.getUuid()); + + // Send callbacks to registered subscribers + categoryCallbackService.sendCategoryCreateCallback(notification.getEvent()); + + logger.info("Published category create notification for category ID: {}", category.getUuid()); + } catch (Exception e) { + logger.error("Error publishing category create notification for category ID: {}", category.getUuid(), e); + } + } + + /** + * Publish a category delete notification + * @param category The deleted category + */ + public void publishCategoryDeleteNotification(Category category) { + try { + CategoryDeleteNotification notification = createCategoryDeleteNotification(category); + eventPublisher.publishEvent(notification, category.getUuid()); + + // Send callbacks to registered subscribers + categoryCallbackService.sendCategoryDeleteCallback(notification.getEvent()); + + logger.info("Published category delete notification for category ID: {}", category.getUuid()); + } catch (Exception e) { + logger.error("Error publishing category delete notification for category ID: {}", category.getUuid(), e); + } + } + + /** + * Create a category create notification + * @param category The created category + * @return CategoryCreateNotification + */ + private CategoryCreateNotification createCategoryCreateNotification(Category category) { + CategoryCreateNotification notification = new CategoryCreateNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(CategoryCreateNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/category/" + category.getUuid()); + + // Create event + CategoryCreateEvent event = new CategoryCreateEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("CategoryCreateEvent"); + event.setTitle("Category Create Event"); + event.setDescription("A category has been created"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + CategoryCreateEventPayload payload = new CategoryCreateEventPayload(); + payload.setCategory(category); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a category delete notification + * @param category The deleted category + * @return CategoryDeleteNotification + */ + private CategoryDeleteNotification createCategoryDeleteNotification(Category category) { + CategoryDeleteNotification notification = new CategoryDeleteNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(CategoryDeleteNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/category/" + category.getUuid()); + + // Create event + CategoryDeleteEvent event = new CategoryDeleteEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("CategoryDeleteEvent"); + event.setTitle("Category Delete Event"); + event.setDescription("A category has been deleted"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + CategoryDeleteEventPayload payload = new CategoryDeleteEventPayload(); + payload.setCategory(category); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java index c7e226c0..be1aae8c 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java @@ -61,6 +61,9 @@ public class ProductCategoryRepoService { private final ProductCategoriesRepository categsRepo; private final ProductOfferingRepository prodsOfferingRepo; + + @Autowired + private CategoryNotificationService categoryNotificationService; /** * from @@ -78,8 +81,14 @@ public class ProductCategoryRepoService { public Category addCategory(Category c) { - - return this.categsRepo.save( c ); + Category savedCategory = this.categsRepo.save( c ); + + // Publish category create notification + if (categoryNotificationService != null) { + categoryNotificationService.publishCategoryCreateNotification(savedCategory); + } + + return savedCategory; } public Category addCategory(@Valid CategoryCreate Category) { @@ -87,7 +96,14 @@ public class ProductCategoryRepoService { Category sc = new Category() ; sc = updateCategoryDataFromAPICall(sc, Category); - return this.categsRepo.save( sc ); + Category savedCategory = this.categsRepo.save( sc ); + + // Publish category create notification + if (categoryNotificationService != null) { + categoryNotificationService.publishCategoryCreateNotification(savedCategory); + } + + return savedCategory; } @@ -138,13 +154,14 @@ public class ProductCategoryRepoService { return false; //has children } + Category categoryToDelete = optionalCat.get(); - if ( optionalCat.get().getParentId() != null ) { - Category parentCat = (this.categsRepo.findByUuid( optionalCat.get().getParentId() )).get(); + if ( categoryToDelete.getParentId() != null ) { + Category parentCat = (this.categsRepo.findByUuid( categoryToDelete.getParentId() )).get(); //remove from parent category for (Category ss : parentCat.getCategoryObj()) { - if ( ss.getId() == optionalCat.get().getId() ) { + if ( ss.getId() == categoryToDelete.getId() ) { parentCat.getCategoryObj().remove(ss); break; } @@ -152,8 +169,13 @@ public class ProductCategoryRepoService { parentCat = this.categsRepo.save(parentCat); } + this.categsRepo.delete( categoryToDelete); + + // Publish category delete notification + if (categoryNotificationService != null) { + categoryNotificationService.publishCategoryDeleteNotification(categoryToDelete); + } - this.categsRepo.delete( optionalCat.get()); return true; } diff --git a/src/main/resources/application-testing.yml b/src/main/resources/application-testing.yml index dc15a9c2..d5506a3b 100644 --- a/src/main/resources/application-testing.yml +++ b/src/main/resources/application-testing.yml @@ -185,6 +185,11 @@ EVENT_PRODUCT_ORDER_STATE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.STATECHANGED" EVENT_PRODUCT_ORDER_DELETE: "jms:topic:EVENT.PRODUCTORDER.DELETE" EVENT_PRODUCT_ORDER_ATTRIBUTE_VALUE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.ATTRCHANGED" +EVENT_PRODUCT_CATALOG_CREATE: "jms:topic:EVENT.PRODUCTCATALOG.CREATE" +EVENT_PRODUCT_CATALOG_DELETE: "jms:topic:EVENT.PRODUCTCATALOG.DELETE" +EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE" +EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE" + #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1ce4f18c..1bfea10e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -212,6 +212,8 @@ EVENT_PRODUCT_ORDER_ATTRIBUTE_VALUE_CHANGED: "jms:topic:EVENT.PRODUCTORDER.ATTRC EVENT_PRODUCT_CATALOG_CREATE: "jms:topic:EVENT.PRODUCTCATALOG.CREATE" EVENT_PRODUCT_CATALOG_DELETE: "jms:topic:EVENT.PRODUCTCATALOG.DELETE" +EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE" +EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java index e08ee16b..0764364c 100644 --- a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java @@ -21,6 +21,7 @@ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabas import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; @@ -70,7 +71,7 @@ public class CatalogCallbackIntegrationTest { @SpyBean private CatalogCallbackService catalogCallbackService; - @SpyBean + @MockBean private RestTemplate restTemplate; @Autowired diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackIntegrationTest.java new file mode 100644 index 00000000..a694ae46 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackIntegrationTest.java @@ -0,0 +1,205 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.etsi.osl.tmf.JsonUtils; +import org.etsi.osl.tmf.OpenAPISpringBoot; +import org.etsi.osl.tmf.pcm620.model.Category; +import org.etsi.osl.tmf.pcm620.model.CategoryCreate; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput; +import org.etsi.osl.tmf.pcm620.reposervices.CategoryCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductCategoryRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RunWith(SpringRunner.class) +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class) +@AutoConfigureMockMvc +@ActiveProfiles("testing") +@AutoConfigureTestDatabase +public class CategoryCallbackIntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private ProductCategoryRepoService productCategoryRepoService; + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @SpyBean + private CategoryCallbackService categoryCallbackService; + + @MockBean + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + mvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + + // Mock RestTemplate to avoid actual HTTP calls in tests + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCompleteCallbackFlow() throws Exception { + // Step 1: Register a callback subscription via Hub API + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:8080/test-callback"); + subscriptionInput.setQuery("category.create,category.delete"); + + MvcResult subscriptionResult = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()) + .andReturn(); + + String subscriptionResponseBody = subscriptionResult.getResponse().getContentAsString(); + EventSubscription createdSubscription = objectMapper.readValue(subscriptionResponseBody, EventSubscription.class); + + // Step 2: Create a category (should trigger callback) + CategoryCreate categoryCreate = new CategoryCreate(); + categoryCreate.setName("Test Callback Category"); + categoryCreate.setDescription("A category to test callback notifications"); + categoryCreate.setVersion("1.0"); + + Category createdCategory = productCategoryRepoService.addCategory(categoryCreate); + + // Step 3: Verify callback was sent + verify(categoryCallbackService, timeout(2000)).sendCategoryCreateCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/categoryCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Step 4: Delete the category (should trigger delete callback) + productCategoryRepoService.deleteById(createdCategory.getUuid()); + + // Step 5: Verify delete callback was sent + verify(categoryCallbackService, timeout(2000)).sendCategoryDeleteCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/categoryDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCallbackFilteringByQuery() throws Exception { + // Step 1: Register subscription only for create events + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:9090/create-only"); + subscriptionInput.setQuery("category.create"); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create and delete a category + CategoryCreate categoryCreate = new CategoryCreate(); + categoryCreate.setName("Test Filter Category"); + categoryCreate.setDescription("A category to test query filtering"); + categoryCreate.setVersion("1.0"); + + Category createdCategory = productCategoryRepoService.addCategory(categoryCreate); + productCategoryRepoService.deleteById(createdCategory.getUuid()); + + // Step 3: Verify only create callback was sent (not delete) + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:9090/create-only/listener/categoryCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Note: In a more sophisticated test, we could verify that the delete callback was NOT sent + // by using verify with never(), but this requires more complex mock setup + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCategoryCallbackWithAllEventsQuery() throws Exception { + // Step 1: Register subscription for all events (empty query) + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:7070/all-events"); + subscriptionInput.setQuery(""); // Empty query should receive all events + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create a category + CategoryCreate categoryCreate = new CategoryCreate(); + categoryCreate.setName("Test All Events Category"); + categoryCreate.setDescription("A category to test all events subscription"); + categoryCreate.setVersion("1.0"); + + Category createdCategory = productCategoryRepoService.addCategory(categoryCreate); + + // Step 3: Verify callback was sent even with empty query + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:7070/all-events/listener/categoryCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackServiceTest.java new file mode 100644 index 00000000..df70e632 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryCallbackServiceTest.java @@ -0,0 +1,161 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.Category; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateEvent; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.reposervices.CategoryCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class CategoryCallbackServiceTest { + + @Mock + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private CategoryCallbackService categoryCallbackService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testSendCategoryCreateCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("category"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + CategoryCreateEvent event = new CategoryCreateEvent(); + event.setEventId("test-event-123"); + + // Act + categoryCallbackService.sendCategoryCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/categoryCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendCategoryDeleteCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("category"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + CategoryDeleteEvent event = new CategoryDeleteEvent(); + event.setEventId("test-event-456"); + + // Act + categoryCallbackService.sendCategoryDeleteCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/categoryDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testCallbackWithTrailingSlash() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback/"); + subscription.setQuery("category"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + CategoryCreateEvent event = new CategoryCreateEvent(); + event.setEventId("test-event-789"); + + // Act + categoryCallbackService.sendCategoryCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/categoryCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testFilterSubscriptionsByQuery() { + // Arrange + EventSubscription categorySubscription = new EventSubscription(); + categorySubscription.setCallback("http://localhost:8080/category-callback"); + categorySubscription.setQuery("category"); + + EventSubscription otherSubscription = new EventSubscription(); + otherSubscription.setCallback("http://localhost:8080/other-callback"); + otherSubscription.setQuery("catalog"); + + List subscriptions = Arrays.asList(categorySubscription, otherSubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + CategoryCreateEvent event = new CategoryCreateEvent(); + event.setEventId("test-event-filter"); + + // Act + categoryCallbackService.sendCategoryCreateCallback(event); + + // Assert - only category subscription should receive callback + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/category-callback/listener/categoryCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CategoryNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryNotificationServiceTest.java new file mode 100644 index 00000000..9de497fc --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CategoryNotificationServiceTest.java @@ -0,0 +1,87 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.Category; +import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification; +import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification; +import org.etsi.osl.tmf.pcm620.reposervices.CategoryNotificationService; +import org.etsi.osl.tmf.pcm620.reposervices.CategoryCallbackService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class CategoryNotificationServiceTest { + + @Mock + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Mock + private CategoryCallbackService categoryCallbackService; + + @InjectMocks + private CategoryNotificationService categoryNotificationService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testPublishCategoryCreateNotification() { + // Arrange + Category category = new Category(); + category.setUuid("test-category-123"); + category.setName("Test Category"); + category.setDescription("A test category for notifications"); + + // Act + categoryNotificationService.publishCategoryCreateNotification(category); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(CategoryCreateNotification.class), eq("test-category-123")); + verify(categoryCallbackService, times(1)).sendCategoryCreateCallback(any()); + } + + @Test + public void testPublishCategoryDeleteNotification() { + // Arrange + Category category = new Category(); + category.setUuid("test-category-456"); + category.setName("Test Category to Delete"); + category.setDescription("A test category for delete notifications"); + + // Act + categoryNotificationService.publishCategoryDeleteNotification(category); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(CategoryDeleteNotification.class), eq("test-category-456")); + verify(categoryCallbackService, times(1)).sendCategoryDeleteCallback(any()); + } + + @Test + public void testCreateNotificationStructure() { + // Arrange + Category category = new Category(); + category.setUuid("test-category-789"); + category.setName("Test Category Structure"); + + // Act + categoryNotificationService.publishCategoryCreateNotification(category); + + // Assert - verify the notification was published with correct structure + verify(eventPublisher).publishEvent(any(CategoryCreateNotification.class), eq("test-category-789")); + verify(categoryCallbackService).sendCategoryCreateCallback(any()); + } +} \ No newline at end of file -- GitLab From 1007bf174ed44a99c889e595a7fc2ef4f717162d Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 01:43:18 +0300 Subject: [PATCH 15/21] adding product spec notifications and events --- .../ProductCatalogApiRouteBuilderEvents.java | 14 +- .../ProductSpecificationCallbackService.java | 158 ++++++++++++++ ...oductSpecificationNotificationService.java | 151 +++++++++++++ .../ProductSpecificationRepoService.java | 15 +- src/main/resources/application-testing.yml | 2 + src/main/resources/application.yml | 2 + ...tSpecificationCallbackIntegrationTest.java | 205 ++++++++++++++++++ ...oductSpecificationCallbackServiceTest.java | 188 ++++++++++++++++ ...tSpecificationNotificationServiceTest.java | 87 ++++++++ 9 files changed, 820 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationCallbackService.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationNotificationService.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackIntegrationTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackServiceTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationNotificationServiceTest.java diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java index 19619394..674c0cfb 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -37,6 +37,8 @@ import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification; import org.etsi.osl.tmf.pcm620.model.CatalogDeleteNotification; import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification; import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteNotification; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -60,6 +62,12 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { @Value("${EVENT_PRODUCT_CATEGORY_DELETE}") private String EVENT_CATEGORY_DELETE = "direct:EVENT_CATEGORY_DELETE"; + + @Value("${EVENT_PRODUCT_SPECIFICATION_CREATE}") + private String EVENT_PRODUCT_SPECIFICATION_CREATE = "direct:EVENT_PRODUCT_SPECIFICATION_CREATE"; + + @Value("${EVENT_PRODUCT_SPECIFICATION_DELETE}") + private String EVENT_PRODUCT_SPECIFICATION_DELETE = "direct:EVENT_PRODUCT_SPECIFICATION_DELETE"; @Value("${spring.application.name}") private String compname; @@ -94,7 +102,11 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { } else if (n instanceof CategoryCreateNotification) { msgtopic = EVENT_CATEGORY_CREATE; } else if (n instanceof CategoryDeleteNotification) { - msgtopic = EVENT_CATEGORY_DELETE; + msgtopic = EVENT_CATEGORY_DELETE; + } else if (n instanceof ProductSpecificationCreateNotification) { + msgtopic = EVENT_PRODUCT_SPECIFICATION_CREATE; + } else if (n instanceof ProductSpecificationDeleteNotification) { + msgtopic = EVENT_PRODUCT_SPECIFICATION_DELETE; } Map map = new HashMap<>(); diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationCallbackService.java new file mode 100644 index 00000000..ae627191 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationCallbackService.java @@ -0,0 +1,158 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class ProductSpecificationCallbackService { + + private static final Logger logger = LoggerFactory.getLogger(ProductSpecificationCallbackService.class); + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Autowired + private RestTemplate restTemplate; + + /** + * Send product specification create event to all registered callback URLs + * @param productSpecificationCreateEvent The product specification create event to send + */ + public void sendProductSpecificationCreateCallback(ProductSpecificationCreateEvent productSpecificationCreateEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productSpecificationCreateEvent")) { + sendProductSpecificationCreateEventToCallback(subscription.getCallback(), productSpecificationCreateEvent); + } + } + } + + /** + * Send product specification delete event to all registered callback URLs + * @param productSpecificationDeleteEvent The product specification delete event to send + */ + public void sendProductSpecificationDeleteCallback(ProductSpecificationDeleteEvent productSpecificationDeleteEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productSpecificationDeleteEvent")) { + sendProductSpecificationDeleteEventToCallback(subscription.getCallback(), productSpecificationDeleteEvent); + } + } + } + + /** + * Send product specification create event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product specification create event + */ + private void sendProductSpecificationCreateEventToCallback(String callbackUrl, ProductSpecificationCreateEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productSpecificationCreateEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product specification create event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product specification create event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product specification delete event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product specification delete event + */ + private void sendProductSpecificationDeleteEventToCallback(String callbackUrl, ProductSpecificationDeleteEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productSpecificationDeleteEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product specification delete event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product specification delete event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Build the full callback URL with the listener endpoint + * @param baseUrl The base callback URL + * @param listenerPath The listener path to append + * @return The complete callback URL + */ + private String buildCallbackUrl(String baseUrl, String listenerPath) { + if (baseUrl.endsWith("/")) { + return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath; + } else { + return baseUrl + listenerPath; + } + } + + /** + * Check if a subscription should be notified for a specific event type + * @param subscription The event subscription + * @param eventType The event type to check + * @return true if the subscription should be notified + */ + private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) { + // If no query is specified, notify all events + if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) { + return true; + } + + // Check if the query contains the event type + String query = subscription.getQuery().toLowerCase(); + return query.contains("productspecification") || + query.contains(eventType.toLowerCase()) || + query.contains("productspecification.create") || + query.contains("productspecification.delete"); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationNotificationService.java new file mode 100644 index 00000000..0818539a --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationNotificationService.java @@ -0,0 +1,151 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.ProductSpecification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteNotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class ProductSpecificationNotificationService { + + private static final Logger logger = LoggerFactory.getLogger(ProductSpecificationNotificationService.class); + + @Autowired + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Autowired + private ProductSpecificationCallbackService productSpecificationCallbackService; + + /** + * Publish a product specification create notification + * @param productSpecification The created product specification + */ + public void publishProductSpecificationCreateNotification(ProductSpecification productSpecification) { + try { + ProductSpecificationCreateNotification notification = createProductSpecificationCreateNotification(productSpecification); + eventPublisher.publishEvent(notification, productSpecification.getUuid()); + + // Send callbacks to registered subscribers + productSpecificationCallbackService.sendProductSpecificationCreateCallback(notification.getEvent()); + + logger.info("Published product specification create notification for product spec ID: {}", productSpecification.getUuid()); + } catch (Exception e) { + logger.error("Error publishing product specification create notification for product spec ID: {}", productSpecification.getUuid(), e); + } + } + + /** + * Publish a product specification delete notification + * @param productSpecification The deleted product specification + */ + public void publishProductSpecificationDeleteNotification(ProductSpecification productSpecification) { + try { + ProductSpecificationDeleteNotification notification = createProductSpecificationDeleteNotification(productSpecification); + eventPublisher.publishEvent(notification, productSpecification.getUuid()); + + // Send callbacks to registered subscribers + productSpecificationCallbackService.sendProductSpecificationDeleteCallback(notification.getEvent()); + + logger.info("Published product specification delete notification for product spec ID: {}", productSpecification.getUuid()); + } catch (Exception e) { + logger.error("Error publishing product specification delete notification for product spec ID: {}", productSpecification.getUuid(), e); + } + } + + /** + * Create a product specification create notification + * @param productSpecification The created product specification + * @return ProductSpecificationCreateNotification + */ + private ProductSpecificationCreateNotification createProductSpecificationCreateNotification(ProductSpecification productSpecification) { + ProductSpecificationCreateNotification notification = new ProductSpecificationCreateNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductSpecificationCreateNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productSpecification/" + productSpecification.getUuid()); + + // Create event + ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductSpecificationCreateEvent"); + event.setTitle("Product Specification Create Event"); + event.setDescription("A product specification has been created"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductSpecificationCreateEventPayload payload = new ProductSpecificationCreateEventPayload(); + payload.setProductSpecification(productSpecification); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product specification delete notification + * @param productSpecification The deleted product specification + * @return ProductSpecificationDeleteNotification + */ + private ProductSpecificationDeleteNotification createProductSpecificationDeleteNotification(ProductSpecification productSpecification) { + ProductSpecificationDeleteNotification notification = new ProductSpecificationDeleteNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductSpecificationDeleteNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productSpecification/" + productSpecification.getUuid()); + + // Create event + ProductSpecificationDeleteEvent event = new ProductSpecificationDeleteEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductSpecificationDeleteEvent"); + event.setTitle("Product Specification Delete Event"); + event.setDescription("A product specification has been deleted"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductSpecificationDeleteEventPayload payload = new ProductSpecificationDeleteEventPayload(); + payload.setProductSpecification(productSpecification); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java index deec0a66..8b8fdfc7 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java @@ -70,6 +70,9 @@ public class ProductSpecificationRepoService { @Autowired ServiceSpecificationRepoService serviceSpecificationRepoService; + + @Autowired + private ProductSpecificationNotificationService productSpecificationNotificationService; private SessionFactory sessionFactory; @@ -94,8 +97,12 @@ public class ProductSpecificationRepoService { serviceSpec = this.updateProductSpecificationDataFromAPIcall(serviceSpec, serviceProductSpecification); serviceSpec = this.prodsOfferingRepo.save(serviceSpec); + // Publish product specification create notification + if (productSpecificationNotificationService != null) { + productSpecificationNotificationService.publishProductSpecificationCreateNotification(serviceSpec); + } - return this.prodsOfferingRepo.save(serviceSpec); + return serviceSpec; } public List findAll() { @@ -252,6 +259,12 @@ public class ProductSpecificationRepoService { */ this.prodsOfferingRepo.delete(s); + + // Publish product specification delete notification + if (productSpecificationNotificationService != null) { + productSpecificationNotificationService.publishProductSpecificationDeleteNotification(s); + } + return null; } diff --git a/src/main/resources/application-testing.yml b/src/main/resources/application-testing.yml index d5506a3b..decd660c 100644 --- a/src/main/resources/application-testing.yml +++ b/src/main/resources/application-testing.yml @@ -189,6 +189,8 @@ EVENT_PRODUCT_CATALOG_CREATE: "jms:topic:EVENT.PRODUCTCATALOG.CREATE" EVENT_PRODUCT_CATALOG_DELETE: "jms:topic:EVENT.PRODUCTCATALOG.DELETE" EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE" EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE" +EVENT_PRODUCT_SPECIFICATION_CREATE: "jms:topic:EVENT.PRODUCTSPECIFICATION.CREATE" +EVENT_PRODUCT_SPECIFICATION_DELETE: "jms:topic:EVENT.PRODUCTSPECIFICATION.DELETE" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1bfea10e..25d47524 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -214,6 +214,8 @@ EVENT_PRODUCT_CATALOG_CREATE: "jms:topic:EVENT.PRODUCTCATALOG.CREATE" EVENT_PRODUCT_CATALOG_DELETE: "jms:topic:EVENT.PRODUCTCATALOG.DELETE" EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE" EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE" +EVENT_PRODUCT_SPECIFICATION_CREATE: "jms:topic:EVENT.PRODUCTSPECIFICATION.CREATE" +EVENT_PRODUCT_SPECIFICATION_DELETE: "jms:topic:EVENT.PRODUCTSPECIFICATION.DELETE" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackIntegrationTest.java new file mode 100644 index 00000000..f85b8671 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackIntegrationTest.java @@ -0,0 +1,205 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.etsi.osl.tmf.JsonUtils; +import org.etsi.osl.tmf.OpenAPISpringBoot; +import org.etsi.osl.tmf.pcm620.model.ProductSpecification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreate; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput; +import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RunWith(SpringRunner.class) +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class) +@AutoConfigureMockMvc +@ActiveProfiles("testing") +@AutoConfigureTestDatabase +public class ProductSpecificationCallbackIntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private ProductSpecificationRepoService productSpecificationRepoService; + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @SpyBean + private ProductSpecificationCallbackService productSpecificationCallbackService; + + @MockBean + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + mvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + + // Mock RestTemplate to avoid actual HTTP calls in tests + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCompleteCallbackFlow() throws Exception { + // Step 1: Register a callback subscription via Hub API + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:8080/test-callback"); + subscriptionInput.setQuery("productspecification.create,productspecification.delete"); + + MvcResult subscriptionResult = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()) + .andReturn(); + + String subscriptionResponseBody = subscriptionResult.getResponse().getContentAsString(); + EventSubscription createdSubscription = objectMapper.readValue(subscriptionResponseBody, EventSubscription.class); + + // Step 2: Create a product specification (should trigger callback) + ProductSpecificationCreate productSpecificationCreate = new ProductSpecificationCreate(); + productSpecificationCreate.setName("Test Callback Product Specification"); + productSpecificationCreate.setDescription("A product specification to test callback notifications"); + productSpecificationCreate.setVersion("1.0"); + + ProductSpecification createdProductSpecification = productSpecificationRepoService.addProductSpecification(productSpecificationCreate); + + // Step 3: Verify callback was sent + verify(productSpecificationCallbackService, timeout(2000)).sendProductSpecificationCreateCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Step 4: Delete the product specification (should trigger delete callback) + productSpecificationRepoService.deleteByUuid(createdProductSpecification.getUuid()); + + // Step 5: Verify delete callback was sent + verify(productSpecificationCallbackService, timeout(2000)).sendProductSpecificationDeleteCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/productSpecificationDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCallbackFilteringByQuery() throws Exception { + // Step 1: Register subscription only for create events + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:9090/create-only"); + subscriptionInput.setQuery("productspecification.create"); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create and delete a product specification + ProductSpecificationCreate productSpecificationCreate = new ProductSpecificationCreate(); + productSpecificationCreate.setName("Test Filter Product Specification"); + productSpecificationCreate.setDescription("A product specification to test query filtering"); + productSpecificationCreate.setVersion("1.0"); + + ProductSpecification createdProductSpecification = productSpecificationRepoService.addProductSpecification(productSpecificationCreate); + productSpecificationRepoService.deleteByUuid(createdProductSpecification.getUuid()); + + // Step 3: Verify only create callback was sent (not delete) + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:9090/create-only/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Note: In a more sophisticated test, we could verify that the delete callback was NOT sent + // by using verify with never(), but this requires more complex mock setup + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testProductSpecificationCallbackWithAllEventsQuery() throws Exception { + // Step 1: Register subscription for all events (empty query) + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:7070/all-events"); + subscriptionInput.setQuery(""); // Empty query should receive all events + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create a product specification + ProductSpecificationCreate productSpecificationCreate = new ProductSpecificationCreate(); + productSpecificationCreate.setName("Test All Events Product Specification"); + productSpecificationCreate.setDescription("A product specification to test all events subscription"); + productSpecificationCreate.setVersion("1.0"); + + ProductSpecification createdProductSpecification = productSpecificationRepoService.addProductSpecification(productSpecificationCreate); + + // Step 3: Verify callback was sent even with empty query + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:7070/all-events/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackServiceTest.java new file mode 100644 index 00000000..1d11a49d --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationCallbackServiceTest.java @@ -0,0 +1,188 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.ProductSpecification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class ProductSpecificationCallbackServiceTest { + + @Mock + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private ProductSpecificationCallbackService productSpecificationCallbackService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testSendProductSpecificationCreateCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productspecification"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent(); + event.setEventId("test-event-123"); + + // Act + productSpecificationCallbackService.sendProductSpecificationCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductSpecificationDeleteCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productspecification"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductSpecificationDeleteEvent event = new ProductSpecificationDeleteEvent(); + event.setEventId("test-event-456"); + + // Act + productSpecificationCallbackService.sendProductSpecificationDeleteCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productSpecificationDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testCallbackWithTrailingSlash() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback/"); + subscription.setQuery("productspecification"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent(); + event.setEventId("test-event-789"); + + // Act + productSpecificationCallbackService.sendProductSpecificationCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testFilterSubscriptionsByQuery() { + // Arrange + EventSubscription productSpecSubscription = new EventSubscription(); + productSpecSubscription.setCallback("http://localhost:8080/productspec-callback"); + productSpecSubscription.setQuery("productspecification"); + + EventSubscription otherSubscription = new EventSubscription(); + otherSubscription.setCallback("http://localhost:8080/other-callback"); + otherSubscription.setQuery("catalog"); + + List subscriptions = Arrays.asList(productSpecSubscription, otherSubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent(); + event.setEventId("test-event-filter"); + + // Act + productSpecificationCallbackService.sendProductSpecificationCreateCallback(event); + + // Assert - only product specification subscription should receive callback + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/productspec-callback/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testProductSpecificationSpecificQueries() { + // Arrange + EventSubscription createOnlySubscription = new EventSubscription(); + createOnlySubscription.setCallback("http://localhost:9090/create-only"); + createOnlySubscription.setQuery("productspecification.create"); + + List subscriptions = Arrays.asList(createOnlySubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductSpecificationCreateEvent event = new ProductSpecificationCreateEvent(); + event.setEventId("test-event-specific-query"); + + // Act + productSpecificationCallbackService.sendProductSpecificationCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:9090/create-only/listener/productSpecificationCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationNotificationServiceTest.java new file mode 100644 index 00000000..b251a053 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductSpecificationNotificationServiceTest.java @@ -0,0 +1,87 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.ProductSpecification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteNotification; +import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationNotificationService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductSpecificationCallbackService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class ProductSpecificationNotificationServiceTest { + + @Mock + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Mock + private ProductSpecificationCallbackService productSpecificationCallbackService; + + @InjectMocks + private ProductSpecificationNotificationService productSpecificationNotificationService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testPublishProductSpecificationCreateNotification() { + // Arrange + ProductSpecification productSpecification = new ProductSpecification(); + productSpecification.setUuid("test-productspec-123"); + productSpecification.setName("Test Product Specification"); + productSpecification.setDescription("A test product specification for notifications"); + + // Act + productSpecificationNotificationService.publishProductSpecificationCreateNotification(productSpecification); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductSpecificationCreateNotification.class), eq("test-productspec-123")); + verify(productSpecificationCallbackService, times(1)).sendProductSpecificationCreateCallback(any()); + } + + @Test + public void testPublishProductSpecificationDeleteNotification() { + // Arrange + ProductSpecification productSpecification = new ProductSpecification(); + productSpecification.setUuid("test-productspec-456"); + productSpecification.setName("Test Product Specification to Delete"); + productSpecification.setDescription("A test product specification for delete notifications"); + + // Act + productSpecificationNotificationService.publishProductSpecificationDeleteNotification(productSpecification); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductSpecificationDeleteNotification.class), eq("test-productspec-456")); + verify(productSpecificationCallbackService, times(1)).sendProductSpecificationDeleteCallback(any()); + } + + @Test + public void testCreateNotificationStructure() { + // Arrange + ProductSpecification productSpecification = new ProductSpecification(); + productSpecification.setUuid("test-productspec-789"); + productSpecification.setName("Test Product Specification Structure"); + + // Act + productSpecificationNotificationService.publishProductSpecificationCreateNotification(productSpecification); + + // Assert - verify the notification was published with correct structure + verify(eventPublisher).publishEvent(any(ProductSpecificationCreateNotification.class), eq("test-productspec-789")); + verify(productSpecificationCallbackService).sendProductSpecificationCreateCallback(any()); + } +} \ No newline at end of file -- GitLab From 4d4728cd921168df8b10390f65287441769d809e Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 02:04:22 +0300 Subject: [PATCH 16/21] adding ProductOffering events --- .../ProductCatalogApiRouteBuilderEvents.java | 26 +- .../ProductOfferingCallbackService.java | 238 ++++++++++++++++ .../ProductOfferingNotificationService.java | 257 +++++++++++++++++ .../ProductOfferingRepoService.java | 33 ++- src/main/resources/application-testing.yml | 4 + src/main/resources/application.yml | 4 + ...roductOfferingCallbackIntegrationTest.java | 254 +++++++++++++++++ .../ProductOfferingCallbackServiceTest.java | 258 ++++++++++++++++++ ...roductOfferingNotificationServiceTest.java | 121 ++++++++ 9 files changed, 1191 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingCallbackService.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingNotificationService.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackServiceTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingNotificationServiceTest.java diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java index 674c0cfb..c0828283 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -39,6 +39,10 @@ import org.etsi.osl.tmf.pcm620.model.CategoryCreateNotification; import org.etsi.osl.tmf.pcm620.model.CategoryDeleteNotification; import org.etsi.osl.tmf.pcm620.model.ProductSpecificationCreateNotification; import org.etsi.osl.tmf.pcm620.model.ProductSpecificationDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeNotification; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -68,6 +72,18 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { @Value("${EVENT_PRODUCT_SPECIFICATION_DELETE}") private String EVENT_PRODUCT_SPECIFICATION_DELETE = "direct:EVENT_PRODUCT_SPECIFICATION_DELETE"; + + @Value("${EVENT_PRODUCT_OFFERING_CREATE}") + private String EVENT_PRODUCT_OFFERING_CREATE = "direct:EVENT_PRODUCT_OFFERING_CREATE"; + + @Value("${EVENT_PRODUCT_OFFERING_DELETE}") + private String EVENT_PRODUCT_OFFERING_DELETE = "direct:EVENT_PRODUCT_OFFERING_DELETE"; + + @Value("${EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE}") + private String EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE = "direct:EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE"; + + @Value("${EVENT_PRODUCT_OFFERING_STATE_CHANGE}") + private String EVENT_PRODUCT_OFFERING_STATE_CHANGE = "direct:EVENT_PRODUCT_OFFERING_STATE_CHANGE"; @Value("${spring.application.name}") private String compname; @@ -106,7 +122,15 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { } else if (n instanceof ProductSpecificationCreateNotification) { msgtopic = EVENT_PRODUCT_SPECIFICATION_CREATE; } else if (n instanceof ProductSpecificationDeleteNotification) { - msgtopic = EVENT_PRODUCT_SPECIFICATION_DELETE; + msgtopic = EVENT_PRODUCT_SPECIFICATION_DELETE; + } else if (n instanceof ProductOfferingCreateNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_CREATE; + } else if (n instanceof ProductOfferingDeleteNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_DELETE; + } else if (n instanceof ProductOfferingAttributeValueChangeNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE; + } else if (n instanceof ProductOfferingStateChangeNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_STATE_CHANGE; } Map map = new HashMap<>(); diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingCallbackService.java new file mode 100644 index 00000000..f6008a08 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingCallbackService.java @@ -0,0 +1,238 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class ProductOfferingCallbackService { + + private static final Logger logger = LoggerFactory.getLogger(ProductOfferingCallbackService.class); + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Autowired + private RestTemplate restTemplate; + + /** + * Send product offering create event to all registered callback URLs + * @param productOfferingCreateEvent The product offering create event to send + */ + public void sendProductOfferingCreateCallback(ProductOfferingCreateEvent productOfferingCreateEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingCreateEvent")) { + sendProductOfferingCreateEventToCallback(subscription.getCallback(), productOfferingCreateEvent); + } + } + } + + /** + * Send product offering delete event to all registered callback URLs + * @param productOfferingDeleteEvent The product offering delete event to send + */ + public void sendProductOfferingDeleteCallback(ProductOfferingDeleteEvent productOfferingDeleteEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingDeleteEvent")) { + sendProductOfferingDeleteEventToCallback(subscription.getCallback(), productOfferingDeleteEvent); + } + } + } + + /** + * Send product offering attribute value change event to all registered callback URLs + * @param productOfferingAttributeValueChangeEvent The product offering attribute value change event to send + */ + public void sendProductOfferingAttributeValueChangeCallback(ProductOfferingAttributeValueChangeEvent productOfferingAttributeValueChangeEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingAttributeValueChangeEvent")) { + sendProductOfferingAttributeValueChangeEventToCallback(subscription.getCallback(), productOfferingAttributeValueChangeEvent); + } + } + } + + /** + * Send product offering state change event to all registered callback URLs + * @param productOfferingStateChangeEvent The product offering state change event to send + */ + public void sendProductOfferingStateChangeCallback(ProductOfferingStateChangeEvent productOfferingStateChangeEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingStateChangeEvent")) { + sendProductOfferingStateChangeEventToCallback(subscription.getCallback(), productOfferingStateChangeEvent); + } + } + } + + /** + * Send product offering create event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering create event + */ + private void sendProductOfferingCreateEventToCallback(String callbackUrl, ProductOfferingCreateEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingCreateEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering create event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering create event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product offering delete event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering delete event + */ + private void sendProductOfferingDeleteEventToCallback(String callbackUrl, ProductOfferingDeleteEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingDeleteEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering delete event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering delete event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product offering attribute value change event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering attribute value change event + */ + private void sendProductOfferingAttributeValueChangeEventToCallback(String callbackUrl, ProductOfferingAttributeValueChangeEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingAttributeValueChangeEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering attribute value change event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering attribute value change event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product offering state change event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering state change event + */ + private void sendProductOfferingStateChangeEventToCallback(String callbackUrl, ProductOfferingStateChangeEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingStateChangeEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering state change event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering state change event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Build the full callback URL with the listener endpoint + * @param baseUrl The base callback URL + * @param listenerPath The listener path to append + * @return The complete callback URL + */ + private String buildCallbackUrl(String baseUrl, String listenerPath) { + if (baseUrl.endsWith("/")) { + return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath; + } else { + return baseUrl + listenerPath; + } + } + + /** + * Check if a subscription should be notified for a specific event type + * @param subscription The event subscription + * @param eventType The event type to check + * @return true if the subscription should be notified + */ + private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) { + // If no query is specified, notify all events + if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) { + return true; + } + + // Check if the query contains the event type + String query = subscription.getQuery().toLowerCase(); + return query.contains("productoffering") || + query.contains(eventType.toLowerCase()) || + query.contains("productoffering.create") || + query.contains("productoffering.delete") || + query.contains("productoffering.attributevaluechange") || + query.contains("productoffering.statechange"); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingNotificationService.java new file mode 100644 index 00000000..e10e25f0 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingNotificationService.java @@ -0,0 +1,257 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.ProductOffering; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeNotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class ProductOfferingNotificationService { + + private static final Logger logger = LoggerFactory.getLogger(ProductOfferingNotificationService.class); + + @Autowired + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Autowired + private ProductOfferingCallbackService productOfferingCallbackService; + + /** + * Publish a product offering create notification + * @param productOffering The created product offering + */ + public void publishProductOfferingCreateNotification(ProductOffering productOffering) { + try { + ProductOfferingCreateNotification notification = createProductOfferingCreateNotification(productOffering); + eventPublisher.publishEvent(notification, productOffering.getUuid()); + + // Send callbacks to registered subscribers + productOfferingCallbackService.sendProductOfferingCreateCallback(notification.getEvent()); + + logger.info("Published product offering create notification for product offering ID: {}", productOffering.getUuid()); + } catch (Exception e) { + logger.error("Error publishing product offering create notification for product offering ID: {}", productOffering.getUuid(), e); + } + } + + /** + * Publish a product offering delete notification + * @param productOffering The deleted product offering + */ + public void publishProductOfferingDeleteNotification(ProductOffering productOffering) { + try { + ProductOfferingDeleteNotification notification = createProductOfferingDeleteNotification(productOffering); + eventPublisher.publishEvent(notification, productOffering.getUuid()); + + // Send callbacks to registered subscribers + productOfferingCallbackService.sendProductOfferingDeleteCallback(notification.getEvent()); + + logger.info("Published product offering delete notification for product offering ID: {}", productOffering.getUuid()); + } catch (Exception e) { + logger.error("Error publishing product offering delete notification for product offering ID: {}", productOffering.getUuid(), e); + } + } + + /** + * Publish a product offering attribute value change notification + * @param productOffering The product offering with changed attributes + */ + public void publishProductOfferingAttributeValueChangeNotification(ProductOffering productOffering) { + try { + ProductOfferingAttributeValueChangeNotification notification = createProductOfferingAttributeValueChangeNotification(productOffering); + eventPublisher.publishEvent(notification, productOffering.getUuid()); + + // Send callbacks to registered subscribers + productOfferingCallbackService.sendProductOfferingAttributeValueChangeCallback(notification.getEvent()); + + logger.info("Published product offering attribute value change notification for product offering ID: {}", productOffering.getUuid()); + } catch (Exception e) { + logger.error("Error publishing product offering attribute value change notification for product offering ID: {}", productOffering.getUuid(), e); + } + } + + /** + * Publish a product offering state change notification + * @param productOffering The product offering with changed state + */ + public void publishProductOfferingStateChangeNotification(ProductOffering productOffering) { + try { + ProductOfferingStateChangeNotification notification = createProductOfferingStateChangeNotification(productOffering); + eventPublisher.publishEvent(notification, productOffering.getUuid()); + + // Send callbacks to registered subscribers + productOfferingCallbackService.sendProductOfferingStateChangeCallback(notification.getEvent()); + + logger.info("Published product offering state change notification for product offering ID: {}", productOffering.getUuid()); + } catch (Exception e) { + logger.error("Error publishing product offering state change notification for product offering ID: {}", productOffering.getUuid(), e); + } + } + + /** + * Create a product offering create notification + * @param productOffering The created product offering + * @return ProductOfferingCreateNotification + */ + private ProductOfferingCreateNotification createProductOfferingCreateNotification(ProductOffering productOffering) { + ProductOfferingCreateNotification notification = new ProductOfferingCreateNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingCreateNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOffering/" + productOffering.getUuid()); + + // Create event + ProductOfferingCreateEvent event = new ProductOfferingCreateEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingCreateEvent"); + event.setTitle("Product Offering Create Event"); + event.setDescription("A product offering has been created"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingCreateEventPayload payload = new ProductOfferingCreateEventPayload(); + payload.setProductOffering(productOffering); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product offering delete notification + * @param productOffering The deleted product offering + * @return ProductOfferingDeleteNotification + */ + private ProductOfferingDeleteNotification createProductOfferingDeleteNotification(ProductOffering productOffering) { + ProductOfferingDeleteNotification notification = new ProductOfferingDeleteNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingDeleteNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOffering/" + productOffering.getUuid()); + + // Create event + ProductOfferingDeleteEvent event = new ProductOfferingDeleteEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingDeleteEvent"); + event.setTitle("Product Offering Delete Event"); + event.setDescription("A product offering has been deleted"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingDeleteEventPayload payload = new ProductOfferingDeleteEventPayload(); + payload.setProductOffering(productOffering); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product offering attribute value change notification + * @param productOffering The product offering with changed attributes + * @return ProductOfferingAttributeValueChangeNotification + */ + private ProductOfferingAttributeValueChangeNotification createProductOfferingAttributeValueChangeNotification(ProductOffering productOffering) { + ProductOfferingAttributeValueChangeNotification notification = new ProductOfferingAttributeValueChangeNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingAttributeValueChangeNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOffering/" + productOffering.getUuid()); + + // Create event + ProductOfferingAttributeValueChangeEvent event = new ProductOfferingAttributeValueChangeEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingAttributeValueChangeEvent"); + event.setTitle("Product Offering Attribute Value Change Event"); + event.setDescription("A product offering attribute value has been changed"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingAttributeValueChangeEventPayload payload = new ProductOfferingAttributeValueChangeEventPayload(); + payload.setProductOffering(productOffering); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product offering state change notification + * @param productOffering The product offering with changed state + * @return ProductOfferingStateChangeNotification + */ + private ProductOfferingStateChangeNotification createProductOfferingStateChangeNotification(ProductOffering productOffering) { + ProductOfferingStateChangeNotification notification = new ProductOfferingStateChangeNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingStateChangeNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOffering/" + productOffering.getUuid()); + + // Create event + ProductOfferingStateChangeEvent event = new ProductOfferingStateChangeEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingStateChangeEvent"); + event.setTitle("Product Offering State Change Event"); + event.setDescription("A product offering state has been changed"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingStateChangeEventPayload payload = new ProductOfferingStateChangeEventPayload(); + payload.setProductOffering(productOffering); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java index 942d0e7d..55d6c1b0 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java @@ -77,6 +77,9 @@ public class ProductOfferingRepoService { @Autowired ServiceSpecificationRepoService serviceSpecificationRepoService; + + @Autowired + private ProductOfferingNotificationService productOfferingNotificationService; private SessionFactory sessionFactory; @@ -101,8 +104,12 @@ public class ProductOfferingRepoService { serviceSpec = this.updateProductOfferingDataFromAPIcall(serviceSpec, serviceProductOffering); serviceSpec = this.prodsOfferingRepo.save(serviceSpec); + // Publish product offering create notification + if (productOfferingNotificationService != null) { + productOfferingNotificationService.publishProductOfferingCreateNotification(serviceSpec); + } - return this.prodsOfferingRepo.save(serviceSpec); + return serviceSpec; } public List findAll() { @@ -261,6 +268,12 @@ public class ProductOfferingRepoService { */ this.prodsOfferingRepo.delete(s); + + // Publish product offering delete notification + if (productOfferingNotificationService != null) { + productOfferingNotificationService.publishProductOfferingDeleteNotification(s); + } + return null; } @@ -273,14 +286,28 @@ public class ProductOfferingRepoService { if (s == null) { return null; } + + // Store original state for comparison + String originalLifecycleStatus = s.getLifecycleStatus(); + ProductOffering prodOff = s; prodOff = this.updateProductOfferingDataFromAPIcall(prodOff, aProductOffering); prodOff = this.prodsOfferingRepo.save(prodOff); + // Publish notifications + if (productOfferingNotificationService != null) { + // Always publish attribute value change notification on update + productOfferingNotificationService.publishProductOfferingAttributeValueChangeNotification(prodOff); + + // Publish state change notification if lifecycle status changed + if (originalLifecycleStatus != null && prodOff.getLifecycleStatus() != null + && !originalLifecycleStatus.equals(prodOff.getLifecycleStatus())) { + productOfferingNotificationService.publishProductOfferingStateChangeNotification(prodOff); + } + } - - return this.prodsOfferingRepo.save(prodOff); + return prodOff; } diff --git a/src/main/resources/application-testing.yml b/src/main/resources/application-testing.yml index decd660c..19366def 100644 --- a/src/main/resources/application-testing.yml +++ b/src/main/resources/application-testing.yml @@ -191,6 +191,10 @@ EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE" EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE" EVENT_PRODUCT_SPECIFICATION_CREATE: "jms:topic:EVENT.PRODUCTSPECIFICATION.CREATE" EVENT_PRODUCT_SPECIFICATION_DELETE: "jms:topic:EVENT.PRODUCTSPECIFICATION.DELETE" +EVENT_PRODUCT_OFFERING_CREATE: "jms:topic:EVENT.PRODUCTOFFERING.CREATE" +EVENT_PRODUCT_OFFERING_DELETE: "jms:topic:EVENT.PRODUCTOFFERING.DELETE" +EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.ATTRCHANGED" +EVENT_PRODUCT_OFFERING_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.STATECHANGED" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 25d47524..66995d33 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -216,6 +216,10 @@ EVENT_PRODUCT_CATEGORY_CREATE: "jms:topic:EVENT.PRODUCTCATEGORY.CREATE" EVENT_PRODUCT_CATEGORY_DELETE: "jms:topic:EVENT.PRODUCTCATEGORY.DELETE" EVENT_PRODUCT_SPECIFICATION_CREATE: "jms:topic:EVENT.PRODUCTSPECIFICATION.CREATE" EVENT_PRODUCT_SPECIFICATION_DELETE: "jms:topic:EVENT.PRODUCTSPECIFICATION.DELETE" +EVENT_PRODUCT_OFFERING_CREATE: "jms:topic:EVENT.PRODUCTOFFERING.CREATE" +EVENT_PRODUCT_OFFERING_DELETE: "jms:topic:EVENT.PRODUCTOFFERING.DELETE" +EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.ATTRCHANGED" +EVENT_PRODUCT_OFFERING_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.STATECHANGED" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java new file mode 100644 index 00000000..1755896d --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java @@ -0,0 +1,254 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.etsi.osl.tmf.JsonUtils; +import org.etsi.osl.tmf.OpenAPISpringBoot; +import org.etsi.osl.tmf.pcm620.model.ProductOffering; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreate; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingUpdate; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.model.EventSubscriptionInput; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@RunWith(SpringRunner.class) +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class) +@AutoConfigureMockMvc +@ActiveProfiles("testing") +@AutoConfigureTestDatabase +public class ProductOfferingCallbackIntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private ProductOfferingRepoService productOfferingRepoService; + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @SpyBean + private ProductOfferingCallbackService productOfferingCallbackService; + + @MockBean + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + mvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + + // Mock RestTemplate to avoid actual HTTP calls in tests + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("OK", HttpStatus.OK)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCompleteCallbackFlow() throws Exception { + // Step 1: Register a callback subscription via Hub API + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:8080/test-callback"); + subscriptionInput.setQuery("productoffering.create,productoffering.delete"); + + MvcResult subscriptionResult = mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()) + .andReturn(); + + String subscriptionResponseBody = subscriptionResult.getResponse().getContentAsString(); + EventSubscription createdSubscription = objectMapper.readValue(subscriptionResponseBody, EventSubscription.class); + + // Step 2: Create a product offering (should trigger callback) + ProductOfferingCreate productOfferingCreate = new ProductOfferingCreate(); + productOfferingCreate.setName("Test Callback Product Offering"); + productOfferingCreate.setDescription("A product offering to test callback notifications"); + productOfferingCreate.setVersion("1.0"); + + ProductOffering createdProductOffering = productOfferingRepoService.addProductOffering(productOfferingCreate); + + // Step 3: Verify callback was sent + verify(productOfferingCallbackService, timeout(2000)).sendProductOfferingCreateCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Step 4: Delete the product offering (should trigger delete callback) + productOfferingRepoService.deleteByUuid(createdProductOffering.getUuid()); + + // Step 5: Verify delete callback was sent + verify(productOfferingCallbackService, timeout(2000)).sendProductOfferingDeleteCallback(any()); + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/test-callback/listener/productOfferingDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testAttributeValueChangeAndStateChangeCallbacks() throws Exception { + // Step 1: Register subscription for attribute and state change events + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:8080/change-callback"); + subscriptionInput.setQuery("productoffering.attributevaluechange,productoffering.statechange"); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create a product offering + ProductOfferingCreate productOfferingCreate = new ProductOfferingCreate(); + productOfferingCreate.setName("Test Change Product Offering"); + productOfferingCreate.setDescription("A product offering to test change notifications"); + productOfferingCreate.setVersion("1.0"); + productOfferingCreate.setLifecycleStatus("Active"); + + ProductOffering createdProductOffering = productOfferingRepoService.addProductOffering(productOfferingCreate); + + // Step 3: Update the product offering (should trigger attribute value change callback) + ProductOfferingUpdate productOfferingUpdate = new ProductOfferingUpdate(); + productOfferingUpdate.setDescription("Updated description for testing"); + productOfferingUpdate.setLifecycleStatus("Retired"); + + productOfferingRepoService.updateProductOffering(createdProductOffering.getUuid(), productOfferingUpdate); + + // Step 4: Verify both attribute value change and state change callbacks were sent + verify(productOfferingCallbackService, timeout(2000)).sendProductOfferingAttributeValueChangeCallback(any()); + verify(productOfferingCallbackService, timeout(2000)).sendProductOfferingStateChangeCallback(any()); + + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/change-callback/listener/productOfferingAttributeValueChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:8080/change-callback/listener/productOfferingStateChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testCallbackFilteringByEventType() throws Exception { + // Step 1: Register subscription only for create events + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:9090/create-only"); + subscriptionInput.setQuery("productoffering.create"); + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create and delete a product offering + ProductOfferingCreate productOfferingCreate = new ProductOfferingCreate(); + productOfferingCreate.setName("Test Filter Product Offering"); + productOfferingCreate.setDescription("A product offering to test query filtering"); + productOfferingCreate.setVersion("1.0"); + + ProductOffering createdProductOffering = productOfferingRepoService.addProductOffering(productOfferingCreate); + productOfferingRepoService.deleteByUuid(createdProductOffering.getUuid()); + + // Step 3: Verify only create callback was sent (not delete) + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:9090/create-only/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + + // Note: In a more sophisticated test, we could verify that the delete callback was NOT sent + // by using verify with never(), but this requires more complex mock setup + } + + @Test + @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + public void testProductOfferingCallbackWithAllEventsQuery() throws Exception { + // Step 1: Register subscription for all events (empty query) + EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); + subscriptionInput.setCallback("http://localhost:7070/all-events"); + subscriptionInput.setQuery(""); // Empty query should receive all events + + mvc.perform(MockMvcRequestBuilders.post("/productCatalogManagement/v4/hub") + .with(SecurityMockMvcRequestPostProcessors.csrf()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(JsonUtils.toJson(subscriptionInput))) + .andExpect(status().isCreated()); + + // Step 2: Create a product offering + ProductOfferingCreate productOfferingCreate = new ProductOfferingCreate(); + productOfferingCreate.setName("Test All Events Product Offering"); + productOfferingCreate.setDescription("A product offering to test all events subscription"); + productOfferingCreate.setVersion("1.0"); + + ProductOffering createdProductOffering = productOfferingRepoService.addProductOffering(productOfferingCreate); + + // Step 3: Verify callback was sent even with empty query + verify(restTemplate, timeout(2000)).exchange( + eq("http://localhost:7070/all-events/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class)); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackServiceTest.java new file mode 100644 index 00000000..40875e4a --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackServiceTest.java @@ -0,0 +1,258 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.ProductOffering; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class ProductOfferingCallbackServiceTest { + + @Mock + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private ProductOfferingCallbackService productOfferingCallbackService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testSendProductOfferingCreateCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productoffering"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingCreateEvent event = new ProductOfferingCreateEvent(); + event.setEventId("test-event-123"); + + // Act + productOfferingCallbackService.sendProductOfferingCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductOfferingDeleteCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productoffering"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingDeleteEvent event = new ProductOfferingDeleteEvent(); + event.setEventId("test-event-456"); + + // Act + productOfferingCallbackService.sendProductOfferingDeleteCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductOfferingAttributeValueChangeCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productoffering.attributevaluechange"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingAttributeValueChangeEvent event = new ProductOfferingAttributeValueChangeEvent(); + event.setEventId("test-event-789"); + + // Act + productOfferingCallbackService.sendProductOfferingAttributeValueChangeCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingAttributeValueChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductOfferingStateChangeCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productoffering.statechange"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingStateChangeEvent event = new ProductOfferingStateChangeEvent(); + event.setEventId("test-event-101"); + + // Act + productOfferingCallbackService.sendProductOfferingStateChangeCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingStateChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testCallbackWithTrailingSlash() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback/"); + subscription.setQuery("productoffering"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingCreateEvent event = new ProductOfferingCreateEvent(); + event.setEventId("test-event-trailing-slash"); + + // Act + productOfferingCallbackService.sendProductOfferingCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testFilterSubscriptionsByQuery() { + // Arrange + EventSubscription productOfferingSubscription = new EventSubscription(); + productOfferingSubscription.setCallback("http://localhost:8080/productoffering-callback"); + productOfferingSubscription.setQuery("productoffering"); + + EventSubscription otherSubscription = new EventSubscription(); + otherSubscription.setCallback("http://localhost:8080/other-callback"); + otherSubscription.setQuery("catalog"); + + List subscriptions = Arrays.asList(productOfferingSubscription, otherSubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingCreateEvent event = new ProductOfferingCreateEvent(); + event.setEventId("test-event-filter"); + + // Act + productOfferingCallbackService.sendProductOfferingCreateCallback(event); + + // Assert - only product offering subscription should receive callback + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/productoffering-callback/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSpecificEventTypeQueries() { + // Arrange + EventSubscription createOnlySubscription = new EventSubscription(); + createOnlySubscription.setCallback("http://localhost:9090/create-only"); + createOnlySubscription.setQuery("productoffering.create"); + + EventSubscription stateChangeOnlySubscription = new EventSubscription(); + stateChangeOnlySubscription.setCallback("http://localhost:9091/state-change-only"); + stateChangeOnlySubscription.setQuery("productoffering.statechange"); + + List subscriptions = Arrays.asList(createOnlySubscription, stateChangeOnlySubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingCreateEvent createEvent = new ProductOfferingCreateEvent(); + createEvent.setEventId("test-create-event"); + + ProductOfferingStateChangeEvent stateChangeEvent = new ProductOfferingStateChangeEvent(); + stateChangeEvent.setEventId("test-state-change-event"); + + // Act + productOfferingCallbackService.sendProductOfferingCreateCallback(createEvent); + productOfferingCallbackService.sendProductOfferingStateChangeCallback(stateChangeEvent); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:9090/create-only/listener/productOfferingCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + verify(restTemplate, times(1)).exchange( + eq("http://localhost:9091/state-change-only/listener/productOfferingStateChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingNotificationServiceTest.java new file mode 100644 index 00000000..037f60ce --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingNotificationServiceTest.java @@ -0,0 +1,121 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.ProductOffering; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeNotification; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingNotificationService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingCallbackService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class ProductOfferingNotificationServiceTest { + + @Mock + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Mock + private ProductOfferingCallbackService productOfferingCallbackService; + + @InjectMocks + private ProductOfferingNotificationService productOfferingNotificationService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testPublishProductOfferingCreateNotification() { + // Arrange + ProductOffering productOffering = new ProductOffering(); + productOffering.setUuid("test-productoffering-123"); + productOffering.setName("Test Product Offering"); + productOffering.setDescription("A test product offering for notifications"); + + // Act + productOfferingNotificationService.publishProductOfferingCreateNotification(productOffering); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingCreateNotification.class), eq("test-productoffering-123")); + verify(productOfferingCallbackService, times(1)).sendProductOfferingCreateCallback(any()); + } + + @Test + public void testPublishProductOfferingDeleteNotification() { + // Arrange + ProductOffering productOffering = new ProductOffering(); + productOffering.setUuid("test-productoffering-456"); + productOffering.setName("Test Product Offering to Delete"); + productOffering.setDescription("A test product offering for delete notifications"); + + // Act + productOfferingNotificationService.publishProductOfferingDeleteNotification(productOffering); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingDeleteNotification.class), eq("test-productoffering-456")); + verify(productOfferingCallbackService, times(1)).sendProductOfferingDeleteCallback(any()); + } + + @Test + public void testPublishProductOfferingAttributeValueChangeNotification() { + // Arrange + ProductOffering productOffering = new ProductOffering(); + productOffering.setUuid("test-productoffering-789"); + productOffering.setName("Test Product Offering Attribute Change"); + productOffering.setDescription("A test product offering for attribute change notifications"); + + // Act + productOfferingNotificationService.publishProductOfferingAttributeValueChangeNotification(productOffering); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingAttributeValueChangeNotification.class), eq("test-productoffering-789")); + verify(productOfferingCallbackService, times(1)).sendProductOfferingAttributeValueChangeCallback(any()); + } + + @Test + public void testPublishProductOfferingStateChangeNotification() { + // Arrange + ProductOffering productOffering = new ProductOffering(); + productOffering.setUuid("test-productoffering-101"); + productOffering.setName("Test Product Offering State Change"); + productOffering.setDescription("A test product offering for state change notifications"); + + // Act + productOfferingNotificationService.publishProductOfferingStateChangeNotification(productOffering); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingStateChangeNotification.class), eq("test-productoffering-101")); + verify(productOfferingCallbackService, times(1)).sendProductOfferingStateChangeCallback(any()); + } + + @Test + public void testCreateNotificationStructure() { + // Arrange + ProductOffering productOffering = new ProductOffering(); + productOffering.setUuid("test-productoffering-structure"); + productOffering.setName("Test Product Offering Structure"); + + // Act + productOfferingNotificationService.publishProductOfferingCreateNotification(productOffering); + + // Assert - verify the notification was published with correct structure + verify(eventPublisher).publishEvent(any(ProductOfferingCreateNotification.class), eq("test-productoffering-structure")); + verify(productOfferingCallbackService).sendProductOfferingCreateCallback(any()); + } +} \ No newline at end of file -- GitLab From b1f1fb4ad33a0c657fb3a781f952c7f105a9ee0d Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 10:44:39 +0300 Subject: [PATCH 17/21] adding productofferringPrice events --- .../ProductCatalogApiRouteBuilderEvents.java | 46 +++- .../ProductOfferingPriceCallbackService.java | 238 ++++++++++++++++ ...oductOfferingPriceNotificationService.java | 257 +++++++++++++++++ .../ProductOfferingPriceRepoService.java | 27 +- src/main/resources/application-testing.yml | 4 + src/main/resources/application.yml | 4 + ...oductOfferingPriceCallbackServiceTest.java | 258 ++++++++++++++++++ ...tOfferingPriceNotificationServiceTest.java | 121 ++++++++ 8 files changed, 943 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceCallbackService.java create mode 100644 src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceNotificationService.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceCallbackServiceTest.java create mode 100644 src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceNotificationServiceTest.java diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java index c0828283..746ac4f7 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -43,6 +43,10 @@ import org.etsi.osl.tmf.pcm620.model.ProductOfferingCreateNotification; import org.etsi.osl.tmf.pcm620.model.ProductOfferingDeleteNotification; import org.etsi.osl.tmf.pcm620.model.ProductOfferingAttributeValueChangeNotification; import org.etsi.osl.tmf.pcm620.model.ProductOfferingStateChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeNotification; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -56,34 +60,46 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { private static final transient Log logger = LogFactory.getLog(ProductCatalogApiRouteBuilderEvents.class.getName()); @Value("${EVENT_PRODUCT_CATALOG_CREATE}") - private String EVENT_CATALOG_CREATE = "direct:EVENT_CATALOG_CREATE"; + private String EVENT_CATALOG_CREATE = ""; @Value("${EVENT_PRODUCT_CATALOG_DELETE}") - private String EVENT_CATALOG_DELETE = "direct:EVENT_CATALOG_DELETE"; + private String EVENT_CATALOG_DELETE = ""; @Value("${EVENT_PRODUCT_CATEGORY_CREATE}") - private String EVENT_CATEGORY_CREATE = "direct:EVENT_CATEGORY_CREATE"; + private String EVENT_CATEGORY_CREATE = ""; @Value("${EVENT_PRODUCT_CATEGORY_DELETE}") - private String EVENT_CATEGORY_DELETE = "direct:EVENT_CATEGORY_DELETE"; + private String EVENT_CATEGORY_DELETE = ""; @Value("${EVENT_PRODUCT_SPECIFICATION_CREATE}") - private String EVENT_PRODUCT_SPECIFICATION_CREATE = "direct:EVENT_PRODUCT_SPECIFICATION_CREATE"; + private String EVENT_PRODUCT_SPECIFICATION_CREATE = ""; @Value("${EVENT_PRODUCT_SPECIFICATION_DELETE}") - private String EVENT_PRODUCT_SPECIFICATION_DELETE = "direct:EVENT_PRODUCT_SPECIFICATION_DELETE"; + private String EVENT_PRODUCT_SPECIFICATION_DELETE = ""; @Value("${EVENT_PRODUCT_OFFERING_CREATE}") - private String EVENT_PRODUCT_OFFERING_CREATE = "direct:EVENT_PRODUCT_OFFERING_CREATE"; + private String EVENT_PRODUCT_OFFERING_CREATE = ""; @Value("${EVENT_PRODUCT_OFFERING_DELETE}") - private String EVENT_PRODUCT_OFFERING_DELETE = "direct:EVENT_PRODUCT_OFFERING_DELETE"; + private String EVENT_PRODUCT_OFFERING_DELETE = ""; @Value("${EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE}") - private String EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE = "direct:EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE"; + private String EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE = ""; @Value("${EVENT_PRODUCT_OFFERING_STATE_CHANGE}") - private String EVENT_PRODUCT_OFFERING_STATE_CHANGE = "direct:EVENT_PRODUCT_OFFERING_STATE_CHANGE"; + private String EVENT_PRODUCT_OFFERING_STATE_CHANGE = ""; + + @Value("${EVENT_PRODUCT_OFFERING_PRICE_CREATE}") + private String EVENT_PRODUCT_OFFERING_PRICE_CREATE = ""; + + @Value("${EVENT_PRODUCT_OFFERING_PRICE_DELETE}") + private String EVENT_PRODUCT_OFFERING_PRICE_DELETE = ""; + + @Value("${EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE}") + private String EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE = ""; + + @Value("${EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE}") + private String EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE = ""; @Value("${spring.application.name}") private String compname; @@ -130,7 +146,15 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { } else if (n instanceof ProductOfferingAttributeValueChangeNotification) { msgtopic = EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE; } else if (n instanceof ProductOfferingStateChangeNotification) { - msgtopic = EVENT_PRODUCT_OFFERING_STATE_CHANGE; + msgtopic = EVENT_PRODUCT_OFFERING_STATE_CHANGE; + } else if (n instanceof ProductOfferingPriceCreateNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_PRICE_CREATE; + } else if (n instanceof ProductOfferingPriceDeleteNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_PRICE_DELETE; + } else if (n instanceof ProductOfferingPriceAttributeValueChangeNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE; + } else if (n instanceof ProductOfferingPriceStateChangeNotification) { + msgtopic = EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE; } Map map = new HashMap<>(); diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceCallbackService.java new file mode 100644 index 00000000..f1533850 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceCallbackService.java @@ -0,0 +1,238 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class ProductOfferingPriceCallbackService { + + private static final Logger logger = LoggerFactory.getLogger(ProductOfferingPriceCallbackService.class); + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Autowired + private RestTemplate restTemplate; + + /** + * Send product offering price create event to all registered callback URLs + * @param productOfferingPriceCreateEvent The product offering price create event to send + */ + public void sendProductOfferingPriceCreateCallback(ProductOfferingPriceCreateEvent productOfferingPriceCreateEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingPriceCreateEvent")) { + sendProductOfferingPriceCreateEventToCallback(subscription.getCallback(), productOfferingPriceCreateEvent); + } + } + } + + /** + * Send product offering price delete event to all registered callback URLs + * @param productOfferingPriceDeleteEvent The product offering price delete event to send + */ + public void sendProductOfferingPriceDeleteCallback(ProductOfferingPriceDeleteEvent productOfferingPriceDeleteEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingPriceDeleteEvent")) { + sendProductOfferingPriceDeleteEventToCallback(subscription.getCallback(), productOfferingPriceDeleteEvent); + } + } + } + + /** + * Send product offering price attribute value change event to all registered callback URLs + * @param productOfferingPriceAttributeValueChangeEvent The product offering price attribute value change event to send + */ + public void sendProductOfferingPriceAttributeValueChangeCallback(ProductOfferingPriceAttributeValueChangeEvent productOfferingPriceAttributeValueChangeEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingPriceAttributeValueChangeEvent")) { + sendProductOfferingPriceAttributeValueChangeEventToCallback(subscription.getCallback(), productOfferingPriceAttributeValueChangeEvent); + } + } + } + + /** + * Send product offering price state change event to all registered callback URLs + * @param productOfferingPriceStateChangeEvent The product offering price state change event to send + */ + public void sendProductOfferingPriceStateChangeCallback(ProductOfferingPriceStateChangeEvent productOfferingPriceStateChangeEvent) { + List subscriptions = eventSubscriptionRepoService.findAll(); + + for (EventSubscription subscription : subscriptions) { + if (shouldNotifySubscription(subscription, "productOfferingPriceStateChangeEvent")) { + sendProductOfferingPriceStateChangeEventToCallback(subscription.getCallback(), productOfferingPriceStateChangeEvent); + } + } + } + + /** + * Send product offering price create event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering price create event + */ + private void sendProductOfferingPriceCreateEventToCallback(String callbackUrl, ProductOfferingPriceCreateEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingPriceCreateEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering price create event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering price create event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product offering price delete event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering price delete event + */ + private void sendProductOfferingPriceDeleteEventToCallback(String callbackUrl, ProductOfferingPriceDeleteEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingPriceDeleteEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering price delete event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering price delete event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product offering price attribute value change event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering price attribute value change event + */ + private void sendProductOfferingPriceAttributeValueChangeEventToCallback(String callbackUrl, ProductOfferingPriceAttributeValueChangeEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingPriceAttributeValueChangeEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering price attribute value change event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering price attribute value change event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Send product offering price state change event to a specific callback URL + * @param callbackUrl The callback URL to send to + * @param event The product offering price state change event + */ + private void sendProductOfferingPriceStateChangeEventToCallback(String callbackUrl, ProductOfferingPriceStateChangeEvent event) { + try { + String url = buildCallbackUrl(callbackUrl, "/listener/productOfferingPriceStateChangeEvent"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(event, headers); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.POST, entity, String.class); + + logger.info("Successfully sent product offering price state change event to callback URL: {} - Response: {}", + url, response.getStatusCode()); + + } catch (Exception e) { + logger.error("Failed to send product offering price state change event to callback URL: {}", callbackUrl, e); + } + } + + /** + * Build the full callback URL with the listener endpoint + * @param baseUrl The base callback URL + * @param listenerPath The listener path to append + * @return The complete callback URL + */ + private String buildCallbackUrl(String baseUrl, String listenerPath) { + if (baseUrl.endsWith("/")) { + return baseUrl.substring(0, baseUrl.length() - 1) + listenerPath; + } else { + return baseUrl + listenerPath; + } + } + + /** + * Check if a subscription should be notified for a specific event type + * @param subscription The event subscription + * @param eventType The event type to check + * @return true if the subscription should be notified + */ + private boolean shouldNotifySubscription(EventSubscription subscription, String eventType) { + // If no query is specified, notify all events + if (subscription.getQuery() == null || subscription.getQuery().trim().isEmpty()) { + return true; + } + + // Check if the query contains the event type + String query = subscription.getQuery().toLowerCase(); + return query.contains("productofferingprice") || + query.contains(eventType.toLowerCase()) || + query.contains("productofferingprice.create") || + query.contains("productofferingprice.delete") || + query.contains("productofferingprice.attributevaluechange") || + query.contains("productofferingprice.statechange"); + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceNotificationService.java new file mode 100644 index 00000000..336c51e6 --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceNotificationService.java @@ -0,0 +1,257 @@ +/*- + * ========================LICENSE_START================================= + * org.etsi.osl.tmf.api + * %% + * Copyright (C) 2019 - 2021 openslice.io + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================LICENSE_END================================== + */ +package org.etsi.osl.tmf.pcm620.reposervices; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.UUID; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPrice; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEventPayload; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeNotification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class ProductOfferingPriceNotificationService { + + private static final Logger logger = LoggerFactory.getLogger(ProductOfferingPriceNotificationService.class); + + @Autowired + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Autowired + private ProductOfferingPriceCallbackService productOfferingPriceCallbackService; + + /** + * Publish a product offering price create notification + * @param productOfferingPrice The created product offering price + */ + public void publishProductOfferingPriceCreateNotification(ProductOfferingPrice productOfferingPrice) { + try { + ProductOfferingPriceCreateNotification notification = createProductOfferingPriceCreateNotification(productOfferingPrice); + eventPublisher.publishEvent(notification, productOfferingPrice.getId()); + + // Send callbacks to registered subscribers + productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(notification.getEvent()); + + logger.info("Published product offering price create notification for product offering price ID: {}", productOfferingPrice.getId()); + } catch (Exception e) { + logger.error("Error publishing product offering price create notification for product offering price ID: {}", productOfferingPrice.getId(), e); + } + } + + /** + * Publish a product offering price delete notification + * @param productOfferingPrice The deleted product offering price + */ + public void publishProductOfferingPriceDeleteNotification(ProductOfferingPrice productOfferingPrice) { + try { + ProductOfferingPriceDeleteNotification notification = createProductOfferingPriceDeleteNotification(productOfferingPrice); + eventPublisher.publishEvent(notification, productOfferingPrice.getId()); + + // Send callbacks to registered subscribers + productOfferingPriceCallbackService.sendProductOfferingPriceDeleteCallback(notification.getEvent()); + + logger.info("Published product offering price delete notification for product offering price ID: {}", productOfferingPrice.getId()); + } catch (Exception e) { + logger.error("Error publishing product offering price delete notification for product offering price ID: {}", productOfferingPrice.getId(), e); + } + } + + /** + * Publish a product offering price attribute value change notification + * @param productOfferingPrice The product offering price with changed attributes + */ + public void publishProductOfferingPriceAttributeValueChangeNotification(ProductOfferingPrice productOfferingPrice) { + try { + ProductOfferingPriceAttributeValueChangeNotification notification = createProductOfferingPriceAttributeValueChangeNotification(productOfferingPrice); + eventPublisher.publishEvent(notification, productOfferingPrice.getId()); + + // Send callbacks to registered subscribers + productOfferingPriceCallbackService.sendProductOfferingPriceAttributeValueChangeCallback(notification.getEvent()); + + logger.info("Published product offering price attribute value change notification for product offering price ID: {}", productOfferingPrice.getId()); + } catch (Exception e) { + logger.error("Error publishing product offering price attribute value change notification for product offering price ID: {}", productOfferingPrice.getId(), e); + } + } + + /** + * Publish a product offering price state change notification + * @param productOfferingPrice The product offering price with changed state + */ + public void publishProductOfferingPriceStateChangeNotification(ProductOfferingPrice productOfferingPrice) { + try { + ProductOfferingPriceStateChangeNotification notification = createProductOfferingPriceStateChangeNotification(productOfferingPrice); + eventPublisher.publishEvent(notification, productOfferingPrice.getId()); + + // Send callbacks to registered subscribers + productOfferingPriceCallbackService.sendProductOfferingPriceStateChangeCallback(notification.getEvent()); + + logger.info("Published product offering price state change notification for product offering price ID: {}", productOfferingPrice.getId()); + } catch (Exception e) { + logger.error("Error publishing product offering price state change notification for product offering price ID: {}", productOfferingPrice.getId(), e); + } + } + + /** + * Create a product offering price create notification + * @param productOfferingPrice The created product offering price + * @return ProductOfferingPriceCreateNotification + */ + private ProductOfferingPriceCreateNotification createProductOfferingPriceCreateNotification(ProductOfferingPrice productOfferingPrice) { + ProductOfferingPriceCreateNotification notification = new ProductOfferingPriceCreateNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingPriceCreateNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOfferingPrice/" + productOfferingPrice.getId()); + + // Create event + ProductOfferingPriceCreateEvent event = new ProductOfferingPriceCreateEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingPriceCreateEvent"); + event.setTitle("Product Offering Price Create Event"); + event.setDescription("A product offering price has been created"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingPriceCreateEventPayload payload = new ProductOfferingPriceCreateEventPayload(); + payload.setProductOfferingPrice(productOfferingPrice); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product offering price delete notification + * @param productOfferingPrice The deleted product offering price + * @return ProductOfferingPriceDeleteNotification + */ + private ProductOfferingPriceDeleteNotification createProductOfferingPriceDeleteNotification(ProductOfferingPrice productOfferingPrice) { + ProductOfferingPriceDeleteNotification notification = new ProductOfferingPriceDeleteNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingPriceDeleteNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOfferingPrice/" + productOfferingPrice.getId()); + + // Create event + ProductOfferingPriceDeleteEvent event = new ProductOfferingPriceDeleteEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingPriceDeleteEvent"); + event.setTitle("Product Offering Price Delete Event"); + event.setDescription("A product offering price has been deleted"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingPriceDeleteEventPayload payload = new ProductOfferingPriceDeleteEventPayload(); + payload.setProductOfferingPrice(productOfferingPrice); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product offering price attribute value change notification + * @param productOfferingPrice The product offering price with changed attributes + * @return ProductOfferingPriceAttributeValueChangeNotification + */ + private ProductOfferingPriceAttributeValueChangeNotification createProductOfferingPriceAttributeValueChangeNotification(ProductOfferingPrice productOfferingPrice) { + ProductOfferingPriceAttributeValueChangeNotification notification = new ProductOfferingPriceAttributeValueChangeNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingPriceAttributeValueChangeNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOfferingPrice/" + productOfferingPrice.getId()); + + // Create event + ProductOfferingPriceAttributeValueChangeEvent event = new ProductOfferingPriceAttributeValueChangeEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingPriceAttributeValueChangeEvent"); + event.setTitle("Product Offering Price Attribute Value Change Event"); + event.setDescription("A product offering price attribute value has been changed"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingPriceAttributeValueChangeEventPayload payload = new ProductOfferingPriceAttributeValueChangeEventPayload(); + payload.setProductOfferingPrice(productOfferingPrice); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } + + /** + * Create a product offering price state change notification + * @param productOfferingPrice The product offering price with changed state + * @return ProductOfferingPriceStateChangeNotification + */ + private ProductOfferingPriceStateChangeNotification createProductOfferingPriceStateChangeNotification(ProductOfferingPrice productOfferingPrice) { + ProductOfferingPriceStateChangeNotification notification = new ProductOfferingPriceStateChangeNotification(); + + // Set common notification properties + notification.setEventId(UUID.randomUUID().toString()); + notification.setEventTime(OffsetDateTime.now(ZoneOffset.UTC)); + notification.setEventType(ProductOfferingPriceStateChangeNotification.class.getName()); + notification.setResourcePath("/productCatalogManagement/v4/productOfferingPrice/" + productOfferingPrice.getId()); + + // Create event + ProductOfferingPriceStateChangeEvent event = new ProductOfferingPriceStateChangeEvent(); + event.setEventId(notification.getEventId()); + event.setEventTime(notification.getEventTime()); + event.setEventType("ProductOfferingPriceStateChangeEvent"); + event.setTitle("Product Offering Price State Change Event"); + event.setDescription("A product offering price state has been changed"); + event.setTimeOcurred(notification.getEventTime()); + + // Create event payload + ProductOfferingPriceStateChangeEventPayload payload = new ProductOfferingPriceStateChangeEventPayload(); + payload.setProductOfferingPrice(productOfferingPrice); + event.setEvent(payload); + + notification.setEvent(event); + return notification; + } +} \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceRepoService.java index bffff81e..3ad54021 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceRepoService.java @@ -55,6 +55,9 @@ public class ProductOfferingPriceRepoService { @Autowired ProductOfferingPriceRepository prodsOfferingRepo; + @Autowired + ProductOfferingPriceNotificationService productOfferingPriceNotificationService; + private SessionFactory sessionFactory; @@ -80,6 +83,10 @@ public class ProductOfferingPriceRepoService { serviceSpec = this.updateProductOfferingPriceDataFromAPIcall(serviceSpec, serviceProductOfferingPrice); serviceSpec = this.prodsOfferingRepo.save(serviceSpec); + // Publish create notification + if (productOfferingPriceNotificationService != null) { + productOfferingPriceNotificationService.publishProductOfferingPriceCreateNotification(serviceSpec); + } return this.prodsOfferingRepo.save(serviceSpec); } @@ -231,6 +238,11 @@ public class ProductOfferingPriceRepoService { * prior deleting we need to delete other dependency objects */ + // Publish delete notification before actual deletion + if (productOfferingPriceNotificationService != null) { + productOfferingPriceNotificationService.publishProductOfferingPriceDeleteNotification(s); + } + this.prodsOfferingRepo.delete(s); return null; } @@ -244,12 +256,25 @@ public class ProductOfferingPriceRepoService { if (s == null) { return null; } + + // Store original state for comparison + String originalLifecycleStatus = s.getLifecycleStatus(); + ProductOfferingPrice prodOff = s; prodOff = this.updateProductOfferingPriceDataFromAPIcall(prodOff, aProductOfferingPrice); prodOff = this.prodsOfferingRepo.save(prodOff); - + // Publish notifications + if (productOfferingPriceNotificationService != null) { + // Always publish attribute value change notification for updates + productOfferingPriceNotificationService.publishProductOfferingPriceAttributeValueChangeNotification(prodOff); + + // Check for state change and publish state change notification if needed + if (originalLifecycleStatus != null && !originalLifecycleStatus.equals(prodOff.getLifecycleStatus())) { + productOfferingPriceNotificationService.publishProductOfferingPriceStateChangeNotification(prodOff); + } + } return this.prodsOfferingRepo.save(prodOff); diff --git a/src/main/resources/application-testing.yml b/src/main/resources/application-testing.yml index 19366def..1c08e30a 100644 --- a/src/main/resources/application-testing.yml +++ b/src/main/resources/application-testing.yml @@ -195,6 +195,10 @@ EVENT_PRODUCT_OFFERING_CREATE: "jms:topic:EVENT.PRODUCTOFFERING.CREATE" EVENT_PRODUCT_OFFERING_DELETE: "jms:topic:EVENT.PRODUCTOFFERING.DELETE" EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.ATTRCHANGED" EVENT_PRODUCT_OFFERING_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.STATECHANGED" +EVENT_PRODUCT_OFFERING_PRICE_CREATE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.CREATE" +EVENT_PRODUCT_OFFERING_PRICE_DELETE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.DELETE" +EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.ATTRCHANGED" +EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.STATECHANGED" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 66995d33..2cf3438a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -220,6 +220,10 @@ EVENT_PRODUCT_OFFERING_CREATE: "jms:topic:EVENT.PRODUCTOFFERING.CREATE" EVENT_PRODUCT_OFFERING_DELETE: "jms:topic:EVENT.PRODUCTOFFERING.DELETE" EVENT_PRODUCT_OFFERING_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.ATTRCHANGED" EVENT_PRODUCT_OFFERING_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERING.STATECHANGED" +EVENT_PRODUCT_OFFERING_PRICE_CREATE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.CREATE" +EVENT_PRODUCT_OFFERING_PRICE_DELETE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.DELETE" +EVENT_PRODUCT_OFFERING_PRICE_ATTRIBUTE_VALUE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.ATTRCHANGED" +EVENT_PRODUCT_OFFERING_PRICE_STATE_CHANGE: "jms:topic:EVENT.PRODUCTOFFERINGPRICE.STATECHANGED" #QUEUE MESSSAGES WITH VNFNSD CATALOG NFV_CATALOG_GET_NSD_BY_ID: "jms:queue:NFVCATALOG.GET.NSD_BY_ID" diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceCallbackServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceCallbackServiceTest.java new file mode 100644 index 00000000..8e8b04a2 --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceCallbackServiceTest.java @@ -0,0 +1,258 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPrice; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeEvent; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeEvent; +import org.etsi.osl.tmf.pcm620.model.EventSubscription; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingPriceCallbackService; +import org.etsi.osl.tmf.pcm620.reposervices.EventSubscriptionRepoService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class ProductOfferingPriceCallbackServiceTest { + + @Mock + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private ProductOfferingPriceCallbackService productOfferingPriceCallbackService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testSendProductOfferingPriceCreateCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productofferingprice"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceCreateEvent event = new ProductOfferingPriceCreateEvent(); + event.setEventId("test-event-123"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingPriceCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductOfferingPriceDeleteCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productofferingprice"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceDeleteEvent event = new ProductOfferingPriceDeleteEvent(); + event.setEventId("test-event-456"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceDeleteCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingPriceDeleteEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductOfferingPriceAttributeValueChangeCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productofferingprice.attributevaluechange"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceAttributeValueChangeEvent event = new ProductOfferingPriceAttributeValueChangeEvent(); + event.setEventId("test-event-789"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceAttributeValueChangeCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingPriceAttributeValueChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSendProductOfferingPriceStateChangeCallback() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback"); + subscription.setQuery("productofferingprice.statechange"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceStateChangeEvent event = new ProductOfferingPriceStateChangeEvent(); + event.setEventId("test-event-101"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceStateChangeCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingPriceStateChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testCallbackWithTrailingSlash() { + // Arrange + EventSubscription subscription = new EventSubscription(); + subscription.setCallback("http://localhost:8080/callback/"); + subscription.setQuery("productofferingprice"); + + List subscriptions = Arrays.asList(subscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceCreateEvent event = new ProductOfferingPriceCreateEvent(); + event.setEventId("test-event-trailing-slash"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(event); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/callback/listener/productOfferingPriceCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testFilterSubscriptionsByQuery() { + // Arrange + EventSubscription productOfferingPriceSubscription = new EventSubscription(); + productOfferingPriceSubscription.setCallback("http://localhost:8080/productofferingprice-callback"); + productOfferingPriceSubscription.setQuery("productofferingprice"); + + EventSubscription otherSubscription = new EventSubscription(); + otherSubscription.setCallback("http://localhost:8080/other-callback"); + otherSubscription.setQuery("catalog"); + + List subscriptions = Arrays.asList(productOfferingPriceSubscription, otherSubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceCreateEvent event = new ProductOfferingPriceCreateEvent(); + event.setEventId("test-event-filter"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(event); + + // Assert - only product offering price subscription should receive callback + verify(restTemplate, times(1)).exchange( + eq("http://localhost:8080/productofferingprice-callback/listener/productOfferingPriceCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + public void testSpecificEventTypeQueries() { + // Arrange + EventSubscription createOnlySubscription = new EventSubscription(); + createOnlySubscription.setCallback("http://localhost:9090/create-only"); + createOnlySubscription.setQuery("productofferingprice.create"); + + EventSubscription stateChangeOnlySubscription = new EventSubscription(); + stateChangeOnlySubscription.setCallback("http://localhost:9091/state-change-only"); + stateChangeOnlySubscription.setQuery("productofferingprice.statechange"); + + List subscriptions = Arrays.asList(createOnlySubscription, stateChangeOnlySubscription); + when(eventSubscriptionRepoService.findAll()).thenReturn(subscriptions); + when(restTemplate.exchange(any(String.class), eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + + ProductOfferingPriceCreateEvent createEvent = new ProductOfferingPriceCreateEvent(); + createEvent.setEventId("test-create-event"); + + ProductOfferingPriceStateChangeEvent stateChangeEvent = new ProductOfferingPriceStateChangeEvent(); + stateChangeEvent.setEventId("test-state-change-event"); + + // Act + productOfferingPriceCallbackService.sendProductOfferingPriceCreateCallback(createEvent); + productOfferingPriceCallbackService.sendProductOfferingPriceStateChangeCallback(stateChangeEvent); + + // Assert + verify(restTemplate, times(1)).exchange( + eq("http://localhost:9090/create-only/listener/productOfferingPriceCreateEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + verify(restTemplate, times(1)).exchange( + eq("http://localhost:9091/state-change-only/listener/productOfferingPriceStateChangeEvent"), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(String.class) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceNotificationServiceTest.java new file mode 100644 index 00000000..e31a0b8c --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingPriceNotificationServiceTest.java @@ -0,0 +1,121 @@ +package org.etsi.osl.services.api.pcm620; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPrice; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceCreateNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceDeleteNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceAttributeValueChangeNotification; +import org.etsi.osl.tmf.pcm620.model.ProductOfferingPriceStateChangeNotification; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingPriceNotificationService; +import org.etsi.osl.tmf.pcm620.reposervices.ProductOfferingPriceCallbackService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@ActiveProfiles("testing") +public class ProductOfferingPriceNotificationServiceTest { + + @Mock + private ProductCatalogApiRouteBuilderEvents eventPublisher; + + @Mock + private ProductOfferingPriceCallbackService productOfferingPriceCallbackService; + + @InjectMocks + private ProductOfferingPriceNotificationService productOfferingPriceNotificationService; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testPublishProductOfferingPriceCreateNotification() { + // Arrange + ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice(); + productOfferingPrice.setUuid("test-productofferingprice-123"); + productOfferingPrice.setName("Test Product Offering Price"); + productOfferingPrice.setDescription("A test product offering price for notifications"); + + // Act + productOfferingPriceNotificationService.publishProductOfferingPriceCreateNotification(productOfferingPrice); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingPriceCreateNotification.class), eq("test-productofferingprice-123")); + verify(productOfferingPriceCallbackService, times(1)).sendProductOfferingPriceCreateCallback(any()); + } + + @Test + public void testPublishProductOfferingPriceDeleteNotification() { + // Arrange + ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice(); + productOfferingPrice.setUuid("test-productofferingprice-456"); + productOfferingPrice.setName("Test Product Offering Price to Delete"); + productOfferingPrice.setDescription("A test product offering price for delete notifications"); + + // Act + productOfferingPriceNotificationService.publishProductOfferingPriceDeleteNotification(productOfferingPrice); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingPriceDeleteNotification.class), eq("test-productofferingprice-456")); + verify(productOfferingPriceCallbackService, times(1)).sendProductOfferingPriceDeleteCallback(any()); + } + + @Test + public void testPublishProductOfferingPriceAttributeValueChangeNotification() { + // Arrange + ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice(); + productOfferingPrice.setUuid("test-productofferingprice-789"); + productOfferingPrice.setName("Test Product Offering Price Attribute Change"); + productOfferingPrice.setDescription("A test product offering price for attribute change notifications"); + + // Act + productOfferingPriceNotificationService.publishProductOfferingPriceAttributeValueChangeNotification(productOfferingPrice); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingPriceAttributeValueChangeNotification.class), eq("test-productofferingprice-789")); + verify(productOfferingPriceCallbackService, times(1)).sendProductOfferingPriceAttributeValueChangeCallback(any()); + } + + @Test + public void testPublishProductOfferingPriceStateChangeNotification() { + // Arrange + ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice(); + productOfferingPrice.setUuid("test-productofferingprice-101"); + productOfferingPrice.setName("Test Product Offering Price State Change"); + productOfferingPrice.setDescription("A test product offering price for state change notifications"); + + // Act + productOfferingPriceNotificationService.publishProductOfferingPriceStateChangeNotification(productOfferingPrice); + + // Assert + verify(eventPublisher, times(1)).publishEvent(any(ProductOfferingPriceStateChangeNotification.class), eq("test-productofferingprice-101")); + verify(productOfferingPriceCallbackService, times(1)).sendProductOfferingPriceStateChangeCallback(any()); + } + + @Test + public void testCreateNotificationStructure() { + // Arrange + ProductOfferingPrice productOfferingPrice = new ProductOfferingPrice(); + productOfferingPrice.setUuid("test-productofferingprice-structure"); + productOfferingPrice.setName("Test Product Offering Price Structure"); + + // Act + productOfferingPriceNotificationService.publishProductOfferingPriceCreateNotification(productOfferingPrice); + + // Assert - verify the notification was published with correct structure + verify(eventPublisher).publishEvent(any(ProductOfferingPriceCreateNotification.class), eq("test-productofferingprice-structure")); + verify(productOfferingPriceCallbackService).sendProductOfferingPriceCreateCallback(any()); + } +} \ No newline at end of file -- GitLab From 0d1c27d4992b42ee5fbf4458498a675c0daf4b48 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 11:02:39 +0300 Subject: [PATCH 18/21] allow USER role to register callback --- .../org/etsi/osl/tmf/pcm620/api/HubApiController.java | 4 ++-- .../osl/services/api/pcm620/HubApiControllerTest.java | 4 ++-- .../pcm620/ProductOfferingCallbackIntegrationTest.java | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java index b15db916..dbf51fb2 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java @@ -71,7 +71,7 @@ public class HubApiController implements HubApi { } @Override - @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')") public ResponseEntity registerListener(@Parameter(description = "Data containing the callback endpoint to deliver the information", required = true) @Valid @RequestBody EventSubscriptionInput data) { try { EventSubscription eventSubscription = eventSubscriptionRepoService.addEventSubscription(data); @@ -86,7 +86,7 @@ public class HubApiController implements HubApi { } @Override - @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')") public ResponseEntity unregisterListener(@Parameter(description = "The id of the registered listener", required = true) @PathVariable("id") String id) { try { EventSubscription existing = eventSubscriptionRepoService.findById(id); diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java index 072e514f..c3d4d0d7 100644 --- a/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java +++ b/src/test/java/org/etsi/osl/services/api/pcm620/HubApiControllerTest.java @@ -180,7 +180,7 @@ public class HubApiControllerTest { .andExpect(status().isBadRequest()); } - @WithMockUser(username = "user", roles = {"USER"}) + @WithMockUser(username = "user", roles = {"OTHER"}) @Test public void testRegisterListenerUnauthorized() throws Exception { File resourceSpecFile = new File("src/test/resources/testPCM620EventSubscriptionInput.json"); @@ -196,7 +196,7 @@ public class HubApiControllerTest { .andExpect(status().isForbidden()); } - @WithMockUser(username = "user", roles = {"USER"}) + @WithMockUser(username = "user", roles = {"OTHER"}) @Test public void testUnregisterListenerUnauthorized() throws Exception { // First create a subscription as admin diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java index 1755896d..0974606f 100644 --- a/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java +++ b/src/test/java/org/etsi/osl/services/api/pcm620/ProductOfferingCallbackIntegrationTest.java @@ -92,7 +92,7 @@ public class ProductOfferingCallbackIntegrationTest { } @Test - @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @WithMockUser(username = "osadmin", roles = {"USER"}) public void testCompleteCallbackFlow() throws Exception { // Step 1: Register a callback subscription via Hub API EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); @@ -139,7 +139,7 @@ public class ProductOfferingCallbackIntegrationTest { } @Test - @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @WithMockUser(username = "osadmin", roles = {"USER"}) public void testAttributeValueChangeAndStateChangeCallbacks() throws Exception { // Step 1: Register subscription for attribute and state change events EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); @@ -187,7 +187,7 @@ public class ProductOfferingCallbackIntegrationTest { } @Test - @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @WithMockUser(username = "osadmin", roles = {"USER"}) public void testCallbackFilteringByEventType() throws Exception { // Step 1: Register subscription only for create events EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); @@ -222,7 +222,7 @@ public class ProductOfferingCallbackIntegrationTest { } @Test - @WithMockUser(username = "osadmin", roles = {"ADMIN"}) + @WithMockUser(username = "osadmin", roles = {"USER"}) public void testProductOfferingCallbackWithAllEventsQuery() throws Exception { // Step 1: Register subscription for all events (empty query) EventSubscriptionInput subscriptionInput = new EventSubscriptionInput(); -- GitLab From 3f4720f4c3c69a3e564f1e3e0e8433dfdff29538 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 Aug 2025 13:16:52 +0300 Subject: [PATCH 19/21] fix lazy init errors and add hub GET --- .../org/etsi/osl/tmf/pcm620/api/HubApi.java | 27 +++++++++++++++++++ .../osl/tmf/pcm620/api/HubApiController.java | 18 +++++++++++++ .../ProductCatalogApiRouteBuilderEvents.java | 3 +++ .../reposervices/CatalogCallbackService.java | 9 ++++--- .../ProductCategoryRepoService.java | 14 ++++++---- .../ProductOfferingRepoService.java | 6 ++--- .../ProductSpecificationRepoService.java | 6 ++--- 7 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApi.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApi.java index 2de26104..c6a77416 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApi.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApi.java @@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import java.util.List; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -116,4 +117,30 @@ public interface HubApi { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } + @Operation(summary = "Get all registered listeners", operationId = "getListeners", description = "Retrieves all registered event subscriptions", tags={ "events subscription", }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Success" ), + @ApiResponse(responseCode = "400", description = "Bad Request" ), + @ApiResponse(responseCode = "401", description = "Unauthorized" ), + @ApiResponse(responseCode = "403", description = "Forbidden" ), + @ApiResponse(responseCode = "500", description = "Internal Server Error" ) }) + @RequestMapping(value = "/hub", + produces = { "application/json;charset=utf-8" }, + method = RequestMethod.GET) + default ResponseEntity> getListeners() { + if(getObjectMapper().isPresent() && getAcceptHeader().isPresent()) { + if (getAcceptHeader().get().contains("application/json")) { + try { + return new ResponseEntity<>(getObjectMapper().get().readValue("[ { \"query\" : \"query\", \"callback\" : \"callback\", \"id\" : \"id\"} ]", List.class), HttpStatus.NOT_IMPLEMENTED); + } catch (IOException e) { + log.error("Couldn't serialize response for content type application/json", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + } else { + log.warn("ObjectMapper or HttpServletRequest not configured in default HubApi interface so no example is generated"); + } + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java index dbf51fb2..649fa957 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/HubApiController.java @@ -19,6 +19,7 @@ */ package org.etsi.osl.tmf.pcm620.api; +import java.util.List; import java.util.Optional; import com.fasterxml.jackson.databind.ObjectMapper; @@ -35,6 +36,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import io.swagger.v3.oas.annotations.Parameter; import jakarta.servlet.http.HttpServletRequest; @@ -70,6 +72,9 @@ public class HubApiController implements HubApi { return Optional.ofNullable(request); } + /* + * to register another OSL for example use "callback": "http://localhost:13082/tmf-api/productCatalogManagement/v4/" + */ @Override @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')") public ResponseEntity registerListener(@Parameter(description = "Data containing the callback endpoint to deliver the information", required = true) @Valid @RequestBody EventSubscriptionInput data) { @@ -87,6 +92,7 @@ public class HubApiController implements HubApi { @Override @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')") + @RequestMapping(value = "/hub/{id}", method = RequestMethod.DELETE, produces = { "application/json;charset=utf-8" }) public ResponseEntity unregisterListener(@Parameter(description = "The id of the registered listener", required = true) @PathVariable("id") String id) { try { EventSubscription existing = eventSubscriptionRepoService.findById(id); @@ -102,4 +108,16 @@ public class HubApiController implements HubApi { } } + @Override + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')") + public ResponseEntity> getListeners() { + try { + List eventSubscriptions = eventSubscriptionRepoService.findAll(); + return new ResponseEntity<>(eventSubscriptions, HttpStatus.OK); + } catch (Exception e) { + log.error("Error retrieving listeners", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java index 746ac4f7..f6c7b6e7 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -25,6 +25,7 @@ import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.apache.camel.ProducerTemplate; import org.apache.camel.builder.RouteBuilder; @@ -174,12 +175,14 @@ public class ProductCatalogApiRouteBuilderEvents extends RouteBuilder { static String toJsonString(Object object) throws IOException { ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper.writeValueAsString(object); } static T toJsonObj(String content, Class valueType) throws IOException { ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper.readValue(content, valueType); } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java index 36f7b5a8..27e0e6cc 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java @@ -80,8 +80,9 @@ public class CatalogCallbackService { * @param event The catalog create event */ private void sendCatalogCreateEventToCallback(String callbackUrl, CatalogCreateEvent event) { + + String url = buildCallbackUrl(callbackUrl, "/listener/catalogCreateEvent"); try { - String url = buildCallbackUrl(callbackUrl, "/listener/catalogCreateEvent"); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -94,7 +95,7 @@ public class CatalogCallbackService { url, response.getStatusCode()); } catch (Exception e) { - logger.error("Failed to send catalog create event to callback URL: {}", callbackUrl, e); + logger.error("Failed to send catalog create event to callback URL: {}", url, e); } } @@ -104,8 +105,8 @@ public class CatalogCallbackService { * @param event The catalog delete event */ private void sendCatalogDeleteEventToCallback(String callbackUrl, CatalogDeleteEvent event) { + String url = buildCallbackUrl(callbackUrl, "/listener/catalogDeleteEvent"); try { - String url = buildCallbackUrl(callbackUrl, "/listener/catalogDeleteEvent"); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -118,7 +119,7 @@ public class CatalogCallbackService { url, response.getStatusCode()); } catch (Exception e) { - logger.error("Failed to send catalog delete event to callback URL: {}", callbackUrl, e); + logger.error("Failed to send catalog delete event to callback URL: {}", url, e); } } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java index be1aae8c..ca2355d3 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java @@ -156,6 +156,15 @@ public class ProductCategoryRepoService { Category categoryToDelete = optionalCat.get(); + // Trigger lazy loading of associations before deletion to avoid lazy initialization exception + categoryToDelete.getProductOfferingObj().size(); // This will initialize the lazy collection + categoryToDelete.getCategoryObj().size(); // This will initialize the lazy collection + + // Publish category delete notification BEFORE deletion to ensure session is still active + if (categoryNotificationService != null) { + categoryNotificationService.publishCategoryDeleteNotification(categoryToDelete); + } + if ( categoryToDelete.getParentId() != null ) { Category parentCat = (this.categsRepo.findByUuid( categoryToDelete.getParentId() )).get(); @@ -171,11 +180,6 @@ public class ProductCategoryRepoService { this.categsRepo.delete( categoryToDelete); - // Publish category delete notification - if (categoryNotificationService != null) { - categoryNotificationService.publishCategoryDeleteNotification(categoryToDelete); - } - return true; } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java index 55d6c1b0..02b0d19f 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java @@ -266,13 +266,13 @@ public class ProductOfferingRepoService { /** * prior deleting we need to delete other dependency objects */ - - this.prodsOfferingRepo.delete(s); - // Publish product offering delete notification + // Publish product offering delete notification BEFORE deletion to ensure session is still active if (productOfferingNotificationService != null) { productOfferingNotificationService.publishProductOfferingDeleteNotification(s); } + + this.prodsOfferingRepo.delete(s); return null; } diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java index 8b8fdfc7..ab2e3ee3 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductSpecificationRepoService.java @@ -257,13 +257,13 @@ public class ProductSpecificationRepoService { /** * prior deleting we need to delete other dependency objects */ - - this.prodsOfferingRepo.delete(s); - // Publish product specification delete notification + // Publish product specification delete notification BEFORE deletion to ensure session is still active if (productSpecificationNotificationService != null) { productSpecificationNotificationService.publishProductSpecificationDeleteNotification(s); } + + this.prodsOfferingRepo.delete(s); return null; } -- GitLab From 1f106c8e9b5f7bdad75bdf08300c3af5c85effcd Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Mon, 22 Sep 2025 13:07:02 +0300 Subject: [PATCH 20/21] fix errors --- .../tmf/pcm620/reposervices/CatalogNotificationService.java | 6 ++++-- .../services/api/pcm620/CatalogNotificationServiceTest.java | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java index 5cc5000f..057f2039 100644 --- a/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java @@ -59,7 +59,8 @@ public class CatalogNotificationService { eventPublisher.publishEvent(notification, catalog.getUuid()); // Send callbacks to registered subscribers - catalogCallbackService.sendCatalogCreateCallback(notification.getEvent()); + if ( catalogCallbackService!=null ) + catalogCallbackService.sendCatalogCreateCallback(notification.getEvent()); logger.info("Published catalog create notification for catalog ID: {}", catalog.getUuid()); } catch (Exception e) { @@ -77,7 +78,8 @@ public class CatalogNotificationService { eventPublisher.publishEvent(notification, catalog.getUuid()); // Send callbacks to registered subscribers - catalogCallbackService.sendCatalogDeleteCallback(notification.getEvent()); + if ( catalogCallbackService!=null ) + catalogCallbackService.sendCatalogDeleteCallback(notification.getEvent()); logger.info("Published catalog delete notification for catalog ID: {}", catalog.getUuid()); } catch (Exception e) { diff --git a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java index a88ded05..2a4c666e 100644 --- a/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java @@ -4,7 +4,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; - +import org.etsi.osl.tmf.OpenAPISpringBoot; import org.etsi.osl.tmf.pcm620.api.ProductCatalogApiRouteBuilderEvents; import org.etsi.osl.tmf.pcm620.model.Catalog; import org.etsi.osl.tmf.pcm620.model.CatalogCreateNotification; @@ -16,11 +16,15 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = OpenAPISpringBoot.class) @ActiveProfiles("testing") +@AutoConfigureMockMvc public class CatalogNotificationServiceTest { @Mock -- GitLab From 909399bb36c15a10661517611d2548bf5e65ae09 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Tue, 23 Sep 2025 22:53:04 +0300 Subject: [PATCH 21/21] fix for memeory heap --- pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pom.xml b/pom.xml index a39c8a0f..a4fa9f6a 100644 --- a/pom.xml +++ b/pom.xml @@ -449,6 +449,14 @@ none alphabetical 1 + false + -- GitLab