From 2c7701cdf019f46cf671d99252248929bda113ce Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:39:32 +0200 Subject: [PATCH] feat(test): adds E2E tests to the CI (#294) * feat(test): add e2e test * checkstyle * run e2e tests on push * enable verbosity --- .github/workflows/run-terraform.yml | 25 +-- .github/workflows/verify.yaml | 13 +- gradle/libs.versions.toml | 12 +- settings.gradle.kts | 2 +- tests/end2end/build.gradle.kts | 26 +++ .../tests/transfer/TransferEndToEndTest.java | 151 ++++++++++++++++++ .../test/resources/negotiation-request.json | 33 ++++ .../src/test/resources/transfer-request.json | 14 ++ tests/system-tests/build.gradle.kts | 22 --- 9 files changed, 259 insertions(+), 39 deletions(-) create mode 100644 tests/end2end/build.gradle.kts create mode 100644 tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java create mode 100644 tests/end2end/src/test/resources/negotiation-request.json create mode 100644 tests/end2end/src/test/resources/transfer-request.json delete mode 100644 tests/system-tests/build.gradle.kts diff --git a/.github/workflows/run-terraform.yml b/.github/workflows/run-terraform.yml index 2701a432..8f83c8c8 100644 --- a/.github/workflows/run-terraform.yml +++ b/.github/workflows/run-terraform.yml @@ -20,11 +20,13 @@ --- name: "Run DCP Demo locally" on: + push: + pull_request: + branches: + - main + + # Allows you to run this workflow manually from the Actions tab workflow_dispatch: - workflow_run: - workflows: [ "Verify" ] - types: - - completed concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -49,7 +51,7 @@ jobs: - uses: eclipse-edc/.github/.github/actions/setup-build@main - name: "Build runtime images" - working-directory: ./runtimes + working-directory: ./ run: | ./gradlew -Ppersistence=true dockerize @@ -61,7 +63,7 @@ jobs: cluster_name: dcp-demo - name: "Load runtime images into KinD" - run: kind load docker-image controlplane:latest identity-hub:latest catalog-server:latest -n dcp-demo + run: kind load docker-image controlplane:latest dataplane:latest identity-hub:latest catalog-server:latest -n dcp-demo - name: "Install nginx ingress controller" run: |- @@ -88,11 +90,14 @@ jobs: run: |- terraform apply "$GITHUB_SHA.out" - - name: "Run Health Checks" + - name: "Seed dataspace" + run: |- + chmod +x seed-k8s.sh + ./seed-k8s.sh + + - name: "Run E2E Test" run: |- - curl --fail http://localhost/provider-qna/health/check/readiness -H "x-api-key: password" - curl --fail http://localhost/provider-manufacturing/health/check/readiness -H "x-api-key: password" - curl --fail http://localhost/consumer/health/check/readiness -H "x-api-key: password" + ./gradlew -DincludeTags="EndToEndTest" test -DverboseTest=true - name: "Destroy the KinD cluster" run: >- diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index 2644e0c8..f4284704 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -22,8 +22,6 @@ name: "Verify" on: push: - branches: - - main pull_request: branches: - main @@ -51,7 +49,7 @@ jobs: exit 1; fi - validate-formating: + checkstyle: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -59,6 +57,15 @@ jobs: - name: Run Checkstyle run: ./gradlew checkstyleMain checkstyleTest + - name: Check Terraform files are properly formatted (run "terraform fmt -recursive" to fix) + run: | + terraform fmt -recursive + git diff --exit-code + + validate-terraform: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 - name: Check Terraform files are properly formatted (run "terraform fmt -recursive" to fix) run: | terraform fmt -recursive diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index db436115..2d84d166 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,15 +3,17 @@ format.version = "1.1" [versions] assertj = "3.24.2" -awaitility = "4.2.0" +awaitility = "4.2.1" edc = "0.8.1-SNAPSHOT" failsafe = "3.3.2" jackson = "2.14.2" +jakarta-json = "2.1.3" jupiter = "5.10.1" mockserver = "5.15.0" nimbus = "9.40" +parsson = "1.1.6" postgres = "42.7.3" -restAssured = "5.3.2" +restAssured = "5.5.0" swagger = "2.2.18" rsApi = "3.1.0" testcontainers = "1.19.1" @@ -148,7 +150,11 @@ edc-controlplane-contract = { module = "org.eclipse.edc:control-plane-contract", # Third party libs nimbus-jwt = { module = "com.nimbusds:nimbus-jose-jwt", version.ref = "nimbus" } postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" } - +awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" } +restAssured = { module = "io.rest-assured:rest-assured", version.ref = "restAssured" } +jakarta-json-api = { module = "jakarta.json:jakarta.json-api", version.ref = "jakarta-json" } +jackson-datatype-jakarta-jsonp = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jakarta-jsonp", version.ref = "jackson" } +parsson = { module = "org.eclipse.parsson:parsson", version.ref = "parsson" } [bundles] dpf = ["edc-dpf-selector-core", "edc-spi-dataplane-selector", "edc-dpf-selector-control-api", "edc-dpf-signaling-client", "edc-dpf-transfer-signaling"] diff --git a/settings.gradle.kts b/settings.gradle.kts index dc801cb7..2f3172a0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,7 +41,7 @@ include(":extensions:catalog-node-resolver") include(":extensions:dcp-impl") include(":extensions:superuser-seed") //include(":tests:performance") -include(":tests:system-tests") +include(":tests:end2end") // launcher modules include(":launchers:identity-hub") diff --git a/tests/end2end/build.gradle.kts b/tests/end2end/build.gradle.kts new file mode 100644 index 00000000..d66843ed --- /dev/null +++ b/tests/end2end/build.gradle.kts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + testImplementation(libs.edc.junit) + testImplementation(libs.jakarta.json.api) + testImplementation(libs.jackson.datatype.jakarta.jsonp) + testImplementation(libs.parsson) + testImplementation(libs.restAssured) + testImplementation(libs.awaitility) +} diff --git a/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java new file mode 100644 index 00000000..cecaf038 --- /dev/null +++ b/tests/end2end/src/test/java/org/eclipse/edc/demo/tests/transfer/TransferEndToEndTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.demo.tests.transfer; + +import io.restassured.specification.RequestSpecification; +import jakarta.json.Json; +import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.eclipse.edc.junit.testfixtures.TestUtils; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicReference; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * This test is designed to run against an MVD deployed in a Kubernetes cluster, with an active ingress controller. + * The cluster MUST be deployed and seeded according to the README before running this test! + */ +@EndToEndTest +public class TransferEndToEndTest { + // Management API base URL of the consumer connector, goes through Ingress controller + private static final String CONSUMER_MANAGEMENT_URL = "http://127.0.0.1/consumer/cp"; + // Catalog Query API URL of the consumer connector, goes through ingress controller + private static final String CONSUMER_CATALOG_URL = "http://127.0.0.1/consumer/fc"; + // DSP service URL of the provider, not reachable outside the cluster + private static final String PROVIDER_DSP_URL = "http://provider-qna-controlplane:8082"; + // DID of the provider company + private static final String PROVIDER_ID = "did:web:provider-identityhub%3A7083:provider"; + // public API endpoint of the provider-qna connector, goes through the incress controller + private static final String PROVIDER_PUBLIC_URL = "http://127.0.0.1/provider-qna/public"; + private static final Duration TEST_TIMEOUT_DURATION = Duration.ofSeconds(60); + + private static RequestSpecification baseRequest() { + return given() + .header("X-Api-Key", "password") + .contentType(JSON) + .when(); + } + + @Test + void transferData() { + var emptyQueryBody = Json.createObjectBuilder() + .add("@context", Json.createObjectBuilder().add("edc", "https://w3id.org/edc/v0.0.1/ns/")) + .add("@type", "QuerySpec") + .build(); + var offerId = new AtomicReference(); + // get catalog, extract offer ID + await().atMost(TEST_TIMEOUT_DURATION) + .untilAsserted(() -> { + var oid = baseRequest() + .body(emptyQueryBody) + .post(CONSUMER_CATALOG_URL + "/api/catalog/v1alpha/catalog/query") + .then() + .log().ifError() + .statusCode(200) + // yes, it's a bit brittle with the hardcoded indexes, but it appears to work. + .extract().body().jsonPath().getString("[0]['http://www.w3.org/ns/dcat#dataset'][1]['http://www.w3.org/ns/dcat#dataset'][0]['odrl:hasPolicy']['@id']"); + + assertThat(oid).isNotNull(); + offerId.set(oid); + }); + + // initiate negotiation + var negotiationRequest = TestUtils.getResourceFileContentAsString("negotiation-request.json") + .replace("{{PROVIDER_ID}}", PROVIDER_ID) + .replace("{{PROVIDER_DSP_URL}}", PROVIDER_DSP_URL) + .replace("{{OFFER_ID}}", offerId.get()); + var negotiationId = baseRequest() + .body(negotiationRequest) + .post(CONSUMER_MANAGEMENT_URL + "/api/management/v3/contractnegotiations") + .then() + .log().ifError() + .statusCode(200) + .extract().body().jsonPath().getString("@id"); + assertThat(negotiationId).isNotNull(); + + //wait until negotiation is FINALIZED + var agreementId = new AtomicReference(); + await().atMost(TEST_TIMEOUT_DURATION) + .untilAsserted(() -> { + var jp = baseRequest() + .get(CONSUMER_MANAGEMENT_URL + "/api/management/v3/contractnegotiations/" + negotiationId) + .then() + .statusCode(200) + .extract().body().jsonPath(); + var state = jp.getString("state"); + assertThat(state).isEqualTo("FINALIZED"); + agreementId.set(jp.getString("contractAgreementId")); + + }); + + //start transfer process + var tpRequest = TestUtils.getResourceFileContentAsString("transfer-request.json") + .replace("{{PROVIDER_ID}}", PROVIDER_ID) + .replace("{{PROVIDER_DSP_URL}}", PROVIDER_DSP_URL) + .replace("{{CONTRACT_ID}}", agreementId.get()); + + var transferProcessId = baseRequest() + .body(tpRequest) + .post(CONSUMER_MANAGEMENT_URL + "/api/management/v3/transferprocesses") + .then() + .log().ifError() + .statusCode(200) + .extract().body().jsonPath().getString("@id"); + + // fetch EDR for transfer process + var endpoint = new AtomicReference(); + var token = new AtomicReference(); + await().atMost(TEST_TIMEOUT_DURATION) + .untilAsserted(() -> { + var jp = baseRequest() + .get(CONSUMER_MANAGEMENT_URL + "/api/management/v3/edrs/%s/dataaddress".formatted(transferProcessId)) + .then() + .statusCode(200) + .extract().body().jsonPath(); + + endpoint.set(jp.getString("endpoint")); + token.set(jp.getString("authorization")); + + assertThat(endpoint.get()).isNotNull().endsWith("/api/public"); + assertThat(token.get()).isNotNull(); + }); + + //download exemplary JSON data from public endpoint + var response = given() + .header("Authorization", token.get()) + .get(PROVIDER_PUBLIC_URL + "/api/public") + .then() + .log().ifError() + .statusCode(200) + .extract().body().asString(); + + assertThat(response).isNotEmpty(); + } +} diff --git a/tests/end2end/src/test/resources/negotiation-request.json b/tests/end2end/src/test/resources/negotiation-request.json new file mode 100644 index 00000000..97218afc --- /dev/null +++ b/tests/end2end/src/test/resources/negotiation-request.json @@ -0,0 +1,33 @@ +{ + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/" + }, + "@type": "https://w3id.org/edc/v0.0.1/ns/ContractRequest", + "counterPartyAddress": "{{PROVIDER_DSP_URL}}/api/dsp", + "counterPartyId": "{{PROVIDER_ID}}", + "protocol": "dataspace-protocol-http", + "policy": { + "@context": "http://www.w3.org/ns/odrl.jsonld", + "@type": "http://www.w3.org/ns/odrl/2/Offer", + "@id": "{{OFFER_ID}}", + "assigner": "{{PROVIDER_ID}}", + "permission": [], + "prohibition": [], + "odrl:obligation": { + "odrl:action": { + "@id": "use" + }, + "odrl:constraint": { + "odrl:leftOperand": { + "@id": "FrameworkCredential.pcf" + }, + "odrl:operator": { + "@id": "odrl:eq" + }, + "odrl:rightOperand": "active" + } + }, + "target": "asset-1" + }, + "callbackAddresses": [] +} \ No newline at end of file diff --git a/tests/end2end/src/test/resources/transfer-request.json b/tests/end2end/src/test/resources/transfer-request.json new file mode 100644 index 00000000..aea7297b --- /dev/null +++ b/tests/end2end/src/test/resources/transfer-request.json @@ -0,0 +1,14 @@ +{ + "@context": { + "odrl": "http://www.w3.org/ns/odrl/2/" + }, + "assetId": "asset-1", + "counterPartyAddress": "{{PROVIDER_DSP_URL}}/api/dsp", + "connectorId": "{{PROVIDER_ID}}", + "contractId": "{{CONTRACT_ID}}", + "dataDestination": { + "type": "HttpProxy" + }, + "protocol": "dataspace-protocol-http", + "transferType": "HttpData-PULL" +} \ No newline at end of file diff --git a/tests/system-tests/build.gradle.kts b/tests/system-tests/build.gradle.kts deleted file mode 100644 index 589da698..00000000 --- a/tests/system-tests/build.gradle.kts +++ /dev/null @@ -1,22 +0,0 @@ -/* -* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) -* -* This program and the accompanying materials are made available under the -* terms of the Apache License, Version 2.0 which is available at -* https://www.apache.org/licenses/LICENSE-2.0 -* -* SPDX-License-Identifier: Apache-2.0 -* -* Contributors: -* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - Initial API and Implementation -* -*/ - -plugins { - `java-library` -} - -dependencies { - testImplementation(libs.edc.junit) - testImplementation(libs.nimbus.jwt) -}