From: elinuxhenrik Date: Thu, 10 Feb 2022 09:46:05 +0000 (+0100) Subject: Seed code for rApp Catalogue new repo X-Git-Tag: 1.1.0~8 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=4746a717d21f31b39302190a892f31c6db51eca8;p=nonrtric%2Fplt%2Frappcatalogue.git Seed code for rApp Catalogue new repo Issue-ID: NONRTRIC-717 Signed-off-by: elinuxhenrik Change-Id: I1115ddd6fde117f7c630e0673d46427f5e928eab --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88a00f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Documentation +.idea/ +.tox +docs/_build/ +.DS_STORE +.swagger* +docs/offeredapis/swagger/README.md + +# Eclipse +.checkstyle +.classpath +target/ +.sts4-cache +.project +.settings +.pydevproject +infer-out/ + +.vscode +.factorypath + +coverage.* + +.swagger-codegen-ignore +.swagger-codegen/ +api/README.md diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..095222a --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +--- +version: 2 + +formats: + - htmlzip + +build: + image: latest + +python: + version: 3.7 + install: + - requirements: docs/requirements-docs.txt + +sphinx: + configuration: docs/conf.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ed4be95 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# +# ============LICENSE_START======================================================= +# Copyright (C) 2020 Nordix Foundation. +# ================================================================================ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ============LICENSE_END========================================================= +# +FROM openjdk:11-jre-slim + +ARG JAR + +WORKDIR /opt/app/r-app-catalogue +RUN mkdir -p /var/log/r-app-catalogue +RUN mkdir -p /opt/app/r-app-catalogue/etc/cert/ + +EXPOSE 8680 8633 + +ADD /config/application.yaml /opt/app/r-app-catalogue/config/application.yaml +ADD /config/r-app-catalogue-keystore.jks /opt/app/r-app-catalogue/etc/cert/keystore.jks +ADD target/${JAR} /opt/app/r-app-catalogue/r-app-catalogue.jar + +ARG user=nonrtric +ARG group=nonrtric + +RUN groupadd $user && \ + useradd -r -g $group $user +RUN chown -R $user:$group /opt/app/r-app-catalogue +RUN chown -R $user:$group /var/log/r-app-catalogue + +USER ${user} + +CMD ["java", "-jar", "/opt/app/r-app-catalogue/r-app-catalogue.jar"] + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..863713d --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# O-RAN-SC Non-RT RIC rAPP Catalogue + +The O-RAN Non-RT RIC rApp Catalogue provides an OpenApi 3.0 REST API for services to register themselves and discover +other services. + +**NOTE!** The definition of the REST API is done in the `api/rac-api.json` file. The yaml version of the file is +generated during compilation. + +The application is a SpringBoot application generated using the openapitools openapi-generator-maven-plugin. + +To start the application run: +`mvn spring-boot:run` + +## License + +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. diff --git a/api/rac-api.json b/api/rac-api.json new file mode 100644 index 0000000..64f19d8 --- /dev/null +++ b/api/rac-api.json @@ -0,0 +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" + }, + "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" + } + } + } + } + } + }, + "tags": [ + "rAPP Catalogue API" + ] + } + }, + "/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" + ] + } + } + }, + "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" + } + } + }, + "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" + } + } + }, + "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 + } + } + } + } + } +} diff --git a/api/rac-api.yaml b/api/rac-api.yaml new file mode 100644 index 0000000..9ba6c39 --- /dev/null +++ b/api/rac-api.yaml @@ -0,0 +1,181 @@ +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: + tags: + - rAPP Catalogue API + summary: Services + operationId: getServices + responses: + "200": + description: Services + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/service' + deprecated: false + /services/{serviceName}: + get: + tags: + - rAPP Catalogue API + summary: Individual Service + operationId: getIndividualService + parameters: + - name: serviceName + in: path + description: serviceName + required: true + style: simple + explode: false + schema: + type: string + example: DroneIdentifier + 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' + deprecated: false + put: + tags: + - rAPP Catalogue API + summary: Create or update a Service + operationId: putIndividualService + parameters: + - name: serviceName + in: path + required: true + style: simple + explode: false + schema: + type: string + example: DroneIdentifier + requestBody: + description: Service to create/update + content: + application/json: + schema: + $ref: '#/components/schemas/inputService' + required: true + responses: + "200": + description: Service updated + "201": + description: Service created + 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: 400 + deprecated: false + delete: + tags: + - rAPP Catalogue API + summary: Remove a Service from the catalogue + operationId: deleteIndividualService + parameters: + - name: serviceName + in: path + required: true + style: simple + explode: false + schema: + type: string + example: DroneIdentifier + responses: + "204": + 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 + 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 + 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 + type: object + properties: + detail: + type: string + description: A human-readable explanation specific to this occurrence of + the problem. + example: Service not found + status: + type: integer + 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/config/application.yaml b/config/application.yaml new file mode 100644 index 0000000..1ef0bdc --- /dev/null +++ b/config/application.yaml @@ -0,0 +1,30 @@ + # ========================LICENSE_START================================= + # Copyright (C) 2021 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=================================== + +spring: + profiles: + active: prod +server: + # Configuration of the HTTP/REST server. The parameters are defined and handled by the springboot framework. + # See springboot documentation. + port : 8633 + http-port: 8680 + ssl: + key-store-type: JKS + key-store: /opt/app/r-app-catalogue/etc/cert/keystore.jks + key-store-password: r-app-catalogue + key-password: r-app-catalogue + key-alias: server-cert diff --git a/config/r-app-catalogue-keystore.jks b/config/r-app-catalogue-keystore.jks new file mode 100644 index 0000000..192fe17 Binary files /dev/null and b/config/r-app-catalogue-keystore.jks differ diff --git a/docs/_static/logo.png b/docs/_static/logo.png new file mode 100644 index 0000000..c3b6ce5 Binary files /dev/null and b/docs/_static/logo.png differ diff --git a/docs/api-docs.rst b/docs/api-docs.rst new file mode 100644 index 0000000..c7a9515 --- /dev/null +++ b/docs/api-docs.rst @@ -0,0 +1,36 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. http://creativecommons.org/licenses/by/4.0 +.. Copyright (C) 2021 Nordix + +.. _api_docs: + +.. |swagger-icon| image:: ./images/swagger.png + :width: 40px + +.. |yaml-icon| image:: ./images/yaml_logo.png + :width: 40px + + +======== +API-Docs +======== + +Here we describe the APIs to access the Non-RT RIC App Catalogue. + + +Non-RT-RIC App Catalogue (Initial) +================================== + +The Service Catalogue provides a way for services to register themselves for other services to discover. + +See `App Catalogue API <./rac-api.html>`_ for full details of the API. + +The API is also described in Swagger-JSON and YAML: + + +.. csv-table:: + :header: "API name", "|swagger-icon|", "|yaml-icon|" + :widths: 10,5, 5 + + "App Catalogue API", ":download:`link <../api/rac-api.json>`", ":download:`link <../api/rac-api.yaml>`" + diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..94649c5 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,25 @@ +from docs_conf.conf import * + +#branch configuration + +branch = 'latest' + +linkcheck_ignore = [ + 'http://localhost.*', + 'http://127.0.0.1.*', + 'https://gerrit.o-ran-sc.org.*', + './rac-api.html', #Generated file that doesn't exist at link check. +] + +extensions = ['sphinxcontrib.redoc', 'sphinx.ext.intersphinx',] + +redoc = [ + { + 'name': 'RAC API', + 'page': 'rac-api', + 'spec': '../api/rac-api.json', + 'embed': True, + } + ] + +redoc_uri = 'https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js' diff --git a/docs/conf.yaml b/docs/conf.yaml new file mode 100644 index 0000000..7235ba1 --- /dev/null +++ b/docs/conf.yaml @@ -0,0 +1,3 @@ +--- +project_cfg: oran +project: nonrtric diff --git a/docs/developer-guide.rst b/docs/developer-guide.rst new file mode 100644 index 0000000..1b84db6 --- /dev/null +++ b/docs/developer-guide.rst @@ -0,0 +1,27 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2021 Nordix + +Developer Guide +=============== + +This document provides a quickstart for developers of the Non-RT RIC App Catalogue. + +Additional developer guides are available on the `O-RAN SC NONRTRIC Developer wiki `_. + +Initial Non-RT-RIC App Catalogue +-------------------------------- + +See the README.md file in the Gerrit repo for more details how to run the component. + +Kubernetes deployment +===================== + +Non-RT RIC can be also deployed in a Kubernetes cluster, `it/dep repository `_. +hosts deployment and integration artifacts. Instructions and helm charts to deploy the Non-RT-RIC functions in the +OSC NONRTRIC integrated test environment can be found in the *./nonrtric* directory. + +For more information on installation of NonRT-RIC in Kubernetes, see `Deploy NONRTRIC in Kubernetes `_. + +For more information see `Integration and Testing documentation on the O-RAN-SC wiki `_. + diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 0000000..00b0fd0 Binary files /dev/null and b/docs/favicon.ico differ diff --git a/docs/images/swagger.png b/docs/images/swagger.png new file mode 100644 index 0000000..f5a9e0c Binary files /dev/null and b/docs/images/swagger.png differ diff --git a/docs/images/yaml_logo.png b/docs/images/yaml_logo.png new file mode 100644 index 0000000..0492eb4 Binary files /dev/null and b/docs/images/yaml_logo.png differ diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..a935f34 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,17 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2021 Nordix + +Non-RT RIC App Catalogue +======================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + ./overview.rst + ./developer-guide.rst + ./api-docs.rst + ./release-notes.rst + +* :ref:`search` diff --git a/docs/overview.rst b/docs/overview.rst new file mode 100644 index 0000000..0167ebc --- /dev/null +++ b/docs/overview.rst @@ -0,0 +1,12 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. Copyright (C) 2021 Nordix + +Non-RT-RIC App Catalogue +~~~~~~~~~~~~~~~~~~~~~~~~ + +Register for Non-RT-RIC Apps. + +* Non-RT-RIC Apps can be registered / queried. +* Limited functionality/integration for now. +* *More work required in coming releases as the rApp concept matures*. diff --git a/docs/release-notes.rst b/docs/release-notes.rst new file mode 100644 index 0000000..e0b8259 --- /dev/null +++ b/docs/release-notes.rst @@ -0,0 +1,106 @@ +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. http://creativecommons.org/licenses/by/4.0 +.. Copyright (C) 2021 Nordix + +============= +Release-Notes +============= + + +This document provides the release notes for the Non-RT RIC App Catalogue. + +.. contents:: + :depth: 1 + :local: + +Version history App Catalogue +================================= + ++------------+----------+------------------+----------------+ +| **Date** | **Ver.** | **Author** | **Comment** | +| | | | | ++------------+----------+------------------+----------------+ +| 2020-12-03 | 1.0.0 | Henrik Andersson | Cherry Release | +| | | | | ++------------+----------+------------------+----------------+ + + +Release Data +============ + +Bronze +------ ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/2466f9d370214b578efedd1d3e38b1de17e6ca1c | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | Bronze | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2020-06-18 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | Improved stability | +| | | ++-----------------------------+---------------------------------------------------+ + +Bronze Maintenance +------------------ ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/5d4f252a530a0d9abbf2a363354c5e56e8f2f33e | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | Bronze | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2020-07-29 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | Introduce configuration of certificates | +| | | ++-----------------------------+---------------------------------------------------+ + +Cherry +------ ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/90ce16238dd6970153e1c0fbddb15e32c68c504f | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | Cherry | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2020-12-03 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | Introduction of Enrichment Service Coordinator | +| | and rAPP Catalogue | +| | | ++-----------------------------+---------------------------------------------------+ + +E Maintenance Release +--------------------- ++-----------------------------+---------------------------------------------------+ +| **Project** | Non-RT RIC | +| | | ++-----------------------------+---------------------------------------------------+ +| **Repo/commit-ID** | nonrtric/4df1f9ca4cd1ebc21e0c5ea57bcb0b7ef096d067 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release designation** | E | +| | | ++-----------------------------+---------------------------------------------------+ +| **Release date** | 2022-02-09 | +| | | ++-----------------------------+---------------------------------------------------+ +| **Purpose of the delivery** | Improvements and bug fixes | +| | | ++-----------------------------+---------------------------------------------------+ diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt new file mode 100644 index 0000000..8f65cbd --- /dev/null +++ b/docs/requirements-docs.txt @@ -0,0 +1,3 @@ +sphinx +sphinxcontrib-redoc +lfdocs-conf diff --git a/eclipse-formatter.xml b/eclipse-formatter.xml new file mode 100644 index 0000000..c8cca2e --- /dev/null +++ b/eclipse-formatter.xml @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0edcae9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,331 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.6.2 + + + org.o-ran-sc.nonrtric + r-app-catalogue + 1.1.0-SNAPSHOT + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + 11 + 1.5.22 + 2.9.2 + 0.2.1 + 5.3.1 + 3.0.31 + 2.12.2 + 1.24.3 + 0.8.6 + 0.30.0 + + + + + io.swagger + swagger-annotations + ${swagger-annotations.version} + + + com.fasterxml.jackson.core + jackson-annotations + + + org.springframework + spring-beans + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework + spring-web + + + org.springframework.boot + spring-boot + + + org.springframework + spring-webmvc + + + org.springframework + spring-context + + + io.springfox + springfox-swagger2 + ${springfox.version} + + + io.springfox + springfox-core + ${springfox.version} + + + io.springfox + springfox-spring-web + ${springfox.version} + + + io.springfox + springfox-spi + ${springfox.version} + + + org.assertj + assertj-core + + + org.apache.tomcat.embed + tomcat-embed-core + + + org.openapitools + jackson-databind-nullable + ${jackson-databind-nullable.version} + + + javax.validation + validation-api + + + com.fasterxml.jackson.core + jackson-databind + + + org.yaml + snakeyaml + runtime + + + + org.springframework + spring-test + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.mockito + mockito-junit-jupiter + test + + + org.mockito + mockito-core + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.apache.httpcomponents + httpclient + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.openapitools + openapi-generator-maven-plugin + ${openapi-generator-maven-plugin.version} + + + + generate + + + ${project.basedir}/api/rac-api.json + spring + org.oransc.rappcatalogue.api + org.oransc.rappcatalogue.model + org.oransc.rappcatalogue + + true + true + + + + + + + io.swagger.codegen.v3 + swagger-codegen-maven-plugin + ${swagger-codegen-maven-plugin.version} + + + + generate + + + ${project.basedir}/api/rac-api.json + openapi-yaml + ${project.basedir}/api/ + + rac-api.yaml + + + + + + + net.revelc.code.formatter + formatter-maven-plugin + ${formatter-maven-plugin.version} + + ${project.basedir}/eclipse-formatter.xml + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless-maven-plugin.version} + + + + + com,java,javax,org + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + + + io.fabric8 + docker-maven-plugin + ${docker-maven-plugin.version} + false + + + generate-r-app-catalogue-image + package + + build + + + ${env.CONTAINER_PULL_REGISTRY} + + + o-ran-sc/nonrtric-r-app-catalogue:${project.version} + + try + ${basedir} + Dockerfile + + ${project.build.finalName}.jar + + + ${project.version} + + + + + + + + push-r-app-catalogue-image + + build + push + + + ${env.CONTAINER_PULL_REGISTRY} + ${env.CONTAINER_PUSH_REGISTRY} + + + o-ran-sc/nonrtric-r-app-catalogue:${project.version} + + ${basedir} + Dockerfile + + ${project.build.finalName}.jar + + + ${project.version} + latest + + + + + + + + + + + diff --git a/pyvenv.cfg b/pyvenv.cfg new file mode 100644 index 0000000..853404e --- /dev/null +++ b/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.8.10 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ + diff --git a/src/main/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisor.java b/src/main/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisor.java new file mode 100644 index 0000000..072a5a0 --- /dev/null +++ b/src/main/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisor.java @@ -0,0 +1,61 @@ +/*- + * ========================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/src/main/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImpl.java b/src/main/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImpl.java new file mode 100644 index 0000000..4615d69 --- /dev/null +++ b/src/main/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImpl.java @@ -0,0 +1,147 @@ +/*- + * ========================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.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 getIndividualService(String serviceName) throws ServiceNotFoundException { + Service service = registeredServices.get(serviceName); + if (service != null) { + return ResponseEntity.ok(service); + } else { + throw new ServiceNotFoundException(serviceName); + } + } + + @Override + public ResponseEntity> getServices() { + return ResponseEntity.ok(new ArrayList<>(registeredServices.values())); + } + + @Override + public ResponseEntity putIndividualService(String serviceName, InputService inputService) + throws InvalidServiceException, HeaderException { + if (isServiceValid(inputService)) { + if (registeredServices.put(serviceName, createService(serviceName, inputService)) == null) { + try { + Optional request = getRequest(); + if (request.isPresent()) { + addLocationHeaderToResponse(serviceName, request.get()); + } + } catch (HeaderException 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) throws HeaderException { + 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, serviceName, + new Exception("Native Request or Response missing")); + } + } catch (IOException e) { + throw new HeaderException(LOCATION_HEADER, serviceName, e); + } + } + + @Override + public ResponseEntity deleteIndividualService(String serviceName) { + registeredServices.remove(serviceName); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + /* + * 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/src/main/java/org/oransc/rappcatalogue/configuration/TomcatConfig.java b/src/main/java/org/oransc/rappcatalogue/configuration/TomcatConfig.java new file mode 100644 index 0000000..a04a332 --- /dev/null +++ b/src/main/java/org/oransc/rappcatalogue/configuration/TomcatConfig.java @@ -0,0 +1,56 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2021 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.configuration; + +import org.apache.catalina.connector.Connector; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.servlet.server.ServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configure embedded Tomcat + */ + +@Configuration +public class TomcatConfig { + + @Value("${server.http-port}") + private int httpPort = 0; + + // Embedded Tomcat with HTTP and HTTPS support + @Bean + public ServletWebServerFactory servletContainer() { + TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory(); + + if (httpPort > 0) { + tomcat.addAdditionalTomcatConnectors(getHttpConnector(httpPort)); + } + return tomcat; + } + + private static Connector getHttpConnector(int httpPort) { + Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); + connector.setScheme("http"); + connector.setPort(httpPort); + connector.setSecure(false); + return connector; + } +} diff --git a/src/main/java/org/oransc/rappcatalogue/exception/HeaderException.java b/src/main/java/org/oransc/rappcatalogue/exception/HeaderException.java new file mode 100644 index 0000000..8f64449 --- /dev/null +++ b/src/main/java/org/oransc/rappcatalogue/exception/HeaderException.java @@ -0,0 +1,30 @@ +/*- + * ========================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 Exception { + + private static final long serialVersionUID = -7798178963078284655L; + + public HeaderException(String header, String serviceName, Exception cause) { + super(String.format("Unable to set header %s in put response for service %s. Cause: %s", header, serviceName, + cause.getMessage())); + } + +} diff --git a/src/main/java/org/oransc/rappcatalogue/exception/InvalidServiceException.java b/src/main/java/org/oransc/rappcatalogue/exception/InvalidServiceException.java new file mode 100644 index 0000000..45ec769 --- /dev/null +++ b/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 Exception { + private static final long serialVersionUID = 3849219105170316564L; + + public InvalidServiceException() { + super("Service is missing required property: version"); + } +} diff --git a/src/main/java/org/oransc/rappcatalogue/exception/ServiceNotFoundException.java b/src/main/java/org/oransc/rappcatalogue/exception/ServiceNotFoundException.java new file mode 100644 index 0000000..8411cf4 --- /dev/null +++ b/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 Exception { + private static final long serialVersionUID = 6579271315716003988L; + + public ServiceNotFoundException(String serviceName) { + super(String.format("Service %s not found", serviceName)); + } +} diff --git a/src/test/java/org/oransc/rappcatalogue/HttpsRequestTest.java b/src/test/java/org/oransc/rappcatalogue/HttpsRequestTest.java new file mode 100644 index 0000000..8a66e14 --- /dev/null +++ b/src/test/java/org/oransc/rappcatalogue/HttpsRequestTest.java @@ -0,0 +1,113 @@ +/*- + * ========================LICENSE_START================================= + * Copyright (C) 2021 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.net.ssl.SSLContext; + +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.client.TestRestTemplate.HttpClientOption; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.ResourceUtils; +import org.springframework.web.client.ResourceAccessException; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@TestPropertySource( + properties = { // + "server.ssl.key-store=./config/r-app-catalogue-keystore.jks", // + "server.http-port=0"}) +public class HttpsRequestTest { + + @Value("${server.ssl.key-store-password}") + private String keyStorePassword; // inject password from config + + @Value("${server.ssl.key-store}") + private String keyStore; // inject keyStore from config + + @LocalServerPort + private int port; + + @Autowired + private AbstractConfigurableWebServerFactory webServerFactory; + + @Test + public void testSsl() { + assertEquals(true, this.webServerFactory.getSsl().isEnabled()); + } + + @Test + public void rest_OverPlainHttp_GetsBadRequestRequiresTLS() throws Exception { + TestRestTemplate template = new TestRestTemplate(); + ResponseEntity responseEntity = + template.getForEntity("http://localhost:" + port + "/services", String.class); + assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode()); + assertTrue(responseEntity.getBody().contains("This combination of host and port requires TLS")); + } + + @Test + public void rest_WithoutSSLConfiguration_ThrowsSSLExceptionUnableFindValidCertPath() throws Exception { + TestRestTemplate template = new TestRestTemplate(); + + ResourceAccessException thrown = assertThrows(ResourceAccessException.class, () -> { + template.getForEntity("https://localhost:" + port + "/services", String.class); + }); + assertTrue(thrown.getMessage().contains("unable to find valid certification path to requested target")); + } + + @Test + public void rest_WithTwoWaySSL_AuthenticatesAndGetsExpectedResponse() throws Exception { + + SSLContext sslContext = new SSLContextBuilder().loadKeyMaterial(ResourceUtils.getFile(keyStore), + keyStorePassword.toCharArray(), keyStorePassword.toCharArray()).build(); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext); + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build(); + HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); + RestTemplateBuilder rtb = + new RestTemplateBuilder().requestFactory(() -> factory).rootUri("https://localhost:" + port); + + TestRestTemplate template = new TestRestTemplate(rtb, null, null, HttpClientOption.SSL); + + ResponseEntity responseEntity = template.getForEntity("/services", String.class); + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertEquals("[]", responseEntity.getBody()); + } + +} diff --git a/src/test/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisorTest.java b/src/test/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisorTest.java new file mode 100644 index 0000000..24afa09 --- /dev/null +++ b/src/test/java/org/oransc/rappcatalogue/api/GeneralRappCatalogueControllerAdvisorTest.java @@ -0,0 +1,78 @@ +/*- + * ========================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(); + + String serviceName = "Service"; + HeaderException exception = new HeaderException("Header", serviceName, 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 put response for service " + serviceName + ". Cause: Cause"); + } +} diff --git a/src/test/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImplTest.java b/src/test/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImplTest.java new file mode 100644 index 0000000..dd10a65 --- /dev/null +++ b/src/test/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImplTest.java @@ -0,0 +1,275 @@ + +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.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.web.context.request.NativeWebRequest; + +@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() throws Exception { + 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() + throws Exception { + 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() throws Exception { + 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 putServiceWhenIoExceptionAddingHeader_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")); + + 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 put response for service " + SERVICE_NAME + ". Cause: Error"); + + ResponseEntity> response = delegateUnderTest.getServices(); + assertThat(response.getBody()).isEmpty(); + } + + @Test + 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()); + } + + @Test + void deleteService_shouldBeOk() throws Exception { + 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; + } + + private String getTodaysDate() { + long millis = System.currentTimeMillis(); + Date date = new Date(millis); + return date.toString(); + } +} diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2705e16 --- /dev/null +++ b/tox.ini @@ -0,0 +1,37 @@ +# ================================================================================== +# Copyright (c) 2020 Nordix +# +# 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. +# ================================================================================== + +# documentation only +[tox] +minversion = 2.0 +envlist = + docs, + docs-linkcheck, +skipsdist = true + +[testenv:docs] +basepython = python3 +deps = -r{toxinidir}/docs/requirements-docs.txt + +commands = + sphinx-build -W -b html -n -d {envtmpdir}/docs/doctrees ./docs/ {toxinidir}/docs/_build/html + echo "Generated docs available in {toxinidir}/docs/_build/html" +whitelist_externals = echo + +[testenv:docs-linkcheck] +basepython = python3 +deps = -r{toxinidir}/docs/requirements-docs.txt +commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/linkcheck