From 36f2e0b839ca6f2c0711a462e6ede3cef6d90d0c Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 17 Oct 2025 12:34:37 +0200 Subject: [PATCH 1/3] feat: add test for streaming data plane --- .dockerignore | 25 --- .github/workflows/test.yml | 34 +++- Samples/Streaming/Consumer/Extensions.cs | 12 +- .../StreamingPull.postman_collection.json | 191 ++++++------------ .../TestRunner/ControlPlaneSimulator.cs | 94 +++++++++ Samples/Streaming/TestRunner/EndToEndTest.cs | 87 ++++++++ .../Streaming/TestRunner/TestRunner.csproj | 35 ++++ dataplane-sdk-dotnet.sln | 17 ++ 8 files changed, 343 insertions(+), 152 deletions(-) delete mode 100644 .dockerignore create mode 100644 Samples/Streaming/TestRunner/ControlPlaneSimulator.cs create mode 100644 Samples/Streaming/TestRunner/EndToEndTest.cs create mode 100644 Samples/Streaming/TestRunner/TestRunner.csproj diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 38bece4..0000000 --- a/.dockerignore +++ /dev/null @@ -1,25 +0,0 @@ -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/.idea -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 53b00cc..d34c26c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,6 +9,10 @@ on: pull_request: branches: [ "main" ] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: @@ -33,5 +37,33 @@ jobs: run: dotnet restore - name: Build run: dotnet build --no-restore - - name: Test + - name: Unit/Integration Tests run: dotnet test --no-build --verbosity normal + + e2e-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - uses: docker/setup-compose-action@v1 + - uses: hoverkraft-tech/compose-action@3846bcd61da338e9eaaf83e7ed0234a12b099b72 + with: + compose-file: './Samples/Streaming/docker-compose.yaml' + + env: + NUGET_USERNAME: ${{ github.actor }} + NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + + - name: Execute E2E Tests + run: | + dotnet restore Samples/Streaming/TestRunner/TestRunner.csproj + dotnet test Samples/Streaming/TestRunner/TestRunner.csproj + + - name: Cleanup + run: | + docker-compose down -v + diff --git a/Samples/Streaming/Consumer/Extensions.cs b/Samples/Streaming/Consumer/Extensions.cs index 8fd15b7..9100def 100644 --- a/Samples/Streaming/Consumer/Extensions.cs +++ b/Samples/Streaming/Consumer/Extensions.cs @@ -32,7 +32,17 @@ public static void AddDataPlaneSdk(this IServiceCollection services, IConfigurat dataService.Start(NatsDataAddress.Create(dataFlow.Destination)).Wait(); return StatusResult.Success(dataFlow); }, - OnTerminate = df => StatusResult.Success(), + OnTerminate = df => + { + if (df.Destination == null) + { + return StatusResult.FromCode(400, "DataFlow.Destination cannot be null"); + } + + var dataService = services.BuildServiceProvider().GetRequiredService(); + dataService.Stop(NatsDataAddress.Create(df.Destination)).Wait(); + return StatusResult.Success(); + }, OnSuspend = _ => StatusResult.Success(), OnPrepare = f => { diff --git a/Samples/Streaming/Resources/Postman/StreamingPull.postman_collection.json b/Samples/Streaming/Resources/Postman/StreamingPull.postman_collection.json index eabf6dc..43d3d70 100644 --- a/Samples/Streaming/Resources/Postman/StreamingPull.postman_collection.json +++ b/Samples/Streaming/Resources/Postman/StreamingPull.postman_collection.json @@ -1,10 +1,10 @@ { "info": { - "_postman_id": "84fd9da2-29f2-405d-9f51-f28fdf4c532b", + "_postman_id": "a5ec8438-2ba7-45df-bec0-c9006f64e347", "name": "Streaming Pull", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "647585", - "_collection_link": "https://galactic-star-228409.postman.co/workspace/Dataplane-work~d820e7a7-8273-4d79-986e-96754a31f467/collection/647585-84fd9da2-29f2-405d-9f51-f28fdf4c532b?action=share&source=collection_link&creator=647585" + "_collection_link": "https://galactic-star-228409.postman.co/workspace/Dataplane-work~d820e7a7-8273-4d79-986e-96754a31f467/collection/647585-a5ec8438-2ba7-45df-bec0-c9006f64e347?action=share&source=collection_link&creator=647585" }, "item": [ { @@ -38,7 +38,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"processId\": \"{{DATAFLOW_ID}}\",\n \"datasetId\": \"test-asset\",\n \"participantId\": \"{{PARTICIPANT_ID}}\",\n \"agreementId\": \"test-agreement\",\n \"transferType\": {\n \"flowType\": \"pull\",\n \"destinationType\": \"NatsStream\"\n }\n}", + "raw": "{\n \"processId\": \"{{DATAFLOW_ID}}\",\n \"datasetId\": \"test-asset\",\n \"participantId\": \"{{PARTICIPANT_ID}}\",\n \"agreementId\": \"test-agreement\",\n \n \"transferType\": {\n \"flowType\": \"pull\",\n \"destinationType\": \"NatsStream\"\n }\n}", "options": { "raw": { "language": "json" @@ -46,12 +46,10 @@ } }, "url": { - "raw": "http://localhost:8081/api/v1/{{PARTICIPANT_ID}}/dataflows/prepare", - "protocol": "http", + "raw": "{{CONSUMER_HOST}}/api/v1/{{PARTICIPANT_ID}}/dataflows/prepare", "host": [ - "localhost" + "{{CONSUMER_HOST}}" ], - "port": "8081", "path": [ "api", "v1", @@ -102,9 +100,9 @@ } }, "url": { - "raw": "{{DATAPLANE_HOST}}/api/v1/{{PARTICIPANT_ID}}/dataflows/start", + "raw": "{{PROVIDER_HOST}}/api/v1/{{PARTICIPANT_ID}}/dataflows/start", "host": [ - "{{DATAPLANE_HOST}}" + "{{PROVIDER_HOST}}" ], "path": [ "api", @@ -119,117 +117,6 @@ }, { "name": "Consumer - Notify Started", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (pm.response.code < 300 && pm.response.code >= 200) {", - " const responseJson = pm.response.json();", - " pm.expect(responseJson).to.have.property('dataAddress');", - " pm.expect(responseJson.dataAddress).to.have.property('properties');", - " pm.expect(responseJson.dataAddress.properties).to.have.property('url');", - " pm.expect(responseJson.dataAddress.properties.url).to.exist;", - "", - " const url = responseJson.dataAddress.properties.url;", - " pm.collectionVariables.set(\"PUBLIC_URL\", url);", - "", - " pm.expect(responseJson.dataAddress.properties).to.have.property('token');", - " const token = responseJson.dataAddress.properties.token;", - " pm.collectionVariables.set(\"API_KEY\", token);", - "}", - "", - "pm.test(\"Status code is >=200 and <300\", function () {", - " pm.expect(pm.response.code < 300 && pm.response.code >= 200).to.be.true", - "});", - "pm.test(\"Public URL is set\", function(){", - " pm.expect(pm.collectionVariables.get(\"PUBLIC_URL\")).not.to.be.undefined", - "})", - "", - "", - "pm.test(\"API is set\", function(){", - " pm.expect(pm.collectionVariables.get(\"API_KEY\")).not.to.be.undefined", - "})", - "" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"processId\": \"{{DATAFLOW_ID}}\",\n \"datasetId\": \"test-asset\",\n \"participantId\": \"{{PARTICIPANT_ID}}\",\n \"agreementId\": \"test-agreement\",\n \"dataAddress\": {\n \"@type\": \"HttpData\",\n \"properties\":{\n \"baseUrl\": \"https://jsonplaceholder.typicode.com/comments/22\"\n }\n },\n \"transferType\": {\n \"flowType\": \"pull\",\n \"destinationType\": \"HttpData\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{DATAPLANE_HOST}}/api/v1/{{PARTICIPANT_ID}}/dataflows/{{CONSUMER_DATAFLOW_ID}}//started", - "host": [ - "{{DATAPLANE_HOST}}" - ], - "path": [ - "api", - "v1", - "{{PARTICIPANT_ID}}", - "dataflows", - "{{CONSUMER_DATAFLOW_ID}}", - "", - "started" - ] - } - }, - "response": [] - }, - { - "name": "Access Public API", - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{API_KEY}}", - "type": "string" - }, - { - "key": "key", - "value": "x-api-key", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{PUBLIC_URL}}", - "host": [ - "{{PUBLIC_URL}}" - ] - } - }, - "response": [] - }, - { - "name": "Complete Dataflow", "event": [ { "listen": "test", @@ -259,7 +146,7 @@ "header": [], "body": { "mode": "raw", - "raw": "", + "raw": "{\n \"processId\": \"{{DATAFLOW_ID}}\",\n \"datasetId\": \"test-asset\",\n \"participantId\": \"{{PARTICIPANT_ID}}\",\n \"agreementId\": \"test-agreement\",\n \"dataAddress\": {\n \"properties\": {\n \"endpointType\": \"https://example.com/natsdp/v1/nats\",\n \"endpoint\": \"nats://nats:4222\",\n \"endpointProperties\": [\n {\n \"key\": \"channel\",\n \"type\": \"string\",\n \"value\": \"{{DATAFLOW_ID}}.forward\"\n },\n {\n \"key\": \"replyChannel\",\n \"type\": \"string\",\n \"value\": \"{{DATAFLOW_ID}}.reply\"\n }\n ]\n },\n \"@context\": {\n \"edc\": \"https://w3id.org/edc/v0.0.1/ns/\"\n },\n \"@type\": \"NatsStream\"\n },\n \"transferType\": {\n \"flowType\": \"pull\",\n \"destinationType\": \"HttpData\"\n }\n}", "options": { "raw": { "language": "json" @@ -267,24 +154,26 @@ } }, "url": { - "raw": "{{DATAPLANE_HOST}}/api/v1/{{PARTICIPANT_ID}}/dataflows/{{DATAFLOW_ID}}/completed", + "raw": "http://localhost:8081/api/v1/{{PARTICIPANT_ID}}/dataflows/{{DATAFLOW_ID}}/started", + "protocol": "http", "host": [ - "{{DATAPLANE_HOST}}" + "localhost" ], + "port": "8081", "path": [ "api", "v1", "{{PARTICIPANT_ID}}", "dataflows", "{{DATAFLOW_ID}}", - "completed" + "started" ] } }, "response": [] }, { - "name": "Terminate Dataflow", + "name": "Provider - Terminate Dataflow", "event": [ { "listen": "test", @@ -338,5 +227,57 @@ }, "response": [] } + ], + "auth": { + "type": "oauth2", + "oauth2": [ + { + "key": "addTokenTo", + "value": "header", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "requests": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "requests": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "PARTICIPANT_ID", + "value": "dataplane-signaling-api" + }, + { + "key": "PROVIDER_HOST", + "value": "http://localhost:8080" + }, + { + "key": "DATAFLOW_ID", + "value": "test-flow-1" + }, + { + "key": "CONSUMER_HOST", + "value": "http://localhost:8081" + } ] } \ No newline at end of file diff --git a/Samples/Streaming/TestRunner/ControlPlaneSimulator.cs b/Samples/Streaming/TestRunner/ControlPlaneSimulator.cs new file mode 100644 index 0000000..b95029f --- /dev/null +++ b/Samples/Streaming/TestRunner/ControlPlaneSimulator.cs @@ -0,0 +1,94 @@ +using System.Net.Http.Json; +using DataPlane.Sdk.Core.Domain.Messages; +using DataPlane.Sdk.Core.Domain.Model; + +namespace TestRunner; + +public class ControlPlaneSimulator +{ + private readonly HttpClient _httpClient = new(); + + public required string ConsumerHost { get; set; } + public required string ProviderHost { get; set; } + public required string ConsumerParticipant { get; set; } + public required string ProviderParticipant { get; set; } + + public async Task PrepareConsumer(string accessToken) + { + var flowId = Guid.NewGuid().ToString(); + var request = new HttpRequestMessage(HttpMethod.Post, $"{ConsumerHost}/api/v1/dataplane-signaling-api/dataflows/prepare"); + request.Headers.Add("Authorization", $"Bearer {accessToken}"); + var body = new DataFlowPrepareMessage + { + ParticipantId = ConsumerParticipant, + ProcessId = flowId, + AgreementId = "test-agreement", + DatasetId = "test-asset", + TransferType = new TransferType + { + DestinationType = "NatsStream", + FlowType = FlowType.Pull + } + }; + + request.Content = JsonContent.Create(body); + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + return flowId; + } + + public async Task StartProvider(string accessToken, string flowId) + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{ProviderHost}/api/v1/{ProviderParticipant}/dataflows/start"); + request.Headers.Add("Authorization", $"Bearer {accessToken}"); + var spRqBody = new DataFlowStartMessage + { + MessageId = Guid.NewGuid().ToString(), + ParticipantId = ProviderParticipant, + CounterPartyId = "consumer", + DataspaceContext = "dataspace-1", + ProcessId = flowId, + AgreementId = "test-agreement", + DatasetId = "test-asset", + CallbackAddress = new Uri("https://example.com/callback"), + TransferType = new TransferType + { + DestinationType = "NatsStream", + FlowType = FlowType.Pull + } + }; + request.Content = JsonContent.Create(spRqBody); + var spResponse = await _httpClient.SendAsync(request); + + spResponse.EnsureSuccessStatusCode(); + return await spResponse.Content.ReadFromJsonAsync(); + } + + public async Task NotifyConsumerStarted(string accessToken, DataAddress providerDa, string flowId) + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{ConsumerHost}/api/v1/{ConsumerParticipant}/dataflows/{flowId}/started"); + request.Headers.Add("Authorization", $"Bearer {accessToken}"); + + var body = new DataFlowStartedNotificationMessage + { + DataAddress = providerDa + }; + request.Content = JsonContent.Create(body); + var response = await _httpClient.SendAsync(request); + return await response.Content.ReadFromJsonAsync(); + } + + public async Task TerminateProvider(string accessToken, string flowId) + { + var request = new HttpRequestMessage(HttpMethod.Post, $"{ConsumerHost}/api/v1/{ConsumerParticipant}/dataflows/{flowId}/terminate"); + request.Headers.Add("Authorization", $"Bearer {accessToken}"); + + var body = new DataFlowTerminateMessage + { + Reason = "normal termination" + }; + request.Content = JsonContent.Create(body); + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } +} \ No newline at end of file diff --git a/Samples/Streaming/TestRunner/EndToEndTest.cs b/Samples/Streaming/TestRunner/EndToEndTest.cs new file mode 100644 index 0000000..39a7df3 --- /dev/null +++ b/Samples/Streaming/TestRunner/EndToEndTest.cs @@ -0,0 +1,87 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using DataPlane.Sdk.Core.Domain.Model; +using Shouldly; + +namespace TestRunner; + +public class EndToEndTest +{ + private const string ConsumerHost = "http://localhost:8081"; + private const string ProviderHost = "http://localhost:8080"; + private const string ParticipantId = "dataplane-signaling-api"; + + private readonly ControlPlaneSimulator _sim = new() + { + ConsumerHost = ConsumerHost, + ProviderHost = ProviderHost, + ConsumerParticipant = ParticipantId, + ProviderParticipant = ParticipantId // same participant id for both for the sake of simplicity + }; + + // same participant id for both for the sake of simplicity + + [Fact] + public async Task StreamingTest() + { + var accessToken = await ObtainAccessToken("dataplane-signaling-api", "mpoTntIrYjsBqhqo0xuzqRUtCWQCWjG3"); + accessToken.ShouldNotBeNull(); + + var client = new HttpClient(); + + // prepare consumer + var flowId = await _sim.PrepareConsumer(accessToken); + + // start provider + var msg = await _sim.StartProvider(accessToken, flowId); + msg.ShouldNotBeNull(); + msg.DataAddress.ShouldNotBeNull(); + msg.DataAddress.Properties.ShouldContainKey("endpoint"); + msg.DataAddress.Properties.ShouldContainKey("endpointProperties"); + + var providerDa = msg.DataAddress; + + // notify consumer started + var response = await _sim.NotifyConsumerStarted(accessToken, providerDa, flowId); + response.ShouldNotBeNull(); + response.DataAddress.ShouldNotBeNull(); + response.State.ShouldBeEquivalentTo(DataFlowState.Started); + + //terminate provider + await _sim.TerminateProvider(accessToken, flowId); + } + + private async Task ObtainAccessToken(string clientId, string clientSecret) + { + var client = new HttpClient(); + client.BaseAddress = new Uri("http://localhost:8088"); + var entries = new List> + { + new("grant_type", "client_credentials"), + new("client_id", clientId), + new("client_secret", clientSecret), + new("scope", "profile email") + }; + var result = await client.PostAsync("/realms/dataplane-signaling-api/protocol/openid-connect/token", new FormUrlEncodedContent(entries)); + + var at = await JsonSerializer.DeserializeAsync(await result.Content.ReadAsStreamAsync()); + + + return at != null ? at.AccessToken : throw new Exception("Failed to obtain access token"); + } + + internal class AccessTokenResponse + { + [JsonPropertyName("access_token")] + public required string AccessToken { get; set; } + + [JsonPropertyName("expires_in")] + public required int ExpiresIn { get; set; } + + [JsonPropertyName("refresh_expires_in")] + public required int RefreshExpiresIn { get; set; } + + [JsonPropertyName("token_type")] + public required string TokenType { get; set; } + } +} \ No newline at end of file diff --git a/Samples/Streaming/TestRunner/TestRunner.csproj b/Samples/Streaming/TestRunner/TestRunner.csproj new file mode 100644 index 0000000..15b4232 --- /dev/null +++ b/Samples/Streaming/TestRunner/TestRunner.csproj @@ -0,0 +1,35 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + + true + true + + + diff --git a/dataplane-sdk-dotnet.sln b/dataplane-sdk-dotnet.sln index 4c3313b..8c6a917 100644 --- a/dataplane-sdk-dotnet.sln +++ b/dataplane-sdk-dotnet.sln @@ -25,12 +25,16 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Streaming", "Streaming", "{9B7B836B-426D-4F48-8467-98E24CE4FCB1}" ProjectSection(SolutionItems) = preProject Samples\Streaming\docker-compose.yaml = Samples\Streaming\docker-compose.yaml + Samples\Streaming\Resources\Postman\StreamingPull.postman_collection.json = Samples\Streaming\Resources\Postman\StreamingPull.postman_collection.json + Samples\Streaming\Resources\Keycloak\dataplane-api-realm.json = Samples\Streaming\Resources\Keycloak\dataplane-api-realm.json EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Provider", "Samples\Streaming\Provider\Provider.csproj", "{C1B9F93E-4499-4506-A654-9B19601D8FA2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Consumer", "Samples\Streaming\Consumer\Consumer.csproj", "{0219D903-F6B5-4E22-BF08-BAFF5EB1895D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestRunner", "Samples\Streaming\TestRunner\TestRunner.csproj", "{83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -137,6 +141,18 @@ Global {0219D903-F6B5-4E22-BF08-BAFF5EB1895D}.Release|x64.Build.0 = Release|Any CPU {0219D903-F6B5-4E22-BF08-BAFF5EB1895D}.Release|x86.ActiveCfg = Release|Any CPU {0219D903-F6B5-4E22-BF08-BAFF5EB1895D}.Release|x86.Build.0 = Release|Any CPU + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}.Debug|x64.ActiveCfg = Debug|Any CPU + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}.Debug|x64.Build.0 = Debug|Any CPU + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}.Debug|x86.ActiveCfg = Debug|Any CPU + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}.Debug|x86.Build.0 = Debug|Any CPU + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}.Release|Any CPU.Build.0 = Release|Any CPU + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}.Release|x64.ActiveCfg = Release|Any CPU + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}.Release|x64.Build.0 = Release|Any CPU + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}.Release|x86.ActiveCfg = Release|Any CPU + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -146,5 +162,6 @@ Global {9B7B836B-426D-4F48-8467-98E24CE4FCB1} = {32055AC6-C4BE-4B7C-A829-F268AB12E577} {C1B9F93E-4499-4506-A654-9B19601D8FA2} = {9B7B836B-426D-4F48-8467-98E24CE4FCB1} {0219D903-F6B5-4E22-BF08-BAFF5EB1895D} = {9B7B836B-426D-4F48-8467-98E24CE4FCB1} + {83F3FC39-4EA3-4DCA-A83C-331FA4CEA1C3} = {9B7B836B-426D-4F48-8467-98E24CE4FCB1} EndGlobalSection EndGlobal From 4bf94941e515d2f8254b87a805fbbd0fac4115fb Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 17 Oct 2025 13:32:18 +0200 Subject: [PATCH 2/3] use async test assertions --- .github/workflows/test.yml | 15 ++++----- Samples/Streaming/TestRunner/EndToEndTest.cs | 31 +++++++++++++------ .../Streaming/TestRunner/TestRunner.csproj | 1 + Samples/Streaming/docker-compose.yaml | 6 ++++ 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d34c26c..067295e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,20 +50,21 @@ jobs: dotnet-version: 9.0.x - uses: docker/setup-compose-action@v1 - - uses: hoverkraft-tech/compose-action@3846bcd61da338e9eaaf83e7ed0234a12b099b72 - with: - compose-file: './Samples/Streaming/docker-compose.yaml' - + + - name: Docker Compose Build env: NUGET_USERNAME: ${{ github.actor }} NUGET_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + run: | + docker compose up --build -d + working-directory: ./Samples/Streaming - name: Execute E2E Tests run: | dotnet restore Samples/Streaming/TestRunner/TestRunner.csproj dotnet test Samples/Streaming/TestRunner/TestRunner.csproj - - name: Cleanup - run: | - docker-compose down -v +# - name: Cleanup +# run: | +# docker-compose down -v diff --git a/Samples/Streaming/TestRunner/EndToEndTest.cs b/Samples/Streaming/TestRunner/EndToEndTest.cs index 39a7df3..427b537 100644 --- a/Samples/Streaming/TestRunner/EndToEndTest.cs +++ b/Samples/Streaming/TestRunner/EndToEndTest.cs @@ -1,6 +1,8 @@ -using System.Text.Json; +using System.Net.Http.Json; using System.Text.Json.Serialization; using DataPlane.Sdk.Core.Domain.Model; +using Polly; +using Polly.Wrap; using Shouldly; namespace TestRunner; @@ -10,6 +12,10 @@ public class EndToEndTest private const string ConsumerHost = "http://localhost:8081"; private const string ProviderHost = "http://localhost:8080"; private const string ParticipantId = "dataplane-signaling-api"; + private const string KeycloakHost = "http://localhost:8088"; + + + private readonly AsyncPolicyWrap _pollyRetry; private readonly ControlPlaneSimulator _sim = new() { @@ -19,18 +25,25 @@ public class EndToEndTest ProviderParticipant = ParticipantId // same participant id for both for the sake of simplicity }; - // same participant id for both for the sake of simplicity + public EndToEndTest() + { + var timeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(5)); + var retry = Policy.Handle().WaitAndRetryForeverAsync(_ => TimeSpan.FromMilliseconds(500)); + _pollyRetry = Policy.WrapAsync(timeout, retry); + } + [Fact] public async Task StreamingTest() { - var accessToken = await ObtainAccessToken("dataplane-signaling-api", "mpoTntIrYjsBqhqo0xuzqRUtCWQCWjG3"); - accessToken.ShouldNotBeNull(); + var accessToken = await _pollyRetry + .ExecuteAsync(async () => await ObtainAccessToken("dataplane-signaling-api", "mpoTntIrYjsBqhqo0xuzqRUtCWQCWjG3")); - var client = new HttpClient(); + accessToken.ShouldNotBeNull(); // prepare consumer var flowId = await _sim.PrepareConsumer(accessToken); + await Task.Delay(1000); // start provider var msg = await _sim.StartProvider(accessToken, flowId); @@ -40,12 +53,14 @@ public async Task StreamingTest() msg.DataAddress.Properties.ShouldContainKey("endpointProperties"); var providerDa = msg.DataAddress; + await Task.Delay(1000); // notify consumer started var response = await _sim.NotifyConsumerStarted(accessToken, providerDa, flowId); response.ShouldNotBeNull(); response.DataAddress.ShouldNotBeNull(); response.State.ShouldBeEquivalentTo(DataFlowState.Started); + await Task.Delay(1000); //terminate provider await _sim.TerminateProvider(accessToken, flowId); @@ -54,7 +69,7 @@ public async Task StreamingTest() private async Task ObtainAccessToken(string clientId, string clientSecret) { var client = new HttpClient(); - client.BaseAddress = new Uri("http://localhost:8088"); + client.BaseAddress = new Uri(KeycloakHost); var entries = new List> { new("grant_type", "client_credentials"), @@ -64,9 +79,7 @@ private async Task ObtainAccessToken(string clientId, string clientSecre }; var result = await client.PostAsync("/realms/dataplane-signaling-api/protocol/openid-connect/token", new FormUrlEncodedContent(entries)); - var at = await JsonSerializer.DeserializeAsync(await result.Content.ReadAsStreamAsync()); - - + var at = await result.Content.ReadFromJsonAsync(); return at != null ? at.AccessToken : throw new Exception("Failed to obtain access token"); } diff --git a/Samples/Streaming/TestRunner/TestRunner.csproj b/Samples/Streaming/TestRunner/TestRunner.csproj index 15b4232..429ae53 100644 --- a/Samples/Streaming/TestRunner/TestRunner.csproj +++ b/Samples/Streaming/TestRunner/TestRunner.csproj @@ -17,6 +17,7 @@ + diff --git a/Samples/Streaming/docker-compose.yaml b/Samples/Streaming/docker-compose.yaml index 6c4a029..485243d 100644 --- a/Samples/Streaming/docker-compose.yaml +++ b/Samples/Streaming/docker-compose.yaml @@ -68,6 +68,12 @@ services: - sample-network volumes: - ./Resources/Keycloak/dataplane-api-realm.json:/opt/keycloak/data/import/realm-export.json + healthcheck: + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8088/health" ] + interval: 10s + timeout: 5s + retries: 3 + start_period: 30s nats: image: nats:latest From 2517bc311c427621b57991a068217d4c67476ea0 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 17 Oct 2025 13:48:32 +0200 Subject: [PATCH 3/3] filter for integration test --- .github/workflows/test.yml | 12 ++++++------ Samples/Streaming/TestRunner/EndToEndTest.cs | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 067295e..08690cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: - name: Build run: dotnet build --no-restore - name: Unit/Integration Tests - run: dotnet test --no-build --verbosity normal + run: dotnet test --no-build --verbosity normal --filter "Category!=Integration" e2e-test: runs-on: ubuntu-latest @@ -62,9 +62,9 @@ jobs: - name: Execute E2E Tests run: | dotnet restore Samples/Streaming/TestRunner/TestRunner.csproj - dotnet test Samples/Streaming/TestRunner/TestRunner.csproj - -# - name: Cleanup -# run: | -# docker-compose down -v + dotnet test Samples/Streaming/TestRunner/TestRunner.csproj --filter "Category=Integration" + + # - name: Cleanup + # run: | + # docker-compose down -v diff --git a/Samples/Streaming/TestRunner/EndToEndTest.cs b/Samples/Streaming/TestRunner/EndToEndTest.cs index 427b537..00443ac 100644 --- a/Samples/Streaming/TestRunner/EndToEndTest.cs +++ b/Samples/Streaming/TestRunner/EndToEndTest.cs @@ -34,6 +34,7 @@ public EndToEndTest() [Fact] + [Trait("Category", "Integration")] public async Task StreamingTest() { var accessToken = await _pollyRetry