From 2d7ba05327959f5381f96fd885b3b82789d8936c Mon Sep 17 00:00:00 2001 From: elinuxhenrik Date: Fri, 30 Oct 2020 09:47:55 +0100 Subject: [PATCH] Add functionality to rAPP Catalogue Change-Id: Ic72a2bf558e2fe2f7bc7d3cfeca35e7386838189 Issue-ID: NONRTRIC-287 Signed-off-by: elinuxhenrik --- r-app-catalogue/api/rac-api.json | 470 +++++++++++---------- r-app-catalogue/api/rac-api.yaml | 94 +++-- r-app-catalogue/pom.xml | 15 + .../api/GeneralRappCatalogueControllerAdvisor.java | 64 +++ .../rappcatalogue/api/ServicesApiDelegateImpl.java | 136 +++++- .../rappcatalogue/exception/HeaderException.java | 29 ++ .../exception/InvalidServiceException.java | 27 ++ .../exception/ServiceNotFoundException.java | 27 ++ .../GeneralRappCatalogueControllerAdvisorTest.java | 76 ++++ .../api/ServicesApiDelegateImplTest.java | 261 +++++++++++- 10 files changed, 896 insertions(+), 303 deletions(-) create mode 100644 r-app-catalogue/src/main/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisor.java create mode 100644 r-app-catalogue/src/main/java/org/oransc/rappcatalogue/exception/HeaderException.java create mode 100644 r-app-catalogue/src/main/java/org/oransc/rappcatalogue/exception/InvalidServiceException.java create mode 100644 r-app-catalogue/src/main/java/org/oransc/rappcatalogue/exception/ServiceNotFoundException.java create mode 100644 r-app-catalogue/src/test/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisorTest.java diff --git a/r-app-catalogue/api/rac-api.json b/r-app-catalogue/api/rac-api.json index 3741bdd1..64f19d82 100644 --- a/r-app-catalogue/api/rac-api.json +++ b/r-app-catalogue/api/rac-api.json @@ -1,246 +1,252 @@ { - "openapi": "3.0.0", - "info": { - "title": "rAPP Catalogue API", - "description": "The Non RT-RIC Service Catalogue provides a way for services to register themselves for other services to discover.", - "version": "1.0.0" - }, - "paths": { - "/services": { - "get": { - "summary": "Service names", - "deprecated": false, - "operationId": "getServiceNamesUsingGET", - "responses": { - "200": { - "description": "Service names", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } + "openapi": "3.0.0", + "info": { + "title": "rAPP Catalogue API", + "description": "The Non RT-RIC Service Catalogue provides a way for services to register themselves for other services to discover.", + "version": "1.0.0" + }, + "servers": [ + { + "url": "/" + } + ], + "paths": { + "/services": { + "get": { + "summary": "Services", + "deprecated": false, + "operationId": "getServices", + "responses": { + "200": { + "description": "Services", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/service" + } + } + } + } + } }, - "example": [ - "DroneIdentifier", - "Collector" + "tags": [ + "rAPP Catalogue API" ] - } } - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Forbidden" - }, - "404": { - "description": "Not used" - } }, - "tags": [ - "rAPP Catalogue API" - ] - } - }, - "/services/{serviceName}": { - "get": { - "summary": "Individual Service", - "deprecated": false, - "operationId": "getIndividualServiceUsingGET", - "responses": { - "200": { - "description": "EI Job", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/service" - } - } + "/services/{serviceName}": { + "get": { + "summary": "Individual Service", + "deprecated": false, + "operationId": "getIndividualService", + "responses": { + "200": { + "description": "Service", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/service" + } + } + } + }, + "404": { + "description": "Service is not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error_information" + } + } + } + } + }, + "parameters": [ + { + "in": "path", + "name": "serviceName", + "description": "serviceName", + "schema": { + "type": "string" + }, + "required": true, + "example": "DroneIdentifier" + } + ], + "tags": [ + "rAPP Catalogue API" + ] + }, + "put": { + "summary": "Create or update a Service", + "deprecated": false, + "operationId": "putIndividualService", + "responses": { + "200": { + "description": "Service updated" + }, + "201": { + "description": "Service created", + "headers": { + "Location": { + "schema": { + "type": "string" + }, + "description": "URL to the created Service" + } + } + }, + "400": { + "description": "Provided service is not correct", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error_information" + }, + "example": { + "detail": "Service is missing required property: version", + "status": 400 + } + } + } + } + }, + "parameters": [ + { + "name": "serviceName", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "example": "DroneIdentifier" + } + ], + "requestBody": { + "description": "Service to create/update", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/inputService" + } + } + } + }, + "tags": [ + "rAPP Catalogue API" + ] + }, + "delete": { + "summary": "Remove a Service from the catalogue", + "deprecated": false, + "operationId": "deleteIndividualService", + "responses": { + "204": { + "description": "Service deleted" + } + }, + "parameters": [ + { + "name": "serviceName", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "example": "DroneIdentifier" + } + ], + "tags": [ + "rAPP Catalogue API" + ] } - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Forbidden" - }, - "404": { - "description": "Service is not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error_information" + } + }, + "components": { + "schemas": { + "inputService": { + "description": "A Service to register", + "type": "object", + "title": "inputService", + "required": [ + "version" + ], + "properties": { + "version": { + "description": "Version of the Service", + "type": "string", + "example": "1.0.0" + }, + "display_name": { + "description": "Display name for the Service", + "type": "string", + "example": "Drone Identifier" + }, + "description": { + "description": "Description of the Service", + "type": "string", + "example": "Detects if a UE is a drone" + } } - } - } - } - }, - "parameters": [ - { - "in": "path", - "name": "serviceName", - "description": "serviceName", - "schema": { - "type": "string" }, - "required": true, - "example": "DroneIdentifier" - } - ], - "tags": [ - "rAPP Catalogue API" - ] - }, - "put": { - "summary": "Create or update a Service", - "deprecated": false, - "operationId": "putIndividualServiceUsingPUT", - "responses": { - "200": { - "description": "Service updated" - }, - "201": { - "description": "Service created" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Forbidden" - }, - "404": { - "description": "Provided service is not correct", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error_information" - }, - "example": { - "detail": "Service is missing required property version", - "status": 404 + "service": { + "description": "A Service", + "type": "object", + "title": "service", + "required": [ + "name", + "version", + "registrationDate" + ], + "properties": { + "name": { + "description": "Unique identifier of the Service", + "type": "string", + "example": "DroneIdentifier" + }, + "version": { + "description": "Version of the Service", + "type": "string", + "example": "1.0.0" + }, + "display_name": { + "description": "Display name for the Service", + "type": "string", + "example": "Drone Identifier" + }, + "description": { + "description": "Description of the Service", + "type": "string", + "example": "Detects if a UE is a drone" + }, + "registrationDate": { + "description": "Date when the Service was registered in the catalogue", + "type": "string", + "example": "2020-11-03" + } } - } - } - } - }, - "parameters": [ - { - "name": "serviceName", - "in": "path", - "required": true, - "schema": { - "type": "string" }, - "example": "DroneIdentifier" - } - ], - "requestBody": { - "description": "Service to create/update", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/service" - } - } - } - }, - "tags": [ - "rAPP Catalogue API" - ] - }, - "delete": { - "summary": "Remove a Service from the catalogue", - "deprecated": false, - "operationId": "deleteIndividualServiceUsingDELETE", - "responses": { - "200": { - "description": "Not used" - }, - "204": { - "description": "Job deleted" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Forbidden" - }, - "404": { - "description": "Service is not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error_information" + "error_information": { + "description": "Problem as defined in https://tools.ietf.org/html/rfc7807", + "type": "object", + "title": "error_information", + "properties": { + "detail": { + "description": "A human-readable explanation specific to this occurrence of the problem.", + "type": "string", + "example": "Service not found" + }, + "status": { + "format": "int32", + "description": "The HTTP status code for this occurrence of the problem.", + "type": "integer", + "example": 404 + } } - } } - } - }, - "parameters": [ - { - "name": "serviceName", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "example": "DroneIdentifier" - } - ], - "tags": [ - "rAPP Catalogue API" - ] - } - } - }, - "components": { - "schemas": { - "service": { - "description": "A Service", - "type": "object", - "title": "service", - "required": [ - "version" - ], - "properties": { - "version": { - "description": "Version of the Service", - "type": "string", - "example": "1.0.0" - }, - "display_name": { - "description": "Display name for the Service", - "type": "string", - "example": "Drone Identifier" - }, - "description": { - "description": "Description of the Service", - "type": "string", - "example": "Detects if a UE is a drone" - } - } - }, - "error_information": { - "description": "Problem as defined in https://tools.ietf.org/html/rfc7807", - "type": "object", - "title": "error_information", - "properties": { - "detail": { - "description": "A human-readable explanation specific to this occurrence of the problem.", - "type": "string", - "example": "Service not found" - }, - "status": { - "format": "int32", - "description": "The HTTP status code generated by the origin server for this occurrence of the problem.", - "type": "integer", - "example": 404 - } } - } } - } -} \ No newline at end of file +} diff --git a/r-app-catalogue/api/rac-api.yaml b/r-app-catalogue/api/rac-api.yaml index 87e2eb97..748cf2b4 100644 --- a/r-app-catalogue/api/rac-api.yaml +++ b/r-app-catalogue/api/rac-api.yaml @@ -11,33 +11,24 @@ paths: get: tags: - rAPP Catalogue API - summary: Service names - operationId: getServiceNamesUsingGET + summary: Services + operationId: getServices responses: 200: - description: Service names + description: Services content: application/json: schema: type: array items: - type: string - example: - - DroneIdentifier - - Collector - 401: - description: Unauthorized - 403: - description: Forbidden - 404: - description: Not used + $ref: '#/components/schemas/service' deprecated: false /services/{serviceName}: get: tags: - rAPP Catalogue API summary: Individual Service - operationId: getIndividualServiceUsingGET + operationId: getIndividualService parameters: - name: serviceName in: path @@ -50,15 +41,11 @@ paths: example: DroneIdentifier responses: 200: - description: EI Job + description: Service content: application/json: schema: $ref: '#/components/schemas/service' - 401: - description: Unauthorized - 403: - description: Forbidden 404: description: Service is not found content: @@ -70,7 +57,7 @@ paths: tags: - rAPP Catalogue API summary: Create or update a Service - operationId: putIndividualServiceUsingPUT + operationId: putIndividualService parameters: - name: serviceName in: path @@ -85,32 +72,35 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/service' + $ref: '#/components/schemas/inputService' required: true responses: 200: description: Service updated 201: description: Service created - 401: - description: Unauthorized - 403: - description: Forbidden - 404: + headers: + Location: + description: URL to the created Service + style: simple + explode: false + schema: + type: string + 400: description: Provided service is not correct content: application/json: schema: $ref: '#/components/schemas/error_information' example: - detail: Service is missing required property version - status: 404 + detail: 'Service is missing required property: version' + status: 400 deprecated: false delete: tags: - rAPP Catalogue API summary: Remove a Service from the catalogue - operationId: deleteIndividualServiceUsingDELETE + operationId: deleteIndividualService parameters: - name: serviceName in: path @@ -121,29 +111,42 @@ paths: type: string example: DroneIdentifier responses: - 200: - description: Not used 204: - description: Job deleted - 401: - description: Unauthorized - 403: - description: Forbidden - 404: - description: Service is not found - content: - application/json: - schema: - $ref: '#/components/schemas/error_information' + description: Service deleted deprecated: false components: schemas: + inputService: + title: inputService + required: + - version + type: object + properties: + version: + type: string + description: Version of the Service + example: 1.0.0 + display_name: + type: string + description: Display name for the Service + example: Drone Identifier + description: + type: string + description: Description of the Service + example: Detects if a UE is a drone + description: A Service to register service: title: service required: + - name + - registrationDate - version type: object properties: + name: + type: string + description: Unique identifier of the Service + example: DroneIdentifier version: type: string description: Version of the Service @@ -156,6 +159,10 @@ components: type: string description: Description of the Service example: Detects if a UE is a drone + registrationDate: + type: string + description: Date when the Service was registered in the catalogue + example: 2020-11-03 description: A Service error_information: title: error_information @@ -168,8 +175,7 @@ components: example: Service not found status: type: integer - description: The HTTP status code generated by the origin server for this - occurrence of the problem. + description: The HTTP status code for this occurrence of the problem. format: int32 example: 404 description: Problem as defined in https://tools.ietf.org/html/rfc7807 diff --git a/r-app-catalogue/pom.xml b/r-app-catalogue/pom.xml index 3ac85627..ea36b582 100644 --- a/r-app-catalogue/pom.xml +++ b/r-app-catalogue/pom.xml @@ -139,6 +139,21 @@ junit-jupiter-api test + + org.mockito + mockito-junit-jupiter + test + + + org.mockito + mockito-core + test + + + org.junit.jupiter + junit-jupiter-engine + test + diff --git a/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisor.java b/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisor.java new file mode 100644 index 00000000..939f7bf9 --- /dev/null +++ b/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisor.java @@ -0,0 +1,64 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2020 Nordix Foundation. All rights reserved. + * ====================================================================== + * 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.oransc.rappcatalogue.api; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import org.oransc.rappcatalogue.exception.HeaderException; +import org.oransc.rappcatalogue.exception.InvalidServiceException; +import org.oransc.rappcatalogue.exception.ServiceNotFoundException; +import org.oransc.rappcatalogue.model.ErrorInformation; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class GeneralRappCatalogueControllerAdvisor extends ResponseEntityExceptionHandler { + @ExceptionHandler(InvalidServiceException.class) + public ResponseEntity handleInvalidServiceException( + InvalidServiceException ex) { + + return new ResponseEntity<>(getErrorInformation(ex, BAD_REQUEST), BAD_REQUEST); + } + + @ExceptionHandler(ServiceNotFoundException.class) + public ResponseEntity handleServiceNotFoundException( + ServiceNotFoundException ex) { + + return new ResponseEntity<>(getErrorInformation(ex, NOT_FOUND), NOT_FOUND); + } + + @ExceptionHandler(HeaderException.class) + public ResponseEntity handleHeaderException( + HeaderException ex) { + + return new ResponseEntity<>(getErrorInformation(ex, INTERNAL_SERVER_ERROR), INTERNAL_SERVER_ERROR); + } + + private ErrorInformation getErrorInformation(Exception cause, HttpStatus status) { + ErrorInformation errorInfo = new ErrorInformation(); + errorInfo.setDetail(cause.getMessage()); + errorInfo.setStatus(status.value()); + return errorInfo; + } +} diff --git a/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImpl.java b/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImpl.java index 701f1d8b..8c9469ac 100644 --- a/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImpl.java +++ b/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImpl.java @@ -1,34 +1,140 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2020 Nordix Foundation. All rights reserved. + * ====================================================================== + * 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.oransc.rappcatalogue.api; -import java.util.Arrays; +import java.io.IOException; +import java.sql.Date; +import java.util.ArrayList; import java.util.List; - +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.oransc.rappcatalogue.exception.HeaderException; +import org.oransc.rappcatalogue.exception.InvalidServiceException; +import org.oransc.rappcatalogue.exception.ServiceNotFoundException; +import org.oransc.rappcatalogue.model.InputService; +import org.oransc.rappcatalogue.model.Service; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.context.request.NativeWebRequest; @org.springframework.stereotype.Service public class ServicesApiDelegateImpl implements ServicesApiDelegate { + private static final String LOCATION_HEADER = "Location"; + + @Autowired + private NativeWebRequest nativeWebRequest; + + private ConcurrentHashMap registeredServices = new ConcurrentHashMap<>(); + + ServicesApiDelegateImpl(NativeWebRequest nativeWebRequest) { + this.nativeWebRequest = nativeWebRequest; + } + + @Override + public Optional getRequest() { + return Optional.of(nativeWebRequest); + } + @Override - public ResponseEntity deleteIndividualServiceUsingDELETE(String serviceName) { - return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + public ResponseEntity getIndividualService(String serviceName) { + Service service = registeredServices.get(serviceName); + if (service != null) { + return ResponseEntity.ok(service); + } else { + throw new ServiceNotFoundException(serviceName); + } } - // @Override - // public ResponseEntity getIndividualServiceUsingGET(String serviceName) { - // return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + @Override + public ResponseEntity> getServices() { + return ResponseEntity.ok(new ArrayList<>(registeredServices.values())); + } + + @Override + public ResponseEntity putIndividualService(String serviceName, InputService inputService) { + if (isServiceValid(inputService)) { + if (registeredServices.put(serviceName, createService(serviceName, inputService)) == null) { + try { + getRequest().ifPresent(request -> addLocationHeaderToResponse(serviceName, request)); + } catch (Exception e) { + registeredServices.remove(serviceName); + throw e; + } + return new ResponseEntity<>(HttpStatus.CREATED); + } else { + return new ResponseEntity<>(HttpStatus.OK); + } + } else { + throw new InvalidServiceException(); + } + } - // } + private void addLocationHeaderToResponse(String serviceName, NativeWebRequest request) { + try { + HttpServletRequest nativeRequest = request.getNativeRequest(HttpServletRequest.class); + HttpServletResponse nativeResponse = request.getNativeResponse(HttpServletResponse.class); + if (nativeRequest != null && nativeResponse != null) { + StringBuffer requestURL = nativeRequest.getRequestURL(); + nativeResponse.addHeader(LOCATION_HEADER, requestURL.toString()); + nativeResponse.getWriter().print(""); + } else { + throw new HeaderException(LOCATION_HEADER, new Exception("Native Request or Response missing")); + } + } catch (IOException e) { + throw new HeaderException(LOCATION_HEADER, e); + } + } @Override - public ResponseEntity> getServiceNamesUsingGET() { - List services = Arrays.asList("a", "b"); - return ResponseEntity.ok(services); + public ResponseEntity deleteIndividualService(String serviceName) { + registeredServices.remove(serviceName); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } - // @Override - // public ResponseEntity putIndividualServiceUsingPUT(String serviceName, Service service) { - // return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + /* + * java:S2589: Boolean expressions should not be gratuitous. + * Even though the version property is marked as @NotNull, it might be null coming from the client, hence the null + * check is needed. + */ + @SuppressWarnings("java:S2589") + private boolean isServiceValid(InputService service) { + String version = service.getVersion(); + return version != null && !version.isBlank(); + } - // } + private Service createService(String serviceName, InputService inputService) { + Service service = new Service(); + service.setName(serviceName); + service.setDescription(inputService.getDescription()); + service.setDisplayName(inputService.getDisplayName()); + service.setVersion(inputService.getVersion()); + service.setRegistrationDate(getTodaysDate()); + return service; + } + + private String getTodaysDate() { + long millis = System.currentTimeMillis(); + Date date = new Date(millis); + return date.toString(); + } } diff --git a/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/exception/HeaderException.java b/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/exception/HeaderException.java new file mode 100644 index 00000000..90568de0 --- /dev/null +++ b/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/exception/HeaderException.java @@ -0,0 +1,29 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2020 Nordix Foundation. All rights reserved. + * ====================================================================== + * 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.oransc.rappcatalogue.exception; + +public class HeaderException extends RuntimeException { + + private static final long serialVersionUID = -7798178963078284655L; + + public HeaderException(String header, Exception cause) { + super(String.format("Unable to set header %s in response. Cause: %s", header, cause.getMessage())); + } + +} diff --git a/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/exception/InvalidServiceException.java b/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/exception/InvalidServiceException.java new file mode 100644 index 00000000..dce815b9 --- /dev/null +++ b/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/exception/InvalidServiceException.java @@ -0,0 +1,27 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2020 Nordix Foundation. All rights reserved. + * ====================================================================== + * 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.oransc.rappcatalogue.exception; + +public class InvalidServiceException extends RuntimeException { + private static final long serialVersionUID = 3849219105170316564L; + + public InvalidServiceException() { + super("Service is missing required property: version"); + } +} diff --git a/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/exception/ServiceNotFoundException.java b/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/exception/ServiceNotFoundException.java new file mode 100644 index 00000000..26b4b271 --- /dev/null +++ b/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/exception/ServiceNotFoundException.java @@ -0,0 +1,27 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2020 Nordix Foundation. All rights reserved. + * ====================================================================== + * 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.oransc.rappcatalogue.exception; + +public class ServiceNotFoundException extends RuntimeException { + private static final long serialVersionUID = 6579271315716003988L; + + public ServiceNotFoundException(String serviceName) { + super(String.format("Service %s not found", serviceName)); + } +} diff --git a/r-app-catalogue/src/test/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisorTest.java b/r-app-catalogue/src/test/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisorTest.java new file mode 100644 index 00000000..1c4d4143 --- /dev/null +++ b/r-app-catalogue/src/test/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisorTest.java @@ -0,0 +1,76 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2020 Nordix Foundation. All rights reserved. + * ====================================================================== + * 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.oransc.rappcatalogue.api; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import org.junit.jupiter.api.Test; +import org.oransc.rappcatalogue.exception.HeaderException; +import org.oransc.rappcatalogue.exception.InvalidServiceException; +import org.oransc.rappcatalogue.exception.ServiceNotFoundException; +import org.oransc.rappcatalogue.model.ErrorInformation; +import org.springframework.http.ResponseEntity; + +class GeneralRappCatalogueControllerAdvisorTest { + + @Test + void handleInvalidServiceException_shouldReturnBadRequestWithMessage() { + GeneralRappCatalogueControllerAdvisor advisorUnderTest = new GeneralRappCatalogueControllerAdvisor(); + + InvalidServiceException exception = new InvalidServiceException(); + + ResponseEntity response = advisorUnderTest.handleInvalidServiceException(exception); + + assertThat(response.getStatusCode()).isEqualTo(BAD_REQUEST); + ErrorInformation body = (ErrorInformation) response.getBody(); + assertThat(body.getStatus()).isEqualTo(BAD_REQUEST.value()); + assertThat(body.getDetail()).isEqualTo("Service is missing required property: version"); + } + + @Test + void handleServiceNotFoundException_shouldReturnNotFoundWithMessage() { + GeneralRappCatalogueControllerAdvisor advisorUnderTest = new GeneralRappCatalogueControllerAdvisor(); + + ServiceNotFoundException exception = new ServiceNotFoundException("Name"); + + ResponseEntity response = advisorUnderTest.handleServiceNotFoundException(exception); + + assertThat(response.getStatusCode()).isEqualTo(NOT_FOUND); + ErrorInformation body = (ErrorInformation) response.getBody(); + assertThat(body.getStatus()).isEqualTo(NOT_FOUND.value()); + assertThat(body.getDetail()).isEqualTo("Service Name not found"); + } + + @Test + void handleHeaderException_shouldReturnInternalServerErrorWithMessage() { + GeneralRappCatalogueControllerAdvisor advisorUnderTest = new GeneralRappCatalogueControllerAdvisor(); + + HeaderException exception = new HeaderException("Header", new Exception("Cause")); + + ResponseEntity response = advisorUnderTest.handleHeaderException(exception); + + assertThat(response.getStatusCode()).isEqualTo(INTERNAL_SERVER_ERROR); + ErrorInformation body = (ErrorInformation) response.getBody(); + assertThat(body.getStatus()).isEqualTo(INTERNAL_SERVER_ERROR.value()); + assertThat(body.getDetail()).isEqualTo("Unable to set header Header in response. Cause: Cause"); + } +} diff --git a/r-app-catalogue/src/test/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImplTest.java b/r-app-catalogue/src/test/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImplTest.java index 53dfc1a5..c19e1de2 100644 --- a/r-app-catalogue/src/test/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImplTest.java +++ b/r-app-catalogue/src/test/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImplTest.java @@ -1,33 +1,270 @@ package org.oransc.rappcatalogue.api; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static org.springframework.http.HttpStatus.OK; +import java.io.IOException; +import java.io.PrintWriter; +import java.sql.Date; import java.util.Arrays; import java.util.List; - +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.http.HttpStatus; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.oransc.rappcatalogue.exception.HeaderException; +import org.oransc.rappcatalogue.exception.InvalidServiceException; +import org.oransc.rappcatalogue.exception.ServiceNotFoundException; +import org.oransc.rappcatalogue.model.InputService; +import org.oransc.rappcatalogue.model.Service; import org.springframework.http.ResponseEntity; -import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.context.request.NativeWebRequest; -@ExtendWith(SpringExtension.class) +@ExtendWith(MockitoExtension.class) class ServicesApiDelegateImplTest { + @Mock + NativeWebRequest webRequestMock; + + private static final String INVALID_SERVICE_MESSAGE = "Service is missing required property: version"; + private static final String SERVICE_NAME = "Service Name"; + private static final String SERVICE_DESCRIPTION = "description"; + private static final String SERVICE_VERSION = "1.0"; + private static final String SERVICE_DISPLAY_NAME = "Display Name"; + + @Test + void getAddedService_shouldReturnService() { + ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl(webRequestMock); + + InputService service = new InputService(); + service.setDescription(SERVICE_DESCRIPTION); + service.setVersion(SERVICE_VERSION); + service.setDisplayName(SERVICE_DISPLAY_NAME); + + whenPrintResponseShouldWork(); + + delegateUnderTest.putIndividualService(SERVICE_NAME, service); + + ResponseEntity response = delegateUnderTest.getIndividualService(SERVICE_NAME); + + assertThat(response.getStatusCode()).isEqualTo(OK); + assertThat(response.getBody().getName()).isEqualTo(SERVICE_NAME); + } + + @Test + void getMissingService_shouldThrowException() { + ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl(null); + + Exception exception = assertThrows(ServiceNotFoundException.class, () -> { + delegateUnderTest.getIndividualService(SERVICE_NAME); + }); + + String expectedMessage = "Service " + SERVICE_NAME + " not found"; + String actualMessage = exception.getMessage(); + + assertThat(actualMessage).isEqualTo(expectedMessage); + } + + @Test + void putNewValidService_shouldBeCreatedAndRegisteredAndUrlToNewServiceAddedToLocationHeaderInResponse() { + ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl(webRequestMock); + + InputService service = new InputService(); + service.setDescription(SERVICE_DESCRIPTION); + service.setVersion(SERVICE_VERSION); + service.setDisplayName(SERVICE_DISPLAY_NAME); + + String urlToCreatedService = "URL to created Service"; + HttpServletResponse servletResponseMock = whenPrintResponseShouldWork(urlToCreatedService); + + ResponseEntity putResponse = delegateUnderTest.putIndividualService(SERVICE_NAME, service); + + assertThat(putResponse.getStatusCode()).isEqualTo(CREATED); + verify(servletResponseMock).addHeader("Location", urlToCreatedService); + + ResponseEntity getResponse = delegateUnderTest.getIndividualService(SERVICE_NAME); + + assertThat(getResponse.getStatusCode()).isEqualTo(OK); + Service body = getResponse.getBody(); + assertThat(body.getName()).isEqualTo(SERVICE_NAME); + assertThat(body.getRegistrationDate()).isEqualTo(getTodaysDate()); + } + + @Test + void putModifiedService_shouldBeModified() { + ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl(webRequestMock); + + InputService service = new InputService(); + service.setDescription(SERVICE_DESCRIPTION); + service.setVersion(SERVICE_VERSION); + service.setDisplayName(SERVICE_DISPLAY_NAME); + + whenPrintResponseShouldWork(); + + delegateUnderTest.putIndividualService(SERVICE_NAME, service); + + String newDescription = "New description"; + service.setDescription(newDescription); + ResponseEntity putResponse = delegateUnderTest.putIndividualService(SERVICE_NAME, service); + + assertThat(putResponse.getStatusCode()).isEqualTo(OK); + + ResponseEntity getResponse = delegateUnderTest.getIndividualService(SERVICE_NAME); + + assertThat(getResponse.getStatusCode()).isEqualTo(OK); + assertThat(getResponse.getBody().getDescription()).isEqualTo(newDescription); + } + + @Test + void putServiceWithVersionNull_shouldThrowException() { + ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl(null); + + InputService service = new InputService(); + service.setDescription(SERVICE_DESCRIPTION); + service.setDisplayName(SERVICE_DISPLAY_NAME); + + Exception exception = assertThrows(InvalidServiceException.class, () -> { + delegateUnderTest.putIndividualService(SERVICE_NAME, service); + }); + + assertThat(exception.getMessage()).isEqualTo(INVALID_SERVICE_MESSAGE); + } + + @Test + void putServiceWithBlankVersion_shouldThrowException() { + ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl(null); + + InputService service = new InputService(); + service.setVersion(""); + service.setDescription(SERVICE_DESCRIPTION); + service.setDisplayName(SERVICE_DISPLAY_NAME); + + Exception exception = assertThrows(InvalidServiceException.class, () -> { + delegateUnderTest.putIndividualService(SERVICE_NAME, service); + }); + + assertThat(exception.getMessage()).isEqualTo(INVALID_SERVICE_MESSAGE); + } + @Test - void putValidService_shouldBeOk() { - ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl(); + void putServiceWhenIoException_shouldThrowExceptionAndNoServiceCreated() throws Exception { + ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl(webRequestMock); + + whenGetRequestUrlThenReturnUrl(); + HttpServletResponse servletResponseMock = mock(HttpServletResponse.class); + when(webRequestMock.getNativeResponse(HttpServletResponse.class)).thenReturn(servletResponseMock); + when(servletResponseMock.getWriter()).thenThrow(new IOException("Error")); - ResponseEntity> response = delegateUnderTest.getServiceNamesUsingGET(); + InputService service = new InputService(); + service.setVersion("1.0"); + service.setDescription(SERVICE_DESCRIPTION); + service.setDisplayName(SERVICE_DISPLAY_NAME); + + Exception exception = assertThrows(HeaderException.class, () -> { + delegateUnderTest.putIndividualService(SERVICE_NAME, service); + }); + + assertThat(exception.getMessage()).isEqualTo("Unable to set header Location in response. Cause: Error"); + + ResponseEntity> response = delegateUnderTest.getServices(); + assertThat(response.getBody()).isEmpty(); } @Test - void getServices_shouldProvideArrayOfServices() throws Exception { - ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl(); + void getServices_shouldProvideArrayOfAddedServiceNames() throws Exception { + ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl(webRequestMock); + + InputService service1 = new InputService(); + service1.setDescription("description 1"); + service1.setVersion(SERVICE_VERSION); + service1.setDisplayName("Display Name 1"); + + InputService service2 = new InputService(); + service2.setDescription("description 2"); + service2.setVersion(SERVICE_VERSION); + service2.setDisplayName("Display Name 2"); + + whenPrintResponseShouldWork(); + + String serviceName1 = "Service Name 1"; + delegateUnderTest.putIndividualService(serviceName1, service1); + String serviceName2 = "Service Name 2"; + delegateUnderTest.putIndividualService(serviceName2, service2); + + ResponseEntity> response = delegateUnderTest.getServices(); + + assertThat(response.getStatusCode()).isEqualTo(OK); + List services = response.getBody(); + assertThat(services).hasSize(2); + List expectedServiceNames = Arrays.asList(serviceName1, serviceName2); + assertThat(expectedServiceNames).contains(services.get(0).getName()) // + .contains(services.get(1).getName()); + } - ResponseEntity> response = delegateUnderTest.getServiceNamesUsingGET(); + @Test + void deleteService_shouldBeOk() { + ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl(webRequestMock); + + InputService service = new InputService(); + service.setDescription(SERVICE_DESCRIPTION); + service.setVersion(SERVICE_VERSION); + service.setDisplayName(SERVICE_DISPLAY_NAME); + + whenPrintResponseShouldWork(); + + delegateUnderTest.putIndividualService(SERVICE_NAME, service); + + ResponseEntity> servicesResponse = delegateUnderTest.getServices(); + + assertThat(servicesResponse.getBody()).hasSize(1); + + ResponseEntity deleteResponse = delegateUnderTest.deleteIndividualService(SERVICE_NAME); + + assertThat(deleteResponse.getStatusCode()).isEqualTo(NO_CONTENT); + + servicesResponse = delegateUnderTest.getServices(); + + assertThat(servicesResponse.getBody()).isEmpty(); + } + + private void whenGetRequestUrlThenReturnUrl() { + whenGetRequestUrlThenReturnUrl("URL"); + } + + private void whenGetRequestUrlThenReturnUrl(String url) { + HttpServletRequest servletRequestMock = mock(HttpServletRequest.class); + when(webRequestMock.getNativeRequest(HttpServletRequest.class)).thenReturn(servletRequestMock); + when(servletRequestMock.getRequestURL()).thenReturn(new StringBuffer(url)); + } + + private HttpServletResponse whenPrintResponseShouldWork() { + return whenPrintResponseShouldWork("URL"); + } + + private HttpServletResponse whenPrintResponseShouldWork(String url) { + whenGetRequestUrlThenReturnUrl(url); + HttpServletResponse servletResponseMock = mock(HttpServletResponse.class); + when(webRequestMock.getNativeResponse(HttpServletResponse.class)).thenReturn(servletResponseMock); + PrintWriter printWriterMock = mock(PrintWriter.class); + try { + when(servletResponseMock.getWriter()).thenReturn(printWriterMock); + } catch (IOException e) { + // Nothing + } + return servletResponseMock; + } - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isEqualTo(Arrays.asList("a", "b")); + private String getTodaysDate() { + long millis = System.currentTimeMillis(); + Date date = new Date(millis); + return date.toString(); } } -- 2.16.6