diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 7345f577..5a75a276 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -29,14 +29,14 @@ jobs: files: ${{ github.workspace }}/target/surefire-reports/**/*.xml - name: Add coverage to PR id: jacoco - uses: madrapps/jacoco-report@v1.6.1 # requires at least two pushes to a PR, see https://github.com/Madrapps/jacoco-report/issues/13 + uses: madrapps/jacoco-report@v1.7.1 # requires at least two pushes to a PR, see https://github.com/Madrapps/jacoco-report/issues/13 with: paths: ${{ github.workspace }}/target/jacoco-report/jacoco.xml token: ${{ secrets.GITHUB_TOKEN }} min-coverage-overall: 40 min-coverage-changed-files: 60 - name: Archive code coverage results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: code-coverage-report path: target/jacoco-report/** @@ -64,7 +64,7 @@ jobs: - name: Check out code uses: actions/checkout@v3 - name: Lint markdowns - uses: nosborn/github-action-markdown-cli@v3.0.1 + uses: nosborn/github-action-markdown-cli@v3.4.0 with: files: '**/*.md' diff --git a/README.md b/README.md index 36bb60a2..97b858d0 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,15 @@ CouchDb Info (port is defined in .env): [http://localhost:5984/](http://localhost:5984/) +### Integration tests with Karate + +The folder `src/test/java/de.bsi.secvisogram.csaf_cms_backend.integration/` & `src/test/java/de.bsi.secvisogram.csaf_cms_backend.integration/` contains [Karate](https://github.com/karatelabs/karate) test files. + +Use `./gradlew integrationTest` to run the tests. CSAF-CMS-Backend and all other components must be up an running with the development setup described above. + +Settings (like hostnames, port, user and password) can be changed in the feature-files. `template.feature`-file contains all basic request to build that can be used to assemble futher workflow. + + ## Contributing You can find our guidelines here [CONTRIBUTING.md](https://github.com/secvisogram/secvisogram/blob/main/CONTRIBUTING.md) diff --git a/pom.xml b/pom.xml index 08204fa7..2ae740ed 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,7 @@ 23.0.3 24.0.0 4.8.4 + 1.5.1 @@ -262,6 +263,13 @@ junit-jupiter test + + + io.karatelabs + karate-junit5 + ${dependency.version.karate} + test + diff --git a/src/test/java/de/bsi/secvisogram/csaf_cms_backend/integration/KarateRunner.java b/src/test/java/de/bsi/secvisogram/csaf_cms_backend/integration/KarateRunner.java new file mode 100644 index 00000000..6bdf9e29 --- /dev/null +++ b/src/test/java/de/bsi/secvisogram/csaf_cms_backend/integration/KarateRunner.java @@ -0,0 +1,22 @@ +package de.bsi.secvisogram.csaf_cms_backend.integration; + +import com.intuit.karate.junit5.Karate; + + +public class KarateRunner { + @Karate.Test + Karate testExport() { + String[] testCases = new String[] { + "classpath:de/bsi/secvisogram/csaf_cms_backend/integration/export.feature" + }; + return Karate.run(testCases); + } + + @Karate.Test + Karate testFullWorkflow() { + String[] testCases = new String[] { + "classpath:de/bsi/secvisogram/csaf_cms_backend/integration/fullworkflow.feature" + }; + return Karate.run(testCases); + } +} diff --git a/src/test/resources/de/bsi/secvisogram/csaf_cms_backend/integration/export.feature b/src/test/resources/de/bsi/secvisogram/csaf_cms_backend/integration/export.feature new file mode 100644 index 00000000..c8e8944d --- /dev/null +++ b/src/test/resources/de/bsi/secvisogram/csaf_cms_backend/integration/export.feature @@ -0,0 +1,90 @@ +Feature: Test export of documents + + Background: + * def authUrl = 'http://localhost:9000' + * def loginPath = '/realms/csaf/protocol/openid-connect/token' + * def logoutPath = '/realms/csaf/protocol/openid-connect/logout' + + * def restUrl = 'http://localhost:8081' + * def apiBase = '/api/v1/advisories' + + * def username = 'all' + * def password = 'all' + + Scenario: Export all formats and store response in folder ./target/ + The first document in the list will be exported in all available + formats. + + # Login + Given url 'http://localhost:9000/realms/csaf/protocol/openid-connect/token' + * form field client_id = 'secvisogram' + * form field username = 'all' + * form field password = 'all' + * form field grant_type = 'password' + * form field response_type = 'code' + * form field audience = 'secvisogram' + * form field requested_token_type = 'ID' + When method post + Then status 200 + * def accessToken = response.access_token + * def refreshToken = response.refresh_token + * def session = response.session_state + + #Get advisory list and store first advisory id + Given url 'http://localhost:8081' + * path apiBase + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method get + Then status 200 + * def advisoryId = response[0].advisoryId + + # Download advisory as JSON + * def format = 'JSON' + Given path apiBase + '/' + advisoryId + '/csaf' + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + * param format = format + When method get + Then status 200 + * karate.write(response, 'advisory.json') + + # Download advisory as HTML + * def format = 'HTML' + Given path apiBase + '/' + advisoryId + '/csaf' + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + * param format = format + When method get + Then status 200 + * karate.write(response, 'advisory.html') + + # Download advisory as PDF + * def format = 'PDF' + Given path apiBase + '/' + advisoryId + '/csaf' + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + * param format = format + When method get + Then status 200 + * karate.write(response, 'advisory.pdf') + + # Download advisory as Markdown + * def format = 'Markdown' + Given path apiBase + '/' + advisoryId + '/csaf' + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + * param format = format + When method get + Then status 200 + * karate.write(response, 'advisory.md') + + # Logout + Given url 'http://localhost:9000/realms/csaf/protocol/openid-connect/logout' + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/x-www-form-urlencoded' + * form field refresh_token = refreshToken + * form field client_id = 'secvisogram' + When method post + * status 204 + \ No newline at end of file diff --git a/src/test/resources/de/bsi/secvisogram/csaf_cms_backend/integration/fullworkflow.feature b/src/test/resources/de/bsi/secvisogram/csaf_cms_backend/integration/fullworkflow.feature new file mode 100644 index 00000000..279f56d0 --- /dev/null +++ b/src/test/resources/de/bsi/secvisogram/csaf_cms_backend/integration/fullworkflow.feature @@ -0,0 +1,128 @@ +Feature: Full workflow + + Background: + * url 'http://localhost:8081' + * def apiBase = '/api/v1/advisories' + + Scenario: oauth 2 flow + # Login + Given url 'http://localhost:9000/realms/csaf/protocol/openid-connect/token' + * form field client_id = 'secvisogram' + * form field username = 'all' + * form field password = 'all' + * form field grant_type = 'password' + * form field response_type = 'code' + * form field audience = 'secvisogram' + * form field requested_token_type = 'ID' + When method post + Then status 200 + * def accessToken = response.access_token + * def refreshToken = response.refresh_token + * def session = response.session_state + + ######## + #Upload + Given url 'http://localhost:8081' + * path apiBase + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + * request read('min.json') + When method post + Then status 201 + * def advisoryId = response.id + * def revision = response.revision + + ######## + #Change workflow state to Review + # + # Draft, Review, Approved, RfPublication, Published + * def workflowStatus = 'Review' + Given path apiBase + '/' + advisoryId + '/workflowstate/' + workflowStatus + * param revision = revision + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method patch + Then status 200 + + # Get new revision after workflow state change + Given path apiBase + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method get + Then status 200 + * def filt = function(x){ return x.advisoryId == advisoryId } + * def items = get response[*] + * def revision = karate.filter(items, filt)[0].revision + + ######## + #Change workflow state to Approved + # + * def workflowStatus = 'Approved' + Given path apiBase + '/' + advisoryId + '/workflowstate/' + workflowStatus + * param revision = revision + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method patch + Then status 200 + # Get new revision after workflow state change + Given path apiBase + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method get + Then status 200 + * def filt = function(x){ return x.advisoryId == advisoryId } + * def items = get response[*] + * def revision = karate.filter(items, filt)[0].revision + + ######## + #Change workflow state to RfPublication + # + * def workflowStatus = 'RfPublication' + Given path apiBase + '/' + advisoryId + '/workflowstate/' + workflowStatus + * param revision = revision + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method patch + Then status 200 + # Get new revision after workflow state change + Given path apiBase + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method get + Then status 200 + * def filt = function(x){ return x.advisoryId == advisoryId } + * def items = get response[*] + * def revision = karate.filter(items, filt)[0].revision + + ######## + #Change workflow state to Published + # + * def workflowStatus = 'Published' + Given path apiBase + '/' + advisoryId + '/workflowstate/' + workflowStatus + * param revision = revision + # final, interim, draft + * param documentTrackingStatus = 'Final' + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method patch + Then status 200 + + # Get new revision after workflow state change + Given path apiBase + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method get + Then status 200 + * def filt = function(x){ return x.advisoryId == advisoryId } + * def items = get response[*] + * def revision = karate.filter(items, filt)[0].revision + + # Logout + Given url 'http://localhost:9000/realms/csaf/protocol/openid-connect/logout' + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/x-www-form-urlencoded' + * form field refresh_token = refreshToken + * form field client_id = 'secvisogram' + When method post + * status 204 + \ No newline at end of file diff --git a/src/test/resources/de/bsi/secvisogram/csaf_cms_backend/integration/min.json b/src/test/resources/de/bsi/secvisogram/csaf_cms_backend/integration/min.json new file mode 100644 index 00000000..158f29d7 --- /dev/null +++ b/src/test/resources/de/bsi/secvisogram/csaf_cms_backend/integration/min.json @@ -0,0 +1,38 @@ +{ + "csaf": { + "document": { + "category": "csaf_base", + "csaf_version": "2.0", + "publisher": { + "category": "other", + "name": "Automated test data", + "namespace": "https://www.example.com" + }, + "title": "title", + "tracking": { + "current_release_date": "2023-07-12T10:00:00.000Z", + "generator": { + "date": "2023-07-12T07:53:29.378Z", + "engine": { + "name": "Secvisogram", + "version": "2.2.5" + } + }, + "id": "document-id", + "initial_release_date": "2023-07-12T10:00:00.000Z", + "revision_history": [ + { + "date": "2023-07-12T10:00:00.000Z", + "number": "1.0.0", + "summary": "Initial publication" + } + ], + "status": "final", + "version": "1.0.0" + } + } + }, + "summary": "-", + "legacyVersion": "" +} + \ No newline at end of file diff --git a/src/test/resources/de/bsi/secvisogram/csaf_cms_backend/integration/template.feature b/src/test/resources/de/bsi/secvisogram/csaf_cms_backend/integration/template.feature new file mode 100644 index 00000000..0cb92ea0 --- /dev/null +++ b/src/test/resources/de/bsi/secvisogram/csaf_cms_backend/integration/template.feature @@ -0,0 +1,78 @@ +Feature: Template + + Background: + * url 'http://localhost:8081' + * def apiBase = '/api/v1/' + + Scenario: oauth 2 flow + + #Login + Given url 'http://localhost:9000/realms/csaf/protocol/openid-connect/token' + * form field client_id = 'secvisogram' + * form field username = 'all' + * form field password = 'all' + * form field grant_type = 'password' + * form field response_type = 'code' + * form field audience = 'secvisogram' + * form field requested_token_type = 'ID' + When method post + Then status 200 + * def accessToken = response.access_token + * def refreshToken = response.refresh_token + * def session = response.session_state + + #Upload + Given url 'http://localhost:8081' + * path apiBase + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + * request read('min.json') + When method post + Then status 201 + * def advisoryId = response.id + * def revision = response.revision + + #Get advisory list + Given path apiBase + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method get + Then status 200 + + #Get advisory + Given path apiBase + '/' + advisoryId + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method get + Then status 200 + + #Change workflow state + + # Draft, Review, Approved, RfPublication, Published + * def workflowStatus = 'Review' + Given path apiBase + '/' + advisoryId + '/workflowstate/' + workflowStatus + * param revision = revision + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method patch + Then status 200 + + # Get new revision after workflow state change + Given path apiBase + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/json' + When method get + Then status 200 + * def filt = function(x){ return x.advisoryId == advisoryId } + * def items = get response[*] + * def revision = karate.filter(items, filt)[0].revision + + #Logout + Given url 'http://localhost:9000/realms/csaf/protocol/openid-connect/logout' + * header Authorization = 'Bearer ' + accessToken + * header Content-Type = 'application/x-www-form-urlencoded' + * form field refresh_token = refreshToken + * form field client_id = 'secvisogram' + When method post + * status 204 + \ No newline at end of file