diff --git a/pom.xml b/pom.xml index a39c8a0fccc31dd2d3e986a00ab1dc4069ff2adf..a4fa9f6aeb2846a0461d0fa6acf1da5d7e4965a9 100644 --- a/pom.xml +++ b/pom.xml @@ -449,6 +449,14 @@ none alphabetical 1 + false + 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 2de26104383c378d8a2b034566518b35e3eefd52..c6a77416a0f00744ac4532c75a3f201d70a2b0ad 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 b15db91667abb9079a912edb82a5c803a9b1fed2..649fa9570c5bf9b723ec214ceef13bd61eb3fb2a 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,8 +72,11 @@ 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')") + @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 +91,8 @@ public class HubApiController implements HubApi { } @Override - @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + @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/ListenerApiController.java b/src/main/java/org/etsi/osl/tmf/pcm620/api/ListenerApiController.java index 77f66dba6f60dbdbbf072faafc1d03b133649088..ddd561725a55dd051afc4c6d9858fb44d4631c04 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); + } + } + } 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 0000000000000000000000000000000000000000..f6c7b6e7244d68e36796bc0b5f38f6264a14fedc --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/api/ProductCatalogApiRouteBuilderEvents.java @@ -0,0 +1,189 @@ +/*- + * ========================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 com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +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.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.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; +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 = ""; + + @Value("${EVENT_PRODUCT_CATALOG_DELETE}") + private String EVENT_CATALOG_DELETE = ""; + + @Value("${EVENT_PRODUCT_CATEGORY_CREATE}") + private String EVENT_CATEGORY_CREATE = ""; + + @Value("${EVENT_PRODUCT_CATEGORY_DELETE}") + private String EVENT_CATEGORY_DELETE = ""; + + @Value("${EVENT_PRODUCT_SPECIFICATION_CREATE}") + private String EVENT_PRODUCT_SPECIFICATION_CREATE = ""; + + @Value("${EVENT_PRODUCT_SPECIFICATION_DELETE}") + private String EVENT_PRODUCT_SPECIFICATION_DELETE = ""; + + @Value("${EVENT_PRODUCT_OFFERING_CREATE}") + private String EVENT_PRODUCT_OFFERING_CREATE = ""; + + @Value("${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 = ""; + + @Value("${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; + + @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; + } else if (n instanceof CategoryCreateNotification) { + msgtopic = EVENT_CATEGORY_CREATE; + } else if (n instanceof CategoryDeleteNotification) { + msgtopic = EVENT_CATEGORY_DELETE; + } else if (n instanceof ProductSpecificationCreateNotification) { + msgtopic = EVENT_PRODUCT_SPECIFICATION_CREATE; + } else if (n instanceof ProductSpecificationDeleteNotification) { + 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; + } 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<>(); + 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.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); + } +} \ 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 0000000000000000000000000000000000000000..d6240fcf50b2d2fbe0d9109704961b35fd23599e --- /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 0000000000000000000000000000000000000000..27e0e6cc1dd7c175014968f0227aa6acaf801f3a --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogCallbackService.java @@ -0,0 +1,159 @@ +/*- + * ========================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) { + + String url = buildCallbackUrl(callbackUrl, "/listener/catalogCreateEvent"); + try { + + 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: {}", url, 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) { + String url = buildCallbackUrl(callbackUrl, "/listener/catalogDeleteEvent"); + try { + + 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: {}", url, 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 0000000000000000000000000000000000000000..057f2039f07c4a8583596a7b55c27f616f7a7ddf --- /dev/null +++ b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CatalogNotificationService.java @@ -0,0 +1,153 @@ +/*- + * ========================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 + if ( catalogCallbackService!=null ) + 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 + if ( catalogCallbackService!=null ) + 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/CategoryCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/CategoryCallbackService.java new file mode 100644 index 0000000000000000000000000000000000000000..4eec9f604215d2a06b141f573323e8ffa1961b86 --- /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 0000000000000000000000000000000000000000..4d200a58b0f1cc7e3ff60f227e51b48eefb6dd6d --- /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/EventSubscriptionRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/EventSubscriptionRepoService.java index 8fc9caad8102825fb4f48d8f46570d0bc4eb2b11..4f5c85952da2a48696ce77e8f59d7aa3bc5bb644 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 3f66f5c7686023d4734ae40376100f99e179e269..71bfe6fe3418d619b594ebea128edc4463a1945a 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/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductCategoryRepoService.java index c7e226c03bbea2d617b95b6c1c4dae13b8820053..ca2355d3b88ffa51c5acef1de5d37ec2633ac215 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,23 @@ public class ProductCategoryRepoService { return false; //has children } + 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 ( 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 +178,8 @@ public class ProductCategoryRepoService { parentCat = this.categsRepo.save(parentCat); } + this.categsRepo.delete( categoryToDelete); - this.categsRepo.delete( optionalCat.get()); return true; } 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 0000000000000000000000000000000000000000..f6008a083c758181ef8a98385103d72152ab8694 --- /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 0000000000000000000000000000000000000000..e10e25f0eab31a7144e79cfdb1f5fda9afbbb8ce --- /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/ProductOfferingPriceCallbackService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingPriceCallbackService.java new file mode 100644 index 0000000000000000000000000000000000000000..f1533850170c174a0ff678341918732667e43c06 --- /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 0000000000000000000000000000000000000000..336c51e643942c42e37966e0418c57afa2ec59ad --- /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 bffff81ec013962bf143190de4dfc67911472caa..3ad540219a9413fe7ca9aa5ac05263851b90a721 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/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java b/src/main/java/org/etsi/osl/tmf/pcm620/reposervices/ProductOfferingRepoService.java index 942d0e7d91331ea7958797bf88af6814d2996736..02b0d19f2ff13377b0b81419da1a0142ff213149 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() { @@ -259,8 +266,14 @@ public class ProductOfferingRepoService { /** * prior deleting we need to delete other dependency objects */ + + // 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; } @@ -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/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 0000000000000000000000000000000000000000..ae6271912ffe06b31d9faa558ace25e2dda1eb4e --- /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 0000000000000000000000000000000000000000..0818539a7fcd92886e10f19547b40b62b6a6aaeb --- /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 deec0a663436f017e7d66f89389fb22bd6cd8ea6..ab2e3ee3bf709274afb6c897c190567c1b51a045 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() { @@ -250,8 +257,14 @@ public class ProductSpecificationRepoService { /** * prior deleting we need to delete other dependency objects */ + + // 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; } diff --git a/src/main/resources/application-testing.yml b/src/main/resources/application-testing.yml index dc15a9c271e512228cced92f454f39da56b09f55..1c08e30a03457f8f454adc1b3717c9719fd5d2be 100644 --- a/src/main/resources/application-testing.yml +++ b/src/main/resources/application-testing.yml @@ -185,6 +185,21 @@ 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" +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" +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 dc4f0ce805b2dccea5a1762bd78947b7093795be..2cf3438ac025cecde55ad09c2d732baa953eebcb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -210,6 +210,21 @@ 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" +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" +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/CatalogCallbackIntegrationTest.java b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0764364cc94fcc015122c4117082b6f40528b5fc --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogCallbackIntegrationTest.java @@ -0,0 +1,174 @@ +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.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 CatalogCallbackIntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private WebApplicationContext context; + + @Autowired + private ProductCatalogRepoService productCatalogRepoService; + + @Autowired + private EventSubscriptionRepoService eventSubscriptionRepoService; + + @SpyBean + private CatalogCallbackService catalogCallbackService; + + @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("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 0000000000000000000000000000000000000000..05312420d20c0efe7b63771305dbd70035fa84e3 --- /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 0000000000000000000000000000000000000000..c5fa4406fa9ba053423bc7f9ddfb9489289bf80c --- /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 0000000000000000000000000000000000000000..2a4c666ea0bf6ea2aae43afbce200752bf4fcb0e --- /dev/null +++ b/src/test/java/org/etsi/osl/services/api/pcm620/CatalogNotificationServiceTest.java @@ -0,0 +1,84 @@ +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.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; +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.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 + 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 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 0000000000000000000000000000000000000000..a694ae4639ef7750f325bced0f4616d5c6013bbe --- /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 0000000000000000000000000000000000000000..df70e6328ef354425aed1bb0f2a313afbaca9d43 --- /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 0000000000000000000000000000000000000000..9de497fc9f1ccd3cb264966547285aa287502467 --- /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 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 072e514f125eca7dde05aa31585e1e73f2e3bd26..c3d4d0d78e4972ddba7f8698801a63a24170e002 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 new file mode 100644 index 0000000000000000000000000000000000000000..0974606f7a9b09c257049dbdea92a8c51126d045 --- /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 = {"USER"}) + 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 = {"USER"}) + 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 = {"USER"}) + 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 = {"USER"}) + 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 0000000000000000000000000000000000000000..40875e4a444a8d056b7ccf75ca8f1b5ab4f30eac --- /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 0000000000000000000000000000000000000000..037f60ce66106f16b83dfb61c806ff8e67031b16 --- /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 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 0000000000000000000000000000000000000000..8e8b04a2b86580d77f87782f190d73fb2c7e3a4b --- /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 0000000000000000000000000000000000000000..e31a0b8cb291388935747e3e40ddf743e2d94949 --- /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 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 0000000000000000000000000000000000000000..f85b8671985b7e4dd42da287795d6ca5ac938137 --- /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 0000000000000000000000000000000000000000..1d11a49d93221ced818804c5a3dfc9055fe5f916 --- /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 0000000000000000000000000000000000000000..b251a05346710ecf0102bf0fa8b32493e3283979 --- /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