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