diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 752de25fb..4023569e8 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -22,10 +22,10 @@ on: jobs: test: timeout-minutes: 20 - strategy: + strategy: fail-fast: false matrix: - framework: [ net6.0, net7.0, net8.0 ] + framework: [ net8.0, net9.0 ] os: [ ubuntu-latest ] configuration: [ release ] runs-on: ${{ matrix.os }} @@ -33,36 +33,36 @@ jobs: env: CLOUDSMITH_CICD_USER: ${{ secrets.CLOUDSMITH_CICD_USER }} steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Login to Cloudsmith - if: ${{ env.CLOUDSMITH_CICD_USER != '' }} - uses: docker/login-action@v3 - with: - registry: docker.eventstore.com - username: ${{ secrets.CLOUDSMITH_CICD_USER }} - password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} - - name: Pull EventStore Image - shell: bash - run: | - docker pull docker.eventstore.com/${{ inputs.docker-image }}:${{ inputs.docker-tag }} - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Run Tests - shell: bash - env: - ES_DOCKER_TAG: ${{ inputs.docker-tag }} - ES_DOCKER_REGISTRY: docker.eventstore.com/${{ inputs.docker-image }} - run: | - sudo ./gencert.sh - dotnet test --configuration ${{ matrix.configuration }} --blame \ - --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ - --framework ${{ matrix.framework }} \ - test/EventStore.Client.${{ inputs.test }}.Tests + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Login to Cloudsmith + if: ${{ env.CLOUDSMITH_CICD_USER != '' }} + uses: docker/login-action@v3 + with: + registry: docker.eventstore.com + username: ${{ secrets.CLOUDSMITH_CICD_USER }} + password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} + - name: Pull EventStore Image + shell: bash + run: | + docker pull docker.eventstore.com/${{ inputs.docker-image }}:${{ inputs.docker-tag }} + - name: Install dotnet SDKs + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 8.0.x + 9.0.x + - name: Run Tests + shell: bash + env: + ES_DOCKER_TAG: ${{ inputs.docker-tag }} + ES_DOCKER_REGISTRY: docker.eventstore.com/${{ inputs.docker-image }} + run: | + sudo ./gencert.sh + dotnet test --configuration ${{ matrix.configuration }} --blame \ + --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ + --framework ${{ matrix.framework }} \ + --filter "Category=Target:${{ inputs.test }}" \ + test/Kurrent.Client.Tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b8e1f9b1..ec0af3697 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: docker-tag: [ ci, lts, previous-lts ] - test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ] + test: [ Streams, PersistentSubscriptions, Operations, ProjectionManagement, UserManagement, Security, Misc ] name: Test CE (${{ matrix.docker-tag }}) with: docker-tag: ${{ matrix.docker-tag }} diff --git a/.github/workflows/dispatch-ce.yml b/.github/workflows/dispatch-ce.yml index 722baaea7..c05c2402f 100644 --- a/.github/workflows/dispatch-ce.yml +++ b/.github/workflows/dispatch-ce.yml @@ -15,10 +15,10 @@ on: jobs: test: uses: ./.github/workflows/base.yml - strategy: + strategy: fail-fast: false matrix: - test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ] + test: [ Streams, PersistentSubscriptions, Operations, ProjectionManagement, UserManagement, Security, Misc ] name: Test CE (${{ inputs.docker-tag }}) with: docker-tag: ${{ inputs.docker-tag }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 598be309d..ef6dfb6bc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,199 +1,201 @@ -name: Publish - -on: - pull_request: - push: - branches: - - master - tags: - - v* - -jobs: - vulnerability-scan: - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - framework: [ net6.0, net7.0, net8.0 ] - os: [ ubuntu-latest, windows-latest ] - runs-on: ${{ matrix.os }} - name: scan-vulnerabilities/${{ matrix.os }}/${{ matrix.framework }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Scan for Vulnerabilities - shell: bash - run: | - dotnet nuget list source - dotnet restore - dotnet list package --vulnerable --include-transitive --framework ${{ matrix.framework }} | tee vulnerabilities.txt - ! cat vulnerabilities.txt | grep -q "has the following vulnerable packages" - - build-samples: - timeout-minutes: 5 - name: build-samples/${{ matrix.framework }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - framework: [ net8.0 ] - services: - esdb: - image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:lts - env: - EVENTSTORE_INSECURE: true - EVENTSTORE_MEM_DB: false - EVENTSTORE_RUN_PROJECTIONS: all - EVENTSTORE_START_STANDARD_PROJECTIONS: true - ports: - - 2113:2113 - options: --health-cmd "exit 0" - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 8.0.x - - name: Compile - shell: bash - run: | - dotnet build samples - - name: Run - shell: bash - run: | - find samples/ -type f -iname "*.csproj" -print0 | xargs -0L1 dotnet run --framework ${{ matrix.framework }} --project - - generate-certificates: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Generate certificates - run: | - mkdir -p certs - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-ca -out /tmp/ca - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin - docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid - - name: Set permissions on certificates - run: | - sudo chown -R $USER:$USER certs - sudo chmod -R 755 certs - - name: Upload certificates - uses: actions/upload-artifact@v4 - with: - name: certs - path: certs - - test: - needs: generate-certificates - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - framework: [ net6.0, net7.0, net8.0 ] - os: [ ubuntu-latest, windows-latest ] - configuration: [ release ] - runs-on: ${{ matrix.os }} - name: test/EventStore.Client/${{ matrix.os }}/${{ matrix.framework }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - shell: bash - run: | - git fetch --prune --unshallow - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Compile - shell: bash - run: | - dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/EventStore.Client - - name: Download certificates - uses: actions/download-artifact@v4 - with: - name: certs - path: certs - - name: Run Tests (Linux) - if: runner.os == 'Linux' - shell: bash - run: | - dotnet test --configuration ${{ matrix.configuration }} --blame \ - --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ - --framework ${{ matrix.framework }} \ - test/EventStore.Client.Tests - - name: Run Tests (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - dotnet test --configuration ${{ matrix.configuration }} --blame ` - --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" ` - --framework ${{ matrix.framework }} ` - test/EventStore.Client.Tests - - publish: - timeout-minutes: 5 - needs: [ vulnerability-scan, test, build-samples ] - runs-on: ubuntu-latest - name: publish - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Get Version - id: get_version - run: | - echo "branch=${GITHUB_REF:10}" >> $GITHUB_OUTPUT - dotnet nuget list source - dotnet tool restore - version=$(dotnet tool run minver -- --tag-prefix=v) - echo "version=${version}" >> $GITHUB_OUTPUT - - shell: bash - run: | - git fetch --prune --unshallow - - name: Install dotnet SDKs - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Dotnet Pack - shell: bash - run: | - mkdir -p packages - dotnet pack /p:Version=${{ steps.get_version.outputs.version }} --configuration=Release \ - /p:PublishDir=./packages \ - /p:NoWarn=NU5105 \ - /p:RepositoryUrl=https://github.com/EventStore/EventStore-Client-Dotnet \ - /p:RepositoryType=git - - name: Publish Artifacts - uses: actions/upload-artifact@v4 - with: - path: packages - name: nuget-packages - - name: Dotnet Push to Github Packages - shell: bash - if: github.event_name == 'push' - run: | - dotnet tool restore - find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.github_token }} --source https://nuget.pkg.github.com/EventStore/index.json --skip-duplicate - - name: Dotnet Push to Nuget.org - shell: bash - if: contains(steps.get_version.outputs.branch, 'v') - run: | - dotnet nuget list source - dotnet tool restore - find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.nuget_key }} --source https://api.nuget.org/v3/index.json --skip-duplicate +#name: Publish +# +#on: +# pull_request: +# push: +# branches: +# - master +# tags: +# - v* +# +#jobs: +# vulnerability-scan: +# timeout-minutes: 10 +# strategy: +# fail-fast: false +# matrix: +# framework: [ net8.0, net9.0 ] +# os: [ ubuntu-latest, windows-latest ] +# runs-on: ${{ matrix.os }} +# name: scan-vulnerabilities/${{ matrix.os }}/${{ matrix.framework }} +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# 9.0.x +# - name: Scan for Vulnerabilities +# shell: bash +# run: | +# dotnet nuget list source +# dotnet restore ./src/Kurrent.Client/Kurrent.Client.csproj +# dotnet restore ./test/Kurrent.Client.Tests/Kurrent.Client.Tests.csproj +# dotnet list package --vulnerable --include-transitive --framework ${{ matrix.framework }} | tee vulnerabilities.txt +# ! cat vulnerabilities.txt | grep -q "has the following vulnerable packages" +# +# build-samples: +# timeout-minutes: 5 +# name: build-samples/${{ matrix.framework }} +# runs-on: ubuntu-latest +# strategy: +# fail-fast: false +# matrix: +# framework: [ net8.0, net9.0 ] +# services: +# esdb: +# image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:lts +# env: +# EVENTSTORE_INSECURE: true +# EVENTSTORE_MEM_DB: false +# EVENTSTORE_RUN_PROJECTIONS: all +# EVENTSTORE_START_STANDARD_PROJECTIONS: true +# ports: +# - 2113:2113 +# options: --health-cmd "exit 0" +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# 9.0.x +# - name: Compile +# shell: bash +# run: | +# dotnet build samples +# - name: Run +# shell: bash +# run: | +# find samples/ -type f -iname "*.csproj" -print0 | xargs -0L1 dotnet run --framework ${{ matrix.framework }} --project +# +# generate-certificates: +# runs-on: ubuntu-latest +# steps: +# - name: Checkout code +# uses: actions/checkout@v4 +# - name: Generate certificates +# run: | +# mkdir -p certs +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-ca -out /tmp/ca +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin +# docker run --rm --user root --volume "$PWD/certs:/tmp" docker.eventstore.com/eventstore-utils/es-gencert-cli:latest create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid +# - name: Set permissions on certificates +# run: | +# sudo chown -R $USER:$USER certs +# sudo chmod -R 755 certs +# - name: Upload certificates +# uses: actions/upload-artifact@v4 +# with: +# name: certs +# path: certs +# +# test: +# needs: generate-certificates +# timeout-minutes: 10 +# strategy: +# fail-fast: false +# matrix: +# framework: [ net8.0, net9.0 ] +# os: [ ubuntu-latest, windows-latest ] +# configuration: [ release ] +# test: [ Streams, PersistentSubscriptions, Operations, ProjectionManagement, UserManagement, Security, Misc ] +# runs-on: ${{ matrix.os }} +# name: ${{ matrix.test }} (${{ matrix.os }}, ${{ matrix.framework }}) +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - shell: bash +# run: | +# git fetch --prune --unshallow +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# 9.0.x +# - name: Compile +# shell: bash +# run: | +# dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/Kurrent.Client +# - name: Download certificates +# uses: actions/download-artifact@v4 +# with: +# name: certs +# path: certs +# - name: Run Tests (Linux) +# if: runner.os == 'Linux' +# shell: bash +# run: | +# dotnet test --configuration ${{ matrix.configuration }} --blame \ +# --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" \ +# --framework ${{ matrix.framework }} \ +# --filter "Category=Target:${{ matrix.test }}" \ +# test/Kurrent.Client.Tests +# - name: Run Tests (Windows) +# if: runner.os == 'Windows' +# shell: pwsh +# run: | +# dotnet test --configuration ${{ matrix.configuration }} --blame ` +# --logger:"GitHubActions;report-warnings=false" --logger:"console;verbosity=normal" ` +# --framework ${{ matrix.framework }} ` +# --filter "Category=Target:${{ matrix.test }}" ` +# test/Kurrent.Client.Tests +# +# publish: +# timeout-minutes: 5 +# needs: [ vulnerability-scan, test, build-samples ] +# runs-on: ubuntu-latest +# name: publish +# steps: +# - name: Checkout +# uses: actions/checkout@v3 +# - name: Get Version +# id: get_version +# run: | +# echo "branch=${GITHUB_REF:10}" >> $GITHUB_OUTPUT +# dotnet nuget list source +# dotnet tool restore +# version=$(dotnet tool run minver -- --tag-prefix=v) +# echo "version=${version}" >> $GITHUB_OUTPUT +# - shell: bash +# run: | +# git fetch --prune --unshallow +# - name: Install dotnet SDKs +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: | +# 8.0.x +# 9.0.x +# - name: Dotnet Pack +# shell: bash +# run: | +# mkdir -p packages +# dotnet pack /p:Version=${{ steps.get_version.outputs.version }} --configuration=Release \ +# /p:PublishDir=./packages \ +# /p:NoWarn=NU5105 \ +# /p:RepositoryUrl=https://github.com/EventStore/EventStore-Client-Dotnet \ +# /p:RepositoryType=git +# - name: Publish Artifacts +# uses: actions/upload-artifact@v4 +# with: +# path: packages +# name: nuget-packages +# - name: Dotnet Push to Github Packages +# shell: bash +# if: github.event_name == 'push' +# run: | +# dotnet tool restore +# find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.github_token }} --source https://nuget.pkg.github.com/EventStore/index.json --skip-duplicate +# - name: Dotnet Push to Nuget.org +# shell: bash +# if: contains(steps.get_version.outputs.branch, 'v') +# run: | +# dotnet nuget list source +# dotnet tool restore +# find . -name "*.nupkg" | xargs -n1 dotnet nuget push --api-key=${{ secrets.nuget_key }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/Directory.Build.props b/Directory.Build.props index dd2dd9339..a22d9632a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,11 +1,10 @@ - net48;net6.0;net7.0;net8.0 + net48;net8.0;net9.0 true enable enable true - true preview Debug @@ -13,8 +12,6 @@ pdbonly true - 2.60.0 - 2.60.0 diff --git a/EventStore.Client.sln b/EventStore.Client.sln deleted file mode 100644 index 72ec92f54..000000000 --- a/EventStore.Client.sln +++ /dev/null @@ -1,127 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 -MinimumVisualStudioVersion = 15.0.26124.0 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EA59C1CB-16DA-4F68-AF8A-642A969B4CF8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.UserManagement", "src\EventStore.Client.UserManagement\EventStore.Client.UserManagement.csproj", "{D3744A86-DD35-4104-AAEE-84B79062C4A2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Streams", "src\EventStore.Client.Streams\EventStore.Client.Streams.csproj", "{362C0CF9-81C9-400F-94C7-A8B190ECE94A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Operations", "src\EventStore.Client.Operations\EventStore.Client.Operations.csproj", "{6EFB980F-C72C-40F1-9232-479BE5617580}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.ProjectionManagement", "src\EventStore.Client.ProjectionManagement\EventStore.Client.ProjectionManagement.csproj", "{B275EF4B-80D3-4205-9C7A-337A2A2BD99C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.PersistentSubscriptions", "src\EventStore.Client.PersistentSubscriptions\EventStore.Client.PersistentSubscriptions.csproj", "{66FDEE78-836E-49F0-A4DD-BF7539C34E54}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client", "src\EventStore.Client\EventStore.Client.csproj", "{8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C51F2C69-45A9-4D0D-A708-4FC319D5D340}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.ProjectionManagement.Tests", "test\EventStore.Client.ProjectionManagement.Tests\EventStore.Client.ProjectionManagement.Tests.csproj", "{8F8548D6-694C-4BAE-9EF3-A020140E04C7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Operations.Tests", "test\EventStore.Client.Operations.Tests\EventStore.Client.Operations.Tests.csproj", "{4BA2E05E-6B45-47C3-9001-8B039244ECA9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Streams.Tests", "test\EventStore.Client.Streams.Tests\EventStore.Client.Streams.Tests.csproj", "{082C77F5-4FF5-41D4-A1F1-710F05956E1C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Tests", "test\EventStore.Client.Tests\EventStore.Client.Tests.csproj", "{FC829F1B-43AD-4C96-9002-23D04BBA3AF3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.PersistentSubscriptions.Tests", "test\EventStore.Client.PersistentSubscriptions.Tests\EventStore.Client.PersistentSubscriptions.Tests.csproj", "{6CEB731F-72E1-461F-A6B3-54DBF3FD786C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.UserManagement.Tests", "test\EventStore.Client.UserManagement.Tests\EventStore.Client.UserManagement.Tests.csproj", "{22634CEE-4F7B-4679-A48D-38A2A8580ECA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Tests.Common", "test\EventStore.Client.Tests.Common\EventStore.Client.Tests.Common.csproj", "{E326832D-DE52-4DE4-9E54-C800908B75F3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Extensions.OpenTelemetry", "src\EventStore.Client.Extensions.OpenTelemetry\EventStore.Client.Extensions.OpenTelemetry.csproj", "{3723933C-585A-49BE-98E8-52D3FAD904CE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Plugins.Tests", "test\EventStore.Client.Plugins.Tests\EventStore.Client.Plugins.Tests.csproj", "{7D929D45-F1D9-462B-BE49-84BEC11D5039}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D3744A86-DD35-4104-AAEE-84B79062C4A2}.Debug|x64.ActiveCfg = Debug|Any CPU - {D3744A86-DD35-4104-AAEE-84B79062C4A2}.Debug|x64.Build.0 = Debug|Any CPU - {D3744A86-DD35-4104-AAEE-84B79062C4A2}.Release|x64.ActiveCfg = Release|Any CPU - {D3744A86-DD35-4104-AAEE-84B79062C4A2}.Release|x64.Build.0 = Release|Any CPU - {362C0CF9-81C9-400F-94C7-A8B190ECE94A}.Debug|x64.ActiveCfg = Debug|Any CPU - {362C0CF9-81C9-400F-94C7-A8B190ECE94A}.Debug|x64.Build.0 = Debug|Any CPU - {362C0CF9-81C9-400F-94C7-A8B190ECE94A}.Release|x64.ActiveCfg = Release|Any CPU - {362C0CF9-81C9-400F-94C7-A8B190ECE94A}.Release|x64.Build.0 = Release|Any CPU - {6EFB980F-C72C-40F1-9232-479BE5617580}.Debug|x64.ActiveCfg = Debug|Any CPU - {6EFB980F-C72C-40F1-9232-479BE5617580}.Debug|x64.Build.0 = Debug|Any CPU - {6EFB980F-C72C-40F1-9232-479BE5617580}.Release|x64.ActiveCfg = Release|Any CPU - {6EFB980F-C72C-40F1-9232-479BE5617580}.Release|x64.Build.0 = Release|Any CPU - {B275EF4B-80D3-4205-9C7A-337A2A2BD99C}.Debug|x64.ActiveCfg = Debug|Any CPU - {B275EF4B-80D3-4205-9C7A-337A2A2BD99C}.Debug|x64.Build.0 = Debug|Any CPU - {B275EF4B-80D3-4205-9C7A-337A2A2BD99C}.Release|x64.ActiveCfg = Release|Any CPU - {B275EF4B-80D3-4205-9C7A-337A2A2BD99C}.Release|x64.Build.0 = Release|Any CPU - {66FDEE78-836E-49F0-A4DD-BF7539C34E54}.Debug|x64.ActiveCfg = Debug|Any CPU - {66FDEE78-836E-49F0-A4DD-BF7539C34E54}.Debug|x64.Build.0 = Debug|Any CPU - {66FDEE78-836E-49F0-A4DD-BF7539C34E54}.Release|x64.ActiveCfg = Release|Any CPU - {66FDEE78-836E-49F0-A4DD-BF7539C34E54}.Release|x64.Build.0 = Release|Any CPU - {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Debug|x64.ActiveCfg = Debug|Any CPU - {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Debug|x64.Build.0 = Debug|Any CPU - {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Release|x64.ActiveCfg = Release|Any CPU - {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3}.Release|x64.Build.0 = Release|Any CPU - {8F8548D6-694C-4BAE-9EF3-A020140E04C7}.Debug|x64.ActiveCfg = Debug|Any CPU - {8F8548D6-694C-4BAE-9EF3-A020140E04C7}.Debug|x64.Build.0 = Debug|Any CPU - {8F8548D6-694C-4BAE-9EF3-A020140E04C7}.Release|x64.ActiveCfg = Release|Any CPU - {8F8548D6-694C-4BAE-9EF3-A020140E04C7}.Release|x64.Build.0 = Release|Any CPU - {4BA2E05E-6B45-47C3-9001-8B039244ECA9}.Debug|x64.ActiveCfg = Debug|Any CPU - {4BA2E05E-6B45-47C3-9001-8B039244ECA9}.Debug|x64.Build.0 = Debug|Any CPU - {4BA2E05E-6B45-47C3-9001-8B039244ECA9}.Release|x64.ActiveCfg = Release|Any CPU - {4BA2E05E-6B45-47C3-9001-8B039244ECA9}.Release|x64.Build.0 = Release|Any CPU - {082C77F5-4FF5-41D4-A1F1-710F05956E1C}.Debug|x64.ActiveCfg = Debug|Any CPU - {082C77F5-4FF5-41D4-A1F1-710F05956E1C}.Debug|x64.Build.0 = Debug|Any CPU - {082C77F5-4FF5-41D4-A1F1-710F05956E1C}.Release|x64.ActiveCfg = Release|Any CPU - {082C77F5-4FF5-41D4-A1F1-710F05956E1C}.Release|x64.Build.0 = Release|Any CPU - {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Debug|x64.ActiveCfg = Debug|Any CPU - {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Debug|x64.Build.0 = Debug|Any CPU - {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Release|x64.ActiveCfg = Release|Any CPU - {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Release|x64.Build.0 = Release|Any CPU - {6CEB731F-72E1-461F-A6B3-54DBF3FD786C}.Debug|x64.ActiveCfg = Debug|Any CPU - {6CEB731F-72E1-461F-A6B3-54DBF3FD786C}.Debug|x64.Build.0 = Debug|Any CPU - {6CEB731F-72E1-461F-A6B3-54DBF3FD786C}.Release|x64.ActiveCfg = Release|Any CPU - {6CEB731F-72E1-461F-A6B3-54DBF3FD786C}.Release|x64.Build.0 = Release|Any CPU - {22634CEE-4F7B-4679-A48D-38A2A8580ECA}.Debug|x64.ActiveCfg = Debug|Any CPU - {22634CEE-4F7B-4679-A48D-38A2A8580ECA}.Debug|x64.Build.0 = Debug|Any CPU - {22634CEE-4F7B-4679-A48D-38A2A8580ECA}.Release|x64.ActiveCfg = Release|Any CPU - {22634CEE-4F7B-4679-A48D-38A2A8580ECA}.Release|x64.Build.0 = Release|Any CPU - {E326832D-DE52-4DE4-9E54-C800908B75F3}.Debug|x64.ActiveCfg = Debug|Any CPU - {E326832D-DE52-4DE4-9E54-C800908B75F3}.Debug|x64.Build.0 = Debug|Any CPU - {E326832D-DE52-4DE4-9E54-C800908B75F3}.Release|x64.ActiveCfg = Release|Any CPU - {E326832D-DE52-4DE4-9E54-C800908B75F3}.Release|x64.Build.0 = Release|Any CPU - {3723933C-585A-49BE-98E8-52D3FAD904CE}.Debug|x64.ActiveCfg = Debug|Any CPU - {3723933C-585A-49BE-98E8-52D3FAD904CE}.Debug|x64.Build.0 = Debug|Any CPU - {3723933C-585A-49BE-98E8-52D3FAD904CE}.Release|x64.ActiveCfg = Release|Any CPU - {3723933C-585A-49BE-98E8-52D3FAD904CE}.Release|x64.Build.0 = Release|Any CPU - {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Debug|x64.ActiveCfg = Debug|Any CPU - {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Debug|x64.Build.0 = Debug|Any CPU - {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Release|x64.ActiveCfg = Release|Any CPU - {7D929D45-F1D9-462B-BE49-84BEC11D5039}.Release|x64.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {D3744A86-DD35-4104-AAEE-84B79062C4A2} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {362C0CF9-81C9-400F-94C7-A8B190ECE94A} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {6EFB980F-C72C-40F1-9232-479BE5617580} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {B275EF4B-80D3-4205-9C7A-337A2A2BD99C} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {66FDEE78-836E-49F0-A4DD-BF7539C34E54} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {8853D875-4A8E-450B-A1BE-9CEF8BEDABC3} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {8F8548D6-694C-4BAE-9EF3-A020140E04C7} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {4BA2E05E-6B45-47C3-9001-8B039244ECA9} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {082C77F5-4FF5-41D4-A1F1-710F05956E1C} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {FC829F1B-43AD-4C96-9002-23D04BBA3AF3} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {6CEB731F-72E1-461F-A6B3-54DBF3FD786C} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {22634CEE-4F7B-4679-A48D-38A2A8580ECA} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {E326832D-DE52-4DE4-9E54-C800908B75F3} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - {3723933C-585A-49BE-98E8-52D3FAD904CE} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} - {7D929D45-F1D9-462B-BE49-84BEC11D5039} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} - EndGlobalSection -EndGlobal diff --git a/Kurrent.Client.sln b/Kurrent.Client.sln new file mode 100644 index 000000000..1ea14ce6f --- /dev/null +++ b/Kurrent.Client.sln @@ -0,0 +1,57 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EA59C1CB-16DA-4F68-AF8A-642A969B4CF8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C51F2C69-45A9-4D0D-A708-4FC319D5D340}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client.Tests", "test\Kurrent.Client.Tests\Kurrent.Client.Tests.csproj", "{FC829F1B-43AD-4C96-9002-23D04BBA3AF3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client", "src\Kurrent.Client\Kurrent.Client.csproj", "{762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client.Tests.Common", "test\Kurrent.Client.Tests.Common\Kurrent.Client.Tests.Common.csproj", "{47BF715B-A0BF-4044-B335-717E56422550}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client.Tests.NeverLoadedAssembly", "test\Kurrent.Client.Tests.NeverLoadedAssembly\Kurrent.Client.Tests.NeverLoadedAssembly.csproj", "{0AC8A7E9-6839-4B4C-B299-950C376DF71F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client.Tests.ExternalAssembly", "test\Kurrent.Client.Tests.ExternalAssembly\Kurrent.Client.Tests.ExternalAssembly.csproj", "{829AF806-1144-408A-85FE-763835775086}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Debug|x64.ActiveCfg = Debug|Any CPU + {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Debug|x64.Build.0 = Debug|Any CPU + {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Release|x64.ActiveCfg = Release|Any CPU + {FC829F1B-43AD-4C96-9002-23D04BBA3AF3}.Release|x64.Build.0 = Release|Any CPU + {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}.Debug|x64.ActiveCfg = Debug|Any CPU + {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}.Debug|x64.Build.0 = Debug|Any CPU + {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}.Release|x64.ActiveCfg = Release|Any CPU + {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0}.Release|x64.Build.0 = Release|Any CPU + {47BF715B-A0BF-4044-B335-717E56422550}.Debug|x64.ActiveCfg = Debug|Any CPU + {47BF715B-A0BF-4044-B335-717E56422550}.Debug|x64.Build.0 = Debug|Any CPU + {47BF715B-A0BF-4044-B335-717E56422550}.Release|x64.ActiveCfg = Release|Any CPU + {47BF715B-A0BF-4044-B335-717E56422550}.Release|x64.Build.0 = Release|Any CPU + {0AC8A7E9-6839-4B4C-B299-950C376DF71F}.Debug|x64.ActiveCfg = Debug|Any CPU + {0AC8A7E9-6839-4B4C-B299-950C376DF71F}.Debug|x64.Build.0 = Debug|Any CPU + {0AC8A7E9-6839-4B4C-B299-950C376DF71F}.Release|x64.ActiveCfg = Release|Any CPU + {0AC8A7E9-6839-4B4C-B299-950C376DF71F}.Release|x64.Build.0 = Release|Any CPU + {829AF806-1144-408A-85FE-763835775086}.Debug|x64.ActiveCfg = Debug|Any CPU + {829AF806-1144-408A-85FE-763835775086}.Debug|x64.Build.0 = Debug|Any CPU + {829AF806-1144-408A-85FE-763835775086}.Release|x64.ActiveCfg = Release|Any CPU + {829AF806-1144-408A-85FE-763835775086}.Release|x64.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {FC829F1B-43AD-4C96-9002-23D04BBA3AF3} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} + {762EECAA-122E-4B0C-BC50-5AA4F72CA4E0} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} + {47BF715B-A0BF-4044-B335-717E56422550} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} + {0AC8A7E9-6839-4B4C-B299-950C376DF71F} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} + {829AF806-1144-408A-85FE-763835775086} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} + EndGlobalSection +EndGlobal diff --git a/EventStore.Client.sln.DotSettings b/Kurrent.Client.sln.DotSettings similarity index 99% rename from EventStore.Client.sln.DotSettings rename to Kurrent.Client.sln.DotSettings index f97c72463..e12ac8bcf 100644 --- a/EventStore.Client.sln.DotSettings +++ b/Kurrent.Client.sln.DotSettings @@ -390,6 +390,7 @@ True True True + True True True True diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index 54497a508..1ab3aed15 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -1,6 +1,6 @@ - net8.0 + net8.0;net9.0 enable enable true @@ -12,4 +12,4 @@ - \ No newline at end of file + diff --git a/samples/Samples.sln b/samples/Samples.sln index 79c979342..ec2611d7b 100644 --- a/samples/Samples.sln +++ b/samples/Samples.sln @@ -7,10 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "quick-start", "quick-start\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "client", "client", "{EBB93BBC-42A7-48E4-B1EA-0EA3953D347C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Client.Streams", "..\src\EventStore.Client.Streams\EventStore.Client.Streams.csproj", "{A32CE3CB-AB71-45C6-A1B2-CD94BE2D2B28}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Client", "..\src\EventStore.Client\EventStore.Client.csproj", "{A71A13F7-8480-4E48-B88D-A2364F7C95B6}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "connecting-to-a-cluster", "connecting-to-a-cluster\connecting-to-a-cluster.csproj", "{C4CA324A-450D-4621-82F1-B4ECD18216B6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "appending-events", "appending-events\appending-events.csproj", "{496D9886-AF65-4579-AECE-2C147B9AF3C1}" @@ -29,17 +25,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "server-side-filtering", "se EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "persistent-subscriptions", "persistent-subscriptions\persistent-subscriptions.csproj", "{A5A5EF0D-1AE4-4647-823D-FA172E8858F2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Client.PersistentSubscriptions", "..\src\EventStore.Client.PersistentSubscriptions\EventStore.Client.PersistentSubscriptions.csproj", "{7200BB01-A405-45D5-A6E8-A8FA8DE39DA0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventStore.Client.ProjectionManagement", "..\src\EventStore.Client.ProjectionManagement\EventStore.Client.ProjectionManagement.csproj", "{79992D7B-C311-4E8A-856F-896C1EA61042}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "projection-management", "projection-management\projection-management.csproj", "{9DEA2684-C38B-465C-91A6-ED2AB67A4338}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "diagnostics", "diagnostics\diagnostics.csproj", "{546496AD-E355-4C20-930C-30D0FC76D26F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "user-certificates", "user-certificates\user-certificates.csproj", "{28112410-D02D-427A-9D36-3FE3A6DC6B0D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Extensions.OpenTelemetry", "..\src\EventStore.Client.Extensions.OpenTelemetry\EventStore.Client.Extensions.OpenTelemetry.csproj", "{29E3F07A-6676-45C1-805C-46BDF6CF325B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kurrent.Client", "..\src\Kurrent.Client\Kurrent.Client.csproj", "{16F61817-6E46-4F52-879F-E4B92A6A90C6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -51,14 +43,6 @@ Global {521987E5-4394-4EE0-B217-E3DC9DB0D327}.Debug|Any CPU.Build.0 = Debug|Any CPU {521987E5-4394-4EE0-B217-E3DC9DB0D327}.Release|Any CPU.ActiveCfg = Release|Any CPU {521987E5-4394-4EE0-B217-E3DC9DB0D327}.Release|Any CPU.Build.0 = Release|Any CPU - {A32CE3CB-AB71-45C6-A1B2-CD94BE2D2B28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A32CE3CB-AB71-45C6-A1B2-CD94BE2D2B28}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A32CE3CB-AB71-45C6-A1B2-CD94BE2D2B28}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A32CE3CB-AB71-45C6-A1B2-CD94BE2D2B28}.Release|Any CPU.Build.0 = Release|Any CPU - {A71A13F7-8480-4E48-B88D-A2364F7C95B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A71A13F7-8480-4E48-B88D-A2364F7C95B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A71A13F7-8480-4E48-B88D-A2364F7C95B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A71A13F7-8480-4E48-B88D-A2364F7C95B6}.Release|Any CPU.Build.0 = Release|Any CPU {C4CA324A-450D-4621-82F1-B4ECD18216B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C4CA324A-450D-4621-82F1-B4ECD18216B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {C4CA324A-450D-4621-82F1-B4ECD18216B6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -95,14 +79,6 @@ Global {A5A5EF0D-1AE4-4647-823D-FA172E8858F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5A5EF0D-1AE4-4647-823D-FA172E8858F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5A5EF0D-1AE4-4647-823D-FA172E8858F2}.Release|Any CPU.Build.0 = Release|Any CPU - {7200BB01-A405-45D5-A6E8-A8FA8DE39DA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7200BB01-A405-45D5-A6E8-A8FA8DE39DA0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7200BB01-A405-45D5-A6E8-A8FA8DE39DA0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7200BB01-A405-45D5-A6E8-A8FA8DE39DA0}.Release|Any CPU.Build.0 = Release|Any CPU - {79992D7B-C311-4E8A-856F-896C1EA61042}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {79992D7B-C311-4E8A-856F-896C1EA61042}.Debug|Any CPU.Build.0 = Debug|Any CPU - {79992D7B-C311-4E8A-856F-896C1EA61042}.Release|Any CPU.ActiveCfg = Release|Any CPU - {79992D7B-C311-4E8A-856F-896C1EA61042}.Release|Any CPU.Build.0 = Release|Any CPU {9DEA2684-C38B-465C-91A6-ED2AB67A4338}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9DEA2684-C38B-465C-91A6-ED2AB67A4338}.Debug|Any CPU.Build.0 = Debug|Any CPU {9DEA2684-C38B-465C-91A6-ED2AB67A4338}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -115,20 +91,16 @@ Global {28112410-D02D-427A-9D36-3FE3A6DC6B0D}.Debug|Any CPU.Build.0 = Debug|Any CPU {28112410-D02D-427A-9D36-3FE3A6DC6B0D}.Release|Any CPU.ActiveCfg = Release|Any CPU {28112410-D02D-427A-9D36-3FE3A6DC6B0D}.Release|Any CPU.Build.0 = Release|Any CPU - {29E3F07A-6676-45C1-805C-46BDF6CF325B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {29E3F07A-6676-45C1-805C-46BDF6CF325B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {29E3F07A-6676-45C1-805C-46BDF6CF325B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {29E3F07A-6676-45C1-805C-46BDF6CF325B}.Release|Any CPU.Build.0 = Release|Any CPU + {16F61817-6E46-4F52-879F-E4B92A6A90C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16F61817-6E46-4F52-879F-E4B92A6A90C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16F61817-6E46-4F52-879F-E4B92A6A90C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16F61817-6E46-4F52-879F-E4B92A6A90C6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {A32CE3CB-AB71-45C6-A1B2-CD94BE2D2B28} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} - {A71A13F7-8480-4E48-B88D-A2364F7C95B6} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} - {7200BB01-A405-45D5-A6E8-A8FA8DE39DA0} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} - {79992D7B-C311-4E8A-856F-896C1EA61042} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} - {29E3F07A-6676-45C1-805C-46BDF6CF325B} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} + {16F61817-6E46-4F52-879F-E4B92A6A90C6} = {EBB93BBC-42A7-48E4-B1EA-0EA3953D347C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2E7E3A6C-21DA-4DBD-9EB3-68DFC5CDE48F} diff --git a/samples/Samples.sln.DotSettings b/samples/Samples.sln.DotSettings index c341e4095..06c105615 100644 --- a/samples/Samples.sln.DotSettings +++ b/samples/Samples.sln.DotSettings @@ -390,6 +390,7 @@ True True True + True True True True diff --git a/samples/appending-events/Program.cs b/samples/appending-events/Program.cs index 2e1bb758f..98f12fb1a 100644 --- a/samples/appending-events/Program.cs +++ b/samples/appending-events/Program.cs @@ -1,10 +1,10 @@ #pragma warning disable CS8321 // Local function is declared but never used -var settings = EventStoreClientSettings.Create("esdb://localhost:2113?tls=false"); +var settings = KurrentClientSettings.Create("esdb://localhost:2113?tls=false"); settings.OperationOptions.ThrowOnAppendFailure = false; -await using var client = new EventStoreClient(settings); +await using var client = new KurrentClient(settings); await AppendToStream(client); await AppendWithConcurrencyCheck(client); @@ -13,7 +13,7 @@ return; -static async Task AppendToStream(EventStoreClient client) { +static async Task AppendToStream(KurrentClient client) { #region append-to-stream var eventData = new EventData( @@ -33,7 +33,7 @@ await client.AppendToStreamAsync( #endregion append-to-stream } -static async Task AppendWithSameId(EventStoreClient client) { +static async Task AppendWithSameId(KurrentClient client) { #region append-duplicate-event var eventData = new EventData( @@ -62,7 +62,7 @@ await client.AppendToStreamAsync( #endregion append-duplicate-event } -static async Task AppendWithNoStream(EventStoreClient client) { +static async Task AppendWithNoStream(KurrentClient client) { #region append-with-no-stream var eventDataOne = new EventData( @@ -97,7 +97,7 @@ await client.AppendToStreamAsync( #endregion append-with-no-stream } -static async Task AppendWithConcurrencyCheck(EventStoreClient client) { +static async Task AppendWithConcurrencyCheck(KurrentClient client) { await client.AppendToStreamAsync( "concurrency-stream", StreamRevision.None, @@ -148,7 +148,7 @@ await client.AppendToStreamAsync( #endregion append-with-concurrency-check } -static async Task AppendOverridingUserCredentials(EventStoreClient client, CancellationToken cancellationToken) { +static async Task AppendOverridingUserCredentials(KurrentClient client, CancellationToken cancellationToken) { var eventData = new EventData( Uuid.NewUuid(), "TestEvent", diff --git a/samples/appending-events/appending-events.csproj b/samples/appending-events/appending-events.csproj index fb93d800e..c74c70ae9 100644 --- a/samples/appending-events/appending-events.csproj +++ b/samples/appending-events/appending-events.csproj @@ -3,11 +3,7 @@ Exe appending_events - - - - - + diff --git a/samples/connecting-to-a-cluster/Program.cs b/samples/connecting-to-a-cluster/Program.cs index a31666fc6..01b033c6c 100644 --- a/samples/connecting-to-a-cluster/Program.cs +++ b/samples/connecting-to-a-cluster/Program.cs @@ -1,10 +1,12 @@ -#pragma warning disable CS8321 // Local function is declared but never used +using EventStore.Client; + +#pragma warning disable CS8321 // Local function is declared but never used static void ConnectingToACluster() { #region connecting-to-a-cluster - using var client = new EventStoreClient( - EventStoreClientSettings.Create("esdb://localhost:1114,localhost:2114,localhost:3114") + using var client = new KurrentClient( + KurrentClientSettings.Create("esdb://localhost:1114,localhost:2114,localhost:3114") ); #endregion connecting-to-a-cluster @@ -13,8 +15,8 @@ static void ConnectingToACluster() { static void ProvidingDefaultCredentials() { #region providing-default-credentials - using var client = new EventStoreClient( - EventStoreClientSettings.Create("esdb://admin:changeit@localhost:1114,localhost:2114,localhost:3114") + using var client = new KurrentClient( + KurrentClientSettings.Create("esdb://admin:changeit@localhost:1114,localhost:2114,localhost:3114") ); #endregion providing-default-credentials @@ -23,11 +25,11 @@ static void ProvidingDefaultCredentials() { static void ConnectingToAClusterComplex() { #region connecting-to-a-cluster-complex - using var client = new EventStoreClient( - EventStoreClientSettings.Create( + using var client = new KurrentClient( + KurrentClientSettings.Create( "esdb://admin:changeit@localhost:1114,localhost:2114,localhost:3114?DiscoveryInterval=30000;GossipTimeout=10000;NodePreference=leader;MaxDiscoverAttempts=5" ) ); #endregion connecting-to-a-cluster-complex -} \ No newline at end of file +} diff --git a/samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj b/samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj index 4754bd0e2..f7304d3f3 100644 --- a/samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj +++ b/samples/connecting-to-a-cluster/connecting-to-a-cluster.csproj @@ -3,11 +3,7 @@ Exe connecting_to_a_cluster - - - - - + diff --git a/samples/connecting-to-a-single-node/Program.cs b/samples/connecting-to-a-single-node/Program.cs index f50e1689f..63d0b3e98 100644 --- a/samples/connecting-to-a-single-node/Program.cs +++ b/samples/connecting-to-a-single-node/Program.cs @@ -1,11 +1,12 @@ using connecting_to_a_single_node; +using EventStore.Client; #pragma warning disable CS8321 // Local function is declared but never used static void SimpleConnection() { #region creating-simple-connection - using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113")); + using var client = new KurrentClient(KurrentClientSettings.Create("esdb://localhost:2113")); #endregion creating-simple-connection } @@ -13,7 +14,7 @@ static void SimpleConnection() { static void ProvidingDefaultCredentials() { #region providing-default-credentials - using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://admin:changeit@localhost:2113")); + using var client = new KurrentClient(KurrentClientSettings.Create("esdb://admin:changeit@localhost:2113")); #endregion providing-default-credentials } @@ -21,8 +22,8 @@ static void ProvidingDefaultCredentials() { static void SpecifyingAConnectionName() { #region setting-the-connection-name - using var client = new EventStoreClient( - EventStoreClientSettings.Create("esdb://admin:changeit@localhost:2113?ConnectionName=SomeConnection") + using var client = new KurrentClient( + KurrentClientSettings.Create("esdb://admin:changeit@localhost:2113?ConnectionName=SomeConnection") ); #endregion setting-the-connection-name @@ -31,8 +32,8 @@ static void SpecifyingAConnectionName() { static void OverridingTheTimeout() { #region overriding-timeout - using var client = new EventStoreClient( - EventStoreClientSettings.Create($"esdb://admin:changeit@localhost:2113?OperationTimeout=30000") + using var client = new KurrentClient( + KurrentClientSettings.Create($"esdb://admin:changeit@localhost:2113?OperationTimeout=30000") ); #endregion overriding-timeout @@ -41,8 +42,8 @@ static void OverridingTheTimeout() { static void CombiningSettings() { #region overriding-timeout - using var client = new EventStoreClient( - EventStoreClientSettings.Create( + using var client = new KurrentClient( + KurrentClientSettings.Create( $"esdb://admin:changeit@localhost:2113?ConnectionName=SomeConnection&OperationTimeout=30000" ) ); @@ -53,7 +54,7 @@ static void CombiningSettings() { static void CreatingAnInterceptor() { #region adding-an-interceptor - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { Interceptors = new[] { new DemoInterceptor() }, ConnectivitySettings = { Address = new Uri("https://localhost:2113") @@ -62,13 +63,13 @@ static void CreatingAnInterceptor() { #endregion adding-an-interceptor - var client = new EventStoreClient(settings); + var client = new KurrentClient(settings); } static void CustomHttpMessageHandler() { #region adding-an-custom-http-message-handler - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { CreateHttpMessageHandler = () => new HttpClientHandler { ServerCertificateCustomValidationCallback = @@ -81,5 +82,5 @@ static void CustomHttpMessageHandler() { #endregion adding-an-custom-http-message-handler - var client = new EventStoreClient(settings); -} \ No newline at end of file + var client = new KurrentClient(settings); +} diff --git a/samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj b/samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj index 4ef794ee4..97609c80a 100644 --- a/samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj +++ b/samples/connecting-to-a-single-node/connecting-to-a-single-node.csproj @@ -2,11 +2,7 @@ Exe - - - - - + diff --git a/samples/diagnostics/Program.cs b/samples/diagnostics/Program.cs index 5804be7a4..86f9a6e3a 100644 --- a/samples/diagnostics/Program.cs +++ b/samples/diagnostics/Program.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using EventStore.Client; using EventStore.Client.Extensions.OpenTelemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -23,17 +24,17 @@ dotnet add package OpenTelemetry.Extensions.Hosting # endregion import-required-packages **/ -var settings = EventStoreClientSettings.Create("esdb://localhost:2113?tls=false"); +var settings = KurrentClientSettings.Create("esdb://localhost:2113?tls=false"); settings.OperationOptions.ThrowOnAppendFailure = false; -await using var client = new EventStoreClient(settings); +await using var client = new KurrentClient(settings); await TraceAppendToStream(client); return; -static async Task TraceAppendToStream(EventStoreClient client) { +static async Task TraceAppendToStream(KurrentClient client) { const string serviceName = "sample"; var host = Host.CreateDefaultBuilder() @@ -76,7 +77,7 @@ static void ConfigureTracerProviderBuilder(TracerProviderBuilder tracerProviderB #region register-instrumentation tracerProviderBuilder - .AddEventStoreClientInstrumentation(); + .AddKurrentClientInstrumentation(); #endregion register-instrumentation diff --git a/samples/diagnostics/diagnostics.csproj b/samples/diagnostics/diagnostics.csproj index 1763a9d9e..56bd7c49c 100644 --- a/samples/diagnostics/diagnostics.csproj +++ b/samples/diagnostics/diagnostics.csproj @@ -5,18 +5,14 @@ - - - + + + - + - - - - - + diff --git a/samples/persistent-subscriptions/Program.cs b/samples/persistent-subscriptions/Program.cs index e1e9387c6..6d07b7947 100644 --- a/samples/persistent-subscriptions/Program.cs +++ b/samples/persistent-subscriptions/Program.cs @@ -1,5 +1,5 @@ -await using var client = new EventStorePersistentSubscriptionsClient( - EventStoreClientSettings.Create("esdb://localhost:2113?tls=false&tlsVerifyCert=false") +await using var client = new KurrentPersistentSubscriptionsClient( + KurrentClientSettings.Create("esdb://localhost:2113?tls=false&tlsVerifyCert=false") ); await DeletePersistentSubscription(client); @@ -36,7 +36,7 @@ return; -static async Task CreatePersistentSubscription(EventStorePersistentSubscriptionsClient client) { +static async Task CreatePersistentSubscription(KurrentPersistentSubscriptionsClient client) { #region create-persistent-subscription-to-stream var userCredentials = new UserCredentials("admin", "changeit"); @@ -54,7 +54,7 @@ await client.CreateToStreamAsync( #endregion create-persistent-subscription-to-stream } -static async Task ConnectToPersistentSubscriptionToStream(EventStorePersistentSubscriptionsClient client, +static async Task ConnectToPersistentSubscriptionToStream(KurrentPersistentSubscriptionsClient client, CancellationToken ct) { #region subscribe-to-persistent-subscription-to-stream @@ -77,7 +77,7 @@ static async Task ConnectToPersistentSubscriptionToStream(EventStorePersistentSu #endregion subscribe-to-persistent-subscription-to-stream } -static async Task CreatePersistentSubscriptionToAll(EventStorePersistentSubscriptionsClient client) { +static async Task CreatePersistentSubscriptionToAll(KurrentPersistentSubscriptionsClient client) { #region create-persistent-subscription-to-all var userCredentials = new UserCredentials("admin", "changeit"); @@ -96,7 +96,7 @@ await client.CreateToAllAsync( #endregion create-persistent-subscription-to-all } -static async Task ConnectToPersistentSubscriptionToAll(EventStorePersistentSubscriptionsClient client, +static async Task ConnectToPersistentSubscriptionToAll(KurrentPersistentSubscriptionsClient client, CancellationToken ct) { #region subscribe-to-persistent-subscription-to-all @@ -118,7 +118,7 @@ static async Task ConnectToPersistentSubscriptionToAll(EventStorePersistentSubsc #endregion subscribe-to-persistent-subscription-to-all } -static async Task ConnectToPersistentSubscriptionWithManualAcks(EventStorePersistentSubscriptionsClient client, +static async Task ConnectToPersistentSubscriptionWithManualAcks(KurrentPersistentSubscriptionsClient client, CancellationToken ct) { #region subscribe-to-persistent-subscription-with-manual-acks @@ -145,7 +145,7 @@ static async Task ConnectToPersistentSubscriptionWithManualAcks(EventStorePersis #endregion subscribe-to-persistent-subscription-with-manual-acks } -static async Task UpdatePersistentSubscription(EventStorePersistentSubscriptionsClient client) { +static async Task UpdatePersistentSubscription(KurrentPersistentSubscriptionsClient client) { #region update-persistent-subscription var userCredentials = new UserCredentials("admin", "changeit"); @@ -163,7 +163,7 @@ await client.UpdateToStreamAsync( #endregion update-persistent-subscription } -static async Task DeletePersistentSubscription(EventStorePersistentSubscriptionsClient client) { +static async Task DeletePersistentSubscription(KurrentPersistentSubscriptionsClient client) { #region delete-persistent-subscription try { @@ -184,7 +184,7 @@ await client.DeleteToStreamAsync( #endregion delete-persistent-subscription } -static async Task DeletePersistentSubscriptionToAll(EventStorePersistentSubscriptionsClient client) { +static async Task DeletePersistentSubscriptionToAll(KurrentPersistentSubscriptionsClient client) { #region delete-persistent-subscription-to-all try { @@ -204,7 +204,7 @@ await client.DeleteToAllAsync( #endregion delete-persistent-subscription-to-all } -static async Task GetPersistentSubscriptionToStreamInfo(EventStorePersistentSubscriptionsClient client) { +static async Task GetPersistentSubscriptionToStreamInfo(KurrentPersistentSubscriptionsClient client) { #region get-persistent-subscription-to-stream-info var userCredentials = new UserCredentials("admin", "changeit"); @@ -219,7 +219,7 @@ static async Task GetPersistentSubscriptionToStreamInfo(EventStorePersistentSubs #endregion get-persistent-subscription-to-stream-info } -static async Task GetPersistentSubscriptionToAllInfo(EventStorePersistentSubscriptionsClient client) { +static async Task GetPersistentSubscriptionToAllInfo(KurrentPersistentSubscriptionsClient client) { #region get-persistent-subscription-to-all-info var userCredentials = new UserCredentials("admin", "changeit"); @@ -233,7 +233,7 @@ static async Task GetPersistentSubscriptionToAllInfo(EventStorePersistentSubscri #endregion get-persistent-subscription-to-all-info } -static async Task ReplayParkedToStream(EventStorePersistentSubscriptionsClient client) { +static async Task ReplayParkedToStream(KurrentPersistentSubscriptionsClient client) { #region replay-parked-of-persistent-subscription-to-stream var userCredentials = new UserCredentials("admin", "changeit"); @@ -249,7 +249,7 @@ await client.ReplayParkedMessagesToStreamAsync( #endregion persistent-subscription-replay-parked-to-stream } -static async Task ReplayParkedToAll(EventStorePersistentSubscriptionsClient client) { +static async Task ReplayParkedToAll(KurrentPersistentSubscriptionsClient client) { #region replay-parked-of-persistent-subscription-to-all var userCredentials = new UserCredentials("admin", "changeit"); @@ -264,7 +264,7 @@ await client.ReplayParkedMessagesToAllAsync( #endregion replay-parked-of-persistent-subscription-to-all } -static async Task ListPersistentSubscriptionsToStream(EventStorePersistentSubscriptionsClient client) { +static async Task ListPersistentSubscriptionsToStream(KurrentPersistentSubscriptionsClient client) { #region list-persistent-subscriptions-to-stream var userCredentials = new UserCredentials("admin", "changeit"); @@ -281,7 +281,7 @@ static async Task ListPersistentSubscriptionsToStream(EventStorePersistentSubscr #endregion list-persistent-subscriptions-to-stream } -static async Task ListPersistentSubscriptionsToAll(EventStorePersistentSubscriptionsClient client) { +static async Task ListPersistentSubscriptionsToAll(KurrentPersistentSubscriptionsClient client) { #region list-persistent-subscriptions-to-all var userCredentials = new UserCredentials("admin", "changeit"); @@ -295,7 +295,7 @@ static async Task ListPersistentSubscriptionsToAll(EventStorePersistentSubscript #endregion list-persistent-subscriptions-to-all } -static async Task ListAllPersistentSubscriptions(EventStorePersistentSubscriptionsClient client) { +static async Task ListAllPersistentSubscriptions(KurrentPersistentSubscriptionsClient client) { #region list-persistent-subscriptions var userCredentials = new UserCredentials("admin", "changeit"); @@ -309,7 +309,7 @@ static async Task ListAllPersistentSubscriptions(EventStorePersistentSubscriptio #endregion list-persistent-subscriptions } -static async Task RestartPersistentSubscriptionSubsystem(EventStorePersistentSubscriptionsClient client) { +static async Task RestartPersistentSubscriptionSubsystem(KurrentPersistentSubscriptionsClient client) { #region restart-persistent-subscription-subsystem var userCredentials = new UserCredentials("admin", "changeit"); diff --git a/samples/persistent-subscriptions/persistent-subscriptions.csproj b/samples/persistent-subscriptions/persistent-subscriptions.csproj index e845bda8e..e296fca91 100644 --- a/samples/persistent-subscriptions/persistent-subscriptions.csproj +++ b/samples/persistent-subscriptions/persistent-subscriptions.csproj @@ -3,10 +3,7 @@ Exe persistent_subscriptions - - - - + diff --git a/samples/projection-management/Program.cs b/samples/projection-management/Program.cs index 0f3d66b70..478b88b10 100644 --- a/samples/projection-management/Program.cs +++ b/samples/projection-management/Program.cs @@ -64,20 +64,20 @@ return; -static EventStoreProjectionManagementClient ManagementClient(string connection) { +static KurrentProjectionManagementClient ManagementClient(string connection) { #region createClient - var settings = EventStoreClientSettings.Create(connection); + var settings = KurrentClientSettings.Create(connection); settings.ConnectionName = "Projection management client"; settings.DefaultCredentials = new UserCredentials("admin", "changeit"); - var managementClient = new EventStoreProjectionManagementClient(settings); + var managementClient = new KurrentProjectionManagementClient(settings); #endregion createClient return managementClient; } -static async Task RestartSubSystem(EventStoreProjectionManagementClient managementClient) { +static async Task RestartSubSystem(KurrentProjectionManagementClient managementClient) { #region RestartSubSystem await managementClient.RestartSubsystemAsync(); @@ -85,7 +85,7 @@ static async Task RestartSubSystem(EventStoreProjectionManagementClient manageme #endregion RestartSubSystem } -static async Task Disable(EventStoreProjectionManagementClient managementClient) { +static async Task Disable(KurrentProjectionManagementClient managementClient) { #region Disable await managementClient.DisableAsync("$by_category"); @@ -93,7 +93,7 @@ static async Task Disable(EventStoreProjectionManagementClient managementClient) #endregion Disable } -static async Task DisableNotFound(EventStoreProjectionManagementClient managementClient) { +static async Task DisableNotFound(KurrentProjectionManagementClient managementClient) { #region DisableNotFound try { @@ -109,7 +109,7 @@ static async Task DisableNotFound(EventStoreProjectionManagementClient managemen #endregion DisableNotFound } -static async Task Enable(EventStoreProjectionManagementClient managementClient) { +static async Task Enable(KurrentProjectionManagementClient managementClient) { #region Enable await managementClient.EnableAsync("$by_category"); @@ -117,7 +117,7 @@ static async Task Enable(EventStoreProjectionManagementClient managementClient) #endregion Enable } -static async Task EnableNotFound(EventStoreProjectionManagementClient managementClient) { +static async Task EnableNotFound(KurrentProjectionManagementClient managementClient) { #region EnableNotFound try { @@ -133,7 +133,7 @@ static async Task EnableNotFound(EventStoreProjectionManagementClient management #endregion EnableNotFound } -static Task Delete(EventStoreProjectionManagementClient managementClient) { +static Task Delete(KurrentProjectionManagementClient managementClient) { #region Delete // this is not yet available in the .net grpc client @@ -143,7 +143,7 @@ static Task Delete(EventStoreProjectionManagementClient managementClient) { return Task.CompletedTask; } -static async Task Abort(EventStoreProjectionManagementClient managementClient) { +static async Task Abort(KurrentProjectionManagementClient managementClient) { try { var js = "fromAll() .when({$init:function(){return {count:0};},$any:function(s, e){s.count += 1;}}).outputState();"; @@ -165,7 +165,7 @@ static async Task Abort(EventStoreProjectionManagementClient managementClient) { #endregion Abort } -static async Task Abort_NotFound(EventStoreProjectionManagementClient managementClient) { +static async Task Abort_NotFound(KurrentProjectionManagementClient managementClient) { #region Abort_NotFound try { @@ -181,7 +181,7 @@ static async Task Abort_NotFound(EventStoreProjectionManagementClient management #endregion Abort_NotFound } -static async Task Reset(EventStoreProjectionManagementClient managementClient) { +static async Task Reset(KurrentProjectionManagementClient managementClient) { try { var js = "fromAll() .when({$init:function(){return {count:0};},$any:function(s, e){s.count += 1;}}).outputState();"; @@ -203,7 +203,7 @@ static async Task Reset(EventStoreProjectionManagementClient managementClient) { #endregion Reset } -static async Task Reset_NotFound(EventStoreProjectionManagementClient managementClient) { +static async Task Reset_NotFound(KurrentProjectionManagementClient managementClient) { #region Reset_NotFound try { @@ -219,14 +219,14 @@ static async Task Reset_NotFound(EventStoreProjectionManagementClient management #endregion Reset_NotFound } -static async Task CreateOneTime(EventStoreProjectionManagementClient managementClient) { +static async Task CreateOneTime(KurrentProjectionManagementClient managementClient) { const string js = "fromAll() .when({$init:function(){return {count:0};},$any:function(s, e){s.count += 1;}}).outputState();"; await managementClient.CreateOneTimeAsync(js); } -static async Task CreateContinuous(EventStoreProjectionManagementClient managementClient) { +static async Task CreateContinuous(KurrentProjectionManagementClient managementClient) { #region CreateContinuous const string js = @"fromAll() @@ -248,7 +248,7 @@ static async Task CreateContinuous(EventStoreProjectionManagementClient manageme #endregion CreateContinuous } -static async Task CreateContinuous_Conflict(EventStoreProjectionManagementClient managementClient) { +static async Task CreateContinuous_Conflict(KurrentProjectionManagementClient managementClient) { const string js = @"fromAll() .when({ $init: function() { @@ -281,7 +281,7 @@ static async Task CreateContinuous_Conflict(EventStoreProjectionManagementClient #endregion CreateContinuous_Conflict } -static async Task Update(EventStoreProjectionManagementClient managementClient) { +static async Task Update(KurrentProjectionManagementClient managementClient) { #region Update const string js = @"fromAll() @@ -305,7 +305,7 @@ static async Task Update(EventStoreProjectionManagementClient managementClient) #endregion Update } -static async Task Update_NotFound(EventStoreProjectionManagementClient managementClient) { +static async Task Update_NotFound(KurrentProjectionManagementClient managementClient) { #region Update_NotFound try { @@ -321,7 +321,7 @@ static async Task Update_NotFound(EventStoreProjectionManagementClient managemen #endregion Update_NotFound } -static async Task ListAll(EventStoreProjectionManagementClient managementClient) { +static async Task ListAll(KurrentProjectionManagementClient managementClient) { #region ListAll var details = managementClient.ListAllAsync(); @@ -333,7 +333,7 @@ static async Task ListAll(EventStoreProjectionManagementClient managementClient) #endregion ListAll } -static async Task ListContinuous(EventStoreProjectionManagementClient managementClient) { +static async Task ListContinuous(KurrentProjectionManagementClient managementClient) { #region ListContinuous var details = managementClient.ListContinuousAsync(); @@ -345,7 +345,7 @@ static async Task ListContinuous(EventStoreProjectionManagementClient management #endregion ListContinuous } -static async Task GetStatus(EventStoreProjectionManagementClient managementClient) { +static async Task GetStatus(KurrentProjectionManagementClient managementClient) { const string js = "fromAll().when({$init:function(){return {count:0};},$any:function(s, e){s.count += 1;}}).outputState();"; @@ -362,7 +362,7 @@ static async Task GetStatus(EventStoreProjectionManagementClient managementClien #endregion GetStatus } -static async Task GetState(EventStoreProjectionManagementClient managementClient) { +static async Task GetState(KurrentProjectionManagementClient managementClient) { // will have to wait for the client to be fixed before we import in the doc #region GetState @@ -393,7 +393,7 @@ static async Task DocToString(JsonDocument d) { #endregion GetState } -static async Task GetResult(EventStoreProjectionManagementClient managementClient) { +static async Task GetResult(KurrentProjectionManagementClient managementClient) { #region GetResult const string js = @"fromAll() @@ -433,9 +433,9 @@ static string DocToString(JsonDocument d) { } static async Task Populate(string connection, int numberOfEvents) { - var settings = EventStoreClientSettings.Create(connection); + var settings = KurrentClientSettings.Create(connection); settings.DefaultCredentials = new UserCredentials("admin", "changeit"); - var client = new EventStoreClient(settings); + var client = new KurrentClient(settings); var messages = Enumerable.Range(0, numberOfEvents).Select( number => new EventData( @@ -452,4 +452,4 @@ public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; -}; \ No newline at end of file +}; diff --git a/samples/projection-management/projection-management.csproj b/samples/projection-management/projection-management.csproj index 3b5b2c3c7..f6a6206aa 100644 --- a/samples/projection-management/projection-management.csproj +++ b/samples/projection-management/projection-management.csproj @@ -2,9 +2,7 @@ projection_management - - - + diff --git a/samples/quick-start/Program.cs b/samples/quick-start/Program.cs index 0cd070137..dd763ae42 100644 --- a/samples/quick-start/Program.cs +++ b/samples/quick-start/Program.cs @@ -7,9 +7,9 @@ const string connectionString = "esdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false"; -var settings = EventStoreClientSettings.Create(connectionString); +var settings = KurrentClientSettings.Create(connectionString); -var client = new EventStoreClient(settings); +var client = new KurrentClient(settings); #endregion createClient @@ -67,4 +67,4 @@ await client.AppendToStreamAsync( public class TestEvent { public string? EntityId { get; set; } public string? ImportantData { get; set; } -} \ No newline at end of file +} diff --git a/samples/quick-start/quick-start.csproj b/samples/quick-start/quick-start.csproj index b33948026..5fe35463c 100644 --- a/samples/quick-start/quick-start.csproj +++ b/samples/quick-start/quick-start.csproj @@ -2,11 +2,7 @@ quick_start - - - - - + diff --git a/samples/reading-events/Program.cs b/samples/reading-events/Program.cs index 854634da8..751086d5c 100644 --- a/samples/reading-events/Program.cs +++ b/samples/reading-events/Program.cs @@ -1,6 +1,6 @@ #pragma warning disable CS8321 // Local function is declared but never used -await using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113?tls=false")); +await using var client = new KurrentClient(KurrentClientSettings.Create("esdb://localhost:2113?tls=false")); var events = Enumerable.Range(0, 20) .Select( @@ -21,7 +21,7 @@ await client.AppendToStreamAsync( return; -static async Task ReadFromStream(EventStoreClient client) { +static async Task ReadFromStream(KurrentClient client) { #region read-from-stream var events = client.ReadStreamAsync( @@ -46,7 +46,7 @@ static async Task ReadFromStream(EventStoreClient client) { #endregion } -static async Task ReadFromStreamMessages(EventStoreClient client) { +static async Task ReadFromStreamMessages(KurrentClient client) { #region read-from-stream-messages var streamPosition = StreamPosition.Start; @@ -90,7 +90,7 @@ static async Task ReadFromStreamMessages(EventStoreClient client) { #endregion iterate-stream-messages } -static async Task ReadFromStreamPosition(EventStoreClient client) { +static async Task ReadFromStreamPosition(KurrentClient client) { #region read-from-stream-position var events = client.ReadStreamAsync( @@ -109,7 +109,7 @@ static async Task ReadFromStreamPosition(EventStoreClient client) { #endregion iterate-stream } -static async Task ReadFromStreamPositionCheck(EventStoreClient client) { +static async Task ReadFromStreamPositionCheck(KurrentClient client) { #region checking-for-stream-presence var result = client.ReadStreamAsync( @@ -126,7 +126,7 @@ static async Task ReadFromStreamPositionCheck(EventStoreClient client) { #endregion checking-for-stream-presence } -static async Task ReadFromStreamBackwards(EventStoreClient client) { +static async Task ReadFromStreamBackwards(KurrentClient client) { #region reading-backwards var events = client.ReadStreamAsync( @@ -140,7 +140,7 @@ static async Task ReadFromStreamBackwards(EventStoreClient client) { #endregion reading-backwards } -static async Task ReadFromStreamMessagesBackwards(EventStoreClient client) { +static async Task ReadFromStreamMessagesBackwards(KurrentClient client) { #region read-from-stream-messages-backwards var results = client.ReadStreamAsync( @@ -175,7 +175,7 @@ static async Task ReadFromStreamMessagesBackwards(EventStoreClient client) { #endregion iterate-stream-messages-backwards } -static async Task ReadFromAllStream(EventStoreClient client) { +static async Task ReadFromAllStream(KurrentClient client) { #region read-from-all-stream var events = client.ReadAllAsync(Direction.Forwards, Position.Start); @@ -189,7 +189,7 @@ static async Task ReadFromAllStream(EventStoreClient client) { #endregion read-from-all-stream-iterate } -static async Task ReadFromAllStreamMessages(EventStoreClient client) { +static async Task ReadFromAllStreamMessages(KurrentClient client) { #region read-from-all-stream-messages var position = Position.Start; @@ -216,7 +216,7 @@ static async Task ReadFromAllStreamMessages(EventStoreClient client) { #endregion iterate-all-stream-messages } -static async Task IgnoreSystemEvents(EventStoreClient client) { +static async Task IgnoreSystemEvents(KurrentClient client) { #region ignore-system-events var events = client.ReadAllAsync(Direction.Forwards, Position.Start); @@ -230,7 +230,7 @@ static async Task IgnoreSystemEvents(EventStoreClient client) { #endregion ignore-system-events } -static async Task ReadFromAllStreamBackwards(EventStoreClient client) { +static async Task ReadFromAllStreamBackwards(KurrentClient client) { #region read-from-all-stream-backwards var events = client.ReadAllAsync(Direction.Backwards, Position.End); @@ -244,7 +244,7 @@ static async Task ReadFromAllStreamBackwards(EventStoreClient client) { #endregion read-from-all-stream-iterate } -static async Task ReadFromAllStreamBackwardsMessages(EventStoreClient client) { +static async Task ReadFromAllStreamBackwardsMessages(KurrentClient client) { #region read-from-all-stream-messages-backwards var position = Position.End; @@ -272,7 +272,7 @@ static async Task ReadFromAllStreamBackwardsMessages(EventStoreClient client) { #endregion iterate-all-stream-messages-backwards } -static async Task FilteringOutSystemEvents(EventStoreClient client) { +static async Task FilteringOutSystemEvents(KurrentClient client) { var events = client.ReadAllAsync(Direction.Forwards, Position.Start); await foreach (var e in events) @@ -280,7 +280,7 @@ static async Task FilteringOutSystemEvents(EventStoreClient client) { Console.WriteLine(Encoding.UTF8.GetString(e.Event.Data.ToArray())); } -static void ReadStreamOverridingUserCredentials(EventStoreClient client, CancellationToken cancellationToken) { +static void ReadStreamOverridingUserCredentials(KurrentClient client, CancellationToken cancellationToken) { #region overriding-user-credentials var result = client.ReadStreamAsync( @@ -294,7 +294,7 @@ static void ReadStreamOverridingUserCredentials(EventStoreClient client, Cancell #endregion overriding-user-credentials } -static void ReadAllOverridingUserCredentials(EventStoreClient client, CancellationToken cancellationToken) { +static void ReadAllOverridingUserCredentials(KurrentClient client, CancellationToken cancellationToken) { #region read-all-overriding-user-credentials var result = client.ReadAllAsync( @@ -307,7 +307,7 @@ static void ReadAllOverridingUserCredentials(EventStoreClient client, Cancellati #endregion read-all-overriding-user-credentials } -static void ReadAllResolvingLinkTos(EventStoreClient client, CancellationToken cancellationToken) { +static void ReadAllResolvingLinkTos(KurrentClient client, CancellationToken cancellationToken) { #region read-from-all-stream-resolving-link-Tos var result = client.ReadAllAsync( @@ -318,4 +318,4 @@ static void ReadAllResolvingLinkTos(EventStoreClient client, CancellationToken c ); #endregion read-from-all-stream-resolving-link-Tos -} \ No newline at end of file +} diff --git a/samples/reading-events/reading-events.csproj b/samples/reading-events/reading-events.csproj index 436254ea7..d79ede9fc 100644 --- a/samples/reading-events/reading-events.csproj +++ b/samples/reading-events/reading-events.csproj @@ -2,10 +2,7 @@ reading_events - - - - + - \ No newline at end of file + diff --git a/samples/secure-with-tls/Program.cs b/samples/secure-with-tls/Program.cs index 621ee46b1..b2dcd05bb 100644 --- a/samples/secure-with-tls/Program.cs +++ b/samples/secure-with-tls/Program.cs @@ -6,7 +6,7 @@ Console.WriteLine($"Connecting to EventStoreDB at: {connectionString}"); -await using var client = new EventStoreClient(EventStoreClientSettings.Create(connectionString)); +await using var client = new KurrentClient(KurrentClientSettings.Create(connectionString)); var eventData = new EventData(Uuid.NewUuid(), "some-event", "{\"id\": \"1\" \"value\": \"some value\"}"u8.ToArray()); @@ -48,4 +48,4 @@ } Console.WriteLine($"FAILED! {exception}"); -} \ No newline at end of file +} diff --git a/samples/secure-with-tls/secure-with-tls.csproj b/samples/secure-with-tls/secure-with-tls.csproj index 7dd4b7c03..a11ffdb55 100644 --- a/samples/secure-with-tls/secure-with-tls.csproj +++ b/samples/secure-with-tls/secure-with-tls.csproj @@ -14,8 +14,6 @@ - - - + diff --git a/samples/server-side-filtering/Program.cs b/samples/server-side-filtering/Program.cs index b9e5de8c8..b8641905a 100644 --- a/samples/server-side-filtering/Program.cs +++ b/samples/server-side-filtering/Program.cs @@ -6,7 +6,7 @@ var semaphore = new SemaphoreSlim(eventCount); -await using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113?tls=false")); +await using var client = new KurrentClient(KurrentClientSettings.Create("esdb://localhost:2113?tls=false")); _ = Task.Run(async () => { await using var subscription = client.SubscribeToAll( @@ -46,7 +46,7 @@ await client.AppendToStreamAsync( return; -static async Task ExcludeSystemEvents(EventStoreClient client) { +static async Task ExcludeSystemEvents(KurrentClient client) { #region exclude-system await using var subscription = client.SubscribeToAll( @@ -63,7 +63,7 @@ static async Task ExcludeSystemEvents(EventStoreClient client) { #endregion exclude-system } -static async Task EventTypePrefix(EventStoreClient client) { +static async Task EventTypePrefix(KurrentClient client) { #region event-type-prefix var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); @@ -80,7 +80,7 @@ static async Task EventTypePrefix(EventStoreClient client) { } } -static async Task EventTypeRegex(EventStoreClient client) { +static async Task EventTypeRegex(KurrentClient client) { #region event-type-regex var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.RegularExpression("^user|^company")); @@ -97,7 +97,7 @@ static async Task EventTypeRegex(EventStoreClient client) { } } -static async Task StreamPrefix(EventStoreClient client) { +static async Task StreamPrefix(KurrentClient client) { #region stream-prefix var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix("user-")); @@ -114,7 +114,7 @@ static async Task StreamPrefix(EventStoreClient client) { } } -static async Task StreamRegex(EventStoreClient client) { +static async Task StreamRegex(KurrentClient client) { #region stream-regex var filterOptions = new SubscriptionFilterOptions(StreamFilter.RegularExpression("^account|^savings")); @@ -131,7 +131,7 @@ static async Task StreamRegex(EventStoreClient client) { } } -static async Task CheckpointCallback(EventStoreClient client) { +static async Task CheckpointCallback(KurrentClient client) { #region checkpoint var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()); @@ -151,7 +151,7 @@ static async Task CheckpointCallback(EventStoreClient client) { #endregion checkpoint } -static async Task CheckpointCallbackWithInterval(EventStoreClient client) { +static async Task CheckpointCallbackWithInterval(KurrentClient client) { #region checkpoint-with-interval var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents(), 1000); diff --git a/samples/server-side-filtering/server-side-filtering.csproj b/samples/server-side-filtering/server-side-filtering.csproj index b69b90670..f3a5ffc58 100644 --- a/samples/server-side-filtering/server-side-filtering.csproj +++ b/samples/server-side-filtering/server-side-filtering.csproj @@ -2,11 +2,7 @@ server_side_filtering - - - - - + - \ No newline at end of file + diff --git a/samples/setting-up-dependency-injection/Controllers/EventStoreController.cs b/samples/setting-up-dependency-injection/Controllers/EventStoreController.cs index d4c3f4d59..f66c237ce 100644 --- a/samples/setting-up-dependency-injection/Controllers/EventStoreController.cs +++ b/samples/setting-up-dependency-injection/Controllers/EventStoreController.cs @@ -5,15 +5,15 @@ namespace setting_up_dependency_injection.Controllers { [Route("[controller]")] public class EventStoreController : ControllerBase { #region using-dependency - private readonly EventStoreClient _eventStoreClient; + private readonly KurrentClient _KurrentClient; - public EventStoreController(EventStoreClient eventStoreClient) { - _eventStoreClient = eventStoreClient; + public EventStoreController(KurrentClient KurrentClient) { + _KurrentClient = KurrentClient; } [HttpGet] public IAsyncEnumerable Get() { - return _eventStoreClient.ReadAllAsync(Direction.Forwards, Position.Start); + return _KurrentClient.ReadAllAsync(Direction.Forwards, Position.Start); } #endregion using-dependency } diff --git a/samples/setting-up-dependency-injection/Startup.cs b/samples/setting-up-dependency-injection/Startup.cs index a1d618aff..cb8c9fb94 100644 --- a/samples/setting-up-dependency-injection/Startup.cs +++ b/samples/setting-up-dependency-injection/Startup.cs @@ -10,7 +10,7 @@ public void ConfigureServices(IServiceCollection services) { #region setting-up-dependency - services.AddEventStoreClient("esdb://admin:changeit@localhost:2113?tls=false"); + services.AddKurrentClient("esdb://admin:changeit@localhost:2113?tls=false"); #endregion setting-up-dependency } @@ -22,4 +22,4 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } -} \ No newline at end of file +} diff --git a/samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj b/samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj index 0e2adfd05..e4f581a89 100644 --- a/samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj +++ b/samples/setting-up-dependency-injection/setting-up-dependency-injection.csproj @@ -4,9 +4,6 @@ - - - - + - \ No newline at end of file + diff --git a/samples/subscribing-to-streams/Program.cs b/samples/subscribing-to-streams/Program.cs index 90fa29737..01990a17e 100644 --- a/samples/subscribing-to-streams/Program.cs +++ b/samples/subscribing-to-streams/Program.cs @@ -3,7 +3,7 @@ // ReSharper disable UnusedParameter.Local // ReSharper disable UnusedVariable -await using var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113?tls=false")); +await using var client = new KurrentClient(KurrentClientSettings.Create("esdb://localhost:2113?tls=false")); await Task.WhenAll(YieldSamples().Select(async sample => { try { @@ -27,7 +27,7 @@ IEnumerable YieldSamples() { yield return OverridingUserCredentials(client, GetCT()); } -static async Task SubscribeToStreamFromPosition(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToStreamFromPosition(KurrentClient client, CancellationToken ct) { #region subscribe-to-stream-from-position await using var subscription = client.SubscribeToStream( @@ -46,7 +46,7 @@ static async Task SubscribeToStreamFromPosition(EventStoreClient client, Cancell #endregion subscribe-to-stream-from-position } -static async Task SubscribeToStreamLive(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToStreamLive(KurrentClient client, CancellationToken ct) { #region subscribe-to-stream-live await using var subscription = client.SubscribeToStream( @@ -65,7 +65,7 @@ static async Task SubscribeToStreamLive(EventStoreClient client, CancellationTok #endregion subscribe-to-stream-live } -static async Task SubscribeToStreamResolvingLinkTos(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToStreamResolvingLinkTos(KurrentClient client, CancellationToken ct) { #region subscribe-to-stream-resolving-linktos await using var subscription = client.SubscribeToStream( @@ -85,7 +85,7 @@ static async Task SubscribeToStreamResolvingLinkTos(EventStoreClient client, Can #endregion subscribe-to-stream-resolving-linktos } -static async Task SubscribeToStreamSubscriptionDropped(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToStreamSubscriptionDropped(KurrentClient client, CancellationToken ct) { #region subscribe-to-stream-subscription-dropped var checkpoint = await ReadStreamCheckpointAsync() switch { @@ -120,7 +120,7 @@ static async Task SubscribeToStreamSubscriptionDropped(EventStoreClient client, #endregion subscribe-to-stream-subscription-dropped } -static async Task SubscribeToStream(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToStream(KurrentClient client, CancellationToken ct) { #region subscribe-to-stream await using var subscription = client.SubscribeToStream( @@ -139,7 +139,7 @@ static async Task SubscribeToStream(EventStoreClient client, CancellationToken c #endregion subscribe-to-stream } -static async Task SubscribeToAll(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToAll(KurrentClient client, CancellationToken ct) { #region subscribe-to-all await using var subscription = client.SubscribeToAll( @@ -157,7 +157,7 @@ static async Task SubscribeToAll(EventStoreClient client, CancellationToken ct) #endregion subscribe-to-all } -static async Task SubscribeToAllFromPosition(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToAllFromPosition(KurrentClient client, CancellationToken ct) { #region subscribe-to-all-from-position var result = await client.AppendToStreamAsync( @@ -182,7 +182,7 @@ static async Task SubscribeToAllFromPosition(EventStoreClient client, Cancellati #endregion subscribe-to-all-from-position } -static async Task SubscribeToAllLive(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToAllLive(KurrentClient client, CancellationToken ct) { #region subscribe-to-all-live var subscription = client.SubscribeToAll( @@ -200,7 +200,7 @@ static async Task SubscribeToAllLive(EventStoreClient client, CancellationToken #endregion subscribe-to-all-live } -static async Task SubscribeToAllSubscriptionDropped(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToAllSubscriptionDropped(KurrentClient client, CancellationToken ct) { #region subscribe-to-all-subscription-dropped var checkpoint = await ReadCheckpointAsync() switch { @@ -237,7 +237,7 @@ static async Task SubscribeToAllSubscriptionDropped(EventStoreClient client, Can #endregion subscribe-to-all-subscription-dropped } -static async Task SubscribeToFiltered(EventStoreClient client, CancellationToken ct) { +static async Task SubscribeToFiltered(KurrentClient client, CancellationToken ct) { #region stream-prefix-filtered-subscription var prefixStreamFilter = new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-")); @@ -267,7 +267,7 @@ static async Task SubscribeToFiltered(EventStoreClient client, CancellationToken #endregion stream-regex-filtered-subscription } -static async Task OverridingUserCredentials(EventStoreClient client, CancellationToken ct) { +static async Task OverridingUserCredentials(KurrentClient client, CancellationToken ct) { #region overriding-user-credentials await using var subscription = client.SubscribeToAll( diff --git a/samples/subscribing-to-streams/subscribing-to-streams.csproj b/samples/subscribing-to-streams/subscribing-to-streams.csproj index 397f07197..83a740e45 100644 --- a/samples/subscribing-to-streams/subscribing-to-streams.csproj +++ b/samples/subscribing-to-streams/subscribing-to-streams.csproj @@ -2,11 +2,7 @@ subscribing_to_streams - - - - - + - \ No newline at end of file + diff --git a/samples/user-certificates/Program.cs b/samples/user-certificates/Program.cs index dddbb1c7f..98f9e882e 100644 --- a/samples/user-certificates/Program.cs +++ b/samples/user-certificates/Program.cs @@ -9,11 +9,11 @@ static async Task ClientWithUserCertificates() { const string userCertFile = "/path/to/user.crt"; const string userKeyFile = "/path/to/user.key"; - var settings = EventStoreClientSettings.Create( + var settings = KurrentClientSettings.Create( $"esdb://localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" ); - await using var client = new EventStoreClient(settings); + await using var client = new KurrentClient(settings); # endregion client-with-user-certificates } catch (InvalidClientCertificateException) { diff --git a/samples/user-certificates/user-certificates.csproj b/samples/user-certificates/user-certificates.csproj index 7eaa70127..14de7d848 100644 --- a/samples/user-certificates/user-certificates.csproj +++ b/samples/user-certificates/user-certificates.csproj @@ -9,8 +9,7 @@ - - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 58ec83eff..38e546298 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,36 +1,21 @@ - - - EventStore.Client - - - - - - - - - $(MSBuildProjectName.Remove(0,18)) - EventStore.Client.Grpc.$(PackageIdSuffix) - ../EventStore.Client/Common/protos/$(PackageIdSuffix.ToLower()).proto + + true + Kurrent.Client + Kurrent.Client - - - - - ouro.png LICENSE.md - https://eventstore.com + https://kurrent.io false - https://eventstore.com/blog/ - eventstore client grpc - Event Store Ltd - Copyright 2012-$([System.DateTime]::Today.Year.ToString()) Event Store Ltd + https://kurrent.io/blog/ + kurrent client grpc + Kurrent Ltd + Copyright 2012-$([System.DateTime]::Today.Year.ToString()) Kurrent Ltd v @@ -38,30 +23,27 @@ - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers - + + + + + + + + + + - + - - - - - diff --git a/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj b/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj deleted file mode 100644 index e32bbacc7..000000000 --- a/src/EventStore.Client.Extensions.OpenTelemetry/EventStore.Client.Extensions.OpenTelemetry.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - EventStore.Client.Extensions.OpenTelemetry - - - - EventStore.Client.Extensions.OpenTelemetry - Extensions used to facilitate instrumentation of the EventStore Client. - - - - - - - diff --git a/src/EventStore.Client.Extensions.OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/EventStore.Client.Extensions.OpenTelemetry/TracerProviderBuilderExtensions.cs deleted file mode 100644 index 6a8b42b96..000000000 --- a/src/EventStore.Client.Extensions.OpenTelemetry/TracerProviderBuilderExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using EventStore.Client.Diagnostics; -using JetBrains.Annotations; -using OpenTelemetry.Trace; - -namespace EventStore.Client.Extensions.OpenTelemetry; - -/// -/// Extension methods used to facilitate tracing instrumentation of the EventStore Client. -/// -[PublicAPI] -public static class TracerProviderBuilderExtensions { - /// - /// Adds the EventStore client ActivitySource name to the list of subscribed sources on the - /// - /// being configured. - /// The instance of to chain configuration. - public static TracerProviderBuilder AddEventStoreClientInstrumentation(this TracerProviderBuilder builder) => - builder.AddSource(EventStoreClientDiagnostics.InstrumentationName); -} \ No newline at end of file diff --git a/src/EventStore.Client.Operations/EventStore.Client.Operations.csproj b/src/EventStore.Client.Operations/EventStore.Client.Operations.csproj deleted file mode 100644 index 37299e1a1..000000000 --- a/src/EventStore.Client.Operations/EventStore.Client.Operations.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - The GRPC client API for Event Store Operations, e.g., Scavenging. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - - diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStore.Client.PersistentSubscriptions.csproj b/src/EventStore.Client.PersistentSubscriptions/EventStore.Client.PersistentSubscriptions.csproj deleted file mode 100644 index 405a77405..000000000 --- a/src/EventStore.Client.PersistentSubscriptions/EventStore.Client.PersistentSubscriptions.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - The GRPC client API for Event Store Persistent Subscriptions. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - - diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs deleted file mode 100644 index 211ffdbeb..000000000 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs +++ /dev/null @@ -1,478 +0,0 @@ -using System.Threading.Channels; -using EventStore.Client.Diagnostics; -using EventStore.Client.PersistentSubscriptions; -using Grpc.Core; - -using static EventStore.Client.PersistentSubscriptions.PersistentSubscriptions; -using static EventStore.Client.PersistentSubscriptions.ReadResp.ContentOneofCase; - -namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { - /// - /// Subscribes to a persistent subscription. - /// - /// - /// - /// - [Obsolete("SubscribeAsync is no longer supported. Use SubscribeToStream with manual acks instead.", false)] - public async Task SubscribeAsync( - string streamName, string groupName, - Func eventAppeared, - Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, int bufferSize = 10, bool autoAck = true, - CancellationToken cancellationToken = default - ) { - if (autoAck) { - throw new InvalidOperationException( - $"AutoAck is no longer supported. Please use {nameof(SubscribeToStream)} with manual acks instead." - ); - } - - return await PersistentSubscription - .Confirm( - SubscribeToStream(streamName, groupName, bufferSize, userCredentials, cancellationToken), - eventAppeared, - subscriptionDropped ?? delegate { }, - _log, - userCredentials, - cancellationToken - ) - .ConfigureAwait(false); - } - - /// - /// Subscribes to a persistent subscription. Messages must be manually acknowledged - /// - /// - /// - /// - public async Task SubscribeToStreamAsync( - string streamName, string groupName, - Func eventAppeared, - Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, int bufferSize = 10, - CancellationToken cancellationToken = default - ) { - return await PersistentSubscription - .Confirm( - SubscribeToStream(streamName, groupName, bufferSize, userCredentials, cancellationToken), - eventAppeared, - subscriptionDropped ?? delegate { }, - _log, - userCredentials, - cancellationToken - ) - .ConfigureAwait(false); - } - - /// - /// Subscribes to a persistent subscription. Messages must be manually acknowledged. - /// - /// The name of the stream to read events from. - /// The name of the persistent subscription group. - /// The size of the buffer. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public PersistentSubscriptionResult SubscribeToStream( - string streamName, string groupName, int bufferSize = 10, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default - ) { - if (streamName == null) { - throw new ArgumentNullException(nameof(streamName)); - } - - if (groupName == null) { - throw new ArgumentNullException(nameof(groupName)); - } - - if (streamName == string.Empty) { - throw new ArgumentException($"{nameof(streamName)} may not be empty.", nameof(streamName)); - } - - if (groupName == string.Empty) { - throw new ArgumentException($"{nameof(groupName)} may not be empty.", nameof(groupName)); - } - - if (bufferSize <= 0) { - throw new ArgumentOutOfRangeException(nameof(bufferSize)); - } - - var readOptions = new ReadReq.Types.Options { - BufferSize = bufferSize, - GroupName = groupName, - UuidOption = new ReadReq.Types.Options.Types.UUIDOption { Structured = new Empty() } - }; - - if (streamName == SystemStreams.AllStream) { - readOptions.All = new Empty(); - } else { - readOptions.StreamIdentifier = streamName; - } - - return new PersistentSubscriptionResult( - streamName, - groupName, - async ct => { - var channelInfo = await GetChannelInfo(ct).ConfigureAwait(false); - - if (streamName == SystemStreams.AllStream && - !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { - throw new NotSupportedException( - "The server does not support persistent subscriptions to $all." - ); - } - - return channelInfo; - }, - new() { Options = readOptions }, - Settings, - userCredentials, - cancellationToken - ); - } - - /// - /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged - /// - public async Task SubscribeToAllAsync( - string groupName, - Func eventAppeared, - Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, int bufferSize = 10, - CancellationToken cancellationToken = default - ) => - await SubscribeToStreamAsync( - SystemStreams.AllStream, - groupName, - eventAppeared, - subscriptionDropped, - userCredentials, - bufferSize, - cancellationToken - ) - .ConfigureAwait(false); - - /// - /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged. - /// - /// The name of the persistent subscription group. - /// The size of the buffer. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public PersistentSubscriptionResult SubscribeToAll( - string groupName, int bufferSize = 10, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default - ) => - SubscribeToStream(SystemStreams.AllStream, groupName, bufferSize, userCredentials, cancellationToken); - - /// - public class PersistentSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable { - const int MaxEventIdLength = 2000; - - readonly ReadReq _request; - readonly Channel _channel; - readonly CancellationTokenSource _cts; - readonly CallOptions _callOptions; - - AsyncDuplexStreamingCall? _call; - int _messagesEnumerated; - - /// - /// The server-generated unique identifier for the subscription. - /// - public string? SubscriptionId { get; private set; } - - /// - /// The name of the stream to read events from. - /// - public string StreamName { get; } - - /// - /// The name of the persistent subscription group. - /// - public string GroupName { get; } - - /// - /// An . Do not enumerate more than once. - /// - public IAsyncEnumerable Messages { - get { - if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) - throw new InvalidOperationException("Messages may only be enumerated once."); - - return GetMessages(); - - async IAsyncEnumerable GetMessages() { - try { - await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { - if (message is PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId)) - SubscriptionId = subscriptionId; - - yield return message; - } - } - finally { - _cts.Cancel(); - } - } - } - } - - internal PersistentSubscriptionResult( - string streamName, string groupName, - Func> selectChannelInfo, - ReadReq request, EventStoreClientSettings settings, UserCredentials? userCredentials, - CancellationToken cancellationToken - ) { - StreamName = streamName; - GroupName = groupName; - - _request = request; - - _callOptions = EventStoreCallOptions.CreateStreaming( - settings, - userCredentials: userCredentials, - cancellationToken: cancellationToken - ); - - _channel = Channel.CreateBounded(ReadBoundedChannelOptions); - - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - _ = PumpMessages(); - - return; - - async Task PumpMessages() { - try { - var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); - var client = new PersistentSubscriptionsClient(channelInfo.CallInvoker); - - _call = client.Read(_callOptions); - - await _call.RequestStream.WriteAsync(_request).ConfigureAwait(false); - - await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { - PersistentSubscriptionMessage subscriptionMessage = response.ContentCase switch { - SubscriptionConfirmation => new PersistentSubscriptionMessage.SubscriptionConfirmation( - response.SubscriptionConfirmation.SubscriptionId - ), - Event => new PersistentSubscriptionMessage.Event( - ConvertToResolvedEvent(response), - response.Event.CountCase switch { - ReadResp.Types.ReadEvent.CountOneofCase.RetryCount => response.Event.RetryCount, - _ => null - } - ), - _ => PersistentSubscriptionMessage.Unknown.Instance - }; - - if (subscriptionMessage is PersistentSubscriptionMessage.Event evnt) - EventStoreClientDiagnostics.ActivitySource.TraceSubscriptionEvent( - SubscriptionId, - evnt.ResolvedEvent, - channelInfo, - settings, - userCredentials - ); - - await _channel.Writer.WriteAsync(subscriptionMessage, _cts.Token).ConfigureAwait(false); - } - - _channel.Writer.TryComplete(); - } catch (Exception ex) { -#if NET48 - switch (ex) { - // The gRPC client for .NET 48 uses WinHttpHandler under the hood for sending HTTP requests. - // In certain scenarios, this can lead to exceptions of type WinHttpException being thrown. - // One such scenario is when the server abruptly closes the connection, which results in a WinHttpException with the error code 12030. - // Additionally, there are cases where the server response does not include the 'grpc-status' header. - // The absence of this header leads to an RpcException with the status code 'Cancelled' and the message "No grpc-status found on response". - // The switch statement below handles these specific exceptions and translates them into the appropriate - // PersistentSubscriptionDroppedByServerException exception. - case RpcException { StatusCode: StatusCode.Unavailable } rex1 when rex1.Status.Detail.Contains("WinHttpException: Error 12030"): - case RpcException { StatusCode: StatusCode.Cancelled } rex2 - when rex2.Status.Detail.Contains("No grpc-status found on response"): - ex = new PersistentSubscriptionDroppedByServerException(StreamName, GroupName, ex); - break; - } -#endif - if (ex is PersistentSubscriptionNotFoundException) { - await _channel.Writer - .WriteAsync(PersistentSubscriptionMessage.NotFound.Instance, cancellationToken) - .ConfigureAwait(false); - - _channel.Writer.TryComplete(); - return; - } - - _channel.Writer.TryComplete(ex); - } - } - } - - /// - /// Acknowledge that a message has completed processing (this will tell the server it has been processed). - /// - /// There is no need to ack a message if you have Auto Ack enabled. - /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(params Uuid[] eventIds) => AckInternal(eventIds); - - /// - /// Acknowledge that a message has completed processing (this will tell the server it has been processed). - /// - /// There is no need to ack a message if you have Auto Ack enabled. - /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(IEnumerable eventIds) => Ack(eventIds.ToArray()); - - /// - /// Acknowledge that a message has completed processing (this will tell the server it has been processed). - /// - /// There is no need to ack a message if you have Auto Ack enabled. - /// The s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(params ResolvedEvent[] resolvedEvents) => - Ack(Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); - - /// - /// Acknowledge that a message has completed processing (this will tell the server it has been processed). - /// - /// There is no need to ack a message if you have Auto Ack enabled. - /// The s to acknowledge. There should not be more than 2000 to ack at a time. - public Task Ack(IEnumerable resolvedEvents) => - Ack(resolvedEvents.Select(resolvedEvent => resolvedEvent.OriginalEvent.EventId)); - - /// - /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). - /// - /// The to take. - /// A reason given. - /// The of the s to nak. There should not be more than 2000 to nak at a time. - /// The number of eventIds exceeded the limit of 2000. - public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => - NackInternal(eventIds, action, reason); - - /// - /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). - /// - /// The to take. - /// A reason given. - /// The s to nak. There should not be more than 2000 to nak at a time. - /// The number of resolvedEvents exceeded the limit of 2000. - public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params ResolvedEvent[] resolvedEvents) => - Nack(action, reason, Array.ConvertAll(resolvedEvents, re => re.OriginalEvent.EventId)); - - static ResolvedEvent ConvertToResolvedEvent(ReadResp response) => new( - ConvertToEventRecord(response.Event.Event)!, - ConvertToEventRecord(response.Event.Link), - response.Event.PositionCase switch { - ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => response.Event.CommitPosition, - _ => null - } - ); - - Task AckInternal(params Uuid[] eventIds) { - if (eventIds.Length > MaxEventIdLength) { - throw new ArgumentException( - $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", - nameof(eventIds) - ); - } - - return _call is null - ? throw new InvalidOperationException() - : _call.RequestStream.WriteAsync( - new ReadReq { - Ack = new ReadReq.Types.Ack { - Ids = { - Array.ConvertAll(eventIds, id => id.ToDto()) - } - } - } - ); - } - - Task NackInternal(Uuid[] eventIds, PersistentSubscriptionNakEventAction action, string reason) { - if (eventIds.Length > MaxEventIdLength) { - throw new ArgumentException( - $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", - nameof(eventIds) - ); - } - - return _call is null - ? throw new InvalidOperationException() - : _call.RequestStream.WriteAsync( - new ReadReq { - Nack = new ReadReq.Types.Nack { - Ids = { - Array.ConvertAll(eventIds, id => id.ToDto()) - }, - Action = action switch { - PersistentSubscriptionNakEventAction.Park => ReadReq.Types.Nack.Types.Action.Park, - PersistentSubscriptionNakEventAction.Retry => ReadReq.Types.Nack.Types.Action.Retry, - PersistentSubscriptionNakEventAction.Skip => ReadReq.Types.Nack.Types.Action.Skip, - PersistentSubscriptionNakEventAction.Stop => ReadReq.Types.Nack.Types.Action.Stop, - _ => ReadReq.Types.Nack.Types.Action.Unknown - }, - Reason = reason - } - } - ); - } - - static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => - e is null - ? null - : new EventRecord( - e.StreamIdentifier!, - Uuid.FromDto(e.Id), - new StreamPosition(e.StreamRevision), - new Position(e.CommitPosition, e.PreparePosition), - e.Metadata, - e.Data.ToByteArray(), - e.CustomMetadata.ToByteArray() - ); - - /// - public async ValueTask DisposeAsync() { - await CastAndDispose(_cts).ConfigureAwait(false); - await CastAndDispose(_call).ConfigureAwait(false); - - return; - - static async Task CastAndDispose(IDisposable? resource) { - switch (resource) { - case null: - return; - - case IAsyncDisposable resourceAsyncDisposable: - await resourceAsyncDisposable.DisposeAsync().ConfigureAwait(false); - break; - - default: - resource.Dispose(); - break; - } - } - } - - /// - public void Dispose() { - _cts.Dispose(); - _call?.Dispose(); - } - - /// - public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { - await foreach (var message in Messages.WithCancellation(cancellationToken)) { - if (message is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) - continue; - - yield return resolvedEvent; - } - } - } - } -} diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs deleted file mode 100644 index f8d491dda..000000000 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClientCollectionExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ReSharper disable CheckNamespace - -using System; -using System.Net.Http; -using EventStore.Client; -using Grpc.Core.Interceptors; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Extensions.DependencyInjection { - /// - /// A set of extension methods for which provide support for an . - /// - public static class EventStorePersistentSubscriptionsClientCollectionExtensions { - /// - /// Adds an to the . - /// - /// - public static IServiceCollection AddEventStorePersistentSubscriptionsClient(this IServiceCollection services, - Uri address, Func? createHttpMessageHandler = null) - => services.AddEventStorePersistentSubscriptionsClient(options => { - options.ConnectivitySettings.Address = address; - options.CreateHttpMessageHandler = createHttpMessageHandler; - }); - - /// - /// Adds an to the . - /// - /// - public static IServiceCollection AddEventStorePersistentSubscriptionsClient(this IServiceCollection services, - Action? configureSettings = null) => - services.AddEventStorePersistentSubscriptionsClient(new EventStoreClientSettings(), - configureSettings); - - /// - /// Adds an to the . - /// - /// - public static IServiceCollection AddEventStorePersistentSubscriptionsClient(this IServiceCollection services, - string connectionString, Action? configureSettings = null) => - services.AddEventStorePersistentSubscriptionsClient(EventStoreClientSettings.Create(connectionString), - configureSettings); - - private static IServiceCollection AddEventStorePersistentSubscriptionsClient(this IServiceCollection services, - EventStoreClientSettings settings, Action? configureSettings) { - if (services == null) { - throw new ArgumentNullException(nameof(services)); - } - - configureSettings?.Invoke(settings); - services.TryAddSingleton(provider => { - settings.LoggerFactory ??= provider.GetService(); - settings.Interceptors ??= provider.GetServices(); - - return new EventStorePersistentSubscriptionsClient(settings); - }); - return services; - } - } -} -// ReSharper restore CheckNamespace diff --git a/src/EventStore.Client.ProjectionManagement/EventStore.Client.ProjectionManagement.csproj b/src/EventStore.Client.ProjectionManagement/EventStore.Client.ProjectionManagement.csproj deleted file mode 100644 index 678656cff..000000000 --- a/src/EventStore.Client.ProjectionManagement/EventStore.Client.ProjectionManagement.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - The GRPC client API for managing Event Store Projections. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - - diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.cs deleted file mode 100644 index be0679928..000000000 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using Grpc.Core; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; - -namespace EventStore.Client { - /// - ///The client used to manage projections on the EventStoreDB. - /// - public sealed partial class EventStoreProjectionManagementClient : EventStoreClientBase { - private readonly ILogger _log; - - /// - /// Constructs a new . This method is not intended to be called directly from your code. - /// - /// - public EventStoreProjectionManagementClient(IOptions options) : this(options.Value) { - } - - /// - /// Constructs a new . - /// - /// - public EventStoreProjectionManagementClient(EventStoreClientSettings? settings) : base(settings, - new Dictionary>()) { - _log = settings?.LoggerFactory?.CreateLogger() ?? - new NullLogger(); - } - } -} diff --git a/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj b/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj deleted file mode 100644 index c878127a5..000000000 --- a/src/EventStore.Client.Streams/EventStore.Client.Streams.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - The GRPC client API for Event Store Streams. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - - diff --git a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs b/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs deleted file mode 100644 index 6f3b2f2c6..000000000 --- a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs +++ /dev/null @@ -1,302 +0,0 @@ -using System.Threading.Channels; -using EventStore.Client.Diagnostics; -using EventStore.Client.Streams; -using Grpc.Core; - -using static EventStore.Client.Streams.ReadResp.ContentOneofCase; - -namespace EventStore.Client { - public partial class EventStoreClient { - /// - /// Subscribes to all events. - /// - /// A (exclusive of) to start the subscription from. - /// A Task invoked and awaited when a new event is received over the subscription. - /// Whether to resolve LinkTo events automatically. - /// An action invoked if the subscription is dropped. - /// The optional to apply. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public Task SubscribeToAllAsync( - FromAll start, - Func eventAppeared, - bool resolveLinkTos = false, - Action? subscriptionDropped = default, - SubscriptionFilterOptions? filterOptions = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) => StreamSubscription.Confirm( - SubscribeToAll(start, resolveLinkTos, filterOptions, userCredentials, cancellationToken), - eventAppeared, - subscriptionDropped, - _log, - filterOptions?.CheckpointReached, - cancellationToken: cancellationToken - ); - - /// - /// Subscribes to all events. - /// - /// A (exclusive of) to start the subscription from. - /// Whether to resolve LinkTo events automatically. - /// The optional to apply. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public StreamSubscriptionResult SubscribeToAll( - FromAll start, - bool resolveLinkTos = false, - SubscriptionFilterOptions? filterOptions = null, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) => new( - async _ => await GetChannelInfo(cancellationToken).ConfigureAwait(false), - new ReadReq { - Options = new ReadReq.Types.Options { - ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, - ResolveLinks = resolveLinkTos, - All = ReadReq.Types.Options.Types.AllOptions.FromSubscriptionPosition(start), - Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), - Filter = GetFilterOptions(filterOptions)!, - UuidOption = new() { Structured = new() } - } - }, - Settings, - userCredentials, - cancellationToken - ); - - /// - /// Subscribes to a stream from a checkpoint. - /// - /// A (exclusive of) to start the subscription from. - /// The name of the stream to read events from. - /// A Task invoked and awaited when a new event is received over the subscription. - /// Whether to resolve LinkTo events automatically. - /// An action invoked if the subscription is dropped. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public Task SubscribeToStreamAsync( - string streamName, - FromStream start, - Func eventAppeared, - bool resolveLinkTos = false, - Action? subscriptionDropped = default, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) => StreamSubscription.Confirm( - SubscribeToStream(streamName, start, resolveLinkTos, userCredentials, cancellationToken), - eventAppeared, - subscriptionDropped, - _log, - cancellationToken: cancellationToken - ); - - /// - /// Subscribes to a stream from a checkpoint. - /// - /// A (exclusive of) to start the subscription from. - /// The name of the stream to read events from. - /// Whether to resolve LinkTo events automatically. - /// The optional user credentials to perform operation with. - /// The optional . - /// - public StreamSubscriptionResult SubscribeToStream( - string streamName, - FromStream start, - bool resolveLinkTos = false, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default - ) => new( - async _ => await GetChannelInfo(cancellationToken).ConfigureAwait(false), - new ReadReq { - Options = new ReadReq.Types.Options { - ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, - ResolveLinks = resolveLinkTos, - Stream = ReadReq.Types.Options.Types.StreamOptions.FromSubscriptionPosition(streamName, start), - Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), - UuidOption = new() { Structured = new() } - } - }, - Settings, - userCredentials, - cancellationToken - ); - - /// - /// A class that represents the result of a subscription operation. You may either enumerate this instance directly or . Do not enumerate more than once. - /// - public class StreamSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable { - private readonly ReadReq _request; - private readonly Channel _channel; - private readonly CancellationTokenSource _cts; - private readonly CallOptions _callOptions; - private readonly EventStoreClientSettings _settings; - private AsyncServerStreamingCall? _call; - - private int _messagesEnumerated; - - /// - /// The server-generated unique identifier for the subscription. - /// - public string? SubscriptionId { get; private set; } - - /// - /// An . Do not enumerate more than once. - /// - public IAsyncEnumerable Messages { - get { - if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) - throw new InvalidOperationException("Messages may only be enumerated once."); - - return GetMessages(); - - async IAsyncEnumerable GetMessages() { - try { - await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { - if (message is StreamMessage.SubscriptionConfirmation(var subscriptionId)) - SubscriptionId = subscriptionId; - - yield return message; - } - } - finally { -#if NET8_0_OR_GREATER - await _cts.CancelAsync().ConfigureAwait(false); -#else - _cts.Cancel(); -#endif - } - } - } - } - - internal StreamSubscriptionResult( - Func> selectChannelInfo, - ReadReq request, EventStoreClientSettings settings, UserCredentials? userCredentials, - CancellationToken cancellationToken - ) { - _request = request; - _settings = settings; - - _callOptions = EventStoreCallOptions.CreateStreaming( - settings, - userCredentials: userCredentials, - cancellationToken: cancellationToken - ); - - _channel = Channel.CreateBounded(ReadBoundedChannelOptions); - - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - if (_request.Options.FilterOptionCase == ReadReq.Types.Options.FilterOptionOneofCase.None) { - _request.Options.NoFilter = new(); - } - - _ = PumpMessages(); - - return; - - async Task PumpMessages() { - try { - var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); - var client = new Streams.Streams.StreamsClient(channelInfo.CallInvoker); - _call = client.Read(_request, _callOptions); - await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { - StreamMessage subscriptionMessage = - response.ContentCase switch { - Confirmation => new StreamMessage.SubscriptionConfirmation(response.Confirmation.SubscriptionId), - Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), - FirstStreamPosition => new StreamMessage.FirstStreamPosition(new StreamPosition(response.FirstStreamPosition)), - LastStreamPosition => new StreamMessage.LastStreamPosition(new StreamPosition(response.LastStreamPosition)), - LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( - new Position( - response.LastAllStreamPosition.CommitPosition, - response.LastAllStreamPosition.PreparePosition - ) - ), - Checkpoint => new StreamMessage.AllStreamCheckpointReached( - new Position( - response.Checkpoint.CommitPosition, - response.Checkpoint.PreparePosition - ) - ), - CaughtUp => StreamMessage.CaughtUp.Instance, - FellBehind => StreamMessage.FellBehind.Instance, - _ => StreamMessage.Unknown.Instance - }; - - if (subscriptionMessage is StreamMessage.Event evt) - EventStoreClientDiagnostics.ActivitySource.TraceSubscriptionEvent( - SubscriptionId, - evt.ResolvedEvent, - channelInfo, - _settings, - userCredentials - ); - - await _channel.Writer - .WriteAsync(subscriptionMessage, _cts.Token) - .ConfigureAwait(false); - } - - _channel.Writer.Complete(); - } catch (Exception ex) { - _channel.Writer.TryComplete(ex); - } - } - } - - /// - public async ValueTask DisposeAsync() { - //TODO SS: Check if `CastAndDispose` is still relevant - await CastAndDispose(_cts).ConfigureAwait(false); - await CastAndDispose(_call).ConfigureAwait(false); - - return; - - static async ValueTask CastAndDispose(IDisposable? resource) { - switch (resource) { - case null: - return; - - case IAsyncDisposable disposable: - await disposable.DisposeAsync().ConfigureAwait(false); - break; - - default: - resource.Dispose(); - break; - } - } - } - - /// - public void Dispose() { - _cts.Dispose(); - _call?.Dispose(); - } - - /// - public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { - try { - await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { - if (message is not StreamMessage.Event e) - continue; - - yield return e.ResolvedEvent; - } - } - finally { -#if NET8_0_OR_GREATER - await _cts.CancelAsync().ConfigureAwait(false); -#else - _cts.Cancel(); -#endif - } - } - } - } -} diff --git a/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj b/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj deleted file mode 100644 index 2c9308e2e..000000000 --- a/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - The GRPC client API for managing users in Event Store. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - - - - - diff --git a/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj.DotSettings b/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj.DotSettings deleted file mode 100644 index 1183b3a73..000000000 --- a/src/EventStore.Client.UserManagement/EventStore.Client.UserManagement.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - True \ No newline at end of file diff --git a/src/EventStore.Client/Common/Diagnostics/EventStoreClientDiagnostics.cs b/src/EventStore.Client/Common/Diagnostics/EventStoreClientDiagnostics.cs deleted file mode 100644 index 6328387ab..000000000 --- a/src/EventStore.Client/Common/Diagnostics/EventStoreClientDiagnostics.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Diagnostics; - -namespace EventStore.Client.Diagnostics; - -public static class EventStoreClientDiagnostics { - public const string InstrumentationName = "eventstoredb"; - public static readonly ActivitySource ActivitySource = new ActivitySource(InstrumentationName); -} \ No newline at end of file diff --git a/src/EventStore.Client/Common/Diagnostics/Telemetry/TelemetryTags.cs b/src/EventStore.Client/Common/Diagnostics/Telemetry/TelemetryTags.cs deleted file mode 100644 index ea05d04fa..000000000 --- a/src/EventStore.Client/Common/Diagnostics/Telemetry/TelemetryTags.cs +++ /dev/null @@ -1,12 +0,0 @@ -// ReSharper disable CheckNamespace - -namespace EventStore.Diagnostics.Telemetry; - -static partial class TelemetryTags { - public static class EventStore { - public const string Stream = "db.eventstoredb.stream"; - public const string SubscriptionId = "db.eventstoredb.subscription.id"; - public const string EventId = "db.eventstoredb.event.id"; - public const string EventType = "db.eventstoredb.event.type"; - } -} \ No newline at end of file diff --git a/src/EventStore.Client/EventStore.Client.csproj b/src/EventStore.Client/EventStore.Client.csproj deleted file mode 100644 index 47a116602..000000000 --- a/src/EventStore.Client/EventStore.Client.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - EventStore.Client - The base GRPC client library for Event Store. Get the open source or commercial versions of Event Store server from https://eventstore.com/ - EventStore.Client.Grpc - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/EventStore.Client/EventStore.Client.csproj.DotSettings b/src/EventStore.Client/EventStore.Client.csproj.DotSettings deleted file mode 100644 index 9b89548c6..000000000 --- a/src/EventStore.Client/EventStore.Client.csproj.DotSettings +++ /dev/null @@ -1,3 +0,0 @@ - - True - True \ No newline at end of file diff --git a/src/EventStore.Client/Certificates/X509Certificates.cs b/src/Kurrent.Client/Core/Certificates/X509Certificates.cs similarity index 66% rename from src/EventStore.Client/Certificates/X509Certificates.cs rename to src/Kurrent.Client/Core/Certificates/X509Certificates.cs index 78efda04e..9cda47a08 100644 --- a/src/EventStore.Client/Certificates/X509Certificates.cs +++ b/src/Kurrent.Client/Core/Certificates/X509Certificates.cs @@ -2,6 +2,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +#pragma warning disable SYSLIB0057 #if NET48 using Org.BouncyCastle.Crypto; @@ -13,29 +14,21 @@ namespace EventStore.Client; static class X509Certificates { - // TODO SS: Use .NET 8 X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) once the Windows32Exception issue is resolved - public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) { - try { - using var publicCert = new X509Certificate2(certPemFilePath); - using var privateKey = RSA.Create().ImportPrivateKeyFromFile(keyPemFilePath); - using var certificate = publicCert.CopyWithPrivateKey(privateKey); - - return new(certificate.Export(X509ContentType.Pfx)); - } - catch (Exception ex) { - throw new CryptographicException($"Failed to load private key: {ex.Message}"); - } - - // Notes: - // using X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath) would be the ideal choice here, - // but it's currently causing a Win32Exception specifically on Windows. Alternative implementation is used until the issue is resolved. - // - // Error: The SSL connection could not be established, see inner exception. AuthenticationException: Authentication failed because the platform - // does not support ephemeral keys. Win32Exception: No credentials are available in the security package - // - // public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) => - // X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); - } + public static X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) { + try { +#if NET8_0_OR_GREATER + using var certificate = X509Certificate2.CreateFromPemFile(certPemFilePath, keyPemFilePath); +#else + using var publicCert = new X509Certificate2(certPemFilePath); + using var privateKey = RSA.Create().ImportPrivateKeyFromFile(keyPemFilePath); + using var certificate = publicCert.CopyWithPrivateKey(privateKey); +#endif + + return new X509Certificate2(certificate.Export(X509ContentType.Pfx)); + } catch (Exception ex) { + throw new CryptographicException($"Failed to load private key: {ex.Message}"); + } + } } public static class RsaExtensions { @@ -104,4 +97,4 @@ public static string ParseKeyLabel(string pemFileHeader) { return label; } -} \ No newline at end of file +} diff --git a/src/EventStore.Client/ChannelBaseExtensions.cs b/src/Kurrent.Client/Core/ChannelBaseExtensions.cs similarity index 99% rename from src/EventStore.Client/ChannelBaseExtensions.cs rename to src/Kurrent.Client/Core/ChannelBaseExtensions.cs index 3edbf59fd..9c44addef 100644 --- a/src/EventStore.Client/ChannelBaseExtensions.cs +++ b/src/Kurrent.Client/Core/ChannelBaseExtensions.cs @@ -7,4 +7,4 @@ public static async ValueTask DisposeAsync(this ChannelBase channel) { await channel.ShutdownAsync().ConfigureAwait(false); (channel as IDisposable)?.Dispose(); } -} \ No newline at end of file +} diff --git a/src/EventStore.Client/ChannelCache.cs b/src/Kurrent.Client/Core/ChannelCache.cs similarity index 96% rename from src/EventStore.Client/ChannelCache.cs rename to src/Kurrent.Client/Core/ChannelCache.cs index a3369e25f..09f7c2b86 100644 --- a/src/EventStore.Client/ChannelCache.cs +++ b/src/Kurrent.Client/Core/ChannelCache.cs @@ -8,13 +8,13 @@ namespace EventStore.Client { internal class ChannelCache : IAsyncDisposable { - private readonly EventStoreClientSettings _settings; + private readonly KurrentClientSettings _settings; private readonly Random _random; private readonly Dictionary _channels; private readonly object _lock = new(); private bool _disposed; - public ChannelCache(EventStoreClientSettings settings) { + public ChannelCache(KurrentClientSettings settings) { _settings = settings; _random = new Random(0); _channels = new Dictionary( diff --git a/src/EventStore.Client/ChannelFactory.cs b/src/Kurrent.Client/Core/ChannelFactory.cs similarity index 93% rename from src/EventStore.Client/ChannelFactory.cs rename to src/Kurrent.Client/Core/ChannelFactory.cs index 0dc28ee8e..c63605bb4 100644 --- a/src/EventStore.Client/ChannelFactory.cs +++ b/src/Kurrent.Client/Core/ChannelFactory.cs @@ -9,7 +9,7 @@ namespace EventStore.Client { internal static class ChannelFactory { private const int MaxReceiveMessageLength = 17 * 1024 * 1024; - public static TChannel CreateChannel(EventStoreClientSettings settings, EndPoint endPoint) { + public static TChannel CreateChannel(KurrentClientSettings settings, EndPoint endPoint) { var address = endPoint.ToUri(!settings.ConnectivitySettings.Insecure); if (settings.ConnectivitySettings.Insecure) { @@ -37,7 +37,7 @@ public static TChannel CreateChannel(EventStoreClientSettings settings, EndPoint #if NET48 - static HttpMessageHandler CreateHandler(EventStoreClientSettings settings) { + static HttpMessageHandler CreateHandler(KurrentClientSettings settings) { if (settings.CreateHttpMessageHandler is not null) return settings.CreateHttpMessageHandler.Invoke(); @@ -67,7 +67,7 @@ static HttpMessageHandler CreateHandler(EventStoreClientSettings settings) { return handler; } #else - static HttpMessageHandler CreateHandler(EventStoreClientSettings settings) { + static HttpMessageHandler CreateHandler(KurrentClientSettings settings) { if (settings.CreateHttpMessageHandler is not null) return settings.CreateHttpMessageHandler.Invoke(); diff --git a/src/EventStore.Client/ChannelInfo.cs b/src/Kurrent.Client/Core/ChannelInfo.cs similarity index 100% rename from src/EventStore.Client/ChannelInfo.cs rename to src/Kurrent.Client/Core/ChannelInfo.cs diff --git a/src/EventStore.Client/ChannelSelector.cs b/src/Kurrent.Client/Core/ChannelSelector.cs similarity index 94% rename from src/EventStore.Client/ChannelSelector.cs rename to src/Kurrent.Client/Core/ChannelSelector.cs index 354c0a9f5..af0fa3031 100644 --- a/src/EventStore.Client/ChannelSelector.cs +++ b/src/Kurrent.Client/Core/ChannelSelector.cs @@ -8,7 +8,7 @@ internal class ChannelSelector : IChannelSelector { private readonly IChannelSelector _inner; public ChannelSelector( - EventStoreClientSettings settings, + KurrentClientSettings settings, ChannelCache channelCache) { _inner = settings.ConnectivitySettings.IsSingleNode ? new SingleNodeChannelSelector(settings, channelCache) diff --git a/src/EventStore.Client/ClusterMessage.cs b/src/Kurrent.Client/Core/ClusterMessage.cs similarity index 100% rename from src/EventStore.Client/ClusterMessage.cs rename to src/Kurrent.Client/Core/ClusterMessage.cs diff --git a/src/EventStore.Client/Common/AsyncStreamReaderExtensions.cs b/src/Kurrent.Client/Core/Common/AsyncStreamReaderExtensions.cs similarity index 100% rename from src/EventStore.Client/Common/AsyncStreamReaderExtensions.cs rename to src/Kurrent.Client/Core/Common/AsyncStreamReaderExtensions.cs diff --git a/src/EventStore.Client/Common/Constants.cs b/src/Kurrent.Client/Core/Common/Constants.cs similarity index 100% rename from src/EventStore.Client/Common/Constants.cs rename to src/Kurrent.Client/Core/Common/Constants.cs diff --git a/src/EventStore.Client/Common/Diagnostics/ActivitySourceExtensions.cs b/src/Kurrent.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs similarity index 72% rename from src/EventStore.Client/Common/Diagnostics/ActivitySourceExtensions.cs rename to src/Kurrent.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs index 71f4372da..ab914d8c9 100644 --- a/src/EventStore.Client/Common/Diagnostics/ActivitySourceExtensions.cs +++ b/src/Kurrent.Client/Core/Common/Diagnostics/ActivitySourceExtensions.cs @@ -1,9 +1,9 @@ // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract using System.Diagnostics; -using EventStore.Diagnostics; -using EventStore.Diagnostics.Telemetry; -using EventStore.Diagnostics.Tracing; +using Kurrent.Diagnostics; +using Kurrent.Diagnostics.Telemetry; +using Kurrent.Diagnostics.Tracing; namespace EventStore.Client.Diagnostics; @@ -12,13 +12,8 @@ public static async ValueTask TraceClientOperation( this ActivitySource source, Func> tracedOperation, string operationName, - Func? tagsFactory = null + ActivityTagsCollection? tags = null ) { - if (source.HasNoActiveListeners()) - return await tracedOperation().ConfigureAwait(false); - - var tags = tagsFactory?.Invoke(); - using var activity = StartActivity(source, operationName, ActivityKind.Client, tags, Activity.Current?.Context); try { @@ -36,7 +31,7 @@ public static void TraceSubscriptionEvent( string? subscriptionId, ResolvedEvent resolvedEvent, ChannelInfo channelInfo, - EventStoreClientSettings settings, + KurrentClientSettings settings, UserCredentials? userCredentials ) { if (source.HasNoActiveListeners() || resolvedEvent.Event is null) @@ -47,12 +42,12 @@ public static void TraceSubscriptionEvent( if (parentContext == default(ActivityContext)) return; var tags = new ActivityTagsCollection() - .WithRequiredTag(TelemetryTags.EventStore.Stream, resolvedEvent.OriginalEvent.EventStreamId) - .WithOptionalTag(TelemetryTags.EventStore.SubscriptionId, subscriptionId) - .WithRequiredTag(TelemetryTags.EventStore.EventId, resolvedEvent.OriginalEvent.EventId.ToString()) - .WithRequiredTag(TelemetryTags.EventStore.EventType, resolvedEvent.OriginalEvent.EventType) + .WithRequiredTag(TelemetryTags.Kurrent.Stream, resolvedEvent.OriginalEvent.EventStreamId) + .WithOptionalTag(TelemetryTags.Kurrent.SubscriptionId, subscriptionId) + .WithRequiredTag(TelemetryTags.Kurrent.EventId, resolvedEvent.OriginalEvent.EventId.ToString()) + .WithRequiredTag(TelemetryTags.Kurrent.EventType, resolvedEvent.OriginalEvent.EventType) // Ensure consistent server.address attribute when connecting to cluster via dns discovery - .WithGrpcChannelServerTags(settings, channelInfo) + .WithGrpcChannelServerTags(channelInfo) .WithClientSettingsServerTags(settings) .WithOptionalTag( TelemetryTags.Database.User, @@ -72,7 +67,7 @@ public static void TraceSubscriptionEvent( return null; (tags ??= new ActivityTagsCollection()) - .WithRequiredTag(TelemetryTags.Database.System, "eventstoredb") + .WithRequiredTag(TelemetryTags.Database.System, "kurrent") .WithRequiredTag(TelemetryTags.Database.Operation, operationName); return source diff --git a/src/EventStore.Client/Common/Diagnostics/ActivityTagsCollectionExtensions.cs b/src/Kurrent.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs similarity index 51% rename from src/EventStore.Client/Common/Diagnostics/ActivityTagsCollectionExtensions.cs rename to src/Kurrent.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs index bc3c2aa75..fd6ad661a 100644 --- a/src/EventStore.Client/Common/Diagnostics/ActivityTagsCollectionExtensions.cs +++ b/src/Kurrent.Client/Core/Common/Diagnostics/ActivityTagsCollectionExtensions.cs @@ -1,30 +1,25 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -using EventStore.Diagnostics; -using EventStore.Diagnostics.Telemetry; +using Kurrent.Diagnostics; +using Kurrent.Diagnostics.Telemetry; namespace EventStore.Client.Diagnostics; static class ActivityTagsCollectionExtensions { [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ActivityTagsCollection WithGrpcChannelServerTags(this ActivityTagsCollection tags, EventStoreClientSettings settings, ChannelInfo? channelInfo) { + public static ActivityTagsCollection WithGrpcChannelServerTags(this ActivityTagsCollection tags, ChannelInfo? channelInfo) { if (channelInfo is null) return tags; + + var authorityParts = channelInfo.Channel.Target.Split(':'); - var authorityParts = channelInfo.Channel.Target.Split([':'], StringSplitOptions.RemoveEmptyEntries); - - string host = authorityParts[0]; - int port = authorityParts.Length == 1 - ? settings.ConnectivitySettings.Insecure ? 80 : 443 - : int.Parse(authorityParts[1]); - - return tags - .WithRequiredTag(TelemetryTags.Server.Address, host) - .WithRequiredTag(TelemetryTags.Server.Port, port); - } + return tags + .WithRequiredTag(TelemetryTags.Server.Address, authorityParts[0]) + .WithRequiredTag(TelemetryTags.Server.Port, int.Parse(authorityParts[1])); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ActivityTagsCollection WithClientSettingsServerTags(this ActivityTagsCollection source, EventStoreClientSettings settings) { + public static ActivityTagsCollection WithClientSettingsServerTags(this ActivityTagsCollection source, KurrentClientSettings settings) { if (settings.ConnectivitySettings.DnsGossipSeeds?.Length != 1) return source; diff --git a/src/EventStore.Client/Common/Diagnostics/Core/ActivityExtensions.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs similarity index 94% rename from src/EventStore.Client/Common/Diagnostics/Core/ActivityExtensions.cs rename to src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs index 4594d1000..4b6404f60 100644 --- a/src/EventStore.Client/Common/Diagnostics/Core/ActivityExtensions.cs +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityExtensions.cs @@ -2,10 +2,10 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -using EventStore.Diagnostics.Telemetry; -using EventStore.Diagnostics.Tracing; +using Kurrent.Diagnostics.Telemetry; +using Kurrent.Diagnostics.Tracing; -namespace EventStore.Diagnostics; +namespace Kurrent.Diagnostics; static class ActivityExtensions { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -49,4 +49,4 @@ static Activity SetActivityStatus(this Activity activity, ActivityStatus status) return activity.IsAllDataRequested ? activity.RecordException(status.Exception) : activity; } -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Common/Diagnostics/Core/ActivityStatus.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs similarity index 93% rename from src/EventStore.Client/Common/Diagnostics/Core/ActivityStatus.cs rename to src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs index a25b7326f..bab790b6c 100644 --- a/src/EventStore.Client/Common/Diagnostics/Core/ActivityStatus.cs +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatus.cs @@ -2,7 +2,7 @@ using System.Diagnostics; -namespace EventStore.Diagnostics; +namespace Kurrent.Diagnostics; record ActivityStatus(ActivityStatusCode StatusCode, string? Description, Exception? Exception) { public static ActivityStatus Ok(string? description = null) => diff --git a/src/EventStore.Client/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs similarity index 95% rename from src/EventStore.Client/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs rename to src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs index 77810aa6c..592501d11 100644 --- a/src/EventStore.Client/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityStatusCodeHelper.cs @@ -6,7 +6,7 @@ using static System.Diagnostics.ActivityStatusCode; using static System.StringComparison; -namespace EventStore.Diagnostics; +namespace Kurrent.Diagnostics; static class ActivityStatusCodeHelper { public const string UnsetStatusCodeTagValue = "UNSET"; @@ -21,4 +21,4 @@ static class ActivityStatusCodeHelper { Ok => OkStatusCodeTagValue, _ => null }; -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs similarity index 97% rename from src/EventStore.Client/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs rename to src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs index 15aa0662e..2c8c8b291 100644 --- a/src/EventStore.Client/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ActivityTagsCollectionExtensions.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -namespace EventStore.Diagnostics; +namespace Kurrent.Diagnostics; static class ActivityTagsCollectionExtensions { [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/EventStore.Client/Common/Diagnostics/Core/ExceptionExtensions.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs similarity index 95% rename from src/EventStore.Client/Common/Diagnostics/Core/ExceptionExtensions.cs rename to src/Kurrent.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs index 8403c3c01..7eb397251 100644 --- a/src/EventStore.Client/Common/Diagnostics/Core/ExceptionExtensions.cs +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/ExceptionExtensions.cs @@ -2,7 +2,7 @@ using System.Globalization; -namespace EventStore.Diagnostics; +namespace Kurrent.Diagnostics; static class ExceptionExtensions { /// @@ -22,4 +22,4 @@ public static string ToInvariantString(this Exception exception) { Thread.CurrentThread.CurrentUICulture = originalUiCulture; } } -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs similarity index 97% rename from src/EventStore.Client/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs rename to src/Kurrent.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs index f7d5e4b17..54487e3bd 100644 --- a/src/EventStore.Client/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/Telemetry/TelemetryTags.cs @@ -1,6 +1,6 @@ // ReSharper disable CheckNamespace -namespace EventStore.Diagnostics.Telemetry; +namespace Kurrent.Diagnostics.Telemetry; // The attributes below match the specification of v1.24.0 of the Open Telemetry semantic conventions. // Some attributes are ignored where not required or relevant. @@ -32,4 +32,4 @@ public static class Otel { public const string StatusCode = "otel.status_code"; public const string StatusDescription = "otel.status_description"; } -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Common/Diagnostics/Core/Tracing/TracingConstants.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs similarity index 83% rename from src/EventStore.Client/Common/Diagnostics/Core/Tracing/TracingConstants.cs rename to src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs index 26aa2be21..1e94c9ef0 100644 --- a/src/EventStore.Client/Common/Diagnostics/Core/Tracing/TracingConstants.cs +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingConstants.cs @@ -1,10 +1,10 @@ // ReSharper disable CheckNamespace -namespace EventStore.Diagnostics.Tracing; +namespace Kurrent.Diagnostics.Tracing; static partial class TracingConstants { public static class Metadata { public const string TraceId = "$traceId"; public const string SpanId = "$spanId"; } -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Common/Diagnostics/Core/Tracing/TracingMetadata.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs similarity index 95% rename from src/EventStore.Client/Common/Diagnostics/Core/Tracing/TracingMetadata.cs rename to src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs index ecb9c68c6..7580753a7 100644 --- a/src/EventStore.Client/Common/Diagnostics/Core/Tracing/TracingMetadata.cs +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Core/Tracing/TracingMetadata.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Text.Json.Serialization; -namespace EventStore.Diagnostics.Tracing; +namespace Kurrent.Diagnostics.Tracing; readonly record struct TracingMetadata( [property: JsonPropertyName(TracingConstants.Metadata.TraceId)] diff --git a/src/EventStore.Client/Common/Diagnostics/EventMetadataExtensions.cs b/src/Kurrent.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs similarity index 97% rename from src/EventStore.Client/Common/Diagnostics/EventMetadataExtensions.cs rename to src/Kurrent.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs index 4245d5bb7..d45b53156 100644 --- a/src/EventStore.Client/Common/Diagnostics/EventMetadataExtensions.cs +++ b/src/Kurrent.Client/Core/Common/Diagnostics/EventMetadataExtensions.cs @@ -1,8 +1,8 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text.Json; -using EventStore.Diagnostics; -using EventStore.Diagnostics.Tracing; +using Kurrent.Diagnostics; +using Kurrent.Diagnostics.Tracing; namespace EventStore.Client.Diagnostics; diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/KurrentClientDiagnostics.cs b/src/Kurrent.Client/Core/Common/Diagnostics/KurrentClientDiagnostics.cs new file mode 100644 index 000000000..48e437463 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/KurrentClientDiagnostics.cs @@ -0,0 +1,8 @@ +using System.Diagnostics; + +namespace EventStore.Client.Diagnostics; + +public static class KurrentClientDiagnostics { + public const string InstrumentationName = "kurrent"; + public static readonly ActivitySource ActivitySource = new(InstrumentationName); +} diff --git a/src/Kurrent.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs new file mode 100644 index 000000000..e4e0b11f9 --- /dev/null +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Telemetry/TelemetryTags.cs @@ -0,0 +1,12 @@ +// ReSharper disable CheckNamespace + +namespace Kurrent.Diagnostics.Telemetry; + +static partial class TelemetryTags { + public static class Kurrent { + public const string Stream = "db.kurrent.stream"; + public const string SubscriptionId = "db.kurrent.subscription.id"; + public const string EventId = "db.kurrent.event.id"; + public const string EventType = "db.kurrent.event.type"; + } +} diff --git a/src/EventStore.Client/Common/Diagnostics/Tracing/TracingConstants.cs b/src/Kurrent.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs similarity index 85% rename from src/EventStore.Client/Common/Diagnostics/Tracing/TracingConstants.cs rename to src/Kurrent.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs index e43102ebe..357654570 100644 --- a/src/EventStore.Client/Common/Diagnostics/Tracing/TracingConstants.cs +++ b/src/Kurrent.Client/Core/Common/Diagnostics/Tracing/TracingConstants.cs @@ -1,10 +1,10 @@ // ReSharper disable CheckNamespace -namespace EventStore.Diagnostics.Tracing; +namespace Kurrent.Diagnostics.Tracing; static partial class TracingConstants { public static class Operations { public const string Append = "streams.append"; public const string Subscribe = "streams.subscribe"; } -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Common/EnumerableTaskExtensions.cs b/src/Kurrent.Client/Core/Common/EnumerableTaskExtensions.cs similarity index 99% rename from src/EventStore.Client/Common/EnumerableTaskExtensions.cs rename to src/Kurrent.Client/Core/Common/EnumerableTaskExtensions.cs index eb4517006..5be066ab5 100644 --- a/src/EventStore.Client/Common/EnumerableTaskExtensions.cs +++ b/src/Kurrent.Client/Core/Common/EnumerableTaskExtensions.cs @@ -8,4 +8,4 @@ static class EnumerableTaskExtensions { [DebuggerStepThrough] public static Task WhenAll(this IEnumerable> source) => Task.WhenAll(source); -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Common/EpochExtensions.cs b/src/Kurrent.Client/Core/Common/EpochExtensions.cs similarity index 99% rename from src/EventStore.Client/Common/EpochExtensions.cs rename to src/Kurrent.Client/Core/Common/EpochExtensions.cs index db59e620d..0643bcecb 100644 --- a/src/EventStore.Client/Common/EpochExtensions.cs +++ b/src/Kurrent.Client/Core/Common/EpochExtensions.cs @@ -22,4 +22,4 @@ static class EpochExtensions { public static DateTime FromTicksSinceEpoch(this long value) => new(UnixEpoch.Ticks + value, DateTimeKind.Utc); public static long ToTicksSinceEpoch(this DateTime value) => (value - UnixEpoch).Ticks; -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Common/EventStoreCallOptions.cs b/src/Kurrent.Client/Core/Common/KurrentCallOptions.cs similarity index 89% rename from src/EventStore.Client/Common/EventStoreCallOptions.cs rename to src/Kurrent.Client/Core/Common/KurrentCallOptions.cs index e6058a170..0644cffea 100644 --- a/src/EventStore.Client/Common/EventStoreCallOptions.cs +++ b/src/Kurrent.Client/Core/Common/KurrentCallOptions.cs @@ -3,10 +3,10 @@ namespace EventStore.Client; -static class EventStoreCallOptions { +static class KurrentCallOptions { // deadline falls back to infinity public static CallOptions CreateStreaming( - EventStoreClientSettings settings, + KurrentClientSettings settings, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default ) => @@ -14,7 +14,7 @@ public static CallOptions CreateStreaming( // deadline falls back to connection DefaultDeadline public static CallOptions CreateNonStreaming( - EventStoreClientSettings settings, + KurrentClientSettings settings, CancellationToken cancellationToken ) => Create( @@ -25,7 +25,7 @@ CancellationToken cancellationToken ); public static CallOptions CreateNonStreaming( - EventStoreClientSettings settings, TimeSpan? deadline, + KurrentClientSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken ) => Create( @@ -36,7 +36,7 @@ public static CallOptions CreateNonStreaming( ); static CallOptions Create( - EventStoreClientSettings settings, TimeSpan? deadline, + KurrentClientSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken ) => new( @@ -71,4 +71,4 @@ static CallOptions Create( : timeoutAfter.Value == TimeSpan.MaxValue || timeoutAfter.Value == InfiniteTimeSpan ? DateTime.MaxValue : DateTime.UtcNow.Add(timeoutAfter.Value); -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Common/MetadataExtensions.cs b/src/Kurrent.Client/Core/Common/MetadataExtensions.cs similarity index 99% rename from src/EventStore.Client/Common/MetadataExtensions.cs rename to src/Kurrent.Client/Core/Common/MetadataExtensions.cs index e547970fd..e7311c37f 100644 --- a/src/EventStore.Client/Common/MetadataExtensions.cs +++ b/src/Kurrent.Client/Core/Common/MetadataExtensions.cs @@ -26,4 +26,4 @@ public static int GetIntValueOrDefault(this Metadata metadata, string key) => metadata.TryGetValue(key, out var s) && int.TryParse(s, out var value) ? value : default; -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Common/Shims/Index.cs b/src/Kurrent.Client/Core/Common/Shims/Index.cs similarity index 100% rename from src/EventStore.Client/Common/Shims/Index.cs rename to src/Kurrent.Client/Core/Common/Shims/Index.cs diff --git a/src/EventStore.Client/Common/Shims/IsExternalInit.cs b/src/Kurrent.Client/Core/Common/Shims/IsExternalInit.cs similarity index 100% rename from src/EventStore.Client/Common/Shims/IsExternalInit.cs rename to src/Kurrent.Client/Core/Common/Shims/IsExternalInit.cs diff --git a/src/EventStore.Client/Common/Shims/Range.cs b/src/Kurrent.Client/Core/Common/Shims/Range.cs similarity index 100% rename from src/EventStore.Client/Common/Shims/Range.cs rename to src/Kurrent.Client/Core/Common/Shims/Range.cs diff --git a/src/EventStore.Client/Common/Shims/TaskCompletionSource.cs b/src/Kurrent.Client/Core/Common/Shims/TaskCompletionSource.cs similarity index 100% rename from src/EventStore.Client/Common/Shims/TaskCompletionSource.cs rename to src/Kurrent.Client/Core/Common/Shims/TaskCompletionSource.cs diff --git a/src/EventStore.Client/DefaultRequestVersionHandler.cs b/src/Kurrent.Client/Core/DefaultRequestVersionHandler.cs similarity index 100% rename from src/EventStore.Client/DefaultRequestVersionHandler.cs rename to src/Kurrent.Client/Core/DefaultRequestVersionHandler.cs diff --git a/src/EventStore.Client/EndPointExtensions.cs b/src/Kurrent.Client/Core/EndPointExtensions.cs similarity index 100% rename from src/EventStore.Client/EndPointExtensions.cs rename to src/Kurrent.Client/Core/EndPointExtensions.cs diff --git a/src/EventStore.Client/EventData.cs b/src/Kurrent.Client/Core/EventData.cs similarity index 100% rename from src/EventStore.Client/EventData.cs rename to src/Kurrent.Client/Core/EventData.cs diff --git a/src/EventStore.Client/EventRecord.cs b/src/Kurrent.Client/Core/EventRecord.cs similarity index 97% rename from src/EventStore.Client/EventRecord.cs rename to src/Kurrent.Client/Core/EventRecord.cs index 67a7b04cf..531362661 100644 --- a/src/EventStore.Client/EventRecord.cs +++ b/src/Kurrent.Client/Core/EventRecord.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - namespace EventStore.Client { /// /// Represents a previously written event. diff --git a/src/EventStore.Client/EventTypeFilter.cs b/src/Kurrent.Client/Core/EventTypeFilter.cs similarity index 100% rename from src/EventStore.Client/EventTypeFilter.cs rename to src/Kurrent.Client/Core/EventTypeFilter.cs diff --git a/src/EventStore.Client/Exceptions/AccessDeniedException.cs b/src/Kurrent.Client/Core/Exceptions/AccessDeniedException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/AccessDeniedException.cs rename to src/Kurrent.Client/Core/Exceptions/AccessDeniedException.cs diff --git a/src/EventStore.Client/Exceptions/ConnectionString/ConnectionStringParseException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs similarity index 85% rename from src/EventStore.Client/Exceptions/ConnectionString/ConnectionStringParseException.cs rename to src/Kurrent.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs index cb79f161d..2cb8c1ac5 100644 --- a/src/EventStore.Client/Exceptions/ConnectionString/ConnectionStringParseException.cs +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/ConnectionStringParseException.cs @@ -2,7 +2,7 @@ namespace EventStore.Client { /// - /// The base exception that is thrown when an EventStoreDB connection string could not be parsed. + /// The base exception that is thrown when an KurrentDB connection string could not be parsed. /// public class ConnectionStringParseException : Exception { /// diff --git a/src/EventStore.Client/Exceptions/ConnectionString/DuplicateKeyException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs similarity index 77% rename from src/EventStore.Client/Exceptions/ConnectionString/DuplicateKeyException.cs rename to src/Kurrent.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs index 4027d08e8..5cc3c3812 100644 --- a/src/EventStore.Client/Exceptions/ConnectionString/DuplicateKeyException.cs +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/DuplicateKeyException.cs @@ -1,6 +1,6 @@ namespace EventStore.Client { /// - /// The exception that is thrown when a key in the EventStoreDB connection string is duplicated. + /// The exception that is thrown when a key in the KurrentDB connection string is duplicated. /// public class DuplicateKeyException : ConnectionStringParseException { /// diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidClientCertificateException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs similarity index 90% rename from src/EventStore.Client/Exceptions/ConnectionString/InvalidClientCertificateException.cs rename to src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs index 8415742f8..1caf9f9fb 100644 --- a/src/EventStore.Client/Exceptions/ConnectionString/InvalidClientCertificateException.cs +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidClientCertificateException.cs @@ -1,6 +1,6 @@ namespace EventStore.Client { /// - /// The exception that is thrown when a certificate is invalid or not found in the EventStoreDB connection string. + /// The exception that is thrown when a certificate is invalid or not found in the KurrentDB connection string. /// public class InvalidClientCertificateException : ConnectionStringParseException { /// @@ -11,4 +11,4 @@ public class InvalidClientCertificateException : ConnectionStringParseException public InvalidClientCertificateException(string message, Exception? innerException = null) : base(message, innerException) { } } -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidHostException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs similarity index 91% rename from src/EventStore.Client/Exceptions/ConnectionString/InvalidHostException.cs rename to src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs index 27ba8f615..696a81c32 100644 --- a/src/EventStore.Client/Exceptions/ConnectionString/InvalidHostException.cs +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidHostException.cs @@ -1,6 +1,6 @@ namespace EventStore.Client { /// - /// The exception that is thrown when there is an invalid host in the EventStoreDB connection string. + /// The exception that is thrown when there is an invalid host in the KurrentDB connection string. /// public class InvalidHostException : ConnectionStringParseException { /// diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidKeyValuePairException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs similarity index 90% rename from src/EventStore.Client/Exceptions/ConnectionString/InvalidKeyValuePairException.cs rename to src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs index 2e6fc58df..d00e08445 100644 --- a/src/EventStore.Client/Exceptions/ConnectionString/InvalidKeyValuePairException.cs +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidKeyValuePairException.cs @@ -1,6 +1,6 @@ namespace EventStore.Client { /// - /// The exception that is thrown when an invalid key value pair is found in an EventStoreDB connection string. + /// The exception that is thrown when an invalid key value pair is found in an KurrentDB connection string. /// public class InvalidKeyValuePairException : ConnectionStringParseException { /// diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidSchemeException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs similarity index 92% rename from src/EventStore.Client/Exceptions/ConnectionString/InvalidSchemeException.cs rename to src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs index 766c13f37..3b5cf2b18 100644 --- a/src/EventStore.Client/Exceptions/ConnectionString/InvalidSchemeException.cs +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSchemeException.cs @@ -1,6 +1,6 @@ namespace EventStore.Client { /// - /// The exception that is thrown when an invalid scheme is defined in the EventStoreDB connection string. + /// The exception that is thrown when an invalid scheme is defined in the KurrentDB connection string. /// public class InvalidSchemeException : ConnectionStringParseException { /// diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidSettingException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs similarity index 90% rename from src/EventStore.Client/Exceptions/ConnectionString/InvalidSettingException.cs rename to src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs index 0bb5da667..71dcc6edc 100644 --- a/src/EventStore.Client/Exceptions/ConnectionString/InvalidSettingException.cs +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidSettingException.cs @@ -1,6 +1,6 @@ namespace EventStore.Client { /// - /// The exception that is thrown when an invalid setting is found in an EventStoreDB connection string. + /// The exception that is thrown when an invalid setting is found in an KurrentDB connection string. /// public class InvalidSettingException : ConnectionStringParseException { /// diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidUserCredentialsException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs similarity index 86% rename from src/EventStore.Client/Exceptions/ConnectionString/InvalidUserCredentialsException.cs rename to src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs index bd515fee4..1d4544a61 100644 --- a/src/EventStore.Client/Exceptions/ConnectionString/InvalidUserCredentialsException.cs +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/InvalidUserCredentialsException.cs @@ -1,6 +1,6 @@ namespace EventStore.Client { /// - /// The exception that is thrown when an invalid is specified in the EventStoreDB connection string. + /// The exception that is thrown when an invalid is specified in the KurrentDB connection string. /// public class InvalidUserCredentialsException : ConnectionStringParseException { /// diff --git a/src/EventStore.Client/Exceptions/ConnectionString/NoSchemeException.cs b/src/Kurrent.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs similarity index 90% rename from src/EventStore.Client/Exceptions/ConnectionString/NoSchemeException.cs rename to src/Kurrent.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs index 7551ad467..08432be97 100644 --- a/src/EventStore.Client/Exceptions/ConnectionString/NoSchemeException.cs +++ b/src/Kurrent.Client/Core/Exceptions/ConnectionString/NoSchemeException.cs @@ -1,6 +1,6 @@ namespace EventStore.Client { /// - /// The exception that is thrown when no scheme was specified in the EventStoreDB connection string. + /// The exception that is thrown when no scheme was specified in the KurrentDB connection string. /// public class NoSchemeException : ConnectionStringParseException { /// diff --git a/src/EventStore.Client/Exceptions/DiscoveryException.cs b/src/Kurrent.Client/Core/Exceptions/DiscoveryException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/DiscoveryException.cs rename to src/Kurrent.Client/Core/Exceptions/DiscoveryException.cs diff --git a/src/EventStore.Client/Exceptions/NotAuthenticatedException.cs b/src/Kurrent.Client/Core/Exceptions/NotAuthenticatedException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/NotAuthenticatedException.cs rename to src/Kurrent.Client/Core/Exceptions/NotAuthenticatedException.cs diff --git a/src/EventStore.Client/Exceptions/NotLeaderException.cs b/src/Kurrent.Client/Core/Exceptions/NotLeaderException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/NotLeaderException.cs rename to src/Kurrent.Client/Core/Exceptions/NotLeaderException.cs diff --git a/src/EventStore.Client/Exceptions/RequiredMetadataPropertyMissingException.cs b/src/Kurrent.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/RequiredMetadataPropertyMissingException.cs rename to src/Kurrent.Client/Core/Exceptions/RequiredMetadataPropertyMissingException.cs diff --git a/src/EventStore.Client/Exceptions/ScavengeNotFoundException.cs b/src/Kurrent.Client/Core/Exceptions/ScavengeNotFoundException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/ScavengeNotFoundException.cs rename to src/Kurrent.Client/Core/Exceptions/ScavengeNotFoundException.cs diff --git a/src/EventStore.Client/Exceptions/StreamDeletedException.cs b/src/Kurrent.Client/Core/Exceptions/StreamDeletedException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/StreamDeletedException.cs rename to src/Kurrent.Client/Core/Exceptions/StreamDeletedException.cs diff --git a/src/EventStore.Client/Exceptions/StreamNotFoundException.cs b/src/Kurrent.Client/Core/Exceptions/StreamNotFoundException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/StreamNotFoundException.cs rename to src/Kurrent.Client/Core/Exceptions/StreamNotFoundException.cs diff --git a/src/EventStore.Client/Exceptions/UserNotFoundException.cs b/src/Kurrent.Client/Core/Exceptions/UserNotFoundException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/UserNotFoundException.cs rename to src/Kurrent.Client/Core/Exceptions/UserNotFoundException.cs diff --git a/src/EventStore.Client/Exceptions/WrongExpectedVersionException.cs b/src/Kurrent.Client/Core/Exceptions/WrongExpectedVersionException.cs similarity index 100% rename from src/EventStore.Client/Exceptions/WrongExpectedVersionException.cs rename to src/Kurrent.Client/Core/Exceptions/WrongExpectedVersionException.cs diff --git a/src/EventStore.Client/FromAll.cs b/src/Kurrent.Client/Core/FromAll.cs similarity index 100% rename from src/EventStore.Client/FromAll.cs rename to src/Kurrent.Client/Core/FromAll.cs diff --git a/src/EventStore.Client/FromStream.cs b/src/Kurrent.Client/Core/FromStream.cs similarity index 100% rename from src/EventStore.Client/FromStream.cs rename to src/Kurrent.Client/Core/FromStream.cs diff --git a/src/EventStore.Client/GossipChannelSelector.cs b/src/Kurrent.Client/Core/GossipChannelSelector.cs similarity index 97% rename from src/EventStore.Client/GossipChannelSelector.cs rename to src/Kurrent.Client/Core/GossipChannelSelector.cs index 633ab0d84..5471120b6 100644 --- a/src/EventStore.Client/GossipChannelSelector.cs +++ b/src/Kurrent.Client/Core/GossipChannelSelector.cs @@ -10,14 +10,14 @@ namespace EventStore.Client { // Thread safe internal class GossipChannelSelector : IChannelSelector { - private readonly EventStoreClientSettings _settings; + private readonly KurrentClientSettings _settings; private readonly ChannelCache _channels; private readonly IGossipClient _gossipClient; private readonly ILogger _log; private readonly NodeSelector _nodeSelector; public GossipChannelSelector( - EventStoreClientSettings settings, + KurrentClientSettings settings, ChannelCache channelCache, IGossipClient gossipClient) { diff --git a/src/EventStore.Client/GrpcGossipClient.cs b/src/Kurrent.Client/Core/GrpcGossipClient.cs similarity index 81% rename from src/EventStore.Client/GrpcGossipClient.cs rename to src/Kurrent.Client/Core/GrpcGossipClient.cs index 655d05b5f..5291bfe6c 100644 --- a/src/EventStore.Client/GrpcGossipClient.cs +++ b/src/Kurrent.Client/Core/GrpcGossipClient.cs @@ -6,9 +6,9 @@ namespace EventStore.Client { internal class GrpcGossipClient : IGossipClient { - private readonly EventStoreClientSettings _settings; + private readonly KurrentClientSettings _settings; - public GrpcGossipClient(EventStoreClientSettings settings) { + public GrpcGossipClient(KurrentClientSettings settings) { _settings = settings; } @@ -16,7 +16,7 @@ public GrpcGossipClient(EventStoreClientSettings settings) { var client = new Gossip.Gossip.GossipClient(channel); using var call = client.ReadAsync( new Empty(), - EventStoreCallOptions.CreateNonStreaming(_settings, ct)); + KurrentCallOptions.CreateNonStreaming(_settings, ct)); var result = await call.ResponseAsync.ConfigureAwait(false); return new(result.Members.Select(x => diff --git a/src/EventStore.Client/GrpcServerCapabilitiesClient.cs b/src/Kurrent.Client/Core/GrpcServerCapabilitiesClient.cs similarity index 93% rename from src/EventStore.Client/GrpcServerCapabilitiesClient.cs rename to src/Kurrent.Client/Core/GrpcServerCapabilitiesClient.cs index 02cd41222..1dc29dce0 100644 --- a/src/EventStore.Client/GrpcServerCapabilitiesClient.cs +++ b/src/Kurrent.Client/Core/GrpcServerCapabilitiesClient.cs @@ -5,9 +5,9 @@ namespace EventStore.Client { internal class GrpcServerCapabilitiesClient : IServerCapabilitiesClient { - private readonly EventStoreClientSettings _settings; + private readonly KurrentClientSettings _settings; - public GrpcServerCapabilitiesClient(EventStoreClientSettings settings) { + public GrpcServerCapabilitiesClient(KurrentClientSettings settings) { _settings = settings; } @@ -18,7 +18,7 @@ public async Task GetAsync( var client = new ServerFeatures.ServerFeatures.ServerFeaturesClient(callInvoker); using var call = client.GetSupportedMethodsAsync( new(), - EventStoreCallOptions.CreateNonStreaming( + KurrentCallOptions.CreateNonStreaming( _settings, _settings.ConnectivitySettings.GossipTimeout, null, @@ -31,7 +31,7 @@ public async Task GetAsync( var supportsPersistentSubscriptionsRestartSubsystem = false; var supportsPersistentSubscriptionsReplayParked = false; var supportsPersistentSubscriptionsList = false; - + var response = await call.ResponseAsync.ConfigureAwait(false); foreach (var supportedMethod in response.Methods) { diff --git a/src/EventStore.Client/HashCode.cs b/src/Kurrent.Client/Core/HashCode.cs similarity index 100% rename from src/EventStore.Client/HashCode.cs rename to src/Kurrent.Client/Core/HashCode.cs diff --git a/src/EventStore.Client/HttpFallback.cs b/src/Kurrent.Client/Core/HttpFallback.cs similarity index 98% rename from src/EventStore.Client/HttpFallback.cs rename to src/Kurrent.Client/Core/HttpFallback.cs index 3e5420e9a..0f2bfe02d 100644 --- a/src/EventStore.Client/HttpFallback.cs +++ b/src/Kurrent.Client/Core/HttpFallback.cs @@ -13,7 +13,7 @@ internal class HttpFallback : IDisposable { private readonly UserCredentials? _defaultCredentials; private readonly string _addressScheme; - internal HttpFallback(EventStoreClientSettings settings) { + internal HttpFallback(KurrentClientSettings settings) { _addressScheme = settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme; _defaultCredentials = settings.DefaultCredentials; diff --git a/src/EventStore.Client/IChannelSelector.cs b/src/Kurrent.Client/Core/IChannelSelector.cs similarity index 100% rename from src/EventStore.Client/IChannelSelector.cs rename to src/Kurrent.Client/Core/IChannelSelector.cs diff --git a/src/EventStore.Client/IEventFilter.cs b/src/Kurrent.Client/Core/IEventFilter.cs similarity index 100% rename from src/EventStore.Client/IEventFilter.cs rename to src/Kurrent.Client/Core/IEventFilter.cs diff --git a/src/EventStore.Client/IGossipClient.cs b/src/Kurrent.Client/Core/IGossipClient.cs similarity index 100% rename from src/EventStore.Client/IGossipClient.cs rename to src/Kurrent.Client/Core/IGossipClient.cs diff --git a/src/EventStore.Client/IPosition.cs b/src/Kurrent.Client/Core/IPosition.cs similarity index 100% rename from src/EventStore.Client/IPosition.cs rename to src/Kurrent.Client/Core/IPosition.cs diff --git a/src/EventStore.Client/IServerCapabilitiesClient.cs b/src/Kurrent.Client/Core/IServerCapabilitiesClient.cs similarity index 100% rename from src/EventStore.Client/IServerCapabilitiesClient.cs rename to src/Kurrent.Client/Core/IServerCapabilitiesClient.cs diff --git a/src/EventStore.Client/Interceptors/ConnectionNameInterceptor.cs b/src/Kurrent.Client/Core/Interceptors/ConnectionNameInterceptor.cs similarity index 100% rename from src/EventStore.Client/Interceptors/ConnectionNameInterceptor.cs rename to src/Kurrent.Client/Core/Interceptors/ConnectionNameInterceptor.cs diff --git a/src/EventStore.Client/Interceptors/ReportLeaderInterceptor.cs b/src/Kurrent.Client/Core/Interceptors/ReportLeaderInterceptor.cs similarity index 100% rename from src/EventStore.Client/Interceptors/ReportLeaderInterceptor.cs rename to src/Kurrent.Client/Core/Interceptors/ReportLeaderInterceptor.cs diff --git a/src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs b/src/Kurrent.Client/Core/Interceptors/TypedExceptionInterceptor.cs similarity index 100% rename from src/EventStore.Client/Interceptors/TypedExceptionInterceptor.cs rename to src/Kurrent.Client/Core/Interceptors/TypedExceptionInterceptor.cs diff --git a/src/EventStore.Client/EventStoreClientBase.cs b/src/Kurrent.Client/Core/KurrentClientBase.cs similarity index 93% rename from src/EventStore.Client/EventStoreClientBase.cs rename to src/Kurrent.Client/Core/KurrentClientBase.cs index 2b4a5b2f6..923c6f334 100644 --- a/src/EventStore.Client/EventStoreClientBase.cs +++ b/src/Kurrent.Client/Core/KurrentClientBase.cs @@ -5,9 +5,9 @@ namespace EventStore.Client { /// - /// The base class used by clients used to communicate with the EventStoreDB. + /// The base class used by clients used to communicate with the KurrentDB. /// - public abstract class EventStoreClientBase : + public abstract class KurrentClientBase : IDisposable, // for grpc.net we can dispose synchronously, but not for grpc.core IAsyncDisposable { private readonly Dictionary> _exceptionMap; @@ -19,15 +19,15 @@ public abstract class EventStoreClientBase : /// The name of the connection. public string ConnectionName { get; } - /// The . - protected EventStoreClientSettings Settings { get; } + /// The . + protected KurrentClientSettings Settings { get; } - /// Constructs a new . - protected EventStoreClientBase( - EventStoreClientSettings? settings, + /// Constructs a new . + protected KurrentClientBase( + KurrentClientSettings? settings, Dictionary> exceptionMap ) { - Settings = settings ?? new EventStoreClientSettings(); + Settings = settings ?? new KurrentClientSettings(); _exceptionMap = exceptionMap; _cts = new CancellationTokenSource(); _channelCache = new(Settings); diff --git a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs b/src/Kurrent.Client/Core/KurrentClientConnectivitySettings.cs similarity index 89% rename from src/EventStore.Client/EventStoreClientConnectivitySettings.cs rename to src/Kurrent.Client/Core/KurrentClientConnectivitySettings.cs index 92890d2f2..4ea90dcaf 100644 --- a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs +++ b/src/Kurrent.Client/Core/KurrentClientConnectivitySettings.cs @@ -4,15 +4,15 @@ namespace EventStore.Client { /// - /// A class used to describe how to connect to an instance of EventStoreDB. + /// A class used to describe how to connect to an instance of KurrentDB. /// - public class EventStoreClientConnectivitySettings { + public class KurrentClientConnectivitySettings { private const int DefaultPort = 2113; private bool _insecure; private Uri? _address; /// - /// The of the EventStoreDB. Use this when connecting to a single node. + /// The of the KurrentDB. Use this when connecting to a single node. /// public Uri? Address { get => IsSingleNode ? _address : null; @@ -84,7 +84,7 @@ public Uri? Address { public TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10); /// - /// True if pointing to a single EventStoreDB node. + /// True if pointing to a single KurrentDB node. /// public bool IsSingleNode => GossipSeeds.Length == 0; @@ -113,9 +113,9 @@ public bool Insecure { public X509Certificate2? ClientCertificate { get; set; } /// - /// The default . + /// The default . /// - public static EventStoreClientConnectivitySettings Default => new EventStoreClientConnectivitySettings { + public static KurrentClientConnectivitySettings Default => new KurrentClientConnectivitySettings { MaxDiscoverAttempts = 10, GossipTimeout = TimeSpan.FromSeconds(5), DiscoveryInterval = TimeSpan.FromMilliseconds(100), diff --git a/src/EventStore.Client/EventStoreClientOperationOptions.cs b/src/Kurrent.Client/Core/KurrentClientOperationOptions.cs similarity index 78% rename from src/EventStore.Client/EventStoreClientOperationOptions.cs rename to src/Kurrent.Client/Core/KurrentClientOperationOptions.cs index 94561e213..36c8d5869 100644 --- a/src/EventStore.Client/EventStoreClientOperationOptions.cs +++ b/src/Kurrent.Client/Core/KurrentClientOperationOptions.cs @@ -6,7 +6,7 @@ namespace EventStore.Client { /// /// A class representing the options to apply to an individual operation. /// - public class EventStoreClientOperationOptions { + public class KurrentClientOperationOptions { /// /// Whether or not to immediately throw a when an append fails. /// @@ -24,9 +24,9 @@ public class EventStoreClientOperationOptions { null!; /// - /// The default . + /// The default . /// - public static EventStoreClientOperationOptions Default => new() { + public static KurrentClientOperationOptions Default => new() { ThrowOnAppendFailure = true, GetAuthenticationHeaderValue = (userCredentials, _) => new ValueTask(userCredentials.ToString()), BatchAppendSize = 3 * 1024 * 1024 @@ -34,10 +34,10 @@ public class EventStoreClientOperationOptions { /// - /// Clones a copy of the current . + /// Clones a copy of the current . /// /// - public EventStoreClientOperationOptions Clone() => new() { + public KurrentClientOperationOptions Clone() => new() { ThrowOnAppendFailure = ThrowOnAppendFailure, GetAuthenticationHeaderValue = GetAuthenticationHeaderValue, BatchAppendSize = BatchAppendSize diff --git a/src/Kurrent.Client/Core/KurrentClientSerializationSettings.cs b/src/Kurrent.Client/Core/KurrentClientSerializationSettings.cs new file mode 100644 index 000000000..294cdc169 --- /dev/null +++ b/src/Kurrent.Client/Core/KurrentClientSerializationSettings.cs @@ -0,0 +1,360 @@ +using System.Text.Json; +using Kurrent.Client.Core.Serialization; + +namespace EventStore.Client; + +/// +/// Provides configuration options for messages serialization and deserialization in the KurrentDB client. +/// +public class KurrentClientSerializationSettings { + /// + /// The serializer responsible for handling JSON-formatted data. This serializer is used both for + /// serializing outgoing JSON messages and deserializing incoming JSON messages. If not specified, + /// a default System.Text.Json serializer will be used with standard settings. + ///
+ /// That also allows you to bring your custom JSON serializer implementation (e.g. JSON.NET) + ///
+ public ISerializer? JsonSerializer { get; set; } + + /// + /// The serializer responsible for handling binary data formats. This is used when working with + /// binary-encoded messages rather than text-based formats (e.g. Protobuf or Avro). Required when storing + /// or retrieving content with "application/octet-stream" content type + /// + public ISerializer? BytesSerializer { get; set; } + + /// + /// Determines which serialization format (JSON or binary) is used by default when writing messages + /// where the content type isn't explicitly specified. The default content type is "application/json" + /// + public ContentType DefaultContentType { get; set; } = ContentType.Json; + + /// + /// Defines the custom strategy used to map between the type name stored in messages and .NET type names. + /// If not provided the default will be used. + /// It resolves the CLR type name to the format: "{stream category name}-{CLR Message Type}". + /// You can provide your own implementation of + /// and register it here to override the default behaviour + /// + public IMessageTypeNamingStrategy? MessageTypeNamingStrategy { get; set; } + + /// + /// Allows to register mapping of CLR message types to their corresponding message type names used in serialized messages. + /// + public IDictionary MessageTypeMap { get; set; } = new Dictionary(); + + /// + /// Registers CLR message types that can be appended to the specific stream category. + /// Types will have message type names resolved based on the used + /// + public IDictionary CategoryMessageTypesMap { get; set; } = new Dictionary(); + + /// + /// Specifies the CLR type that should be used when deserializing metadata for all events. + /// When set, the client will attempt to deserialize event metadata into this type. + /// If not provided, will be used. + /// + public Type? DefaultMetadataType { get; set; } + + /// + /// Creates a new instance of serialization settings with either default values or custom configuration. + /// This factory method is the recommended way to create serialization settings for the KurrentDB client. + /// + /// Optional callback to customize the settings. If null, default settings are used. + /// A fully configured instance ready to be used with the KurrentDB client. + /// + /// + /// var settings = KurrentClientSerializationSettings.Default(options => { + /// options.RegisterMessageType<UserCreated>("user-created"); + /// options.RegisterMessageType<UserUpdated>("user-updated"); + /// options.RegisterMessageTypeForCategory<UserCreated>("user"); + /// }); + /// + /// + public static KurrentClientSerializationSettings Default( + Action? configure = null + ) { + var settings = new KurrentClientSerializationSettings(); + + configure?.Invoke(settings); + + return settings; + } + + /// + /// Configures the JSON serializer using custom options while inheriting from the default System.Text.Json settings. + /// This allows fine-tuning serialization behavior such as case sensitivity, property naming, etc. + /// + /// A function that receives the default options and returns modified options. + /// The current instance for method chaining. + /// + /// + /// settings.UseJsonSettings(options => { + /// options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + /// options.WriteIndented = true; + /// options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + /// return options; + /// }); + /// + /// + public KurrentClientSerializationSettings UseJsonSettings( + Func configure + ) { + JsonSerializer = new SystemTextJsonSerializer( + new SystemTextJsonSerializationSettings + { Options = configure(SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions) } + ); + + return this; + } + + /// + /// Configures JSON serialization using provided System.Text.Json serializer options. + /// + /// The JSON serializer options to use. + /// The current instance for method chaining. + public KurrentClientSerializationSettings UseJsonSettings(JsonSerializerOptions systemTextJsonSerializerOptions) { + JsonSerializer = new SystemTextJsonSerializer( + new SystemTextJsonSerializationSettings { Options = systemTextJsonSerializerOptions } + ); + + return this; + } + + /// + /// Configures JSON serialization using provided + /// + /// The SystemTextJson serialization settings to use. + /// The current instance for method chaining. + public KurrentClientSerializationSettings UseJsonSettings( + SystemTextJsonSerializationSettings jsonSerializationSettings + ) { + JsonSerializer = new SystemTextJsonSerializer(jsonSerializationSettings); + + return this; + } + + /// + /// Sets a custom JSON serializer implementation. + /// That also allows you to bring your custom JSON serializer implementation (e.g. JSON.NET) + /// + /// The serializer to use for JSON content. + /// The current instance for method chaining. + public KurrentClientSerializationSettings UseJsonSerializer(ISerializer serializer) { + JsonSerializer = serializer; + + return this; + } + + /// + /// Sets a custom binary serializer implementation. + /// That also allows you to bring your custom binary serializer implementation (e.g. Protobuf or Avro) + /// + /// The serializer to use for binary content. + /// The current instance for method chaining. + public KurrentClientSerializationSettings UseBytesSerializer(ISerializer serializer) { + BytesSerializer = serializer; + + return this; + } + + /// + /// Configures a custom message type naming strategy. + /// + /// The type of naming strategy to use. + /// The current instance for method chaining. + public KurrentClientSerializationSettings UseMessageTypeNamingStrategy() + where TCustomMessageTypeResolutionStrategy : IMessageTypeNamingStrategy, new() => + UseMessageTypeNamingStrategy(new TCustomMessageTypeResolutionStrategy()); + + /// + /// Configures a custom message type naming strategy. + /// + /// The naming strategy instance to use. + /// The current instance for method chaining. + public KurrentClientSerializationSettings UseMessageTypeNamingStrategy( + IMessageTypeNamingStrategy messageTypeNamingStrategy + ) { + MessageTypeNamingStrategy = messageTypeNamingStrategy; + + return this; + } + + /// + /// Associates a message type with a specific stream category to enable automatic deserialization. + /// In event sourcing, streams are often prefixed with a category (e.g., "user-123", "order-456"). + /// This method tells the client which message types can appear in streams of a given category. + /// + /// The event or message type that can appear in the category's streams. + /// The category prefix (e.g., "user", "order", "account"). + /// The current instance for method chaining. + /// + /// + /// // Register event types that can appear in user streams + /// settings.RegisterMessageTypeForCategory<UserCreated>("user") + /// .RegisterMessageTypeForCategory<UserUpdated>("user") + /// .RegisterMessageTypeForCategory<UserDeleted>("user"); + /// + /// + public KurrentClientSerializationSettings RegisterMessageTypeForCategory(string categoryName) => + RegisterMessageTypeForCategory(categoryName, typeof(T)); + + /// + /// Registers multiple message types for a specific stream category. + /// + /// The category name to register the types with. + /// The message types to register. + /// The current instance for method chaining. + public KurrentClientSerializationSettings RegisterMessageTypeForCategory(string categoryName, params Type[] types) { + CategoryMessageTypesMap[categoryName] = CategoryMessageTypesMap.TryGetValue(categoryName, out var current) + ? [..current, ..types] + : types; + + return this; + } + + /// + /// Maps a .NET type to a specific message type name that will be stored in the message metadata. + /// This mapping is used during automatic deserialization, as it tells the client which CLR type + /// to instantiate when encountering a message with a particular type name in the database. + /// + /// The .NET type to register (typically a message class). + /// The string identifier to use for this type in the database. + /// The current instance for method chaining. + /// + /// The type name is often different from the .NET type name to support versioning and evolution + /// of your domain model without breaking existing stored messages. + /// + /// + /// + /// // Register me types with their corresponding type identifiers + /// settings.RegisterMessageType<UserCreated>("user-created-v1") + /// .RegisterMessageType<OrderPlaced>("order-placed-v2"); + /// + /// + public KurrentClientSerializationSettings RegisterMessageType(string typeName) => + RegisterMessageType(typeof(T), typeName); + + /// + /// Registers a message type with a specific type name. + /// + /// The message type to register. + /// The type name to register for the message type. + /// The current instance for method chaining. + public KurrentClientSerializationSettings RegisterMessageType(Type type, string typeName) { + MessageTypeMap[type] = typeName; + + return this; + } + + /// + /// Registers multiple message types with their corresponding type names. + /// + /// Dictionary mapping types to their type names. + /// The current instance for method chaining. + public KurrentClientSerializationSettings RegisterMessageTypes(IDictionary typeMap) { + foreach (var map in typeMap) { + MessageTypeMap[map.Key] = map.Value; + } + + return this; + } + + /// + /// Configures a strongly-typed metadata class for all mes in the system. + /// This enables accessing metadata properties in a type-safe manner rather than using dynamic objects. + /// + /// The metadata class type containing properties matching the expected metadata fields. + /// The current instance for method chaining. + public KurrentClientSerializationSettings UseMetadataType() => + UseMetadataType(typeof(T)); + + /// + /// Configures a strongly-typed metadata class for all mes in the system. + /// This enables accessing metadata properties in a type-safe manner rather than using dynamic objects. + /// + /// The metadata class type containing properties matching the expected metadata fields. + /// The current instance for method chaining. + public KurrentClientSerializationSettings UseMetadataType(Type type) { + DefaultMetadataType = type; + + return this; + } + + /// + /// Creates a deep copy of the current serialization settings. + /// + /// A new instance with copied settings. + internal KurrentClientSerializationSettings Clone() { + return new KurrentClientSerializationSettings { + BytesSerializer = BytesSerializer, + JsonSerializer = JsonSerializer, + DefaultContentType = DefaultContentType, + MessageTypeMap = new Dictionary(MessageTypeMap), + CategoryMessageTypesMap = new Dictionary(CategoryMessageTypesMap), + MessageTypeNamingStrategy = MessageTypeNamingStrategy + }; + } +} + +/// +/// Provides operation-specific serialization settings that override the global client configuration +/// for individual operations like reading from or appending to streams. This allows fine-tuning +/// serialization behavior on a per-operation basis without changing the client-wide settings. +/// +public class OperationSerializationSettings { + /// + /// Controls whether mes should be automatically deserialized for this specific operation. + /// When enabled (the default), messages will be converted to their appropriate CLR types. + /// When disabled, messages will be returned in their raw serialized form. + /// + public AutomaticDeserialization AutomaticDeserialization { get; private set; } = AutomaticDeserialization.Enabled; + + /// + /// A callback that allows customizing serialization settings for this specific operation. + /// This can be used to override type mappings, serializers, or other settings just for + /// the scope of a single operation without affecting other operations. + /// + public Action? ConfigureSettings { get; private set; } + + /// + /// A pre-configured settings instance that disables automatic deserialization. + /// Use this when you need to access raw message data in its serialized form. + /// + public static readonly OperationSerializationSettings Disabled = new OperationSerializationSettings { + AutomaticDeserialization = AutomaticDeserialization.Disabled + }; + + /// + /// Creates operation-specific serialization settings with custom configuration while keeping + /// automatic deserialization enabled. This allows operation-specific type mappings or + /// serializer settings without changing the global client configuration. + /// + /// A callback to customize serialization settings for this operation. + /// A configured instance of with enabled deserialization. + public static OperationSerializationSettings Configure(Action configure) => + new OperationSerializationSettings { + AutomaticDeserialization = AutomaticDeserialization.Enabled, + ConfigureSettings = configure + }; +} + +/// +/// Controls whether the KurrentDB client should automatically deserialize message payloads +/// into their corresponding CLR types based on the configured type mappings. +/// +public enum AutomaticDeserialization { + /// + /// Disables automatic deserialization. Messages will be returned in their raw serialized form, + /// requiring manual deserialization by the application. Use this when you need direct access to the raw data + /// or when working with messages that don't have registered type mappings. + /// + Disabled = 0, + + /// + /// Enables automatic deserialization. The client will attempt to convert messages into their appropriate + /// CLR types using the configured serializers and type mappings. This simplifies working with strongly-typed + /// domain messages but requires proper type registration. + /// + Enabled = 1 +} diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs similarity index 82% rename from src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs rename to src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs index 48eb84956..57be0c950 100644 --- a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs +++ b/src/Kurrent.Client/Core/KurrentClientSettings.ConnectionString.cs @@ -1,24 +1,33 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Security; -using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Timeout_ = System.Threading.Timeout; namespace EventStore.Client { - public partial class EventStoreClientSettings { + public partial class KurrentClientSettings { /// /// Creates client settings from a connection string /// /// /// - public static EventStoreClientSettings Create(string connectionString) => + public static KurrentClientSettings Create(string connectionString) => ConnectionStringParser.Parse(connectionString); + /// + /// Creates client settings from a connection string with additional configuration + /// + /// + /// allows you to make additional customization of client settings + /// + public static KurrentClientSettings Create(string connectionString, Action configure) { + var settings = ConnectionStringParser.Parse(connectionString); + + configure(settings); + + return settings; + } + private static class ConnectionStringParser { private const string SchemeSeparator = "://"; private const string UserInfoSeparator = "@"; @@ -41,13 +50,13 @@ private static class ConnectionStringParser { private const string ThrowOnAppendFailure = nameof(ThrowOnAppendFailure); private const string KeepAliveInterval = nameof(KeepAliveInterval); private const string KeepAliveTimeout = nameof(KeepAliveTimeout); - private const string UserCertFile = nameof(UserCertFile); + private const string UserCertFile = nameof(UserCertFile); private const string UserKeyFile = nameof(UserKeyFile); private const string UriSchemeDiscover = "esdb+discover"; private static readonly string[] Schemes = { "esdb", UriSchemeDiscover }; - private static readonly int DefaultPort = EventStoreClientConnectivitySettings.Default.ResolvedAddressOrDefault.Port; + private static readonly int DefaultPort = KurrentClientConnectivitySettings.Default.ResolvedAddressOrDefault.Port; private static readonly bool DefaultUseTls = true; private static readonly Dictionary SettingsType = @@ -64,11 +73,11 @@ private static class ConnectionStringParser { { ThrowOnAppendFailure, typeof(bool) }, { KeepAliveInterval, typeof(int) }, { KeepAliveTimeout, typeof(int) }, - { UserCertFile, typeof(string)}, - { UserKeyFile, typeof(string)}, + { UserCertFile, typeof(string) }, + { UserKeyFile, typeof(string) }, }; - public static EventStoreClientSettings Parse(string connectionString) { + public static KurrentClientSettings Parse(string connectionString) { var currentIndex = 0; var schemeIndex = connectionString.IndexOf(SchemeSeparator, currentIndex, StringComparison.Ordinal); if (schemeIndex == -1) @@ -77,10 +86,10 @@ public static EventStoreClientSettings Parse(string connectionString) { var scheme = ParseScheme(connectionString.Substring(0, schemeIndex)); currentIndex = schemeIndex + SchemeSeparator.Length; - var userInfoIndex = connectionString.IndexOf(UserInfoSeparator, currentIndex, StringComparison.Ordinal); - (string user, string pass)? userInfo = null; + var userInfoIndex = connectionString.IndexOf(UserInfoSeparator, currentIndex, StringComparison.Ordinal); + (string user, string pass)? userInfo = null; if (userInfoIndex != -1) { - userInfo = ParseUserInfo(connectionString.Substring(currentIndex, userInfoIndex - currentIndex)); + userInfo = ParseUserInfo(connectionString.Substring(currentIndex, userInfoIndex - currentIndex)); currentIndex = userInfoIndex + UserInfoSeparator.Length; } @@ -93,7 +102,7 @@ public static EventStoreClientSettings Parse(string connectionString) { if (questionMarkIndex == -1) questionMarkIndex = int.MaxValue; var hostSeparatorIndex = Math.Min(Math.Min(slashIndex, questionMarkIndex), endIndex); - var hosts = ParseHosts(connectionString.Substring(currentIndex, hostSeparatorIndex - currentIndex)); + var hosts = ParseHosts(connectionString.Substring(currentIndex, hostSeparatorIndex - currentIndex)); currentIndex = hostSeparatorIndex; string path = ""; @@ -117,13 +126,13 @@ public static EventStoreClientSettings Parse(string connectionString) { return CreateSettings(scheme, userInfo, hosts, options); } - private static EventStoreClientSettings CreateSettings( + private static KurrentClientSettings CreateSettings( string scheme, (string user, string pass)? userInfo, EndPoint[] hosts, Dictionary options ) { - var settings = new EventStoreClientSettings { - ConnectivitySettings = EventStoreClientConnectivitySettings.Default, - OperationOptions = EventStoreClientOperationOptions.Default + var settings = new KurrentClientSettings { + ConnectivitySettings = KurrentClientConnectivitySettings.Default, + OperationOptions = KurrentClientOperationOptions.Default }; if (userInfo.HasValue) @@ -163,11 +172,11 @@ private static EventStoreClientSettings CreateSettings( if (typedOptions.TryGetValue(NodePreference, out object? nodePreference)) { settings.ConnectivitySettings.NodePreference = ((string)nodePreference).ToLowerInvariant() switch { - "leader" => EventStore.Client.NodePreference.Leader, - "follower" => EventStore.Client.NodePreference.Follower, - "random" => EventStore.Client.NodePreference.Random, + "leader" => EventStore.Client.NodePreference.Leader, + "follower" => EventStore.Client.NodePreference.Follower, + "random" => EventStore.Client.NodePreference.Random, "readonlyreplica" => EventStore.Client.NodePreference.ReadOnlyReplica, - _ => throw new InvalidSettingException($"Invalid NodePreference: {nodePreference}") + _ => throw new InvalidSettingException($"Invalid NodePreference: {nodePreference}") }; } @@ -184,17 +193,17 @@ private static EventStoreClientSettings CreateSettings( if (typedOptions.TryGetValue(KeepAliveInterval, out var keepAliveIntervalMs)) { settings.ConnectivitySettings.KeepAliveInterval = keepAliveIntervalMs switch { - -1 => Timeout_.InfiniteTimeSpan, + -1 => Timeout_.InfiniteTimeSpan, int value and >= 0 => TimeSpan.FromMilliseconds(value), - _ => throw new InvalidSettingException($"Invalid KeepAliveInterval: {keepAliveIntervalMs}") + _ => throw new InvalidSettingException($"Invalid KeepAliveInterval: {keepAliveIntervalMs}") }; } if (typedOptions.TryGetValue(KeepAliveTimeout, out var keepAliveTimeoutMs)) { settings.ConnectivitySettings.KeepAliveTimeout = keepAliveTimeoutMs switch { - -1 => Timeout_.InfiniteTimeSpan, + -1 => Timeout_.InfiniteTimeSpan, int value and >= 0 => TimeSpan.FromMilliseconds(value), - _ => throw new InvalidSettingException($"Invalid KeepAliveTimeout: {keepAliveTimeoutMs}") + _ => throw new InvalidSettingException($"Invalid KeepAliveTimeout: {keepAliveTimeoutMs}") }; } @@ -221,7 +230,11 @@ private static EventStoreClientSettings CreateSettings( } try { +#if NET9_0_OR_GREATER + settings.ConnectivitySettings.TlsCaFile = X509CertificateLoader.LoadCertificateFromFile(tlsCaFilePath); +#else settings.ConnectivitySettings.TlsCaFile = new X509Certificate2(tlsCaFilePath); +#endif } catch (CryptographicException) { throw new InvalidClientCertificateException("Failed to load certificate. Invalid file format."); } @@ -239,9 +252,9 @@ HttpMessageHandler CreateDefaultHandler() { return settings.CreateHttpMessageHandler.Invoke(); var handler = new WinHttpHandler { - TcpKeepAliveEnabled = true, - TcpKeepAliveTime = settings.ConnectivitySettings.KeepAliveTimeout, - TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, + TcpKeepAliveEnabled = true, + TcpKeepAliveTime = settings.ConnectivitySettings.KeepAliveTimeout, + TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, EnableMultipleHttp2Connections = true }; @@ -285,7 +298,7 @@ HttpMessageHandler CreateDefaultHandler() { true when settings.ConnectivitySettings.TlsCaFile is not null => (sender, certificate, chain, errors) => { if (certificate is not X509Certificate2 peerCertificate || chain is null) return false; - chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; chain.ChainPolicy.CustomTrustStore.Add(settings.ConnectivitySettings.TlsCaFile); return chain.Build(peerCertificate); }, @@ -297,7 +310,7 @@ HttpMessageHandler CreateDefaultHandler() { #endif } - static void ConfigureClientCertificate(EventStoreClientSettings settings, IReadOnlyDictionary options) { + static void ConfigureClientCertificate(KurrentClientSettings settings, IReadOnlyDictionary options) { var certPemFilePath = GetOptionValueAsString(UserCertFile); var keyPemFilePath = GetOptionValueAsString(UserKeyFile); @@ -318,8 +331,7 @@ static void ConfigureClientCertificate(EventStoreClientSettings settings, IReadO ); try { - settings.ConnectivitySettings.ClientCertificate = - X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); + settings.ConnectivitySettings.ClientCertificate = X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); } catch (Exception ex) { throw new InvalidClientCertificateException("Failed to create client certificate.", ex); } diff --git a/src/EventStore.Client/EventStoreClientSettings.cs b/src/Kurrent.Client/Core/KurrentClientSettings.cs similarity index 65% rename from src/EventStore.Client/EventStoreClientSettings.cs rename to src/Kurrent.Client/Core/KurrentClientSettings.cs index f6a65219e..71bfa446e 100644 --- a/src/EventStore.Client/EventStoreClientSettings.cs +++ b/src/Kurrent.Client/Core/KurrentClientSettings.cs @@ -8,9 +8,9 @@ namespace EventStore.Client { /// - /// A class that represents the settings to use for operations made from an implementation of . + /// A class that represents the settings to use for operations made from an implementation of . /// - public partial class EventStoreClientSettings { + public partial class KurrentClientSettings { /// /// An optional list of s to use. /// @@ -37,16 +37,16 @@ public partial class EventStoreClientSettings { public ChannelCredentials? ChannelCredentials { get; set; } /// - /// The default to use. + /// The default to use. /// - public EventStoreClientOperationOptions OperationOptions { get; set; } = - EventStoreClientOperationOptions.Default; + public KurrentClientOperationOptions OperationOptions { get; set; } = + KurrentClientOperationOptions.Default; /// - /// The to use. + /// The to use. /// - public EventStoreClientConnectivitySettings ConnectivitySettings { get; set; } = - EventStoreClientConnectivitySettings.Default; + public KurrentClientConnectivitySettings ConnectivitySettings { get; set; } = + KurrentClientConnectivitySettings.Default; /// /// The optional to use if none have been supplied to the operation. @@ -57,5 +57,11 @@ public partial class EventStoreClientSettings { /// The default deadline for calls. Will not be applied to reads or subscriptions. /// public TimeSpan? DefaultDeadline { get; set; } = TimeSpan.FromSeconds(10); + + /// + /// Provides configuration options for messages serialization and deserialization in the KurrentDB client. + /// If null, default settings are used. + /// + public KurrentClientSerializationSettings Serialization { get; set; } = KurrentClientSerializationSettings.Default(); } } diff --git a/src/EventStore.Client/NodePreference.cs b/src/Kurrent.Client/Core/NodePreference.cs similarity index 88% rename from src/EventStore.Client/NodePreference.cs rename to src/Kurrent.Client/Core/NodePreference.cs index a3f453eb3..530edb87d 100644 --- a/src/EventStore.Client/NodePreference.cs +++ b/src/Kurrent.Client/Core/NodePreference.cs @@ -1,6 +1,6 @@ namespace EventStore.Client { /// - /// Indicates the preferred EventStoreDB node type to connect to. + /// Indicates the preferred KurrentDB node type to connect to. /// public enum NodePreference { /// diff --git a/src/EventStore.Client/NodePreferenceComparers.cs b/src/Kurrent.Client/Core/NodePreferenceComparers.cs similarity index 100% rename from src/EventStore.Client/NodePreferenceComparers.cs rename to src/Kurrent.Client/Core/NodePreferenceComparers.cs diff --git a/src/EventStore.Client/NodeSelector.cs b/src/Kurrent.Client/Core/NodeSelector.cs similarity index 97% rename from src/EventStore.Client/NodeSelector.cs rename to src/Kurrent.Client/Core/NodeSelector.cs index 94c00cc56..661e1e190 100644 --- a/src/EventStore.Client/NodeSelector.cs +++ b/src/Kurrent.Client/Core/NodeSelector.cs @@ -26,7 +26,7 @@ internal class NodeSelector { private readonly Random _random; private readonly IComparer? _nodeStateComparer; - public NodeSelector(EventStoreClientSettings settings) { + public NodeSelector(KurrentClientSettings settings) { _random = new Random(0); _nodeStateComparer = settings.ConnectivitySettings.NodePreference switch { NodePreference.Leader => NodePreferenceComparers.Leader, diff --git a/src/EventStore.Client/Position.cs b/src/Kurrent.Client/Core/Position.cs similarity index 100% rename from src/EventStore.Client/Position.cs rename to src/Kurrent.Client/Core/Position.cs diff --git a/src/EventStore.Client/PrefixFilterExpression.cs b/src/Kurrent.Client/Core/PrefixFilterExpression.cs similarity index 100% rename from src/EventStore.Client/PrefixFilterExpression.cs rename to src/Kurrent.Client/Core/PrefixFilterExpression.cs diff --git a/src/EventStore.Client/ReconnectionRequired.cs b/src/Kurrent.Client/Core/ReconnectionRequired.cs similarity index 100% rename from src/EventStore.Client/ReconnectionRequired.cs rename to src/Kurrent.Client/Core/ReconnectionRequired.cs diff --git a/src/EventStore.Client/RegularFilterExpression.cs b/src/Kurrent.Client/Core/RegularFilterExpression.cs similarity index 100% rename from src/EventStore.Client/RegularFilterExpression.cs rename to src/Kurrent.Client/Core/RegularFilterExpression.cs diff --git a/src/EventStore.Client/ResolvedEvent.cs b/src/Kurrent.Client/Core/ResolvedEvent.cs similarity index 57% rename from src/EventStore.Client/ResolvedEvent.cs rename to src/Kurrent.Client/Core/ResolvedEvent.cs index 25ca13a78..3b4209082 100644 --- a/src/EventStore.Client/ResolvedEvent.cs +++ b/src/Kurrent.Client/Core/ResolvedEvent.cs @@ -1,3 +1,5 @@ +using Kurrent.Client.Core.Serialization; + namespace EventStore.Client { /// /// A structure representing a single event or a resolved link event. @@ -22,6 +24,23 @@ public readonly struct ResolvedEvent { /// public EventRecord OriginalEvent => Link ?? Event; + /// + /// Returns the deserialized message + /// It will be provided or equal to null, depending on the automatic deserialization settings you choose. + /// If it's null, you can use to deserialize it manually. + /// + public readonly Message? Message; + + /// + /// Returns the deserialized message data. + /// + public object? DeserializedData => Message?.Data; + + /// + /// Returns the deserialized message metadata. + /// + public object? DeserializedMetadata => Message?.Metadata; + /// /// Position of the if available. /// @@ -49,12 +68,44 @@ public readonly struct ResolvedEvent { /// /// /// - public ResolvedEvent(EventRecord @event, EventRecord? link, ulong? commitPosition) { - Event = @event; - Link = link; + public ResolvedEvent(EventRecord @event, EventRecord? link, ulong? commitPosition) : this( + @event, + link, + null, + commitPosition + ) { } + + /// + /// Constructs a new . + /// + /// + /// + /// + /// + ResolvedEvent( + EventRecord @event, + EventRecord? link, + Message? message, + ulong? commitPosition + ) { + Event = @event; + Link = link; + Message = message; OriginalPosition = commitPosition.HasValue ? new Position(commitPosition.Value, (link ?? @event).Position.PreparePosition) : new Position?(); } + + internal static ResolvedEvent From( + EventRecord @event, + EventRecord? link, + ulong? commitPosition, + IMessageSerializer messageSerializer + ) { + var originalEvent = link ?? @event; + return messageSerializer.TryDeserialize(originalEvent, out var message) + ? new ResolvedEvent(@event, link, message, commitPosition) + : new ResolvedEvent(@event, link, commitPosition); + } } } diff --git a/src/Kurrent.Client/Core/Serialization/ISerializer.cs b/src/Kurrent.Client/Core/Serialization/ISerializer.cs new file mode 100644 index 000000000..93382428d --- /dev/null +++ b/src/Kurrent.Client/Core/Serialization/ISerializer.cs @@ -0,0 +1,30 @@ +namespace Kurrent.Client.Core.Serialization; + +/// +/// Defines the core serialization capabilities required by the KurrentDB client. +/// Implementations of this interface handle the conversion between .NET objects and their +/// binary representation for storage in and retrieval from the event store. +///
+/// The client ships default System.Text.Json implementation, but custom implementations can be provided or other formats. +///
+public interface ISerializer { + /// + /// Converts a .NET object to its binary representation for storage in the event store. + /// + /// The object to serialize. This could be an event, command, or metadata object. + /// + /// A binary representation of the object that can be stored in KurrentDB. + /// + public ReadOnlyMemory Serialize(object value); + + /// + /// Reconstructs a .NET object from its binary representation retrieved from the event store. + /// + /// The binary data to deserialize, typically retrieved from a KurrentDB event. + /// The target .NET type to deserialize the data into, determined from message type mappings. + /// + /// The deserialized object cast to the specified type, or null if the data cannot be deserialized. + /// The returned object will be an instance of the specified type or a compatible subtype. + /// + public object? Deserialize(ReadOnlyMemory data, Type type); +} diff --git a/src/Kurrent.Client/Core/Serialization/Message.cs b/src/Kurrent.Client/Core/Serialization/Message.cs new file mode 100644 index 000000000..83c07bd5e --- /dev/null +++ b/src/Kurrent.Client/Core/Serialization/Message.cs @@ -0,0 +1,109 @@ +using EventStore.Client; + +namespace Kurrent.Client.Core.Serialization; + +/// +/// Represents a message wrapper in the KurrentDB system, containing both domain data and optional metadata. +/// Messages can represent events, commands, or other domain objects along with their associated metadata. +/// +/// The message domain data. +/// Optional metadata providing additional context about the message, such as correlation IDs, timestamps, or user information. +/// Unique identifier for this specific message instance. When null, the system will auto-generate an ID. +public record Message(object Data, object? Metadata, Uuid? MessageId = null) { + /// + /// Creates a new Message with the specified domain data and message ID, but without metadata. + /// This factory method is a convenient shorthand when working with systems that don't require metadata. + /// + /// The message domain data. + /// Unique identifier for this message instance. Must not be Uuid.Empty. + /// A new immutable Message instance containing the provided data and ID with null metadata. + /// + /// + /// // Create a message with a specific ID + /// var userCreated = new UserCreated { Id = "123", Name = "Alice" }; + /// var messageId = Uuid.NewUuid(); + /// var message = Message.From(userCreated, messageId); + /// + /// + public static Message From(object data, Uuid messageId) => + From(data, null, messageId); + + /// + /// Creates a new Message with the specified domain data and message ID, but without metadata. + /// This factory method is a convenient shorthand when working with systems that don't require metadata. + /// + /// The message domain data. + /// Unique identifier for this message instance. Must not be Uuid.Empty. + /// A new immutable Message instance containing the provided data and ID with null metadata. + /// + /// + /// // Create a message with a specific ID + /// var userCreated = new UserCreated { Id = "123", Name = "Alice" }; + /// var messageId = Uuid.NewUuid(); + /// var message = Message.From(userCreated, messageId); + /// + /// + public static Message From(TMessage data, Uuid messageId) where TMessage : notnull => + From(data, null, messageId); + + /// + /// Creates a new Message with the specified domain data and message ID and metadata. + /// + /// The message domain data. + /// Optional metadata providing additional context about the message, such as correlation IDs, timestamps, or user information. + /// Unique identifier for this specific message instance. + /// A new immutable Message instance with the specified properties. + /// Thrown when messageId is explicitly set to Uuid.Empty, which is an invalid identifier. + /// + /// + /// // Create a message with data and metadata + /// var orderPlaced = new OrderPlaced { OrderId = "ORD-123", Amount = 99.99m }; + /// var metadata = new EventMetadata { + /// UserId = "user-456", + /// Timestamp = DateTimeOffset.UtcNow, + /// CorrelationId = correlationId + /// }; + /// + /// // Let the system assign an ID automatically + /// var message = Message.From(orderPlaced, metadata); + /// + /// // Or specify a custom ID + /// var messageWithId = Message.From(orderPlaced, metadata, Uuid.NewUuid()); + /// + /// + public static Message From(object data, object? metadata = null, Uuid? messageId = null) { + if (messageId == Uuid.Empty) + throw new ArgumentOutOfRangeException(nameof(messageId), "Message ID cannot be an empty UUID."); + + return new Message(data, metadata, messageId); + } + + /// + /// Creates a new Message with the specified domain data and message ID and metadata. + /// + /// The message domain data. + /// Optional metadata providing additional context about the message, such as correlation IDs, timestamps, or user information. + /// Unique identifier for this specific message instance. + /// A new immutable Message instance with the specified properties. + /// Thrown when messageId is explicitly set to Uuid.Empty, which is an invalid identifier. + /// + /// + /// // Create a message with data and metadata + /// var orderPlaced = new OrderPlaced { OrderId = "ORD-123", Amount = 99.99m }; + /// var metadata = new EventMetadata { + /// UserId = "user-456", + /// Timestamp = DateTimeOffset.UtcNow, + /// CorrelationId = correlationId + /// }; + /// + /// // Let the system assign an ID automatically + /// var message = Message.From(orderPlaced, metadata); + /// + /// // Or specify a custom ID + /// var messageWithId = Message.From(orderPlaced, metadata, Uuid.NewUuid()); + /// + /// + public static Message From(TMessage data, object? metadata = null, Uuid? messageId = null) + where TMessage : notnull => + From((object)data, metadata, messageId); +} diff --git a/src/Kurrent.Client/Core/Serialization/MessageSerializer.cs b/src/Kurrent.Client/Core/Serialization/MessageSerializer.cs new file mode 100644 index 000000000..de66372b2 --- /dev/null +++ b/src/Kurrent.Client/Core/Serialization/MessageSerializer.cs @@ -0,0 +1,148 @@ +using System.Diagnostics.CodeAnalysis; +using EventStore.Client; + +namespace Kurrent.Client.Core.Serialization; + +using static ContentTypeExtensions; + +interface IMessageSerializer { + public EventData Serialize(Message value, MessageSerializationContext context); + +#if NET48 + public bool TryDeserialize(EventRecord record, out Message? deserialized); +#else + public bool TryDeserialize(EventRecord record, [NotNullWhen(true)] out Message? deserialized); +#endif +} + +record MessageSerializationContext( + string StreamName, + ContentType ContentType +) { + public string CategoryName => + StreamName.Split('-').FirstOrDefault() ?? "no_stream_category"; +} + +static class MessageSerializerExtensions { + public static EventData[] Serialize( + this IMessageSerializer serializer, + IEnumerable messages, + MessageSerializationContext context + ) { + return messages.Select(m => serializer.Serialize(m, context)).ToArray(); + } + + public static IMessageSerializer With( + this IMessageSerializer defaultMessageSerializer, + KurrentClientSerializationSettings defaultSettings, + OperationSerializationSettings? operationSettings + ) { + if (operationSettings == null) + return defaultMessageSerializer; + + if (operationSettings.AutomaticDeserialization == AutomaticDeserialization.Disabled) + return NullMessageSerializer.Instance; + + if (operationSettings.ConfigureSettings == null) + return defaultMessageSerializer; + + var settings = defaultSettings.Clone(); + operationSettings.ConfigureSettings.Invoke(settings); + + return new MessageSerializer(SchemaRegistry.From(settings)); + } +} + +class MessageSerializer(SchemaRegistry schemaRegistry) : IMessageSerializer { + readonly ISerializer _jsonSerializer = + schemaRegistry.GetSerializer(ContentType.Json); + + readonly IMessageTypeNamingStrategy _messageTypeNamingStrategy = + schemaRegistry.MessageTypeNamingStrategy; + + public EventData Serialize(Message message, MessageSerializationContext serializationContext) { + var (data, metadata, eventId) = message; + + var eventType = _messageTypeNamingStrategy + .ResolveTypeName( + message.Data.GetType(), + new MessageTypeNamingResolutionContext(serializationContext.CategoryName) + ); + + var serializedData = schemaRegistry + .GetSerializer(serializationContext.ContentType) + .Serialize(data); + + var serializedMetadata = metadata != null + ? _jsonSerializer.Serialize(metadata) + : ReadOnlyMemory.Empty; + + return new EventData( + eventId ?? Uuid.NewUuid(), + eventType, + serializedData, + serializedMetadata, + serializationContext.ContentType.ToMessageContentType() + ); + } + +#if NET48 + public bool TryDeserialize(EventRecord record, out Message? deserialized) { +#else + public bool TryDeserialize(EventRecord record, [NotNullWhen(true)] out Message? deserialized) { +#endif + if (!TryResolveClrType(record, out var clrType)) { + deserialized = null; + return false; + } + + var data = schemaRegistry + .GetSerializer(FromMessageContentType(record.ContentType)) + .Deserialize(record.Data, clrType!); + + if (data == null) { + deserialized = null; + return false; + } + + object? metadata = record.Metadata.Length > 0 && TryResolveClrMetadataType(record, out var clrMetadataType) + ? _jsonSerializer.Deserialize(record.Metadata, clrMetadataType!) + : null; + + deserialized = Message.From(data, metadata, record.EventId); + return true; + } + + public static MessageSerializer From(KurrentClientSerializationSettings? settings = null) { + settings ??= KurrentClientSerializationSettings.Default(); + + return new MessageSerializer(SchemaRegistry.From(settings)); + } + + bool TryResolveClrType(EventRecord record, out Type? clrType) => + schemaRegistry + .MessageTypeNamingStrategy + .TryResolveClrType(record.EventType, out clrType); + + bool TryResolveClrMetadataType(EventRecord record, out Type? clrMetadataType) => + schemaRegistry + .MessageTypeNamingStrategy + .TryResolveClrMetadataType(record.EventType, out clrMetadataType); +} + +class NullMessageSerializer : IMessageSerializer { + public static readonly NullMessageSerializer Instance = new NullMessageSerializer(); + + public EventData Serialize(Message value, MessageSerializationContext context) { + throw new InvalidOperationException("Cannot serialize, automatic deserialization is disabled"); + } + +#if NET48 + public bool TryDeserialize(EventRecord record, out Message? deserialized) { +#else + public bool TryDeserialize(EventRecord eventRecord, [NotNullWhen(true)] out Message? deserialized) { +#endif + deserialized = null; + return false; + } +} diff --git a/src/Kurrent.Client/Core/Serialization/MessageTypeRegistry.cs b/src/Kurrent.Client/Core/Serialization/MessageTypeRegistry.cs new file mode 100644 index 000000000..68752884a --- /dev/null +++ b/src/Kurrent.Client/Core/Serialization/MessageTypeRegistry.cs @@ -0,0 +1,76 @@ +using System.Collections.Concurrent; + +namespace Kurrent.Client.Core.Serialization; + +interface IMessageTypeRegistry { + void Register(Type messageType, string messageTypeName); + string? GetTypeName(Type messageType); + string GetOrAddTypeName(Type clrType, Func getTypeName); + Type? GetClrType(string messageTypeName); + Type? GetOrAddClrType(string messageTypeName, Func getClrType); +} + +class MessageTypeRegistry : IMessageTypeRegistry { + readonly ConcurrentDictionary _typeMap = new(); + readonly ConcurrentDictionary _typeNameMap = new(); + + public void Register(Type messageType, string messageTypeName) { + _typeNameMap.AddOrUpdate(messageType, messageTypeName, (_, _) => messageTypeName); + _typeMap.AddOrUpdate(messageTypeName, messageType, (_, type) => type); + } + + public string? GetTypeName(Type messageType) => +#if NET48 + _typeNameMap.TryGetValue(messageType, out var typeName) ? typeName : null; +#else + _typeNameMap.GetValueOrDefault(messageType); +#endif + + public string GetOrAddTypeName(Type clrType, Func getTypeName) => + _typeNameMap.GetOrAdd( + clrType, + _ => { + var typeName = getTypeName(clrType); + + _typeMap.TryAdd(typeName, clrType); + + return typeName; + } + ); + + public Type? GetClrType(string messageTypeName) => +#if NET48 + _typeMap.TryGetValue(messageTypeName, out var clrType) ? clrType : null; +#else + _typeMap.GetValueOrDefault(messageTypeName); +#endif + + public Type? GetOrAddClrType(string messageTypeName, Func getClrType) => + _typeMap.GetOrAdd( + messageTypeName, + _ => { + var clrType = getClrType(messageTypeName); + + if (clrType == null) + return null; + + _typeNameMap.TryAdd(clrType, messageTypeName); + + return clrType; + } + ); +} + +static class MessageTypeRegistryExtensions { + public static void Register(this IMessageTypeRegistry messageTypeRegistry, string messageTypeName) => + messageTypeRegistry.Register(typeof(T), messageTypeName); + + public static void Register(this IMessageTypeRegistry messageTypeRegistry, IDictionary typeMap) { + foreach (var map in typeMap) { + messageTypeRegistry.Register(map.Key, map.Value); + } + } + + public static string? GetTypeName(this IMessageTypeRegistry messageTypeRegistry) => + messageTypeRegistry.GetTypeName(typeof(TMessageType)); +} diff --git a/src/Kurrent.Client/Core/Serialization/MessageTypeResolutionStrategy.cs b/src/Kurrent.Client/Core/Serialization/MessageTypeResolutionStrategy.cs new file mode 100644 index 000000000..12ff65f6b --- /dev/null +++ b/src/Kurrent.Client/Core/Serialization/MessageTypeResolutionStrategy.cs @@ -0,0 +1,100 @@ +using System.Diagnostics.CodeAnalysis; +using Kurrent.Diagnostics.Tracing; + +namespace Kurrent.Client.Core.Serialization; + +public interface IMessageTypeNamingStrategy { + string ResolveTypeName(Type messageType, MessageTypeNamingResolutionContext resolutionContext); + +#if NET48 + bool TryResolveClrType(string messageTypeName, out Type? type); +#else + bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? type); +#endif + + +#if NET48 + bool TryResolveClrMetadataType(string messageTypeName, out Type? type); +#else + bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? type); +#endif +} + +public record MessageTypeNamingResolutionContext(string CategoryName); + +class MessageTypeNamingStrategyWrapper( + IMessageTypeRegistry messageTypeRegistry, + IMessageTypeNamingStrategy messageTypeNamingStrategy +) : IMessageTypeNamingStrategy { + public string ResolveTypeName(Type messageType, MessageTypeNamingResolutionContext resolutionContext) { + return messageTypeRegistry.GetOrAddTypeName( + messageType, + _ => messageTypeNamingStrategy.ResolveTypeName(messageType, resolutionContext) + ); + } + +#if NET48 + public bool TryResolveClrType(string messageTypeName, out Type? type) { +#else + public bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? type) { +#endif + type = messageTypeRegistry.GetOrAddClrType( + messageTypeName, + _ => messageTypeNamingStrategy.TryResolveClrType(messageTypeName, out var resolvedType) + ? resolvedType + : null + ); + + return type != null; + } + +#if NET48 + public bool TryResolveClrMetadataType(string messageTypeName, out Type? type) { +#else + public bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? type) { +#endif + type = messageTypeRegistry.GetOrAddClrType( + $"{messageTypeName}-metadata", + _ => messageTypeNamingStrategy.TryResolveClrMetadataType(messageTypeName, out var resolvedType) + ? resolvedType + : null + ); + + return type != null; + } +} + +public class DefaultMessageTypeNamingStrategy(Type? defaultMetadataType) : IMessageTypeNamingStrategy { + readonly Type _defaultMetadataType = defaultMetadataType ?? typeof(TracingMetadata); + + public string ResolveTypeName(Type messageType, MessageTypeNamingResolutionContext resolutionContext) => + $"{resolutionContext.CategoryName}-{messageType.FullName}"; + +#if NET48 + public bool TryResolveClrType(string messageTypeName, out Type? type) { +#else + public bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? type) { +#endif + var categorySeparatorIndex = messageTypeName.IndexOf('-'); + + if (categorySeparatorIndex == -1 || categorySeparatorIndex == messageTypeName.Length - 1) { + type = null; + return false; + } + + var clrTypeName = messageTypeName[(categorySeparatorIndex + 1)..]; + + type = TypeProvider.GetTypeByFullName(clrTypeName); + + return type != null; + } + +#if NET48 + public bool TryResolveClrMetadataType(string messageTypeName, out Type? type) { +#else + public bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? type) { +#endif + type = _defaultMetadataType; + return true; + } +} diff --git a/src/Kurrent.Client/Core/Serialization/SchemaRegistry.cs b/src/Kurrent.Client/Core/Serialization/SchemaRegistry.cs new file mode 100644 index 000000000..9a5ad7515 --- /dev/null +++ b/src/Kurrent.Client/Core/Serialization/SchemaRegistry.cs @@ -0,0 +1,91 @@ +using EventStore.Client; + +namespace Kurrent.Client.Core.Serialization; + +using static Constants.Metadata.ContentTypes; + +public enum ContentType { + Json = 1, + + // Protobuf = 2, + // Avro = 3, + Bytes = 4 +} + +static class ContentTypeExtensions { + public static ContentType FromMessageContentType(string contentType) => + contentType == ApplicationJson + ? ContentType.Json + : ContentType.Bytes; + + public static string ToMessageContentType(this ContentType contentType) => + contentType switch { + ContentType.Json => ApplicationJson, + ContentType.Bytes => ApplicationOctetStream, + _ => throw new ArgumentOutOfRangeException(nameof(contentType), contentType, null) + }; +} + +class SchemaRegistry( + IDictionary serializers, + IMessageTypeNamingStrategy messageTypeNamingStrategy +) { + public IMessageTypeNamingStrategy MessageTypeNamingStrategy { get; } = messageTypeNamingStrategy; + + public ISerializer GetSerializer(ContentType schemaType) => + serializers[schemaType]; + + public static SchemaRegistry From(KurrentClientSerializationSettings settings) { + var messageTypeNamingStrategy = + settings.MessageTypeNamingStrategy ?? new DefaultMessageTypeNamingStrategy(settings.DefaultMetadataType); + + var categoriesTypeMap = ResolveMessageTypeUsingNamingStrategy( + settings.CategoryMessageTypesMap, + messageTypeNamingStrategy + ); + + var messageTypeRegistry = new MessageTypeRegistry(); + messageTypeRegistry.Register(settings.MessageTypeMap); + messageTypeRegistry.Register(categoriesTypeMap); + + var serializers = new Dictionary { + { + ContentType.Json, + settings.JsonSerializer ?? new SystemTextJsonSerializer() + }, { + ContentType.Bytes, + settings.BytesSerializer ?? new SystemTextJsonSerializer() + } + }; + + return new SchemaRegistry( + serializers, + new MessageTypeNamingStrategyWrapper( + messageTypeRegistry, + settings.MessageTypeNamingStrategy ?? new DefaultMessageTypeNamingStrategy(settings.DefaultMetadataType) + ) + ); + } + + static Dictionary ResolveMessageTypeUsingNamingStrategy( + IDictionary categoryMessageTypesMap, + IMessageTypeNamingStrategy messageTypeNamingStrategy + ) => + categoryMessageTypesMap + .SelectMany( + categoryTypes => categoryTypes.Value.Select( + type => + ( + Type: type, + TypeName: messageTypeNamingStrategy.ResolveTypeName( + type, + new MessageTypeNamingResolutionContext(categoryTypes.Key) + ) + ) + ) + ) + .ToDictionary( + ks => ks.Type, + vs => vs.TypeName + ); +} diff --git a/src/Kurrent.Client/Core/Serialization/SystemTextJsonSerializer.cs b/src/Kurrent.Client/Core/Serialization/SystemTextJsonSerializer.cs new file mode 100644 index 000000000..4efa7be96 --- /dev/null +++ b/src/Kurrent.Client/Core/Serialization/SystemTextJsonSerializer.cs @@ -0,0 +1,32 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Kurrent.Client.Core.Serialization; + +public class SystemTextJsonSerializationSettings { + public static readonly JsonSerializerOptions DefaultJsonSerializerOptions = + new JsonSerializerOptions(JsonSerializerOptions.Default) { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + Converters = { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase), + } + }; + + public JsonSerializerOptions Options { get; set; } = DefaultJsonSerializerOptions; +} + +public class SystemTextJsonSerializer(SystemTextJsonSerializationSettings? options = null) : ISerializer { + readonly JsonSerializerOptions _options = options?.Options ?? SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions; + + public ReadOnlyMemory Serialize(object value) => + JsonSerializer.SerializeToUtf8Bytes(value, _options); + + public object? Deserialize(ReadOnlyMemory data, Type type) => + !data.IsEmpty ? JsonSerializer.Deserialize(data.Span, type, _options) : null; +} diff --git a/src/Kurrent.Client/Core/Serialization/TypeProvider.cs b/src/Kurrent.Client/Core/Serialization/TypeProvider.cs new file mode 100644 index 000000000..f05f23f87 --- /dev/null +++ b/src/Kurrent.Client/Core/Serialization/TypeProvider.cs @@ -0,0 +1,15 @@ +namespace Kurrent.Client.Core.Serialization; + +static class TypeProvider { + public static Type? GetTypeByFullName(string fullName) => + Type.GetType(fullName) ?? GetFirstMatchingTypeFromCurrentDomainAssembly(fullName); + + static Type? GetFirstMatchingTypeFromCurrentDomainAssembly(string fullName) { + var firstNamespacePart = fullName.Split('.')[0]; + + return AppDomain.CurrentDomain.GetAssemblies() + .OrderByDescending(assembly => assembly.FullName?.StartsWith(firstNamespacePart) == true) + .Select(assembly => assembly.GetType(fullName)) + .FirstOrDefault(type => type != null); + } +} diff --git a/src/EventStore.Client/ServerCapabilities.cs b/src/Kurrent.Client/Core/ServerCapabilities.cs similarity index 100% rename from src/EventStore.Client/ServerCapabilities.cs rename to src/Kurrent.Client/Core/ServerCapabilities.cs diff --git a/src/EventStore.Client/SharingProvider.cs b/src/Kurrent.Client/Core/SharingProvider.cs similarity index 100% rename from src/EventStore.Client/SharingProvider.cs rename to src/Kurrent.Client/Core/SharingProvider.cs diff --git a/src/EventStore.Client/SingleNodeChannelSelector.cs b/src/Kurrent.Client/Core/SingleNodeChannelSelector.cs similarity index 96% rename from src/EventStore.Client/SingleNodeChannelSelector.cs rename to src/Kurrent.Client/Core/SingleNodeChannelSelector.cs index 79c3affb0..83449e689 100644 --- a/src/EventStore.Client/SingleNodeChannelSelector.cs +++ b/src/Kurrent.Client/Core/SingleNodeChannelSelector.cs @@ -12,7 +12,7 @@ internal class SingleNodeChannelSelector : IChannelSelector { private readonly DnsEndPoint _endPoint; public SingleNodeChannelSelector( - EventStoreClientSettings settings, + KurrentClientSettings settings, ChannelCache channelCache) { _log = settings.LoggerFactory?.CreateLogger() ?? diff --git a/src/EventStore.Client/SingleNodeHttpHandler.cs b/src/Kurrent.Client/Core/SingleNodeHttpHandler.cs similarity index 81% rename from src/EventStore.Client/SingleNodeHttpHandler.cs rename to src/Kurrent.Client/Core/SingleNodeHttpHandler.cs index b8560152e..0c346a00f 100644 --- a/src/EventStore.Client/SingleNodeHttpHandler.cs +++ b/src/Kurrent.Client/Core/SingleNodeHttpHandler.cs @@ -5,9 +5,9 @@ namespace EventStore.Client { internal class SingleNodeHttpHandler : DelegatingHandler { - private readonly EventStoreClientSettings _settings; + private readonly KurrentClientSettings _settings; - public SingleNodeHttpHandler(EventStoreClientSettings settings) { + public SingleNodeHttpHandler(KurrentClientSettings settings) { _settings = settings; } diff --git a/src/EventStore.Client/StreamFilter.cs b/src/Kurrent.Client/Core/StreamFilter.cs similarity index 100% rename from src/EventStore.Client/StreamFilter.cs rename to src/Kurrent.Client/Core/StreamFilter.cs diff --git a/src/EventStore.Client/StreamIdentifier.cs b/src/Kurrent.Client/Core/StreamIdentifier.cs similarity index 94% rename from src/EventStore.Client/StreamIdentifier.cs rename to src/Kurrent.Client/Core/StreamIdentifier.cs index 73e0ee25e..9ca6c9e7e 100644 --- a/src/EventStore.Client/StreamIdentifier.cs +++ b/src/Kurrent.Client/Core/StreamIdentifier.cs @@ -3,7 +3,7 @@ namespace EventStore.Client { #pragma warning disable 1591 - public partial class StreamIdentifier { + internal partial class StreamIdentifier { private string? _cached; public static implicit operator string?(StreamIdentifier? source) { diff --git a/src/EventStore.Client/StreamPosition.cs b/src/Kurrent.Client/Core/StreamPosition.cs similarity index 100% rename from src/EventStore.Client/StreamPosition.cs rename to src/Kurrent.Client/Core/StreamPosition.cs diff --git a/src/EventStore.Client/StreamRevision.cs b/src/Kurrent.Client/Core/StreamRevision.cs similarity index 100% rename from src/EventStore.Client/StreamRevision.cs rename to src/Kurrent.Client/Core/StreamRevision.cs diff --git a/src/EventStore.Client/StreamState.cs b/src/Kurrent.Client/Core/StreamState.cs similarity index 100% rename from src/EventStore.Client/StreamState.cs rename to src/Kurrent.Client/Core/StreamState.cs diff --git a/src/EventStore.Client/SubscriptionDroppedReason.cs b/src/Kurrent.Client/Core/SubscriptionDroppedReason.cs similarity index 100% rename from src/EventStore.Client/SubscriptionDroppedReason.cs rename to src/Kurrent.Client/Core/SubscriptionDroppedReason.cs diff --git a/src/EventStore.Client/SystemRoles.cs b/src/Kurrent.Client/Core/SystemRoles.cs similarity index 100% rename from src/EventStore.Client/SystemRoles.cs rename to src/Kurrent.Client/Core/SystemRoles.cs diff --git a/src/EventStore.Client/SystemStreams.cs b/src/Kurrent.Client/Core/SystemStreams.cs similarity index 90% rename from src/EventStore.Client/SystemStreams.cs rename to src/Kurrent.Client/Core/SystemStreams.cs index 67899794b..c8b72503a 100644 --- a/src/EventStore.Client/SystemStreams.cs +++ b/src/Kurrent.Client/Core/SystemStreams.cs @@ -4,12 +4,12 @@ namespace EventStore.Client { ///
public static class SystemStreams { /// - /// A stream containing all events in the EventStoreDB transaction file. + /// A stream containing all events in the KurrentDB transaction file. /// public const string AllStream = "$all"; /// - /// A stream containing links pointing to each stream in the EventStoreDB. + /// A stream containing links pointing to each stream in the KurrentDB. /// public const string StreamsStream = "$streams"; diff --git a/src/EventStore.Client/TaskExtensions.cs b/src/Kurrent.Client/Core/TaskExtensions.cs similarity index 100% rename from src/EventStore.Client/TaskExtensions.cs rename to src/Kurrent.Client/Core/TaskExtensions.cs diff --git a/src/EventStore.Client/UserCredentials.cs b/src/Kurrent.Client/Core/UserCredentials.cs similarity index 95% rename from src/EventStore.Client/UserCredentials.cs rename to src/Kurrent.Client/Core/UserCredentials.cs index d944d90d7..6f9012951 100644 --- a/src/EventStore.Client/UserCredentials.cs +++ b/src/Kurrent.Client/Core/UserCredentials.cs @@ -5,7 +5,7 @@ namespace EventStore.Client { /// /// Represents either a username/password pair or a JWT token used for authentication and - /// authorization to perform operations on the EventStoreDB. + /// authorization to perform operations on the KurrentDB. /// public class UserCredentials { // ReSharper disable once InconsistentNaming @@ -51,4 +51,4 @@ public UserCredentials(string bearerToken) { ///
public static implicit operator string(UserCredentials self) => self.ToString(); } -} \ No newline at end of file +} diff --git a/src/EventStore.Client/Uuid.cs b/src/Kurrent.Client/Core/Uuid.cs similarity index 98% rename from src/EventStore.Client/Uuid.cs rename to src/Kurrent.Client/Core/Uuid.cs index 6c96b1e2d..cbe7a7686 100644 --- a/src/EventStore.Client/Uuid.cs +++ b/src/Kurrent.Client/Core/Uuid.cs @@ -51,7 +51,7 @@ namespace EventStore.Client { ///
/// /// - public static Uuid FromDto(UUID dto) => + internal static Uuid FromDto(UUID dto) => dto == null ? throw new ArgumentNullException(nameof(dto)) : dto.ValueCase switch { @@ -106,7 +106,7 @@ private Uuid(long msb, long lsb) { /// Converts the to the gRPC wire format. ///
/// - public UUID ToDto() => + internal UUID ToDto() => new UUID { Structured = new UUID.Types.Structured { LeastSignificantBits = _lsb, diff --git a/src/EventStore.Client/Common/protos/code.proto b/src/Kurrent.Client/Core/protos/code.proto similarity index 100% rename from src/EventStore.Client/Common/protos/code.proto rename to src/Kurrent.Client/Core/protos/code.proto diff --git a/src/EventStore.Client/Common/protos/gossip.proto b/src/Kurrent.Client/Core/protos/gossip.proto similarity index 93% rename from src/EventStore.Client/Common/protos/gossip.proto rename to src/Kurrent.Client/Core/protos/gossip.proto index 192b3b0a4..f4ea9bcd4 100644 --- a/src/EventStore.Client/Common/protos/gossip.proto +++ b/src/Kurrent.Client/Core/protos/gossip.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package event_store.client.gossip; -option java_package = "com.eventstore.client.gossip"; +option java_package = "io.kurrent.client.gossip"; import "shared.proto"; diff --git a/src/EventStore.Client/Common/protos/operations.proto b/src/Kurrent.Client/Core/protos/operations.proto similarity index 94% rename from src/EventStore.Client/Common/protos/operations.proto rename to src/Kurrent.Client/Core/protos/operations.proto index e8d5d2726..f4f9ae3c3 100644 --- a/src/EventStore.Client/Common/protos/operations.proto +++ b/src/Kurrent.Client/Core/protos/operations.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package event_store.client.operations; -option java_package = "com.eventstore.client.operations"; +option java_package = "io.kurrent.client.operations"; import "shared.proto"; diff --git a/src/EventStore.Client/Common/protos/persistentsubscriptions.proto b/src/Kurrent.Client/Core/protos/persistentsubscriptions.proto similarity index 99% rename from src/EventStore.Client/Common/protos/persistentsubscriptions.proto rename to src/Kurrent.Client/Core/protos/persistentsubscriptions.proto index a4109cad2..ac63a3a6d 100644 --- a/src/EventStore.Client/Common/protos/persistentsubscriptions.proto +++ b/src/Kurrent.Client/Core/protos/persistentsubscriptions.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package event_store.client.persistent_subscriptions; -option java_package = "com.eventstore.dbclient.proto.persistentsubscriptions"; +option java_package = "io.kurrent.dbclient.proto.persistentsubscriptions"; import "shared.proto"; diff --git a/src/EventStore.Client/Common/protos/projectionmanagement.proto b/src/Kurrent.Client/Core/protos/projectionmanagement.proto similarity index 98% rename from src/EventStore.Client/Common/protos/projectionmanagement.proto rename to src/Kurrent.Client/Core/protos/projectionmanagement.proto index f1733b55a..f16877012 100644 --- a/src/EventStore.Client/Common/protos/projectionmanagement.proto +++ b/src/Kurrent.Client/Core/protos/projectionmanagement.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package event_store.client.projections; -option java_package = "com.eventstore.client.projections"; +option java_package = "io.kurrent.client.projections"; import "google/protobuf/struct.proto"; import "shared.proto"; diff --git a/src/EventStore.Client/Common/protos/serverfeatures.proto b/src/Kurrent.Client/Core/protos/serverfeatures.proto similarity index 85% rename from src/EventStore.Client/Common/protos/serverfeatures.proto rename to src/Kurrent.Client/Core/protos/serverfeatures.proto index cba04f2c6..61c4ab773 100644 --- a/src/EventStore.Client/Common/protos/serverfeatures.proto +++ b/src/Kurrent.Client/Core/protos/serverfeatures.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package event_store.client.server_features; -option java_package = "com.eventstore.dbclient.proto.serverfeatures"; +option java_package = "io.kurrent.dbclient.proto.serverfeatures"; import "shared.proto"; service ServerFeatures { diff --git a/src/EventStore.Client/Common/protos/shared.proto b/src/Kurrent.Client/Core/protos/shared.proto similarity index 94% rename from src/EventStore.Client/Common/protos/shared.proto rename to src/Kurrent.Client/Core/protos/shared.proto index 2b113aed8..24780afc3 100644 --- a/src/EventStore.Client/Common/protos/shared.proto +++ b/src/Kurrent.Client/Core/protos/shared.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package event_store.client; -option java_package = "com.eventstore.dbclient.proto.shared"; +option java_package = "io.kurrent.dbclient.proto.shared"; import "google/protobuf/empty.proto"; message UUID { diff --git a/src/EventStore.Client/Common/protos/status.proto b/src/Kurrent.Client/Core/protos/status.proto similarity index 100% rename from src/EventStore.Client/Common/protos/status.proto rename to src/Kurrent.Client/Core/protos/status.proto diff --git a/src/EventStore.Client/Common/protos/streams.proto b/src/Kurrent.Client/Core/protos/streams.proto similarity index 99% rename from src/EventStore.Client/Common/protos/streams.proto rename to src/Kurrent.Client/Core/protos/streams.proto index ac599fb82..0eb05295c 100644 --- a/src/EventStore.Client/Common/protos/streams.proto +++ b/src/Kurrent.Client/Core/protos/streams.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package event_store.client.streams; -option java_package = "com.eventstore.dbclient.proto.streams"; +option java_package = "io.kurrent.dbclient.proto.streams"; import "shared.proto"; import "status.proto"; diff --git a/src/EventStore.Client/Common/protos/usermanagement.proto b/src/Kurrent.Client/Core/protos/usermanagement.proto similarity index 97% rename from src/EventStore.Client/Common/protos/usermanagement.proto rename to src/Kurrent.Client/Core/protos/usermanagement.proto index 1fab5a73e..4b55251fd 100644 --- a/src/EventStore.Client/Common/protos/usermanagement.proto +++ b/src/Kurrent.Client/Core/protos/usermanagement.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package event_store.client.users; -option java_package = "com.eventstore.client.users"; +option java_package = "io.kurrent.client.users"; service Users { rpc Create (CreateReq) returns (CreateResp); diff --git a/src/Kurrent.Client/Kurrent.Client.csproj b/src/Kurrent.Client/Kurrent.Client.csproj new file mode 100644 index 000000000..be11d5081 --- /dev/null +++ b/src/Kurrent.Client/Kurrent.Client.csproj @@ -0,0 +1,113 @@ + + + + Kurrent.Client + The base GRPC client library for the Kurrent Platform. Get the open source or commercial versions of KurrentDB from https://kurrent.io/ + Kurrent.Client + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Kurrent.Client/OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/Kurrent.Client/OpenTelemetry/TracerProviderBuilderExtensions.cs new file mode 100644 index 000000000..19885375b --- /dev/null +++ b/src/Kurrent.Client/OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -0,0 +1,19 @@ +using EventStore.Client.Diagnostics; +using JetBrains.Annotations; +using OpenTelemetry.Trace; + +namespace EventStore.Client.Extensions.OpenTelemetry; + +/// +/// Extension methods used to facilitate tracing instrumentation of the EventStore Client. +/// +[PublicAPI] +public static class TracerProviderBuilderExtensions { + /// + /// Adds the EventStore client ActivitySource name to the list of subscribed sources on the + /// + /// being configured. + /// The instance of to chain configuration. + public static TracerProviderBuilder AddKurrentClientInstrumentation(this TracerProviderBuilder builder) => + builder.AddSource(KurrentClientDiagnostics.InstrumentationName); +} diff --git a/src/EventStore.Client.Operations/DatabaseScavengeResult.cs b/src/Kurrent.Client/Operations/DatabaseScavengeResult.cs similarity index 100% rename from src/EventStore.Client.Operations/DatabaseScavengeResult.cs rename to src/Kurrent.Client/Operations/DatabaseScavengeResult.cs diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs b/src/Kurrent.Client/Operations/KurrentOperationsClient.Admin.cs similarity index 85% rename from src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs rename to src/Kurrent.Client/Operations/KurrentOperationsClient.Admin.cs index bfa750145..368fdca13 100644 --- a/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs +++ b/src/Kurrent.Client/Operations/KurrentOperationsClient.Admin.cs @@ -4,11 +4,11 @@ using EventStore.Client.Operations; namespace EventStore.Client { - public partial class EventStoreOperationsClient { + public partial class KurrentOperationsClient { private static readonly Empty EmptyResult = new Empty(); /// - /// Shuts down the EventStoreDB node. + /// Shuts down the KurrentDB node. /// /// /// @@ -21,7 +21,7 @@ public async Task ShutdownAsync( var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).ShutdownAsync(EmptyResult, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -39,7 +39,7 @@ public async Task MergeIndexesAsync( var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).MergeIndexesAsync(EmptyResult, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -57,7 +57,7 @@ public async Task ResignNodeAsync( var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).ResignNodeAsync(EmptyResult, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -77,7 +77,7 @@ public async Task SetNodePriorityAsync(int nodePriority, using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).SetNodePriorityAsync( new SetNodePriorityReq {Priority = nodePriority}, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -96,7 +96,7 @@ public async Task RestartPersistentSubscriptions( using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).RestartPersistentSubscriptionsAsync( EmptyResult, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } } diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs b/src/Kurrent.Client/Operations/KurrentOperationsClient.Scavenge.cs similarity index 92% rename from src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs rename to src/Kurrent.Client/Operations/KurrentOperationsClient.Scavenge.cs index 43dcfc50f..eb2c88c54 100644 --- a/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs +++ b/src/Kurrent.Client/Operations/KurrentOperationsClient.Scavenge.cs @@ -4,7 +4,7 @@ using EventStore.Client.Operations; namespace EventStore.Client { - public partial class EventStoreOperationsClient { + public partial class KurrentOperationsClient { /// /// Starts a scavenge operation. /// @@ -38,7 +38,7 @@ public async Task StartScavengeAsync( StartFromChunk = startFromChunk } }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); var result = await call.ResponseAsync.ConfigureAwait(false); return result.ScavengeResult switch { @@ -68,7 +68,7 @@ public async Task StopScavengeAsync( Options = new StopScavengeReq.Types.Options { ScavengeId = scavengeId } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) .ResponseAsync.ConfigureAwait(false); return result.ScavengeResult switch { diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.cs b/src/Kurrent.Client/Operations/KurrentOperationsClient.cs similarity index 51% rename from src/EventStore.Client.Operations/EventStoreOperationsClient.cs rename to src/Kurrent.Client/Operations/KurrentOperationsClient.cs index 672383348..d392aa578 100644 --- a/src/EventStore.Client.Operations/EventStoreOperationsClient.cs +++ b/src/Kurrent.Client/Operations/KurrentOperationsClient.cs @@ -6,9 +6,9 @@ namespace EventStore.Client; /// -/// The client used to perform maintenance and other administrative tasks on the EventStoreDB. +/// The client used to perform maintenance and other administrative tasks on the KurrentDB. /// -public sealed partial class EventStoreOperationsClient : EventStoreClientBase { +public sealed partial class KurrentOperationsClient : KurrentClientBase { static readonly Dictionary> ExceptionMap = new() { [Constants.Exceptions.ScavengeNotFound] = ex => new ScavengeNotFoundException( @@ -19,15 +19,15 @@ public sealed partial class EventStoreOperationsClient : EventStoreClientBase { readonly ILogger _log; /// - /// Constructs a new . This method is not intended to be called directly in your code. + /// Constructs a new . This method is not intended to be called directly in your code. /// /// - public EventStoreOperationsClient(IOptions options) : this(options.Value) { } + public KurrentOperationsClient(IOptions options) : this(options.Value) { } /// - /// Constructs a new . + /// Constructs a new . /// /// - public EventStoreOperationsClient(EventStoreClientSettings? settings = null) : base(settings, ExceptionMap) => - _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); -} \ No newline at end of file + public KurrentOperationsClient(KurrentClientSettings? settings = null) : base(settings, ExceptionMap) => + _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); +} diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClientServiceCollectionExtensions.cs b/src/Kurrent.Client/Operations/KurrentOperationsClientServiceCollectionExtensions.cs similarity index 52% rename from src/EventStore.Client.Operations/EventStoreOperationsClientServiceCollectionExtensions.cs rename to src/Kurrent.Client/Operations/KurrentOperationsClientServiceCollectionExtensions.cs index 9f664277f..fc3ce9505 100644 --- a/src/EventStore.Client.Operations/EventStoreOperationsClientServiceCollectionExtensions.cs +++ b/src/Kurrent.Client/Operations/KurrentOperationsClientServiceCollectionExtensions.cs @@ -6,54 +6,53 @@ using Grpc.Core.Interceptors; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; -using EventStoreOperationsClient = EventStore.Client.EventStoreOperationsClient; namespace Microsoft.Extensions.DependencyInjection { /// - /// A set of extension methods for which provide support for an . + /// A set of extension methods for which provide support for an . /// - public static class EventStoreOperationsClientServiceCollectionExtensions { + public static class KurrentOperationsClientServiceCollectionExtensions { /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// /// - public static IServiceCollection AddEventStoreOperationsClient(this IServiceCollection services, Uri address, + public static IServiceCollection AddKurrentOperationsClient(this IServiceCollection services, Uri address, Func? createHttpMessageHandler = null) - => services.AddEventStoreOperationsClient(options => { + => services.AddKurrentOperationsClient(options => { options.ConnectivitySettings.Address = address; options.CreateHttpMessageHandler = createHttpMessageHandler; }); /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// - public static IServiceCollection AddEventStoreOperationsClient(this IServiceCollection services, - Action? configureOptions = null) => - services.AddEventStoreOperationsClient(new EventStoreClientSettings(), configureOptions); + public static IServiceCollection AddKurrentOperationsClient(this IServiceCollection services, + Action? configureOptions = null) => + services.AddKurrentOperationsClient(new KurrentClientSettings(), configureOptions); /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// /// - public static IServiceCollection AddEventStoreOperationsClient(this IServiceCollection services, - string connectionString, Action? configureOptions = null) => - services.AddEventStoreOperationsClient(EventStoreClientSettings.Create(connectionString), configureOptions); + public static IServiceCollection AddKurrentOperationsClient(this IServiceCollection services, + string connectionString, Action? configureOptions = null) => + services.AddKurrentOperationsClient(KurrentClientSettings.Create(connectionString), configureOptions); - private static IServiceCollection AddEventStoreOperationsClient(this IServiceCollection services, - EventStoreClientSettings options, Action? configureOptions) { + private static IServiceCollection AddKurrentOperationsClient(this IServiceCollection services, + KurrentClientSettings options, Action? configureOptions) { if (services == null) { throw new ArgumentNullException(nameof(services)); } @@ -64,7 +63,7 @@ private static IServiceCollection AddEventStoreOperationsClient(this IServiceCol options.LoggerFactory ??= provider.GetService(); options.Interceptors ??= provider.GetServices(); - return new EventStoreOperationsClient(options); + return new KurrentOperationsClient(options); }); return services; diff --git a/src/EventStore.Client.Operations/ScavengeResult.cs b/src/Kurrent.Client/Operations/ScavengeResult.cs similarity index 100% rename from src/EventStore.Client.Operations/ScavengeResult.cs rename to src/Kurrent.Client/Operations/ScavengeResult.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Create.cs similarity index 98% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs rename to src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Create.cs index 4cb7acac0..80987b3ce 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Create.cs @@ -5,7 +5,7 @@ using EventStore.Client.PersistentSubscriptions; namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { + partial class KurrentPersistentSubscriptionsClient { private static readonly IDictionary NamedConsumerStrategyToCreateProto = new Dictionary { [SystemConsumerStrategies.DispatchToSingle] = CreateReq.Types.ConsumerStrategy.DispatchToSingle, @@ -245,7 +245,7 @@ private async Task CreateInternalAsync(string streamName, string groupName, IEve ReadBatchSize = settings.ReadBatchSize } } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } } diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Delete.cs similarity index 93% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs rename to src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Delete.cs index 40ef522ba..d84c0e61a 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Delete.cs @@ -4,7 +4,7 @@ using EventStore.Client.PersistentSubscriptions; namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { + partial class KurrentPersistentSubscriptionsClient { /// /// Deletes a persistent subscription. /// @@ -39,7 +39,7 @@ public async Task DeleteToStreamAsync(string streamName, string groupName, TimeS new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient( channelInfo.CallInvoker) .DeleteAsync(new DeleteReq {Options = deleteOptions}, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Info.cs similarity index 94% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs rename to src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Info.cs index b1cc4bebf..78ececf72 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Info.cs @@ -6,7 +6,7 @@ #nullable enable namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { + partial class KurrentPersistentSubscriptionsClient { /// /// Gets the status of a persistent subscription to $all /// @@ -56,7 +56,7 @@ private async Task GetInfoGrpcAsync(GetInfoReq req, UserCredentials? userCredentials, CallInvoker callInvoker, CancellationToken cancellationToken) { var result = await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) - .GetInfoAsync(req, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) + .GetInfoAsync(req, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) .ConfigureAwait(false); return PersistentSubscriptionInfo.From(result.SubscriptionInfo); diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.List.cs similarity index 93% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs rename to src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.List.cs index ce588e3f7..a2c92b733 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.List.cs @@ -1,14 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using EventStore.Client.PersistentSubscriptions; using Grpc.Core; #nullable enable namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { + partial class KurrentPersistentSubscriptionsClient { /// /// Lists persistent subscriptions to $all. /// @@ -90,7 +85,7 @@ private async Task> ListGrpcAsync(ListRe UserCredentials? userCredentials, CallInvoker callInvoker, CancellationToken cancellationToken) { using var call = new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) - .ListAsync(req, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + .ListAsync(req, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); ListResp? response = await call.ResponseAsync.ConfigureAwait(false); diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs new file mode 100644 index 000000000..1ed1b40a0 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Read.cs @@ -0,0 +1,783 @@ +using System.Threading.Channels; +using EventStore.Client.PersistentSubscriptions; +using EventStore.Client.Diagnostics; +using Grpc.Core; +using Kurrent.Client.Core.Serialization; +using static EventStore.Client.PersistentSubscriptions.PersistentSubscriptions; +using static EventStore.Client.PersistentSubscriptions.ReadResp.ContentOneofCase; + +namespace EventStore.Client { + public class SubscribeToPersistentSubscriptionOptions { + /// + /// The size of the buffer. + /// + public int BufferSize { get; set; } = 10; + + /// + /// The optional user credentials to perform operation with. + /// + public UserCredentials? UserCredentials { get; set; } = null; + + /// + /// Allows to customize or disable the automatic deserialization + /// + public OperationSerializationSettings? SerializationSettings { get; set; } + } + + public class PersistentSubscriptionListener { + /// + /// A handler called when a new event is received over the subscription. + /// +#if NET48 + public Func EventAppeared { get; set; } = + null!; +#else + public required Func EventAppeared { + get; + set; + } +#endif + /// + /// A handler called if the subscription is dropped. + /// + public Action? SubscriptionDropped { get; set; } + + /// + /// Returns the subscription listener with configured handlers + /// + /// Handler invoked when a new event is received over the subscription. + /// A handler invoked if the subscription is dropped. + /// A handler called when a checkpoint is reached. + /// Set the checkpointInterval in subscription filter options to define how often this method is called. + /// + /// + public static PersistentSubscriptionListener Handle( + Func eventAppeared, + Action? subscriptionDropped = null + ) => + new PersistentSubscriptionListener { + EventAppeared = eventAppeared, + SubscriptionDropped = subscriptionDropped + }; + } + + partial class KurrentPersistentSubscriptionsClient { + /// + /// Subscribes to a persistent subscription. + /// + /// + /// + /// + [Obsolete("SubscribeAsync is no longer supported. Use SubscribeToStream with manual acks instead.", false)] + public async Task SubscribeAsync( + string streamName, + string groupName, + Func eventAppeared, + Action? subscriptionDropped = null, + UserCredentials? userCredentials = null, + int bufferSize = 10, + bool autoAck = true, + CancellationToken cancellationToken = default + ) { + if (autoAck) { + throw new InvalidOperationException( + $"AutoAck is no longer supported. Please use {nameof(SubscribeToStream)} with manual acks instead." + ); + } + + return await SubscribeToStreamAsync( + streamName, + groupName, + PersistentSubscriptionListener.Handle(eventAppeared, subscriptionDropped), + new SubscribeToPersistentSubscriptionOptions { + UserCredentials = userCredentials, + BufferSize = bufferSize, + SerializationSettings = OperationSerializationSettings.Disabled + }, + cancellationToken + ); + } + + /// + /// Subscribes to a persistent subscription. Messages must be manually acknowledged + /// + /// + /// + /// + public Task SubscribeToStreamAsync( + string streamName, + string groupName, + Func eventAppeared, + Action? subscriptionDropped = null, + UserCredentials? userCredentials = null, + int bufferSize = 10, + CancellationToken cancellationToken = default + ) => + SubscribeToStreamAsync( + streamName, + groupName, + PersistentSubscriptionListener.Handle(eventAppeared, subscriptionDropped), + new SubscribeToPersistentSubscriptionOptions { + UserCredentials = userCredentials, + BufferSize = bufferSize, + SerializationSettings = OperationSerializationSettings.Disabled + }, + cancellationToken + ); + + /// + /// Subscribes to a persistent subscription. Messages must be manually acknowledged + /// + /// + /// + /// + public async Task SubscribeToStreamAsync( + string streamName, + string groupName, + PersistentSubscriptionListener listener, + SubscribeToPersistentSubscriptionOptions options, + CancellationToken cancellationToken = default + ) { + return await PersistentSubscription + .Confirm( + SubscribeToStream(streamName, groupName, options, cancellationToken), + listener, + _log, + cancellationToken + ) + .ConfigureAwait(false); + } + + /// + /// Subscribes to a persistent subscription. Messages must be manually acknowledged. + /// + /// The name of the stream to read events from. + /// The name of the persistent subscription group. + /// The size of the buffer. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public PersistentSubscriptionResult SubscribeToStream( + string streamName, + string groupName, + int bufferSize, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) { + return SubscribeToStream( + streamName, + groupName, + new SubscribeToPersistentSubscriptionOptions { + BufferSize = bufferSize, + UserCredentials = userCredentials, + SerializationSettings = OperationSerializationSettings.Disabled + }, + cancellationToken + ); + } + + + + /// + /// Subscribes to a persistent subscription. Messages must be manually acknowledged. + /// + /// The name of the stream to read events from. + /// The name of the persistent subscription group. + /// The size of the buffer. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public PersistentSubscriptionResult SubscribeToStream( + string streamName, + string groupName, + UserCredentials? userCredentials, + CancellationToken cancellationToken = default + ) { + return SubscribeToStream( + streamName, + groupName, + new SubscribeToPersistentSubscriptionOptions { + UserCredentials = userCredentials, + SerializationSettings = OperationSerializationSettings.Disabled + }, + cancellationToken + ); + } + + /// + /// Subscribes to a persistent subscription. Messages must be manually acknowledged. + /// + /// The name of the stream to read events from. + /// The name of the persistent subscription group. + /// Optional settings to configure subscription + /// The optional . + /// + public PersistentSubscriptionResult SubscribeToStream( + string streamName, + string groupName, + SubscribeToPersistentSubscriptionOptions options, + CancellationToken cancellationToken = default + ) { + if (streamName == null) { + throw new ArgumentNullException(nameof(streamName)); + } + + if (groupName == null) { + throw new ArgumentNullException(nameof(groupName)); + } + + if (streamName == string.Empty) { + throw new ArgumentException($"{nameof(streamName)} may not be empty.", nameof(streamName)); + } + + if (groupName == string.Empty) { + throw new ArgumentException($"{nameof(groupName)} may not be empty.", nameof(groupName)); + } + + if (options.BufferSize <= 0) { + throw new ArgumentOutOfRangeException(nameof(options.BufferSize)); + } + + var readOptions = new ReadReq.Types.Options { + BufferSize = options.BufferSize, + GroupName = groupName, + UuidOption = new ReadReq.Types.Options.Types.UUIDOption { Structured = new Empty() } + }; + + if (streamName == SystemStreams.AllStream) { + readOptions.All = new Empty(); + } else { + readOptions.StreamIdentifier = streamName; + } + + return new PersistentSubscriptionResult( + streamName, + groupName, + async ct => { + var channelInfo = await GetChannelInfo(ct).ConfigureAwait(false); + + if (streamName == SystemStreams.AllStream && + !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { + throw new NotSupportedException( + "The server does not support persistent subscriptions to $all." + ); + } + + return channelInfo; + }, + new() { Options = readOptions }, + Settings, + options.UserCredentials, + _messageSerializer.With(Settings.Serialization, options.SerializationSettings), + cancellationToken + ); + } + + /// + /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged + /// + public Task SubscribeToAllAsync( + string groupName, + Func eventAppeared, + Action? subscriptionDropped = null, + UserCredentials? userCredentials = null, + int bufferSize = 10, + CancellationToken cancellationToken = default + ) => + SubscribeToAllAsync( + groupName, + PersistentSubscriptionListener.Handle(eventAppeared, subscriptionDropped), + new SubscribeToPersistentSubscriptionOptions { + BufferSize = bufferSize, + UserCredentials = userCredentials, + SerializationSettings = OperationSerializationSettings.Disabled + }, + cancellationToken + ); + + /// + /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged + /// + public Task SubscribeToAllAsync( + string groupName, + PersistentSubscriptionListener listener, + SubscribeToPersistentSubscriptionOptions options, + CancellationToken cancellationToken = default + ) => + SubscribeToStreamAsync( + SystemStreams.AllStream, + groupName, + listener, + options, + cancellationToken + ); + + /// + /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged. + /// + /// The name of the persistent subscription group. + /// The size of the buffer. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public PersistentSubscriptionResult SubscribeToAll( + string groupName, + int bufferSize, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) => + SubscribeToStream( + SystemStreams.AllStream, + groupName, + new SubscribeToPersistentSubscriptionOptions { + BufferSize = bufferSize, + UserCredentials = userCredentials, + SerializationSettings = OperationSerializationSettings.Disabled + }, + cancellationToken + ); + + + /// + /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged. + /// + /// The name of the persistent subscription group. + /// The size of the buffer. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public PersistentSubscriptionResult SubscribeToAll( + string groupName, + UserCredentials? userCredentials, + CancellationToken cancellationToken = default + ) => + SubscribeToStream( + SystemStreams.AllStream, + groupName, + new SubscribeToPersistentSubscriptionOptions { + UserCredentials = userCredentials, + SerializationSettings = OperationSerializationSettings.Disabled + }, + cancellationToken + ); + + /// + /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged. + /// + /// The name of the persistent subscription group. + /// Optional settings to configure subscription + /// The optional . + /// + public PersistentSubscriptionResult SubscribeToAll( + string groupName, + SubscribeToPersistentSubscriptionOptions options, + CancellationToken cancellationToken = default + ) => + SubscribeToStream( + SystemStreams.AllStream, + groupName, + options, + cancellationToken + ); + + /// + public class PersistentSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable { + const int MaxEventIdLength = 2000; + + readonly ReadReq _request; + readonly Channel _channel; + readonly CancellationTokenSource _cts; + readonly CallOptions _callOptions; + + AsyncDuplexStreamingCall? _call; + int _messagesEnumerated; + + /// + /// The server-generated unique identifier for the subscription. + /// + public string? SubscriptionId { get; private set; } + + /// + /// The name of the stream to read events from. + /// + public string StreamName { get; } + + /// + /// The name of the persistent subscription group. + /// + public string GroupName { get; } + + /// + /// An . Do not enumerate more than once. + /// + public IAsyncEnumerable Messages { + get { + if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) + throw new InvalidOperationException("Messages may only be enumerated once."); + + return GetMessages(); + + async IAsyncEnumerable GetMessages() { + try { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { + if (message is PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId)) + SubscriptionId = subscriptionId; + + yield return message; + } + } finally { + _cts.Cancel(); + } + } + } + } + + internal PersistentSubscriptionResult( + string streamName, + string groupName, + Func> selectChannelInfo, + ReadReq request, + KurrentClientSettings settings, + UserCredentials? userCredentials, + IMessageSerializer messageSerializer, + CancellationToken cancellationToken + ) { + StreamName = streamName; + GroupName = groupName; + + _request = request; + + _callOptions = KurrentCallOptions.CreateStreaming( + settings, + userCredentials: userCredentials, + cancellationToken: cancellationToken + ); + + _channel = Channel.CreateBounded(ReadBoundedChannelOptions); + + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + _ = PumpMessages(); + + return; + + async Task PumpMessages() { + try { + var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); + var client = new PersistentSubscriptionsClient(channelInfo.CallInvoker); + + _call = client.Read(_callOptions); + + await _call.RequestStream.WriteAsync(_request).ConfigureAwait(false); + + await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token) + .ConfigureAwait(false)) { + PersistentSubscriptionMessage subscriptionMessage = response.ContentCase switch { + SubscriptionConfirmation => new PersistentSubscriptionMessage.SubscriptionConfirmation( + response.SubscriptionConfirmation.SubscriptionId + ), + Event => new PersistentSubscriptionMessage.Event( + ConvertToResolvedEvent(response, messageSerializer), + response.Event.CountCase switch { + ReadResp.Types.ReadEvent.CountOneofCase.RetryCount => response.Event.RetryCount, + _ => null + } + ), + _ => PersistentSubscriptionMessage.Unknown.Instance + }; + + if (subscriptionMessage is PersistentSubscriptionMessage.Event evnt) + KurrentClientDiagnostics.ActivitySource.TraceSubscriptionEvent( + SubscriptionId, + evnt.ResolvedEvent, + channelInfo, + settings, + userCredentials + ); + + await _channel.Writer.WriteAsync(subscriptionMessage, _cts.Token).ConfigureAwait(false); + } + + _channel.Writer.TryComplete(); + } catch (Exception ex) { +#if NET48 + switch (ex) { + // The gRPC client for .NET 48 uses WinHttpHandler under the hood for sending HTTP requests. + // In certain scenarios, this can lead to exceptions of type WinHttpException being thrown. + // One such scenario is when the server abruptly closes the connection, which results in a WinHttpException with the error code 12030. + // Additionally, there are cases where the server response does not include the 'grpc-status' header. + // The absence of this header leads to an RpcException with the status code 'Cancelled' and the message "No grpc-status found on response". + // The switch statement below handles these specific exceptions and translates them into the appropriate + // PersistentSubscriptionDroppedByServerException exception. + case RpcException { StatusCode: StatusCode.Unavailable } rex1 + when rex1.Status.Detail.Contains("WinHttpException: Error 12030"): + case RpcException { StatusCode: StatusCode.Cancelled } rex2 + when rex2.Status.Detail.Contains("No grpc-status found on response"): + ex = new PersistentSubscriptionDroppedByServerException(StreamName, GroupName, ex); + break; + } +#endif + if (ex is PersistentSubscriptionNotFoundException) { + await _channel.Writer + .WriteAsync(PersistentSubscriptionMessage.NotFound.Instance, cancellationToken) + .ConfigureAwait(false); + + _channel.Writer.TryComplete(); + return; + } + + _channel.Writer.TryComplete(ex); + } + } + } + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(params Uuid[] eventIds) => AckInternal(eventIds); + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The of the s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(IEnumerable eventIds) => Ack(eventIds.ToArray()); + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(params ResolvedEvent[] resolvedEvents) => + Ack(Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); + + /// + /// Acknowledge that a message has completed processing (this will tell the server it has been processed). + /// + /// There is no need to ack a message if you have Auto Ack enabled. + /// The s to acknowledge. There should not be more than 2000 to ack at a time. + public Task Ack(IEnumerable resolvedEvents) => + Ack(resolvedEvents.Select(resolvedEvent => resolvedEvent.OriginalEvent.EventId)); + + /// + /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). + /// + /// The to take. + /// A reason given. + /// The of the s to nak. There should not be more than 2000 to nak at a time. + /// The number of eventIds exceeded the limit of 2000. + public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => + NackInternal(eventIds, action, reason); + + /// + /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). + /// + /// The to take. + /// A reason given. + /// The s to nak. There should not be more than 2000 to nak at a time. + /// The number of resolvedEvents exceeded the limit of 2000. + public Task Nack( + PersistentSubscriptionNakEventAction action, string reason, params ResolvedEvent[] resolvedEvents + ) => + Nack(action, reason, Array.ConvertAll(resolvedEvents, re => re.OriginalEvent.EventId)); + + static ResolvedEvent ConvertToResolvedEvent( + ReadResp response, + IMessageSerializer messageSerializer + ) => + ResolvedEvent.From( + ConvertToEventRecord(response.Event.Event)!, + ConvertToEventRecord(response.Event.Link), + response.Event.PositionCase switch { + ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => response.Event.CommitPosition, + _ => null + }, + messageSerializer + ); + + Task AckInternal(params Uuid[] eventIds) { + if (eventIds.Length > MaxEventIdLength) { + throw new ArgumentException( + $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", + nameof(eventIds) + ); + } + + return _call is null + ? throw new InvalidOperationException() + : _call.RequestStream.WriteAsync( + new ReadReq { + Ack = new ReadReq.Types.Ack { + Ids = { + Array.ConvertAll(eventIds, id => id.ToDto()) + } + } + } + ); + } + + Task NackInternal(Uuid[] eventIds, PersistentSubscriptionNakEventAction action, string reason) { + if (eventIds.Length > MaxEventIdLength) { + throw new ArgumentException( + $"The number of eventIds exceeds the maximum length of {MaxEventIdLength}.", + nameof(eventIds) + ); + } + + return _call is null + ? throw new InvalidOperationException() + : _call.RequestStream.WriteAsync( + new ReadReq { + Nack = new ReadReq.Types.Nack { + Ids = { + Array.ConvertAll(eventIds, id => id.ToDto()) + }, + Action = action switch { + PersistentSubscriptionNakEventAction.Park => ReadReq.Types.Nack.Types.Action.Park, + PersistentSubscriptionNakEventAction.Retry => ReadReq.Types.Nack.Types.Action.Retry, + PersistentSubscriptionNakEventAction.Skip => ReadReq.Types.Nack.Types.Action.Skip, + PersistentSubscriptionNakEventAction.Stop => ReadReq.Types.Nack.Types.Action.Stop, + _ => ReadReq.Types.Nack.Types.Action.Unknown + }, + Reason = reason + } + } + ); + } + + static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => + e is null + ? null + : new EventRecord( + e.StreamIdentifier!, + Uuid.FromDto(e.Id), + new StreamPosition(e.StreamRevision), + new Position(e.CommitPosition, e.PreparePosition), + e.Metadata, + e.Data.ToByteArray(), + e.CustomMetadata.ToByteArray() + ); + + /// + public async ValueTask DisposeAsync() { + await CastAndDispose(_cts).ConfigureAwait(false); + await CastAndDispose(_call).ConfigureAwait(false); + + return; + + static async Task CastAndDispose(IDisposable? resource) { + switch (resource) { + case null: + return; + + case IAsyncDisposable resourceAsyncDisposable: + await resourceAsyncDisposable.DisposeAsync().ConfigureAwait(false); + break; + + default: + resource.Dispose(); + break; + } + } + } + + /// + public void Dispose() { + _cts.Dispose(); + _call?.Dispose(); + } + + /// + public async IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default + ) { + await foreach (var message in Messages.WithCancellation(cancellationToken)) { + if (message is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) + continue; + + yield return resolvedEvent; + } + } + } + } + + public static class KurrentClientPersistentSubscriptionsExtensions { + /// + /// Subscribes to a persistent subscription. Messages must be manually acknowledged + /// + /// + /// + /// + public static Task SubscribeToStreamAsync( + this KurrentPersistentSubscriptionsClient kurrentClient, + string streamName, + string groupName, + PersistentSubscriptionListener listener, + CancellationToken cancellationToken = default + ) => + kurrentClient.SubscribeToStreamAsync( + streamName, + groupName, + listener, + new SubscribeToPersistentSubscriptionOptions(), + cancellationToken + ); + + /// + /// Subscribes to a persistent subscription. Messages must be manually acknowledged. + /// + /// + /// The name of the stream to read events from. + /// The name of the persistent subscription group. + /// The optional . + /// + public static KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult SubscribeToStream( + this KurrentPersistentSubscriptionsClient kurrentClient, + string streamName, + string groupName, + CancellationToken cancellationToken = default + ) => + kurrentClient.SubscribeToStream( + streamName, + groupName, + new SubscribeToPersistentSubscriptionOptions(), + cancellationToken + ); + + /// + /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged + /// + public static Task SubscribeToAllAsync( + this KurrentPersistentSubscriptionsClient kurrentClient, + string groupName, + PersistentSubscriptionListener listener, + CancellationToken cancellationToken = default + ) => + kurrentClient.SubscribeToAllAsync( + groupName, + listener, + new SubscribeToPersistentSubscriptionOptions(), + cancellationToken + ); + + /// + /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged. + /// + /// + /// The name of the persistent subscription group. + /// The optional . + /// + public static KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult SubscribeToAll( + this KurrentPersistentSubscriptionsClient kurrentClient, + string groupName, + CancellationToken cancellationToken = default + ) => + kurrentClient.SubscribeToAll( + groupName, + new SubscribeToPersistentSubscriptionOptions(), + cancellationToken + ); + } +} diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.ReplayParked.cs similarity index 95% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs rename to src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.ReplayParked.cs index 248ed9143..e13d4a4ad 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.ReplayParked.cs @@ -7,7 +7,7 @@ #nullable enable namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { + partial class KurrentPersistentSubscriptionsClient { /// /// Retry the parked messages of the persistent subscription /// @@ -75,7 +75,7 @@ private async Task ReplayParkedGrpcAsync(ReplayParkedReq req, long? numberOfEven } await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(callInvoker) - .ReplayParkedAsync(req, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) + .ReplayParkedAsync(req, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) .ConfigureAwait(false); } diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.RestartSubsystem.cs similarity index 90% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs rename to src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.RestartSubsystem.cs index 4d01967d8..a82587442 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.RestartSubsystem.cs @@ -4,7 +4,7 @@ #nullable enable namespace EventStore.Client { - partial class EventStorePersistentSubscriptionsClient { + partial class KurrentPersistentSubscriptionsClient { /// /// Restarts the persistent subscriptions subsystem. /// @@ -14,7 +14,7 @@ public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentia var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsRestartSubsystem) { await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(channelInfo.CallInvoker) - .RestartSubsystemAsync(new Empty(), EventStoreCallOptions + .RestartSubsystemAsync(new Empty(), KurrentCallOptions .CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)) .ConfigureAwait(false); return; diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Update.cs similarity index 95% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs rename to src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Update.cs index 1eae92a25..19eb67d4b 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.Update.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using EventStore.Client.PersistentSubscriptions; namespace EventStore.Client { - public partial class EventStorePersistentSubscriptionsClient { + public partial class KurrentPersistentSubscriptionsClient { private static readonly IDictionary NamedConsumerStrategyToUpdateProto = new Dictionary { [SystemConsumerStrategies.DispatchToSingle] = UpdateReq.Types.ConsumerStrategy.DispatchToSingle, @@ -64,7 +60,7 @@ private static UpdateReq.Types.AllOptions AllOptionsForUpdateProto(Position posi [Obsolete("UpdateAsync is no longer supported. Use UpdateToStreamAsync instead.", false)] public Task UpdateAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => + CancellationToken cancellationToken = default) => UpdateToStreamAsync(streamName, groupName, settings, deadline, userCredentials, cancellationToken); /// @@ -148,7 +144,7 @@ public async Task UpdateToStreamAsync(string streamName, string groupName, Persi } } }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.cs similarity index 64% rename from src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs rename to src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.cs index 411de3e70..0d251f59c 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClient.cs @@ -1,26 +1,28 @@ using System.Text.Encodings.Web; using System.Threading.Channels; using Grpc.Core; +using Kurrent.Client.Core.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; namespace EventStore.Client { /// - /// The client used to manage persistent subscriptions in the EventStoreDB. + /// The client used to manage persistent subscriptions in the KurrentDB. /// - public sealed partial class EventStorePersistentSubscriptionsClient : EventStoreClientBase { - private static BoundedChannelOptions ReadBoundedChannelOptions = new (1) { + public sealed partial class KurrentPersistentSubscriptionsClient : KurrentClientBase { + static BoundedChannelOptions ReadBoundedChannelOptions = new (1) { SingleReader = true, SingleWriter = true, AllowSynchronousContinuations = true }; - private readonly ILogger _log; + readonly ILogger _log; + readonly IMessageSerializer _messageSerializer; /// - /// Constructs a new . + /// Constructs a new . /// - public EventStorePersistentSubscriptionsClient(EventStoreClientSettings? settings) : base(settings, + public KurrentPersistentSubscriptionsClient(KurrentClientSettings? settings) : base(settings, new Dictionary> { [Constants.Exceptions.PersistentSubscriptionDoesNotExist] = ex => new PersistentSubscriptionNotFoundException( @@ -35,8 +37,10 @@ public EventStorePersistentSubscriptionsClient(EventStoreClientSettings? setting ex.Trailers.First(x => x.Key == Constants.Exceptions.StreamName).Value, ex.Trailers.First(x => x.Key == Constants.Exceptions.GroupName).Value, ex) }) { - _log = Settings.LoggerFactory?.CreateLogger() - ?? new NullLogger(); + _log = Settings.LoggerFactory?.CreateLogger() + ?? new NullLogger(); + + _messageSerializer = MessageSerializer.From(settings?.Serialization); } private static string UrlEncode(string s) { diff --git a/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClientCollectionExtensions.cs b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClientCollectionExtensions.cs new file mode 100644 index 000000000..9ed65e091 --- /dev/null +++ b/src/Kurrent.Client/PersistentSubscriptions/KurrentPersistentSubscriptionsClientCollectionExtensions.cs @@ -0,0 +1,61 @@ +// ReSharper disable CheckNamespace + +using System; +using System.Net.Http; +using EventStore.Client; +using Grpc.Core.Interceptors; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.DependencyInjection { + /// + /// A set of extension methods for which provide support for an . + /// + public static class KurrentPersistentSubscriptionsClientCollectionExtensions { + /// + /// Adds an to the . + /// + /// + public static IServiceCollection AddKurrentPersistentSubscriptionsClient(this IServiceCollection services, + Uri address, Func? createHttpMessageHandler = null) + => services.AddKurrentPersistentSubscriptionsClient(options => { + options.ConnectivitySettings.Address = address; + options.CreateHttpMessageHandler = createHttpMessageHandler; + }); + + /// + /// Adds an to the . + /// + /// + public static IServiceCollection AddKurrentPersistentSubscriptionsClient(this IServiceCollection services, + Action? configureSettings = null) => + services.AddKurrentPersistentSubscriptionsClient(new KurrentClientSettings(), + configureSettings); + + /// + /// Adds an to the . + /// + /// + public static IServiceCollection AddKurrentPersistentSubscriptionsClient(this IServiceCollection services, + string connectionString, Action? configureSettings = null) => + services.AddKurrentPersistentSubscriptionsClient(KurrentClientSettings.Create(connectionString), + configureSettings); + + private static IServiceCollection AddKurrentPersistentSubscriptionsClient(this IServiceCollection services, + KurrentClientSettings settings, Action? configureSettings) { + if (services == null) { + throw new ArgumentNullException(nameof(services)); + } + + configureSettings?.Invoke(settings); + services.TryAddSingleton(provider => { + settings.LoggerFactory ??= provider.GetService(); + settings.Interceptors ??= provider.GetServices(); + + return new KurrentPersistentSubscriptionsClient(settings); + }); + return services; + } + } +} +// ReSharper restore CheckNamespace diff --git a/src/EventStore.Client.PersistentSubscriptions/MaximumSubscribersReachedException.cs b/src/Kurrent.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/MaximumSubscribersReachedException.cs rename to src/Kurrent.Client/PersistentSubscriptions/MaximumSubscribersReachedException.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscription.cs similarity index 75% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs rename to src/Kurrent.Client/PersistentSubscriptions/PersistentSubscription.cs index f3d19b42e..0674cb9ac 100644 --- a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscription.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscription.cs @@ -7,7 +7,9 @@ namespace EventStore.Client { /// Represents a persistent subscription connection. /// public class PersistentSubscription : IDisposable { - private readonly EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult _persistentSubscriptionResult; + private readonly KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult + _persistentSubscriptionResult; + private readonly IAsyncEnumerator _enumerator; private readonly Func _eventAppeared; private readonly Action _subscriptionDropped; @@ -22,10 +24,11 @@ public class PersistentSubscription : IDisposable { public string SubscriptionId { get; } internal static async Task Confirm( - EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult, - Func eventAppeared, - Action subscriptionDropped, - ILogger log, UserCredentials? userCredentials, CancellationToken cancellationToken = default) { + KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult, + PersistentSubscriptionListener listener, + ILogger log, + CancellationToken cancellationToken = default + ) { var enumerator = persistentSubscriptionResult .Messages .GetAsyncEnumerator(cancellationToken); @@ -34,29 +37,39 @@ internal static async Task Confirm( return (result, enumerator.Current) switch { (true, PersistentSubscriptionMessage.SubscriptionConfirmation (var subscriptionId)) => - new PersistentSubscription(persistentSubscriptionResult, enumerator, subscriptionId, eventAppeared, - subscriptionDropped, log, cancellationToken), + new PersistentSubscription( + persistentSubscriptionResult, + enumerator, + subscriptionId, + listener, + log, + cancellationToken + ), (true, PersistentSubscriptionMessage.NotFound) => - throw new PersistentSubscriptionNotFoundException(persistentSubscriptionResult.StreamName, - persistentSubscriptionResult.GroupName), + throw new PersistentSubscriptionNotFoundException( + persistentSubscriptionResult.StreamName, + persistentSubscriptionResult.GroupName + ), _ => throw new InvalidOperationException("Subscription could not be confirmed.") }; } // PersistentSubscription takes responsibility for disposing the call and the disposable private PersistentSubscription( - EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult, - IAsyncEnumerator enumerator, string subscriptionId, - Func eventAppeared, - Action subscriptionDropped, ILogger log, - CancellationToken cancellationToken) { + KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult persistentSubscriptionResult, + IAsyncEnumerator enumerator, + string subscriptionId, + PersistentSubscriptionListener listener, + ILogger log, + CancellationToken cancellationToken + ) { _persistentSubscriptionResult = persistentSubscriptionResult; - _enumerator = enumerator; - SubscriptionId = subscriptionId; - _eventAppeared = eventAppeared; - _subscriptionDropped = subscriptionDropped; - _log = log; - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _enumerator = enumerator; + SubscriptionId = subscriptionId; + _eventAppeared = listener.EventAppeared; + _subscriptionDropped = listener.SubscriptionDropped ?? delegate { }; + _log = log; + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); Task.Run(Subscribe, _cts.Token); } @@ -91,7 +104,6 @@ public Task Ack(params ResolvedEvent[] resolvedEvents) => public Task Ack(IEnumerable resolvedEvents) => Ack(resolvedEvents.Select(resolvedEvent => resolvedEvent.OriginalEvent.EventId)); - /// /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). /// @@ -99,7 +111,8 @@ public Task Ack(IEnumerable resolvedEvents) => /// A reason given. /// The of the s to nak. There should not be more than 2000 to nak at a time. /// The number of eventIds exceeded the limit of 2000. - public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => NackInternal(eventIds, action, reason); + public Task Nack(PersistentSubscriptionNakEventAction action, string reason, params Uuid[] eventIds) => + NackInternal(eventIds, action, reason); /// /// Acknowledge that a message has failed processing (this will tell the server it has not been processed). @@ -108,10 +121,15 @@ public Task Ack(IEnumerable resolvedEvents) => /// A reason given. /// The s to nak. There should not be more than 2000 to nak at a time. /// The number of resolvedEvents exceeded the limit of 2000. - public Task Nack(PersistentSubscriptionNakEventAction action, string reason, - params ResolvedEvent[] resolvedEvents) => - Nack(action, reason, - Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId)); + public Task Nack( + PersistentSubscriptionNakEventAction action, string reason, + params ResolvedEvent[] resolvedEvents + ) => + Nack( + action, + reason, + Array.ConvertAll(resolvedEvents, resolvedEvent => resolvedEvent.OriginalEvent.EventId) + ); /// public void Dispose() => SubscriptionDropped(SubscriptionDroppedReason.Disposed); @@ -121,7 +139,8 @@ private async Task Subscribe() { try { while (await _enumerator.MoveNextAsync(_cts.Token).ConfigureAwait(false)) { - if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, var retryCount)) { + if (_enumerator.Current is not + PersistentSubscriptionMessage.Event(var resolvedEvent, var retryCount)) { continue; } @@ -129,39 +148,54 @@ private async Task Subscribe() { if (_subscriptionDroppedInvoked != 0) { return; } - SubscriptionDropped(SubscriptionDroppedReason.ServerError, + + SubscriptionDropped( + SubscriptionDroppedReason.ServerError, new PersistentSubscriptionNotFoundException( - _persistentSubscriptionResult.StreamName, _persistentSubscriptionResult.GroupName)); + _persistentSubscriptionResult.StreamName, + _persistentSubscriptionResult.GroupName + ) + ); + return; } - + _log.LogTrace( "Persistent Subscription {subscriptionId} received event {streamName}@{streamRevision} {position}", - SubscriptionId, resolvedEvent.OriginalEvent.EventStreamId, - resolvedEvent.OriginalEvent.EventNumber, resolvedEvent.OriginalEvent.Position); + SubscriptionId, + resolvedEvent.OriginalEvent.EventStreamId, + resolvedEvent.OriginalEvent.EventNumber, + resolvedEvent.OriginalEvent.Position + ); try { await _eventAppeared( this, resolvedEvent, retryCount, - _cts.Token).ConfigureAwait(false); + _cts.Token + ).ConfigureAwait(false); } catch (Exception ex) when (ex is ObjectDisposedException or OperationCanceledException) { if (_subscriptionDroppedInvoked != 0) { return; } - _log.LogWarning(ex, + _log.LogWarning( + ex, "Persistent Subscription {subscriptionId} was dropped because cancellation was requested by another caller.", - SubscriptionId); + SubscriptionId + ); SubscriptionDropped(SubscriptionDroppedReason.Disposed); return; } catch (Exception ex) { - _log.LogError(ex, + _log.LogError( + ex, "Persistent Subscription {subscriptionId} was dropped because the subscriber made an error.", - SubscriptionId); + SubscriptionId + ); + SubscriptionDropped(SubscriptionDroppedReason.SubscriberError, ex); return; @@ -169,16 +203,21 @@ await _eventAppeared( } } catch (Exception ex) { if (_subscriptionDroppedInvoked == 0) { - _log.LogError(ex, + _log.LogError( + ex, "Persistent Subscription {subscriptionId} was dropped because an error occurred on the server.", - SubscriptionId); + SubscriptionId + ); + SubscriptionDropped(SubscriptionDroppedReason.ServerError, ex); } } finally { if (_subscriptionDroppedInvoked == 0) { _log.LogError( "Persistent Subscription {subscriptionId} was unexpectedly terminated.", - SubscriptionId); + SubscriptionId + ); + SubscriptionDropped(SubscriptionDroppedReason.ServerError); } } diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs similarity index 91% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs rename to src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs index 29b42bb49..b25ea4f72 100644 --- a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionDroppedByServerException.cs @@ -2,7 +2,7 @@ namespace EventStore.Client { /// - /// The exception that is thrown when the EventStoreDB drops a persistent subscription. + /// The exception that is thrown when the KurrentDB drops a persistent subscription. /// public class PersistentSubscriptionDroppedByServerException : Exception { /// diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs rename to src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionExtraStatistic.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionInfo.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionInfo.cs rename to src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionInfo.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs similarity index 86% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs rename to src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs index eabde9b62..7cd0d848d 100644 --- a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionMessage.cs +++ b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionMessage.cs @@ -4,10 +4,10 @@ namespace EventStore.Client { /// public abstract record PersistentSubscriptionMessage { /// - /// A that represents a . + /// A that represents a . /// - /// The . - /// The number of times the has been retried. + /// The . + /// The number of times the has been retried. public record Event(ResolvedEvent ResolvedEvent, int? RetryCount) : PersistentSubscriptionMessage; /// diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs rename to src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionNakEventAction.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs rename to src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionNotFoundException.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionSettings.cs b/src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionSettings.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/PersistentSubscriptionSettings.cs rename to src/Kurrent.Client/PersistentSubscriptions/PersistentSubscriptionSettings.cs diff --git a/src/EventStore.Client.PersistentSubscriptions/SystemConsumerStrategies.cs b/src/Kurrent.Client/PersistentSubscriptions/SystemConsumerStrategies.cs similarity index 100% rename from src/EventStore.Client.PersistentSubscriptions/SystemConsumerStrategies.cs rename to src/Kurrent.Client/PersistentSubscriptions/SystemConsumerStrategies.cs diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Control.cs similarity index 89% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs rename to src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Control.cs index e21da92fc..ff6ba9619 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Control.cs @@ -4,7 +4,7 @@ using EventStore.Client.Projections; namespace EventStore.Client { - public partial class EventStoreProjectionManagementClient { + public partial class KurrentProjectionManagementClient { /// /// Enables a projection. /// @@ -21,7 +21,7 @@ public async Task EnableAsync(string name, TimeSpan? deadline = null, UserCreden Options = new EnableReq.Types.Options { Name = name } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -42,7 +42,7 @@ public async Task ResetAsync(string name, TimeSpan? deadline = null, UserCredent Name = name, WriteCheckpoint = true } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -82,7 +82,7 @@ public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentia var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).RestartSubsystemAsync(new Empty(), - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -95,7 +95,7 @@ private async Task DisableInternalAsync(string name, bool writeCheckpoint, TimeS Name = name, WriteCheckpoint = writeCheckpoint } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } } diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Create.cs similarity index 88% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs rename to src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Create.cs index 54498cabf..cae9b400e 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Create.cs @@ -4,7 +4,7 @@ using EventStore.Client.Projections; namespace EventStore.Client { - public partial class EventStoreProjectionManagementClient { + public partial class KurrentProjectionManagementClient { /// /// Creates a one-time projection. /// @@ -22,7 +22,7 @@ public async Task CreateOneTimeAsync(string query, TimeSpan? deadline = null, OneTime = new Empty(), Query = query } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -49,7 +49,7 @@ public async Task CreateContinuousAsync(string name, string query, bool trackEmi }, Query = query } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -73,7 +73,7 @@ public async Task CreateTransientAsync(string name, string query, TimeSpan? dead }, Query = query } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } } diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.State.cs similarity index 96% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs rename to src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.State.cs index 64187fe1f..ca24d6ef9 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.State.cs @@ -8,7 +8,7 @@ using Type = System.Type; namespace EventStore.Client { - public partial class EventStoreProjectionManagementClient { + public partial class KurrentProjectionManagementClient { static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions(); /// @@ -80,7 +80,7 @@ private async ValueTask GetResultInternalAsync(string name, string? parti Name = name, Partition = partition ?? string.Empty } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); var response = await call.ResponseAsync.ConfigureAwait(false); return response.Result; @@ -155,7 +155,7 @@ private async ValueTask GetStateInternalAsync(string name, string? partit Name = name, Partition = partition ?? string.Empty } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); var response = await call.ResponseAsync.ConfigureAwait(false); return response.State; diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Statistics.cs similarity index 89% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs rename to src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Statistics.cs index 0b49b8b80..425831165 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Statistics.cs +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Statistics.cs @@ -8,7 +8,7 @@ using Grpc.Core; namespace EventStore.Client { - public partial class EventStoreProjectionManagementClient { + public partial class KurrentProjectionManagementClient { /// /// List the of all one-time projections. /// @@ -21,7 +21,7 @@ public IAsyncEnumerable ListOneTimeAsync(TimeSpan? deadline = ListInternalAsync(new StatisticsReq.Types.Options { OneTime = new Empty() }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), cancellationToken); /// @@ -37,7 +37,7 @@ public IAsyncEnumerable ListContinuousAsync(TimeSpan? deadlin ListInternalAsync(new StatisticsReq.Types.Options { Continuous = new Empty() }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), cancellationToken); /// @@ -53,7 +53,7 @@ public IAsyncEnumerable ListContinuousAsync(TimeSpan? deadlin CancellationToken cancellationToken = default) => ListInternalAsync(new StatisticsReq.Types.Options { Name = name }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), cancellationToken) .FirstOrDefaultAsync(cancellationToken).AsTask(); @@ -69,7 +69,7 @@ public IAsyncEnumerable ListAllAsync(TimeSpan? deadline = nul ListInternalAsync(new StatisticsReq.Types.Options { All = new Empty() }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken), cancellationToken); private async IAsyncEnumerable ListInternalAsync(StatisticsReq.Types.Options options, diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Update.cs similarity index 87% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs rename to src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Update.cs index 8f577e92c..368ff3be4 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.Update.cs @@ -4,7 +4,7 @@ using EventStore.Client.Projections; namespace EventStore.Client { - public partial class EventStoreProjectionManagementClient { + public partial class KurrentProjectionManagementClient { /// /// Updates a projection. /// @@ -32,7 +32,7 @@ public async Task UpdateAsync(string name, string query, bool? emitEnabled = nul using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).UpdateAsync(new UpdateReq { Options = options - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } diff --git a/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.cs new file mode 100644 index 000000000..12e1abf18 --- /dev/null +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClient.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace EventStore.Client { + /// + ///The client used to manage projections on the KurrentDB. + /// + public sealed partial class KurrentProjectionManagementClient : KurrentClientBase { + private readonly ILogger _log; + + /// + /// Constructs a new . This method is not intended to be called directly from your code. + /// + /// + public KurrentProjectionManagementClient(IOptions options) : this(options.Value) { + } + + /// + /// Constructs a new . + /// + /// + public KurrentProjectionManagementClient(KurrentClientSettings? settings) : base(settings, + new Dictionary>()) { + _log = settings?.LoggerFactory?.CreateLogger() ?? + new NullLogger(); + } + } +} diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClientCollectionExtensions.cs similarity index 52% rename from src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs rename to src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClientCollectionExtensions.cs index ea1f634f6..5fe3932b7 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClientCollectionExtensions.cs +++ b/src/Kurrent.Client/ProjectionManagement/KurrentProjectionManagementClientCollectionExtensions.cs @@ -9,51 +9,51 @@ namespace Microsoft.Extensions.DependencyInjection { /// - /// A set of extension methods for which provide support for an . + /// A set of extension methods for which provide support for an . /// - public static class EventStoreProjectionManagementClientCollectionExtensions { + public static class KurrentProjectionManagementClientCollectionExtensions { /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// /// - public static IServiceCollection AddEventStoreProjectionManagementClient(this IServiceCollection services, + public static IServiceCollection AddKurrentProjectionManagementClient(this IServiceCollection services, Uri address, Func? createHttpMessageHandler = null) - => services.AddEventStoreProjectionManagementClient(options => { + => services.AddKurrentProjectionManagementClient(options => { options.ConnectivitySettings.Address = address; options.CreateHttpMessageHandler = createHttpMessageHandler; }); /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// - public static IServiceCollection AddEventStoreProjectionManagementClient(this IServiceCollection services, - Action? configureSettings = null) => - services.AddEventStoreProjectionManagementClient(new EventStoreClientSettings(), configureSettings); + public static IServiceCollection AddKurrentProjectionManagementClient(this IServiceCollection services, + Action? configureSettings = null) => + services.AddKurrentProjectionManagementClient(new KurrentClientSettings(), configureSettings); /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// /// - public static IServiceCollection AddEventStoreProjectionManagementClient(this IServiceCollection services, - string connectionString, Action? configureSettings = null) => - services.AddEventStoreProjectionManagementClient(EventStoreClientSettings.Create(connectionString), + public static IServiceCollection AddKurrentProjectionManagementClient(this IServiceCollection services, + string connectionString, Action? configureSettings = null) => + services.AddKurrentProjectionManagementClient(KurrentClientSettings.Create(connectionString), configureSettings); - private static IServiceCollection AddEventStoreProjectionManagementClient(this IServiceCollection services, - EventStoreClientSettings settings, Action? configureSettings) { + private static IServiceCollection AddKurrentProjectionManagementClient(this IServiceCollection services, + KurrentClientSettings settings, Action? configureSettings) { if (services == null) { throw new ArgumentNullException(nameof(services)); } @@ -64,7 +64,7 @@ private static IServiceCollection AddEventStoreProjectionManagementClient(this I settings.LoggerFactory ??= provider.GetService(); settings.Interceptors ??= provider.GetServices(); - return new EventStoreProjectionManagementClient(settings); + return new KurrentProjectionManagementClient(settings); }); return services; diff --git a/src/EventStore.Client.ProjectionManagement/ProjectionDetails.cs b/src/Kurrent.Client/ProjectionManagement/ProjectionDetails.cs similarity index 100% rename from src/EventStore.Client.ProjectionManagement/ProjectionDetails.cs rename to src/Kurrent.Client/ProjectionManagement/ProjectionDetails.cs diff --git a/src/EventStore.Client.Streams/ConditionalWriteResult.cs b/src/Kurrent.Client/Streams/ConditionalWriteResult.cs similarity index 100% rename from src/EventStore.Client.Streams/ConditionalWriteResult.cs rename to src/Kurrent.Client/Streams/ConditionalWriteResult.cs diff --git a/src/EventStore.Client.Streams/ConditionalWriteStatus.cs b/src/Kurrent.Client/Streams/ConditionalWriteStatus.cs similarity index 100% rename from src/EventStore.Client.Streams/ConditionalWriteStatus.cs rename to src/Kurrent.Client/Streams/ConditionalWriteStatus.cs diff --git a/src/EventStore.Client.Streams/DeadLine.cs b/src/Kurrent.Client/Streams/DeadLine.cs similarity index 100% rename from src/EventStore.Client.Streams/DeadLine.cs rename to src/Kurrent.Client/Streams/DeadLine.cs diff --git a/src/Kurrent.Client/Streams/DecisionMaking/Aggregate.cs b/src/Kurrent.Client/Streams/DecisionMaking/Aggregate.cs new file mode 100644 index 000000000..b87ff7095 --- /dev/null +++ b/src/Kurrent.Client/Streams/DecisionMaking/Aggregate.cs @@ -0,0 +1,38 @@ +using Kurrent.Client.Core.Serialization; +using Kurrent.Client.Streams.GettingState; + +namespace Kurrent.Client.Streams.DecisionMaking; + +public interface IAggregate : IState { + Message[] DequeueUncommittedMessages(); +} + +public interface IAggregate : IAggregate, IState; + +public class Aggregate : Aggregate, IAggregate; + +public abstract class Aggregate : IAggregate where TEvent : notnull { + readonly Queue _uncommittedEvents = new(); + + public virtual void Apply(TEvent @event) { } + + Message[] IAggregate.DequeueUncommittedMessages() { + var dequeuedEvents = _uncommittedEvents.ToArray(); + + _uncommittedEvents.Clear(); + + return dequeuedEvents; + } + + protected void Enqueue(TEvent @event) { + Apply(@event); + _uncommittedEvents.Enqueue(Message.From(@event)); + } + + protected void Enqueue(Message message) { + if (message.Data is TEvent @event) + Apply(@event); + + _uncommittedEvents.Enqueue(message); + } +} diff --git a/src/Kurrent.Client/Streams/DecisionMaking/AggregateStore.cs b/src/Kurrent.Client/Streams/DecisionMaking/AggregateStore.cs new file mode 100644 index 000000000..e709806e7 --- /dev/null +++ b/src/Kurrent.Client/Streams/DecisionMaking/AggregateStore.cs @@ -0,0 +1,192 @@ +using EventStore.Client; +using Kurrent.Client.Streams.GettingState; + +namespace Kurrent.Client.Streams.DecisionMaking; + +public interface IAggregateStore + where TAggregate : IAggregate { + Task> GetAsync( + string streamName, + GetStreamStateOptions? getStreamStateOptions, + CancellationToken ct = default + ); + + Task AddAsync( + string streamName, + TAggregate aggregate, + AppendToStreamOptions? appendToStreamOptions, + CancellationToken ct = default + ); + + Task UpdateAsync( + string streamName, + TAggregate aggregate, + AppendToStreamOptions? appendToStreamOptions, + CancellationToken ct = default + ); + + Task HandleAsync( + string streamName, + Func handle, + DecideOptions? decideOption, + CancellationToken ct = default + ); +} + +public interface IAggregateStore : IAggregateStore + where TAggregate : IAggregate; + + +public class AggregateStoreOptions where TState : notnull { +#if NET48 + public IStateBuilder StateBuilder { get; set; } = null!; +#else + public required IStateBuilder StateBuilder { get; set; } +#endif + + public DecideOptions? DecideOptions { get; set; } +} + +public class AggregateStore(KurrentClient client, AggregateStoreOptions options) + : IAggregateStore + where TAggregate : IAggregate + where TEvent : notnull { + public virtual Task> GetAsync( + string streamName, + GetStreamStateOptions? getStreamStateOptions, + CancellationToken ct = default + ) => + client.GetStateAsync( + streamName, + options.StateBuilder, + getStreamStateOptions ?? options.DecideOptions?.GetStateOptions, + ct + ); + + public virtual Task AddAsync( + string streamName, + TAggregate aggregate, + AppendToStreamOptions? appendToStreamOptions, + CancellationToken ct = default + ) { + appendToStreamOptions ??= new AppendToStreamOptions(); + + if (appendToStreamOptions.ExpectedStreamState == null && appendToStreamOptions.ExpectedStreamRevision == null) + appendToStreamOptions.ExpectedStreamState = StreamState.NoStream; + + return client.AppendToStreamAsync( + streamName, + aggregate.DequeueUncommittedMessages(), + appendToStreamOptions, + ct + ); + } + + public virtual Task UpdateAsync( + string streamName, + TAggregate aggregate, + AppendToStreamOptions? appendToStreamOptions, + CancellationToken ct = default + ) { + appendToStreamOptions ??= new AppendToStreamOptions(); + + if (appendToStreamOptions.ExpectedStreamState == null && appendToStreamOptions.ExpectedStreamRevision == null) + appendToStreamOptions.ExpectedStreamState = StreamState.StreamExists; + + return client.AppendToStreamAsync( + streamName, + aggregate.DequeueUncommittedMessages(), + appendToStreamOptions, + ct + ); + } + + public virtual Task HandleAsync( + string streamName, + Func handle, + DecideOptions? decideOption, + CancellationToken ct = default + ) => + client.DecideAsync( + streamName, + async (state, token) => { + await handle(state, token); + return state.DequeueUncommittedMessages(); + }, + options.StateBuilder, + decideOption ?? options.DecideOptions, + ct + ); +} + +public class AggregateStore(KurrentClient client, AggregateStoreOptions options) + : AggregateStore(client, options), IAggregateStore + where TAggregate : IAggregate; + +public static class AggregateStoreExtensions { + public static Task> GetAsync( + this IAggregateStore aggregateStore, + string streamName, + CancellationToken ct = default + ) where TAggregate : IAggregate => + aggregateStore.GetAsync(streamName, null, ct); + + public static Task AddAsync( + this IAggregateStore aggregateStore, + string streamName, + TAggregate aggregate, + CancellationToken ct = default + ) where TAggregate : IAggregate => + aggregateStore.AddAsync( + streamName, + aggregate, + new AppendToStreamOptions { ExpectedStreamState = StreamState.NoStream }, + ct + ); + + public static Task HandleAsync( + this IAggregateStore aggregateStore, + string streamName, + Func handle, + CancellationToken ct = default + ) where TAggregate : IAggregate => + aggregateStore.HandleAsync( + streamName, + handle, + new DecideOptions(), + ct + ); + + public static Task HandleAsync( + this IAggregateStore aggregateStore, + string streamName, + Action handle, + CancellationToken ct = default + ) where TAggregate : IAggregate => + aggregateStore.HandleAsync( + streamName, + (state, _) => { + handle(state); + return new ValueTask(); + }, + new DecideOptions(), + ct + ); + + public static Task HandleAsync( + this IAggregateStore aggregateStore, + string streamName, + Action handle, + DecideOptions? decideOption, + CancellationToken ct = default + ) where TAggregate : IAggregate => + aggregateStore.HandleAsync( + streamName, + (state, _) => { + handle(state); + return new ValueTask(); + }, + decideOption, + ct + ); +} diff --git a/src/Kurrent.Client/Streams/DecisionMaking/Decide.cs b/src/Kurrent.Client/Streams/DecisionMaking/Decide.cs new file mode 100644 index 000000000..30cf45bfa --- /dev/null +++ b/src/Kurrent.Client/Streams/DecisionMaking/Decide.cs @@ -0,0 +1,184 @@ +using EventStore.Client; +using Kurrent.Client.Streams.GettingState; +using Polly; + +namespace Kurrent.Client.Streams.DecisionMaking; + +using static AsyncDecider; + +public class DecideOptions where TState : notnull { + public GetStreamStateOptions? GetStateOptions { get; set; } + public AppendToStreamOptions? AppendToStreamOptions { get; set; } + public IAsyncPolicy? RetryPolicy { get; set; } +} + +public static class KurrentClientDecisionMakingExtensions { + public static Task DecideAsync( + this KurrentClient eventStore, + string streamName, + CommandHandler decide, + IStateBuilder stateBuilder, + DecideOptions? options, + CancellationToken cancellationToken = default + ) where TState : notnull => + DecideRetryPolicy(options).ExecuteAsync( + async ct => { + var (state, streamPosition, position) = + await eventStore.GetStateAsync(streamName, stateBuilder, options?.GetStateOptions, ct) + .ConfigureAwait(false); + + var messages = await decide(state, ct).ConfigureAwait(false); + + if (messages.Length == 0) { + return new SuccessResult( + streamPosition.HasValue + ? StreamRevision.FromStreamPosition(streamPosition.Value) + : StreamRevision.None, + position ?? Position.Start + ); + } + + var appendToStreamOptions = options?.AppendToStreamOptions ?? new AppendToStreamOptions(); + + if (streamPosition.HasValue) + appendToStreamOptions.ExpectedStreamRevision ??= + StreamRevision.FromStreamPosition(streamPosition.Value); + else + appendToStreamOptions.ExpectedStreamState ??= StreamState.NoStream; + + return await eventStore.AppendToStreamAsync( + streamName, + messages, + appendToStreamOptions, + ct + ).ConfigureAwait(false); + }, + cancellationToken + ); + + public static Task DecideAsync( + this KurrentClient eventStore, + string streamName, + TCommand command, + Decider decider, + CancellationToken ct = default + ) where TState : notnull + where TEvent : notnull => + eventStore.DecideAsync( + streamName, + command, + decider.ToAsyncDecider(), + ct + ); + + public static Task DecideAsync( + this KurrentClient eventStore, + string streamName, + TCommand command, + Decider decider, + DecideOptions? options, + CancellationToken ct = default + ) where TState : notnull + where TEvent : notnull => + eventStore.DecideAsync( + streamName, + command, + decider.ToAsyncDecider(), + options, + ct + ); + + public static Task DecideAsync( + this KurrentClient eventStore, + string streamName, + TCommand command, + Decider decider, + CancellationToken ct = default + ) where TState : notnull => + eventStore.DecideAsync( + streamName, + command, + decider.ToAsyncDecider(), + ct + ); + + public static Task DecideAsync( + this KurrentClient eventStore, + string streamName, + TCommand command, + AsyncDecider asyncDecider, + CancellationToken ct = default + ) where TState : notnull => + eventStore.DecideAsync( + streamName, + (state, token) => asyncDecider.Decide(command, state, token), + asyncDecider, + ct + ); + + public static Task DecideAsync( + this KurrentClient eventStore, + string streamName, + TCommand command, + AsyncDecider asyncDecider, + DecideOptions? options, + CancellationToken ct = default + ) where TState : notnull => + eventStore.DecideAsync( + streamName, + (state, token) => asyncDecider.Decide(command, state, token), + asyncDecider, + options, + ct + ); + + public static Task DecideAsync( + this KurrentClient eventStore, + string streamName, + CommandHandler handle, + IStateBuilder stateBuilder, + CancellationToken ct = default + ) where TState : notnull => + eventStore.DecideAsync( + streamName, + handle, + stateBuilder, + new DecideOptions(), + ct + ); + + public static Task DecideAsync( + this KurrentClient eventStore, + string streamName, + CommandHandler handle, + CancellationToken ct = default + ) where TState : IState, new() => + eventStore.DecideAsync( + streamName, + handle, + StateBuilder.For(), + new DecideOptions(), + ct + ); +} + +public static class AsyncDecider { + public static readonly IAsyncPolicy DefaultRetryPolicy = + Policy + .Handle() + .WaitAndRetryAsync( + retryCount: 3, + sleepDurationProvider: retryAttempt => TimeSpan.FromMilliseconds(20 * retryAttempt) + ); + + public static bool HasUserProvidedExpectedVersioning(AppendToStreamOptions? options) => + options != null && (options.ExpectedStreamState.HasValue || options.ExpectedStreamRevision.HasValue); + + public static IAsyncPolicy DecideRetryPolicy(DecideOptions? options) + where TState : notnull => + options?.RetryPolicy ?? + (HasUserProvidedExpectedVersioning(options?.AppendToStreamOptions) + // it doesn't make sense to retry, as expected state will be always the same + ? Policy.NoOpAsync() + : DefaultRetryPolicy); +} diff --git a/src/Kurrent.Client/Streams/DecisionMaking/Decider.cs b/src/Kurrent.Client/Streams/DecisionMaking/Decider.cs new file mode 100644 index 000000000..8a37c875e --- /dev/null +++ b/src/Kurrent.Client/Streams/DecisionMaking/Decider.cs @@ -0,0 +1,56 @@ +using EventStore.Client; +using Kurrent.Client.Core.Serialization; +using Kurrent.Client.Streams.GettingState; + +namespace Kurrent.Client.Streams.DecisionMaking; + +public delegate ValueTask CommandHandler(TState state, CancellationToken ct = default); + +public record AsyncDecider( + Func> Decide, + Func Evolve, + Func GetInitialState +) : StateBuilder( + Evolve, + GetInitialState +) where TState : notnull; + +public record AsyncDecider( + Func> Decide, + Func Evolve, + Func GetInitialState +) : AsyncDecider( + Decide, + Evolve, + GetInitialState +) where TState : notnull; + +public record Decider( + Func Decide, + Func Evolve, + Func GetInitialState +) where TEvent : notnull + where TState : notnull; + +public record Decider( + Func Decide, + Func Evolve, + Func GetInitialState +) : Decider(Decide, Evolve, GetInitialState) + where TState : notnull; + +public static class AsyncDeciderExtensions { + public static AsyncDecider ToAsyncDecider( + this Decider decider + ) where TEvent : notnull + where TState : notnull => + new AsyncDecider( + (command, state, _) => + new ValueTask(decider.Decide(command, state).Select(m => Message.From(m)).ToArray()), + (state, resolvedEvent) => + resolvedEvent.DeserializedData is TEvent @event + ? decider.Evolve(state, @event) + : state, + decider.GetInitialState + ); +} diff --git a/src/Kurrent.Client/Streams/DecisionMaking/StateStore.cs b/src/Kurrent.Client/Streams/DecisionMaking/StateStore.cs new file mode 100644 index 000000000..287e7b3e2 --- /dev/null +++ b/src/Kurrent.Client/Streams/DecisionMaking/StateStore.cs @@ -0,0 +1,276 @@ +using EventStore.Client; +using Kurrent.Client.Core.Serialization; +using Kurrent.Client.Streams.GettingState; + +namespace Kurrent.Client.Streams.DecisionMaking; + +public interface IStateStore where TState : notnull { + Task> GetAsync( + string streamName, + GetStreamStateOptions? getStreamStateOptions, + CancellationToken ct = default + ); + + Task AddAsync( + string streamName, + IEnumerable events, + AppendToStreamOptions? appendToStreamOptions, + CancellationToken ct = default + ); + + Task UpdateAsync( + string streamName, + IEnumerable events, + AppendToStreamOptions? appendToStreamOptions, + CancellationToken ct = default + ); + + Task Handle( + string streamName, + CommandHandler handle, + DecideOptions? decideOptions, + CancellationToken ct = default + ); +} + +public class StateStoreOptions where TState : notnull { +#if NET48 + public IStateBuilder StateBuilder { get; set; } = null!; +#else + public required IStateBuilder StateBuilder { get; set; } +#endif + + public GetStreamStateOptions? GetStreamStateOptions { get; set; } +} + +public class StateStore(KurrentClient client, StateStoreOptions options) + : IStateStore + where TState : notnull { + public virtual Task> GetAsync( + string streamName, + GetStreamStateOptions? getStreamStateOptions, + CancellationToken ct = default + ) => + client.GetStateAsync( + streamName, + options.StateBuilder, + getStreamStateOptions ?? options.GetStreamStateOptions, + ct + ); + + public virtual Task AddAsync( + string streamName, + IEnumerable events, + AppendToStreamOptions? appendToStreamOptions, + CancellationToken ct = default + ) { + appendToStreamOptions ??= new AppendToStreamOptions(); + + if (appendToStreamOptions.ExpectedStreamState == null && appendToStreamOptions.ExpectedStreamRevision == null) + appendToStreamOptions.ExpectedStreamState = StreamState.NoStream; + + return client.AppendToStreamAsync(streamName, events, appendToStreamOptions, ct); + } + + public virtual Task UpdateAsync( + string streamName, + IEnumerable messages, + AppendToStreamOptions? appendToStreamOptions, + CancellationToken ct = default + ) { + appendToStreamOptions ??= new AppendToStreamOptions(); + + if (appendToStreamOptions.ExpectedStreamState == null && appendToStreamOptions.ExpectedStreamRevision == null) + appendToStreamOptions.ExpectedStreamState = StreamState.StreamExists; + + return client.AppendToStreamAsync(streamName, messages, appendToStreamOptions, ct); + } + + public virtual Task Handle( + string streamName, + CommandHandler handle, + DecideOptions? decideOptions, + CancellationToken ct = default + ) => + client.DecideAsync( + streamName, + handle, + options.StateBuilder, + decideOptions ?? new DecideOptions(), + ct + ); +} + +public static class StateStoreExtensions { + public static Task> GetAsync( + this IStateStore stateStore, + string streamName, + CancellationToken ct = default + ) where TState : notnull => + stateStore.GetAsync(streamName, null, ct); + + public static Task AddAsync( + this IStateStore stateStore, + string streamName, + IEnumerable messages, + CancellationToken ct = default + ) where TState : notnull => + stateStore.AddAsync( + streamName, + messages, + null, + ct + ); + + public static Task AddAsync( + this IStateStore stateStore, + string streamName, + IEnumerable events, + CancellationToken ct = default + ) where TState : notnull => + stateStore.AddAsync( + streamName, + events.Select(e => Message.From(e)), + new AppendToStreamOptions { ExpectedStreamState = StreamState.NoStream }, + ct + ); + + public static Task AddAsync( + this IStateStore stateStore, + string streamName, + IEnumerable events, + CancellationToken ct = default + ) where TState : notnull + where TEvent : notnull => + stateStore.AddAsync( + streamName, + events.Select(e => Message.From(e)), + new AppendToStreamOptions { ExpectedStreamState = StreamState.NoStream }, + ct + ); + + public static Task UpdateAsync( + this IStateStore stateStore, + string streamName, + IEnumerable events, + CancellationToken ct = default + ) where TState : notnull + where TEvent : notnull => + stateStore.UpdateAsync( + streamName, + events.Select(e => Message.From(e)), + new AppendToStreamOptions { ExpectedStreamState = StreamState.StreamExists }, + ct + ); + + public static Task UpdateAsync( + this IStateStore stateStore, + string streamName, + IEnumerable events, + CancellationToken ct = default + ) where TState : notnull => + stateStore.UpdateAsync( + streamName, + events.Select(e => Message.From(e)), + new AppendToStreamOptions { ExpectedStreamState = StreamState.StreamExists }, + ct + ); + + public static Task UpdateAsync( + this IStateStore stateStore, + string streamName, + IEnumerable events, + StreamRevision expectedStreamRevision, + CancellationToken ct = default + ) where TState : notnull + where TEvent : notnull => + stateStore.UpdateAsync( + streamName, + events.Select(e => Message.From(e)), + new AppendToStreamOptions { ExpectedStreamRevision = expectedStreamRevision }, + ct + ); + + public static Task UpdateAsync( + this IStateStore stateStore, + string streamName, + IEnumerable events, + StreamRevision expectedStreamRevision, + CancellationToken ct = default + ) where TState : notnull => + stateStore.UpdateAsync( + streamName, + events.Select(e => Message.From(e)), + new AppendToStreamOptions { ExpectedStreamRevision = expectedStreamRevision }, + ct + ); + + public static Task Handle( + this IStateStore stateStore, + string streamName, + CommandHandler handle, + CancellationToken ct = default + ) where TState : notnull => + stateStore.Handle( + streamName, + handle, + null, + ct + ); + + public static Task Handle( + this IStateStore stateStore, + string streamName, + Func handle, + CancellationToken ct = default + ) where TState : notnull + where TEvent : notnull => + stateStore.Handle( + streamName, + (state, _) => new ValueTask(handle(state).Select(m => Message.From(m)).ToArray()), + null, + ct + ); + + public static Task Handle( + this IStateStore stateStore, + string streamName, + Func handle, + CancellationToken ct = default + ) where TState : notnull => + stateStore.Handle( + streamName, + (state, _) => new ValueTask(handle(state).Select(m => Message.From(m)).ToArray()), + null, + ct + ); + + public static Task Handle( + this IStateStore stateStore, + string streamName, + Func handle, + DecideOptions? decideOptions, + CancellationToken ct = default + ) where TState : notnull + where TEvent : notnull => + stateStore.Handle( + streamName, + (state, _) => new ValueTask(handle(state).Select(m => Message.From(m)).ToArray()), + decideOptions, + ct + ); + + public static Task Handle( + this IStateStore stateStore, + string streamName, + Func handle, + DecideOptions? decideOptions, + CancellationToken ct = default + ) where TState : notnull => + stateStore.Handle( + streamName, + (state, _) => new ValueTask(handle(state).Select(m => Message.From(m)).ToArray()), + decideOptions, + ct + ); +} diff --git a/src/EventStore.Client.Streams/DeleteResult.cs b/src/Kurrent.Client/Streams/DeleteResult.cs similarity index 100% rename from src/EventStore.Client.Streams/DeleteResult.cs rename to src/Kurrent.Client/Streams/DeleteResult.cs diff --git a/src/EventStore.Client.Streams/Direction.cs b/src/Kurrent.Client/Streams/Direction.cs similarity index 100% rename from src/EventStore.Client.Streams/Direction.cs rename to src/Kurrent.Client/Streams/Direction.cs diff --git a/src/Kurrent.Client/Streams/GettingState/GetState.cs b/src/Kurrent.Client/Streams/GettingState/GetState.cs new file mode 100644 index 000000000..4cebcb53f --- /dev/null +++ b/src/Kurrent.Client/Streams/GettingState/GetState.cs @@ -0,0 +1,223 @@ +using EventStore.Client; + +namespace Kurrent.Client.Streams.GettingState; + +public class GetStreamStateOptions : ReadStreamOptions where TState : notnull { + public GetSnapshot? GetSnapshot { get; set; } +} + +public delegate ValueTask> GetSnapshot( + GetSnapshotOptions options, + CancellationToken ct = default +) where TState : notnull; + +public record GetSnapshotOptions { + public string? StreamName { get; set; } + + public string? SnapshotVersion { get; set; } + + public static GetSnapshotOptions ForStream(string streamName) => + new GetSnapshotOptions { StreamName = streamName }; + + public static GetSnapshotOptions ForAll() => + new GetSnapshotOptions(); +} + +public static class KurrentClientGettingStateClientExtensions { + public static async Task> GetStateAsync( + this KurrentClient eventStore, + string streamName, + IStateBuilder stateBuilder, + GetStreamStateOptions? options, + CancellationToken ct = default + ) where TState : notnull { + StateAtPointInTime? stateAtPointInTime = null; + + options ??= new GetStreamStateOptions(); + + if (options.GetSnapshot != null) + stateAtPointInTime = await options.GetSnapshot( + GetSnapshotOptions.ForStream(streamName), + ct + ); + + // TODO: CHeck if I'm passing the actual snapshot state + options.StreamPosition = stateAtPointInTime?.LastStreamPosition ?? StreamPosition.Start; + + return await eventStore.ReadStreamAsync(streamName, options, ct) + .GetStateAsync(stateBuilder, ct); + } + + public static async Task> GetStateAsync( + this IAsyncEnumerable messages, + TState initialState, + Func evolve, + CancellationToken ct + ) where TState : notnull { + var state = initialState; + + if (messages is KurrentClient.ReadStreamResult readStreamResult) { + if (await readStreamResult.ReadState.ConfigureAwait(false) == ReadState.StreamNotFound) + return new StateAtPointInTime(state); + } + + ResolvedEvent? lastEvent = null; + + await foreach (var resolvedEvent in messages.WithCancellation(ct)) { + lastEvent = resolvedEvent; + + state = evolve(state, resolvedEvent); + } + + return new StateAtPointInTime(state, lastEvent?.Event.EventNumber, lastEvent?.Event.Position); + } + + public static Task> GetStateAsync( + this KurrentClient eventStore, + string streamName, + IStateBuilder streamStateBuilder, + CancellationToken ct = default + ) where TState : notnull => + eventStore.GetStateAsync(streamName, streamStateBuilder, new GetStreamStateOptions(), ct); + + public static Task> GetStateAsync( + this KurrentClient eventStore, + string streamName, + GetStreamStateOptions options, + CancellationToken ct = default + ) where TState : IState, new() => + eventStore.GetStateAsync( + streamName, + StateBuilder.For(), + options, + ct + ); + + public static Task> GetStateAsync( + this KurrentClient eventStore, + string streamName, + CancellationToken ct = default + ) where TState : IState, new() => + eventStore.GetStateAsync(streamName, new GetStreamStateOptions(), ct); + + public static Task> GetStateAsync( + this KurrentClient eventStore, + string streamName, + CancellationToken ct = default + ) where TState : IState, new() => + eventStore.GetStateAsync(streamName, new GetStreamStateOptions(), ct); + + public static Task> GetStateAsync( + this KurrentClient eventStore, + string streamName, + Func getInitialState, + CancellationToken ct = default + ) where TState : IState => + eventStore.GetStateAsync( + streamName, + StateBuilder.For(getInitialState), + ct + ); + + public static Task> GetStateAsync( + this KurrentClient eventStore, + string streamName, + Func getInitialState, + GetStreamStateOptions options, + CancellationToken ct = default + ) where TState : IState => + eventStore.GetStateAsync( + streamName, + StateBuilder.For(getInitialState), + options, + ct + ); + + public static Task> GetStateAsync( + this KurrentClient eventStore, + string streamName, + Func getInitialState, + CancellationToken ct = default + ) where TState : IState => + eventStore.GetStateAsync( + streamName, + StateBuilder.For(getInitialState), + ct + ); + + public static Task> GetStateAsync( + this KurrentClient eventStore, + string streamName, + Func getInitialState, + GetStreamStateOptions options, + CancellationToken ct = default + ) where TState : IState => + eventStore.GetStateAsync( + streamName, + StateBuilder.For(getInitialState), + options, + ct + ); +} + +public static class KurrentClientGettingStateReadAndSubscribeExtensions { + public static Task> GetStateAsync( + this KurrentClient.ReadStreamResult readStreamResult, + IStateBuilder stateBuilder, + GetStateOptions options, + CancellationToken ct = default + ) where TState : notnull => + stateBuilder.GetAsync(readStreamResult, options, ct); + + public static Task> GetStateAsync( + this KurrentClient.ReadStreamResult readStreamResult, + IStateBuilder stateBuilder, + CancellationToken ct = default + ) where TState : notnull => + stateBuilder.GetAsync(readStreamResult, new GetStateOptions(), ct); + + public static Task> GetStateAsync( + this KurrentClient.ReadAllStreamResult readAllStreamResult, + IStateBuilder stateBuilder, + GetStateOptions options, + CancellationToken ct = default + ) where TState : notnull => + stateBuilder.GetAsync(readAllStreamResult, options, ct); + + public static Task> GetStateAsync( + this KurrentClient.ReadAllStreamResult readAllStreamResult, + IStateBuilder stateBuilder, + CancellationToken ct = default + ) where TState : notnull => + stateBuilder.GetAsync(readAllStreamResult, new GetStateOptions(), ct); + + public static Task> GetStateAsync( + this KurrentClient.StreamSubscriptionResult subscriptionResult, + IStateBuilder stateBuilder, + GetStateOptions options, + CancellationToken ct = default + ) where TState : notnull => + stateBuilder.GetAsync(subscriptionResult, options, ct); + + public static Task> GetStateAsync( + this KurrentClient.StreamSubscriptionResult subscriptionResult, + IStateBuilder stateBuilder, + CancellationToken ct = default + ) where TState : notnull => + stateBuilder.GetAsync(subscriptionResult, new GetStateOptions(), ct); + + public static Task> GetStateAsync( + this KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult subscriptionResult, + IStateBuilder stateBuilder, + GetStateOptions options, + CancellationToken ct = default + ) where TState : notnull => + stateBuilder.GetAsync(subscriptionResult, options, ct); + + public static Task> GetStateAsync( + this KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult subscriptionResult, + IStateBuilder stateBuilder, + CancellationToken ct = default + ) where TState : notnull => + stateBuilder.GetAsync(subscriptionResult, new GetStateOptions(), ct); +} diff --git a/src/Kurrent.Client/Streams/GettingState/ProjectState.cs b/src/Kurrent.Client/Streams/GettingState/ProjectState.cs new file mode 100644 index 000000000..0b15a3dfc --- /dev/null +++ b/src/Kurrent.Client/Streams/GettingState/ProjectState.cs @@ -0,0 +1,158 @@ +using System.Runtime.CompilerServices; +using EventStore.Client; + +namespace Kurrent.Client.Streams.GettingState; + +public class ProjectStateOptions { + public Func? GetProjectedId { get; set; } + + public IStateCache? StateCache { get; set; } +} + +public static class KurrentClientProjectStateExtensions { + public static async IAsyncEnumerable> ProjectState( + this IAsyncEnumerable messages, + Func evolve, + Func> getInitialState, + ProjectStateOptions? options, + [EnumeratorCancellation] CancellationToken ct + ) where TState : notnull { + var getProjectedId = options?.GetProjectedId ?? (resolvedEvent => resolvedEvent.OriginalStreamId); + var stateCache = options?.StateCache ?? new DictionaryStateCache(); + + if (messages is KurrentClient.ReadStreamResult readStreamResult) { + if (await readStreamResult.ReadState.ConfigureAwait(false) == ReadState.StreamNotFound) + yield break; + } + + await foreach (var resolvedEvent in messages.WithCancellation(ct)) { + var projectedId = getProjectedId(resolvedEvent); + var initialState = await getInitialState(resolvedEvent, ct); + + var state = await stateCache.GetValueOrDefaultAsync(projectedId, initialState, ct).ConfigureAwait(false); + + state = evolve(state, resolvedEvent); + + await stateCache.SetValueAsync(projectedId, state, ct).ConfigureAwait(false); + + yield return new StateAtPointInTime( + state, + resolvedEvent.Event.EventNumber, + resolvedEvent.Event.Position + ); + } + } + + public static IAsyncEnumerable> ProjectState( + this IAsyncEnumerable messages, + Func evolve, + Func> getInitialState, + CancellationToken ct = default + ) where TState : notnull => + messages.ProjectState(evolve, getInitialState, null, ct); + + + public static IAsyncEnumerable> ProjectState( + this IAsyncEnumerable messages, + Func evolve, + Func getInitialState, + ProjectStateOptions? options, + CancellationToken ct = default + ) where TState : notnull => + messages.ProjectState( + evolve, + (resolvedEvent, _) => new ValueTask(getInitialState(resolvedEvent)), + options, + ct + ); + + public static IAsyncEnumerable> ProjectState( + this IAsyncEnumerable messages, + Func evolve, + Func getInitialState, + CancellationToken ct = default + ) where TState : notnull => + messages.ProjectState( + evolve, + (resolvedEvent, _) => new ValueTask(getInitialState(resolvedEvent)), + null, + ct + ); + + public static IAsyncEnumerable> ProjectState( + this IAsyncEnumerable messages, + Func evolve, + Func getInitialState, + ProjectStateOptions? options, + CancellationToken ct = default + ) where TState : notnull => + messages.ProjectState( + evolve, + (_, _) => new ValueTask(getInitialState()), + options, + ct + ); + + public static IAsyncEnumerable> ProjectState( + this IAsyncEnumerable messages, + Func evolve, + Func getInitialState, + CancellationToken ct = default + ) where TState : notnull => + messages.ProjectState( + evolve, + (_, _) => new ValueTask(getInitialState()), + null, + ct + ); + + public static IAsyncEnumerable> ProjectState( + this IAsyncEnumerable messages, + StateBuilder stateBuilder, + ProjectStateOptions? options, + CancellationToken ct = default + ) where TState : notnull => + messages.ProjectState( + stateBuilder.Evolve, + (_, _) => new ValueTask(stateBuilder.GetInitialState()), + options, + ct + ); + + public static IAsyncEnumerable> ProjectState( + this IAsyncEnumerable messages, + StateBuilder stateBuilder, + CancellationToken ct = default + ) where TState : notnull => + messages.ProjectState( + stateBuilder.Evolve, + (_, _) => new ValueTask(stateBuilder.GetInitialState()), + null, + ct + ); +} + +public interface IStateCache { + public ValueTask GetValueOrDefaultAsync(string key, TState defaultValue, CancellationToken ct = default); + + public ValueTask SetValueAsync(string key, TState state, CancellationToken ct = default); +} + +public class DictionaryStateCache : IStateCache { + readonly Dictionary _states = new Dictionary(); + + public ValueTask GetValueOrDefaultAsync(string key, TState defaultValue, CancellationToken ct = default) { +#if NET48 + var state = _states.TryGetValue(key, out TState? value) ? value : defaultValue; +#else + var state = _states.GetValueOrDefault(key, defaultValue); +#endif + return new ValueTask(state); + } + + public ValueTask SetValueAsync(string key, TState state, CancellationToken ct = default) { + _states[key] = state; + + return new ValueTask(); + } +} diff --git a/src/Kurrent.Client/Streams/GettingState/StateBuilder.cs b/src/Kurrent.Client/Streams/GettingState/StateBuilder.cs new file mode 100644 index 000000000..2afe47c10 --- /dev/null +++ b/src/Kurrent.Client/Streams/GettingState/StateBuilder.cs @@ -0,0 +1,128 @@ +using EventStore.Client; +using Kurrent.Client.Core.Serialization; + +namespace Kurrent.Client.Streams.GettingState; + +public record StateAtPointInTime( + TState State, + StreamPosition? LastStreamPosition = null, + Position? LastPosition = null +) where TState : notnull; + +public interface IStateBuilder where TState : notnull { + public Task> GetAsync( + IAsyncEnumerable messages, + GetStateOptions options, + CancellationToken ct = default + ); +} + +public record GetStateOptions where TState : notnull { + public StateAtPointInTime? CurrentState { get; set; } +} + +public interface IState : IState; + +public interface IState { + public void Apply(TEvent @event); +} + +public record StateBuilder( + Func Evolve, + Func GetInitialState +) : IStateBuilder where TState : notnull { + public Task> GetAsync( + IAsyncEnumerable messages, + GetStateOptions options, + CancellationToken ct + ) => + messages.GetStateAsync( + options.CurrentState is { } state ? state.State : GetInitialState(), + Evolve, + ct + ); +} + +public static class StateBuilder { + public static StateBuilder For( + Func evolve, + Func getInitialState + ) where TState : notnull => + new StateBuilder( + (state, resolvedEvent) => + resolvedEvent.DeserializedData is TEvent @event + ? evolve(state, @event) + : state, + getInitialState + ); + + public static StateBuilder For( + Func evolve, + Func getInitialState + ) where TState : notnull => + new StateBuilder( + (state, resolvedEvent) => resolvedEvent.DeserializedData != null + ? evolve(state, resolvedEvent.DeserializedData) + : state, + getInitialState + ); + + public static StateBuilder For( + Func evolve, + Func getInitialState + ) where TState : notnull => + new StateBuilder( + (state, resolvedEvent) => resolvedEvent.Message != null + ? evolve(state, resolvedEvent.Message) + : state, + getInitialState + ); + + public static StateBuilder For() + where TState : IState, new() => + new StateBuilder( + (state, resolvedEvent) => { + if (resolvedEvent.DeserializedData is TEvent @event) + state.Apply(@event); + + return state; + }, + () => new TState() + ); + + public static StateBuilder For() + where TState : IState, new() => + new StateBuilder( + (state, resolvedEvent) => { + if (resolvedEvent.DeserializedData != null) + state.Apply(resolvedEvent.DeserializedData); + + return state; + }, + () => new TState() + ); + + public static StateBuilder For(Func getInitialState) + where TState : IState => + new StateBuilder( + (state, resolvedEvent) => { + if (resolvedEvent.DeserializedData is TEvent @event) + state.Apply(@event); + + return state; + }, + getInitialState + ); + + public static StateBuilder For(Func getInitialState) + where TState : IState => + new StateBuilder( + (state, resolvedEvent) => { + if (resolvedEvent.DeserializedData != null) + state.Apply(resolvedEvent.DeserializedData); + + return state; + }, + getInitialState + ); +} diff --git a/src/EventStore.Client.Streams/IWriteResult.cs b/src/Kurrent.Client/Streams/IWriteResult.cs similarity index 100% rename from src/EventStore.Client.Streams/IWriteResult.cs rename to src/Kurrent.Client/Streams/IWriteResult.cs diff --git a/src/EventStore.Client.Streams/InvalidTransactionException.cs b/src/Kurrent.Client/Streams/InvalidTransactionException.cs similarity index 97% rename from src/EventStore.Client.Streams/InvalidTransactionException.cs rename to src/Kurrent.Client/Streams/InvalidTransactionException.cs index 17933b654..f71505671 100644 --- a/src/EventStore.Client.Streams/InvalidTransactionException.cs +++ b/src/Kurrent.Client/Streams/InvalidTransactionException.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.Serialization; -namespace EventStore.Client; +namespace EventStore.Client; /// /// Exception thrown if there is an attempt to operate inside a @@ -28,4 +28,4 @@ public InvalidTransactionException(string message, Exception innerException) : b /// [Obsolete("Obsolete")] protected InvalidTransactionException(SerializationInfo info, StreamingContext context) : base(info, context) { } -} \ No newline at end of file +} diff --git a/src/EventStore.Client.Streams/EventStoreClient.Append.cs b/src/Kurrent.Client/Streams/KurrentClient.Append.cs similarity index 57% rename from src/EventStore.Client.Streams/EventStoreClient.Append.cs rename to src/Kurrent.Client/Streams/KurrentClient.Append.cs index 9b244b3a4..593d2ab40 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Append.cs +++ b/src/Kurrent.Client/Streams/KurrentClient.Append.cs @@ -6,21 +6,64 @@ using Grpc.Core; using Microsoft.Extensions.Logging; using EventStore.Client.Diagnostics; -using EventStore.Diagnostics; -using EventStore.Diagnostics.Telemetry; -using EventStore.Diagnostics.Tracing; +using Kurrent.Client.Core.Serialization; +using Kurrent.Diagnostics; +using Kurrent.Diagnostics.Telemetry; +using Kurrent.Diagnostics.Tracing; using static EventStore.Client.Streams.AppendResp.Types.WrongExpectedVersion; using static EventStore.Client.Streams.Streams; namespace EventStore.Client { - public partial class EventStoreClient { + public partial class KurrentClient { + /// + /// Appends events asynchronously to a stream. Messages are serialized using default or custom serialization configured through + /// + /// The name of the stream to append events to. + /// Messages to append to the stream. + /// Optional settings for the append operation, e.g. expected stream position for optimistic concurrency check + /// The optional . + /// + public Task AppendToStreamAsync( + string streamName, + IEnumerable messages, + AppendToStreamOptions options, + CancellationToken cancellationToken = default + ) { + var serializationContext = new MessageSerializationContext( + streamName, + Settings.Serialization.DefaultContentType + ); + + var eventsData = _messageSerializer.Serialize(messages, serializationContext); + + return options.ExpectedStreamRevision.HasValue + ? AppendToStreamAsync( + streamName, + options.ExpectedStreamRevision.Value, + eventsData, + options.ConfigureOperationOptions, + options.Deadline, + options.UserCredentials, + cancellationToken + ) + : AppendToStreamAsync( + streamName, + options.ExpectedStreamState ?? StreamState.Any, + eventsData, + options.ConfigureOperationOptions, + options.Deadline, + options.UserCredentials, + cancellationToken + ); + } + /// /// Appends events asynchronously to a stream. /// /// The name of the stream to append events to. /// The expected of the stream to append to. /// An to append to the stream. - /// An to configure the operation's options. + /// An to configure the operation's options. /// /// The for the operation. /// The optional . @@ -29,7 +72,7 @@ public async Task AppendToStreamAsync( string streamName, StreamRevision expectedRevision, IEnumerable eventData, - Action? configureOperationOptions = null, + Action? configureOperationOptions = null, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default @@ -65,7 +108,7 @@ await GetChannelInfo(cancellationToken).ConfigureAwait(false), /// The name of the stream to append events to. /// The expected of the stream to append to. /// An to append to the stream. - /// An to configure the operation's options. + /// An to configure the operation's options. /// /// The for the operation. /// The optional . @@ -74,7 +117,7 @@ public async Task AppendToStreamAsync( string streamName, StreamState expectedState, IEnumerable eventData, - Action? configureOperationOptions = null, + Action? configureOperationOptions = null, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default @@ -108,16 +151,34 @@ ValueTask AppendToStreamInternal( ChannelInfo channelInfo, AppendReq header, IEnumerable eventData, - EventStoreClientOperationOptions operationOptions, + KurrentClientOperationOptions operationOptions, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken ) { - return EventStoreClientDiagnostics.ActivitySource.TraceClientOperation(Operation, TracingConstants.Operations.Append, AppendTags); + var tags = new ActivityTagsCollection() + .WithRequiredTag( + TelemetryTags.Kurrent.Stream, + header.Options.StreamIdentifier.StreamName.ToStringUtf8() + ) + .WithGrpcChannelServerTags(channelInfo) + .WithClientSettingsServerTags(Settings) + .WithOptionalTag( + TelemetryTags.Database.User, + userCredentials?.Username ?? Settings.DefaultCredentials?.Username + ); + + return KurrentClientDiagnostics.ActivitySource.TraceClientOperation( + Operation, + TracingConstants.Operations.Append, + tags + ); async ValueTask Operation() { using var call = new StreamsClient(channelInfo.CallInvoker) - .Append(EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + .Append( + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken) + ); await call.RequestStream .WriteAsync(header) @@ -151,20 +212,16 @@ await call.RequestStream return HandleWrongExpectedRevision(response, header, operationOptions); } - - ActivityTagsCollection AppendTags() => new ActivityTagsCollection() - .WithRequiredTag(TelemetryTags.EventStore.Stream, header.Options.StreamIdentifier.StreamName.ToStringUtf8()) - .WithGrpcChannelServerTags(Settings, channelInfo) - .WithClientSettingsServerTags(Settings) - .WithOptionalTag(TelemetryTags.Database.User, userCredentials?.Username ?? Settings.DefaultCredentials?.Username); } IWriteResult HandleSuccessAppend(AppendResp response, AppendReq header) { - var currentRevision = response.Success.CurrentRevisionOptionCase == AppendResp.Types.Success.CurrentRevisionOptionOneofCase.NoStream + var currentRevision = response.Success.CurrentRevisionOptionCase + == AppendResp.Types.Success.CurrentRevisionOptionOneofCase.NoStream ? StreamRevision.None : new StreamRevision(response.Success.CurrentRevision); - var position = response.Success.PositionOptionCase == AppendResp.Types.Success.PositionOptionOneofCase.Position + var position = response.Success.PositionOptionCase + == AppendResp.Types.Success.PositionOptionOneofCase.Position ? new Position(response.Success.Position.CommitPosition, response.Success.Position.PreparePosition) : default; @@ -179,9 +236,10 @@ IWriteResult HandleSuccessAppend(AppendResp response, AppendReq header) { } IWriteResult HandleWrongExpectedRevision( - AppendResp response, AppendReq header, EventStoreClientOperationOptions operationOptions + AppendResp response, AppendReq header, KurrentClientOperationOptions operationOptions ) { - var actualStreamRevision = response.WrongExpectedVersion.CurrentRevisionOptionCase == CurrentRevisionOptionOneofCase.CurrentRevision + var actualStreamRevision = response.WrongExpectedVersion.CurrentRevisionOptionCase + == CurrentRevisionOptionOneofCase.CurrentRevision ? new StreamRevision(response.WrongExpectedVersion.CurrentRevision) : StreamRevision.None; @@ -193,7 +251,8 @@ IWriteResult HandleWrongExpectedRevision( ); if (operationOptions.ThrowOnAppendFailure) { - if (response.WrongExpectedVersion.ExpectedRevisionOptionCase == ExpectedRevisionOptionOneofCase.ExpectedRevision) { + if (response.WrongExpectedVersion.ExpectedRevisionOptionCase + == ExpectedRevisionOptionOneofCase.ExpectedRevision) { throw new WrongExpectedVersionException( header.Options.StreamIdentifier!, new StreamRevision(response.WrongExpectedVersion.ExpectedRevision), @@ -215,7 +274,8 @@ IWriteResult HandleWrongExpectedRevision( ); } - var expectedRevision = response.WrongExpectedVersion.ExpectedRevisionOptionCase == ExpectedRevisionOptionOneofCase.ExpectedRevision + var expectedRevision = response.WrongExpectedVersion.ExpectedRevisionOptionCase + == ExpectedRevisionOptionOneofCase.ExpectedRevision ? new StreamRevision(response.WrongExpectedVersion.ExpectedRevision) : StreamRevision.None; @@ -227,7 +287,7 @@ IWriteResult HandleWrongExpectedRevision( } class StreamAppender : IDisposable { - readonly EventStoreClientSettings _settings; + readonly KurrentClientSettings _settings; readonly CancellationToken _cancellationToken; readonly Action _onException; readonly Channel _channel; @@ -238,7 +298,7 @@ class StreamAppender : IDisposable { AsyncDuplexStreamingCall? _call; public StreamAppender( - EventStoreClientSettings settings, + KurrentClientSettings settings, ValueTask channelInfoTask, CancellationToken cancellationToken, Action onException @@ -282,10 +342,16 @@ ValueTask AppendInternal( IEnumerable events, CancellationToken cancellationToken ) { - return EventStoreClientDiagnostics.ActivitySource.TraceClientOperation( + var tags = new ActivityTagsCollection() + .WithRequiredTag(TelemetryTags.Kurrent.Stream, options.StreamIdentifier.StreamName.ToStringUtf8()) + .WithGrpcChannelServerTags(_channelInfo) + .WithClientSettingsServerTags(_settings) + .WithOptionalTag(TelemetryTags.Database.User, _settings.DefaultCredentials?.Username); + + return KurrentClientDiagnostics.ActivitySource.TraceClientOperation( Operation, TracingConstants.Operations.Append, - AppendTags + tags ); async ValueTask Operation() { @@ -296,20 +362,13 @@ async ValueTask Operation() { try { foreach (var appendRequest in GetRequests(events, options, correlationId)) await _channel.Writer.WriteAsync(appendRequest, cancellationToken).ConfigureAwait(false); - } - catch (ChannelClosedException ex) { + } catch (ChannelClosedException ex) { // channel is closed, our tcs won't necessarily get completed, don't wait for it. throw ex.InnerException ?? ex; } return await complete.Task.ConfigureAwait(false); } - - ActivityTagsCollection AppendTags() => new ActivityTagsCollection() - .WithRequiredTag(TelemetryTags.EventStore.Stream, options.StreamIdentifier.StreamName.ToStringUtf8()) - .WithGrpcChannelServerTags(_settings, _channelInfo) - .WithClientSettingsServerTags(_settings) - .WithOptionalTag(TelemetryTags.Database.User, _settings.DefaultCredentials?.Username); } async Task Duplex(ValueTask channelInfoTask) { @@ -322,7 +381,7 @@ async Task Duplex(ValueTask channelInfoTask) { } _call = new StreamsClient(_channelInfo.CallInvoker).BatchAppend( - EventStoreCallOptions.CreateStreaming( + KurrentCallOptions.CreateStreaming( _settings, userCredentials: _settings.DefaultCredentials, cancellationToken: _cancellationToken @@ -333,8 +392,7 @@ async Task Duplex(ValueTask channelInfoTask) { _ = Task.Run(Receive, _cancellationToken); _isUsable.TrySetResult(true); - } - catch (Exception ex) { + } catch (Exception ex) { _isUsable.TrySetException(ex); _onException(ex); } @@ -344,7 +402,8 @@ async Task Duplex(ValueTask channelInfoTask) { async Task Send() { if (_call is null) return; - await foreach (var appendRequest in _channel.Reader.ReadAllAsync(_cancellationToken).ConfigureAwait(false)) + await foreach (var appendRequest in _channel.Reader.ReadAllAsync(_cancellationToken) + .ConfigureAwait(false)) await _call.RequestStream.WriteAsync(appendRequest).ConfigureAwait(false); await _call.RequestStream.CompleteAsync().ConfigureAwait(false); @@ -354,20 +413,22 @@ async Task Receive() { if (_call is null) return; try { - await foreach (var response in _call.ResponseStream.ReadAllAsync(_cancellationToken).ConfigureAwait(false)) { - if (!_pendingRequests.TryRemove(Uuid.FromDto(response.CorrelationId), out var writeResult)) { + await foreach (var response in _call.ResponseStream.ReadAllAsync(_cancellationToken) + .ConfigureAwait(false)) { + if (!_pendingRequests.TryRemove( + Uuid.FromDto(response.CorrelationId), + out var writeResult + )) { continue; // TODO: Log? } try { writeResult.TrySetResult(response.ToWriteResult()); - } - catch (Exception ex) { + } catch (Exception ex) { writeResult.TrySetException(ex); } } - } - catch (Exception ex) { + } catch (Exception ex) { // signal that no tcs added to _pendingRequests after this point will necessarily complete _channel.Writer.TryComplete(ex); @@ -380,7 +441,9 @@ async Task Receive() { } } - IEnumerable GetRequests(IEnumerable events, BatchAppendReq.Types.Options options, Uuid correlationId) { + IEnumerable GetRequests( + IEnumerable events, BatchAppendReq.Types.Options options, Uuid correlationId + ) { var batchSize = 0; var first = true; var correlationIdDto = correlationId.ToDto(); @@ -427,4 +490,153 @@ public void Dispose() { } } } + + public static class KurrentClientAppendToStreamExtensions { + /// + /// Appends events asynchronously to a stream. Messages are serialized using default or custom serialization configured through + /// + /// + /// The name of the stream to append events to. + /// Messages to append to the stream. + /// The optional . + /// + public static Task AppendToStreamAsync( + this KurrentClient client, + string streamName, + IEnumerable messages, + CancellationToken cancellationToken = default + ) + => client.AppendToStreamAsync( + streamName, + messages, + new AppendToStreamOptions(), + cancellationToken + ); + + /// + /// Appends events asynchronously to a stream. Messages are serialized using default or custom serialization configured through + /// + /// + /// The name of the stream to append events to. + /// Messages to append to the stream. + /// The optional . + /// + public static Task AppendToStreamAsync( + this KurrentClient client, + string streamName, + IEnumerable messages, + CancellationToken cancellationToken = default + ) + => client.AppendToStreamAsync( + streamName, + messages.Select(m => Message.From(m)), + new AppendToStreamOptions(), + cancellationToken + ); + + + /// + /// Appends events asynchronously to a stream. Messages are serialized using default or custom serialization configured through + /// + /// + /// The name of the stream to append events to. + /// The expected of the stream to append to. + /// Messages to append to the stream. + /// The optional . + /// + public static Task AppendToStreamAsync( + this KurrentClient client, + string streamName, + StreamRevision expectedRevision, + IEnumerable messages, + CancellationToken cancellationToken = default + ) + => client.AppendToStreamAsync( + streamName, + messages, + new AppendToStreamOptions { + ExpectedStreamRevision = expectedRevision + }, + cancellationToken + ); + + /// + /// Appends events asynchronously to a stream. Messages are serialized using default or custom serialization configured through + /// + /// + /// The name of the stream to append events to. + /// The expected of the stream to append to. + /// Messages to append to the stream. + /// The optional . + /// + public static Task AppendToStreamAsync( + this KurrentClient client, + string streamName, + StreamRevision expectedRevision, + IEnumerable messages, + CancellationToken cancellationToken = default + ) + => client.AppendToStreamAsync( + streamName, + messages.Select(m => Message.From(m)), + new AppendToStreamOptions{ ExpectedStreamRevision = expectedRevision}, + cancellationToken + ); + + /// + /// Appends events asynchronously to a stream. Messages are serialized using default or custom serialization configured through + /// + /// + /// The name of the stream to append events to. + /// Messages to append to the stream. + /// Optional settings for the append operation, e.g. expected stream position for optimistic concurrency check + /// The optional . + /// + public static Task AppendToStreamAsync( + this KurrentClient client, + string streamName, + IEnumerable messages, + AppendToStreamOptions options, + CancellationToken cancellationToken = default + ) + => client.AppendToStreamAsync( + streamName, + messages.Select(m => Message.From(m)), + options, + cancellationToken + ); + } + + // TODO: In the follow up PR merge StreamState and StreamRevision into a one thing + public class AppendToStreamOptions { + /// + /// The expected of the stream to append to. + /// + public StreamState? ExpectedStreamState { get; set; } + + /// + /// The expected of the stream to append to. + /// + public StreamRevision? ExpectedStreamRevision { get; set; } + + /// + /// An to configure the operation's options. + /// + public Action? ConfigureOperationOptions { get; set; } + + /// + /// Maximum time that the operation will be run + /// + public TimeSpan? Deadline { get; set; } + + /// + /// The for the operation. + /// + public UserCredentials? UserCredentials { get; set; } + + /// + /// Allows to customize or disable the automatic deserialization + /// + public OperationSerializationSettings? SerializationSettings { get; set; } + } } diff --git a/src/EventStore.Client.Streams/EventStoreClient.Delete.cs b/src/Kurrent.Client/Streams/KurrentClient.Delete.cs similarity index 95% rename from src/EventStore.Client.Streams/EventStoreClient.Delete.cs rename to src/Kurrent.Client/Streams/KurrentClient.Delete.cs index dfaac235f..50a174e25 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Delete.cs +++ b/src/Kurrent.Client/Streams/KurrentClient.Delete.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; namespace EventStore.Client { - public partial class EventStoreClient { + public partial class KurrentClient { /// /// Deletes a stream asynchronously. /// @@ -56,7 +56,7 @@ private async Task DeleteInternal(DeleteReq request, var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Streams.Streams.StreamsClient( channelInfo.CallInvoker).DeleteAsync(request, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); var result = await call.ResponseAsync.ConfigureAwait(false); return new DeleteResult(new Position(result.Position.CommitPosition, result.Position.PreparePosition)); diff --git a/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs b/src/Kurrent.Client/Streams/KurrentClient.Metadata.cs similarity index 91% rename from src/EventStore.Client.Streams/EventStoreClient.Metadata.cs rename to src/Kurrent.Client/Streams/KurrentClient.Metadata.cs index 19de629e7..f8185607c 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs +++ b/src/Kurrent.Client/Streams/KurrentClient.Metadata.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging; namespace EventStore.Client { - public partial class EventStoreClient { + public partial class KurrentClient { /// /// Asynchronously reads the metadata for a stream /// @@ -44,13 +44,13 @@ public async Task GetStreamMetadataAsync(string streamName /// The name of the stream to set metadata for. /// The of the stream to append to. /// A representing the new metadata. - /// An to configure the operation's options. + /// An to configure the operation's options. /// /// The optional to perform operation with. /// The optional . /// public Task SetStreamMetadataAsync(string streamName, StreamState expectedState, - StreamMetadata metadata, Action? configureOperationOptions = null, + StreamMetadata metadata, Action? configureOperationOptions = null, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var options = Settings.OperationOptions.Clone(); @@ -69,13 +69,13 @@ public Task SetStreamMetadataAsync(string streamName, StreamState /// The name of the stream to set metadata for. /// The of the stream to append to. /// A representing the new metadata. - /// An to configure the operation's options. + /// An to configure the operation's options. /// /// The optional to perform operation with. /// The optional . /// public Task SetStreamMetadataAsync(string streamName, StreamRevision expectedRevision, - StreamMetadata metadata, Action? configureOperationOptions = null, + StreamMetadata metadata, Action? configureOperationOptions = null, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { var options = Settings.OperationOptions.Clone(); @@ -91,7 +91,7 @@ public Task SetStreamMetadataAsync(string streamName, StreamRevisi private async Task SetStreamMetadataInternal(StreamMetadata metadata, AppendReq appendReq, - EventStoreClientOperationOptions operationOptions, + KurrentClientOperationOptions operationOptions, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { diff --git a/src/EventStore.Client.Streams/EventStoreClient.Read.cs b/src/Kurrent.Client/Streams/KurrentClient.Read.cs similarity index 58% rename from src/EventStore.Client.Streams/EventStoreClient.Read.cs rename to src/Kurrent.Client/Streams/KurrentClient.Read.cs index 7960622ea..0aff8be49 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Read.cs +++ b/src/Kurrent.Client/Streams/KurrentClient.Read.cs @@ -1,11 +1,59 @@ using System.Threading.Channels; using EventStore.Client.Streams; using Grpc.Core; +using Kurrent.Client.Core.Serialization; using static EventStore.Client.Streams.ReadResp; using static EventStore.Client.Streams.ReadResp.ContentOneofCase; namespace EventStore.Client { - public partial class EventStoreClient { + public partial class KurrentClient { + /// + /// Asynchronously reads all events. By default, it reads all of them from the start. The options parameter allows you to fine-tune it to your needs. + /// + /// Optional settings like: max count, in which to read, the to start reading from, etc. + /// The optional . + /// + public ReadAllStreamResult ReadAllAsync( + ReadAllOptions options, + CancellationToken cancellationToken = default + ) { + if (options.MaxCount <= 0) + throw new ArgumentOutOfRangeException(nameof(options.MaxCount)); + + var readReq = new ReadReq { + Options = new() { + ReadDirection = options.Direction switch { + Direction.Backwards => ReadReq.Types.Options.Types.ReadDirection.Backwards, + Direction.Forwards => ReadReq.Types.Options.Types.ReadDirection.Forwards, + _ => throw InvalidOption(options.Direction) + }, + ResolveLinks = options.ResolveLinkTos, + All = new() { + Position = new() { + CommitPosition = options.Position.CommitPosition, + PreparePosition = options.Position.PreparePosition + } + }, + Count = (ulong)options.MaxCount, + UuidOption = new() { Structured = new() }, + ControlOption = new() { Compatibility = 1 }, + Filter = GetFilterOptions(options.Filter) + } + }; + + return new ReadAllStreamResult( + async _ => { + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + return channelInfo.CallInvoker; + }, + readReq, + Settings, + options, + _messageSerializer.With(Settings.Serialization, options.SerializationSettings), + cancellationToken + ); + } + /// /// Asynchronously reads all events. /// @@ -25,16 +73,20 @@ public ReadAllStreamResult ReadAllAsync( TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default - ) => ReadAllAsync( - direction, - position, - eventFilter: null, - maxCount, - resolveLinkTos, - deadline, - userCredentials, - cancellationToken - ); + ) => + ReadAllAsync( + new ReadAllOptions { + Direction = direction, + Position = position, + Filter = null, + MaxCount = maxCount, + ResolveLinkTos = resolveLinkTos, + Deadline = deadline, + UserCredentials = userCredentials, + SerializationSettings = OperationSerializationSettings.Disabled + }, + cancellationToken + ); /// /// Asynchronously reads all events with filtering. @@ -61,36 +113,17 @@ public ReadAllStreamResult ReadAllAsync( if (maxCount <= 0) throw new ArgumentOutOfRangeException(nameof(maxCount)); - var readReq = new ReadReq { - Options = new() { - ReadDirection = direction switch { - Direction.Backwards => ReadReq.Types.Options.Types.ReadDirection.Backwards, - Direction.Forwards => ReadReq.Types.Options.Types.ReadDirection.Forwards, - _ => throw InvalidOption(direction) - }, - ResolveLinks = resolveLinkTos, - All = new() { - Position = new() { - CommitPosition = position.CommitPosition, - PreparePosition = position.PreparePosition - } - }, - Count = (ulong)maxCount, - UuidOption = new() { Structured = new() }, - ControlOption = new() { Compatibility = 1 }, - Filter = GetFilterOptions(eventFilter) - } - }; - - return new ReadAllStreamResult( - async _ => { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); - return channelInfo.CallInvoker; + return ReadAllAsync( + new ReadAllOptions { + Direction = direction, + Position = position, + Filter = eventFilter, + MaxCount = maxCount, + ResolveLinkTos = resolveLinkTos, + Deadline = deadline, + UserCredentials = userCredentials, + SerializationSettings = OperationSerializationSettings.Disabled }, - readReq, - Settings, - deadline, - userCredentials, cancellationToken ); } @@ -130,8 +163,7 @@ async IAsyncEnumerable GetMessages() { yield return message; } - } - finally { + } finally { _cts.Cancel(); } } @@ -139,14 +171,17 @@ async IAsyncEnumerable GetMessages() { } internal ReadAllStreamResult( - Func> selectCallInvoker, ReadReq request, - EventStoreClientSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, + Func> selectCallInvoker, + ReadReq request, + KurrentClientSettings settings, + ReadAllOptions options, + IMessageSerializer messageSerializer, CancellationToken cancellationToken ) { - var callOptions = EventStoreCallOptions.CreateStreaming( + var callOptions = KurrentCallOptions.CreateStreaming( settings, - deadline, - userCredentials, + options.Deadline, + options.UserCredentials, cancellationToken ); @@ -157,7 +192,7 @@ CancellationToken cancellationToken if (request.Options.FilterOptionCase == ReadReq.Types.Options.FilterOptionOneofCase.None) request.Options.NoFilter = new(); - + _ = PumpMessages(); return; @@ -167,14 +202,21 @@ async Task PumpMessages() { var callInvoker = await selectCallInvoker(linkedCancellationToken).ConfigureAwait(false); var client = new Streams.Streams.StreamsClient(callInvoker); using var call = client.Read(request, callOptions); + await foreach (var response in call.ResponseStream.ReadAllAsync(linkedCancellationToken) .ConfigureAwait(false)) { await _channel.Writer.WriteAsync( response.ContentCase switch { - StreamNotFound => StreamMessage.NotFound.Instance, - Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), - FirstStreamPosition => new StreamMessage.FirstStreamPosition(new StreamPosition(response.FirstStreamPosition)), - LastStreamPosition => new StreamMessage.LastStreamPosition(new StreamPosition(response.LastStreamPosition)), + StreamNotFound => StreamMessage.NotFound.Instance, + Event => new StreamMessage.Event( + ConvertToResolvedEvent(response.Event, messageSerializer) + ), + FirstStreamPosition => new StreamMessage.FirstStreamPosition( + new StreamPosition(response.FirstStreamPosition) + ), + LastStreamPosition => new StreamMessage.LastStreamPosition( + new StreamPosition(response.LastStreamPosition) + ), LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( new Position( response.LastAllStreamPosition.CommitPosition, @@ -188,8 +230,7 @@ await _channel.Writer.WriteAsync( } _channel.Writer.Complete(); - } - catch (Exception ex) { + } catch (Exception ex) { _channel.Writer.TryComplete(ex); } } @@ -200,15 +241,15 @@ public async IAsyncEnumerator GetAsyncEnumerator( CancellationToken cancellationToken = default ) { try { - await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { + await foreach (var message in + _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { if (message is not StreamMessage.Event e) { continue; } yield return e.ResolvedEvent; } - } - finally { + } finally { _cts.Cancel(); } } @@ -219,27 +260,17 @@ public async IAsyncEnumerator GetAsyncEnumerator( /// /// The result could also be inspected as a means to avoid handling exceptions as the would indicate whether or not the stream is readable./> /// - /// The in which to read. /// The name of the stream to read. - /// The to start reading from. - /// The number of events to read from the stream. - /// Whether to resolve LinkTo events automatically. - /// - /// The optional to perform operation with. + /// Optional settings like: max count, in which to read, the to start reading from, etc. /// The optional . /// public ReadStreamResult ReadStreamAsync( - Direction direction, string streamName, - StreamPosition revision, - long maxCount = long.MaxValue, - bool resolveLinkTos = false, - TimeSpan? deadline = null, - UserCredentials? userCredentials = null, + ReadStreamOptions options, CancellationToken cancellationToken = default ) { - if (maxCount <= 0) - throw new ArgumentOutOfRangeException(nameof(maxCount)); + if (options.MaxCount <= 0) + throw new ArgumentOutOfRangeException(nameof(options.MaxCount)); return new ReadStreamResult( async _ => { @@ -248,25 +279,68 @@ public ReadStreamResult ReadStreamAsync( }, new ReadReq { Options = new() { - ReadDirection = direction switch { + ReadDirection = options.Direction switch { Direction.Backwards => ReadReq.Types.Options.Types.ReadDirection.Backwards, Direction.Forwards => ReadReq.Types.Options.Types.ReadDirection.Forwards, - _ => throw InvalidOption(direction) + _ => throw InvalidOption(options.Direction) }, - ResolveLinks = resolveLinkTos, + ResolveLinks = options.ResolveLinkTos, Stream = ReadReq.Types.Options.Types.StreamOptions.FromStreamNameAndRevision( streamName, - revision + options.StreamPosition ), - Count = (ulong)maxCount, + Count = (ulong)options.MaxCount, UuidOption = new() { Structured = new() }, NoFilter = new(), ControlOption = new() { Compatibility = 1 } } }, Settings, - deadline, - userCredentials, + options.Deadline, + options.UserCredentials, + _messageSerializer.With(Settings.Serialization, options.SerializationSettings), + cancellationToken + ); + } + + /// + /// Asynchronously reads all the events from a stream. + /// + /// The result could also be inspected as a means to avoid handling exceptions as the would indicate whether or not the stream is readable./> + /// + /// The in which to read. + /// The name of the stream to read. + /// The to start reading from. + /// The number of events to read from the stream. + /// Whether to resolve LinkTo events automatically. + /// + /// The optional to perform operation with. + /// The optional . + /// + public ReadStreamResult ReadStreamAsync( + Direction direction, + string streamName, + StreamPosition revision, + long maxCount = long.MaxValue, + bool resolveLinkTos = false, + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) { + if (maxCount <= 0) + throw new ArgumentOutOfRangeException(nameof(maxCount)); + + return ReadStreamAsync( + streamName, + new ReadStreamOptions { + Direction = direction, + StreamPosition = revision, + MaxCount = maxCount, + ResolveLinkTos = resolveLinkTos, + Deadline = deadline, + UserCredentials = userCredentials, + SerializationSettings = OperationSerializationSettings.Disabled + }, cancellationToken ); } @@ -308,7 +382,8 @@ async IAsyncEnumerable GetMessages() { } try { - await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token) + .ConfigureAwait(false)) { switch (message) { case StreamMessage.FirstStreamPosition(var streamPosition): FirstStreamPosition = streamPosition; @@ -324,8 +399,7 @@ async IAsyncEnumerable GetMessages() { yield return message; } - } - finally { + } finally { _cts.Cancel(); } } @@ -338,11 +412,15 @@ async IAsyncEnumerable GetMessages() { public Task ReadState { get; } internal ReadStreamResult( - Func> selectCallInvoker, ReadReq request, - EventStoreClientSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, + Func> selectCallInvoker, + ReadReq request, + KurrentClientSettings settings, + TimeSpan? deadline, + UserCredentials? userCredentials, + IMessageSerializer messageSerializer, CancellationToken cancellationToken ) { - var callOptions = EventStoreCallOptions.CreateStreaming( + var callOptions = KurrentCallOptions.CreateStreaming( settings, deadline, userCredentials, @@ -382,8 +460,7 @@ await _channel.Writer.WriteAsync(StreamMessage.Ok.Instance, linkedCancellationTo .ConfigureAwait(false); tcs.SetResult(Client.ReadState.Ok); - } - else { + } else { tcs.SetResult(Client.ReadState.StreamNotFound); } } @@ -391,7 +468,9 @@ await _channel.Writer.WriteAsync(StreamMessage.Ok.Instance, linkedCancellationTo await _channel.Writer.WriteAsync( response.ContentCase switch { StreamNotFound => StreamMessage.NotFound.Instance, - Event => new StreamMessage.Event(ConvertToResolvedEvent(response.Event)), + Event => new StreamMessage.Event( + ConvertToResolvedEvent(response.Event, messageSerializer) + ), ContentOneofCase.FirstStreamPosition => new StreamMessage.FirstStreamPosition( new StreamPosition(response.FirstStreamPosition) ), @@ -411,8 +490,7 @@ await _channel.Writer.WriteAsync( } _channel.Writer.Complete(); - } - catch (Exception ex) { + } catch (Exception ex) { tcs.TrySetException(ex); _channel.Writer.TryComplete(ex); } @@ -424,7 +502,8 @@ public async IAsyncEnumerator GetAsyncEnumerator( CancellationToken cancellationToken = default ) { try { - await foreach (var message in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { + await foreach (var message in + _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { if (message is StreamMessage.NotFound) { throw new StreamNotFoundException(StreamName); } @@ -435,21 +514,24 @@ public async IAsyncEnumerator GetAsyncEnumerator( yield return e.ResolvedEvent; } - } - finally { + } finally { _cts.Cancel(); } } } - static ResolvedEvent ConvertToResolvedEvent(ReadResp.Types.ReadEvent readEvent) => - new ResolvedEvent( + static ResolvedEvent ConvertToResolvedEvent( + Types.ReadEvent readEvent, + IMessageSerializer messageSerializer + ) => + ResolvedEvent.From( ConvertToEventRecord(readEvent.Event)!, ConvertToEventRecord(readEvent.Link), readEvent.PositionCase switch { - ReadResp.Types.ReadEvent.PositionOneofCase.CommitPosition => readEvent.CommitPosition, - _ => null - } + Types.ReadEvent.PositionOneofCase.CommitPosition => readEvent.CommitPosition, + _ => null + }, + messageSerializer ); static EventRecord? ConvertToEventRecord(ReadResp.Types.ReadEvent.Types.RecordedEvent? e) => @@ -465,4 +547,126 @@ static ResolvedEvent ConvertToResolvedEvent(ReadResp.Types.ReadEvent readEvent) e.CustomMetadata.ToByteArray() ); } -} \ No newline at end of file + + /// + /// Optional settings to customize reading all messages, for instance: max count, + /// in which to read, the to start reading from, etc. + /// + public class ReadAllOptions { + /// + /// The in which to read. + /// + public Direction Direction { get; set; } = Direction.Forwards; + + /// + /// The to start reading from. + /// + public Position Position { get; set; } = Position.Start; + + /// + /// The to apply. + /// + public IEventFilter? Filter { get; set; } + + /// + /// The number of events to read from the stream. + /// + public long MaxCount { get; set; } = long.MaxValue; + + /// + /// Whether to resolve LinkTo events automatically. + /// + public bool ResolveLinkTos { get; set; } + + /// + /// Maximum time that the operation will be run + /// + public TimeSpan? Deadline { get; set; } + + /// + /// The optional to perform operation with. + /// + public UserCredentials? UserCredentials { get; set; } + + /// + /// Allows to customize or disable the automatic deserialization + /// + public OperationSerializationSettings? SerializationSettings { get; set; } + } + + /// + /// Optional settings to customize reading stream messages, for instance: max count, + /// in which to read, the to start reading from, etc. + /// + public class ReadStreamOptions { + /// + /// The in which to read. + /// + public Direction Direction { get; set; } = Direction.Forwards; + + /// + /// The to start reading from. + /// + public StreamPosition StreamPosition { get; set; } = StreamPosition.Start; + + /// + /// The number of events to read from the stream. + /// + public long MaxCount { get; set; } = long.MaxValue; + + /// + /// Whether to resolve LinkTo events automatically. + /// + public bool ResolveLinkTos { get; set; } + + /// + /// Maximum time that the operation will be run + /// + public TimeSpan? Deadline { get; set; } + + /// + /// The optional to perform operation with. + /// + public UserCredentials? UserCredentials { get; set; } + + /// + /// Allows to customize or disable the automatic deserialization + /// + public OperationSerializationSettings? SerializationSettings { get; set; } + } + + public static class KurrentClientReadExtensions { + /// + /// Asynchronously reads all events. By default, it reads all of them from the start. The options parameter allows you to fine-tune it to your needs. + /// + /// + /// The optional . + /// + public static KurrentClient.ReadAllStreamResult ReadAllAsync( + this KurrentClient client, + CancellationToken cancellationToken = default + ) => + client.ReadAllAsync(new ReadAllOptions(), cancellationToken); + + /// + /// Asynchronously reads all the events from a stream. + /// + /// The result could also be inspected as a means to avoid handling exceptions as the would indicate whether or not the stream is readable./> + /// + /// + /// The name of the stream to read. + /// Optional settings like: max count, in which to read, the to start reading from, etc. + /// The optional . + /// + public static KurrentClient.ReadStreamResult ReadStreamAsync( + this KurrentClient client, + string streamName, + CancellationToken cancellationToken = default + ) => + client.ReadStreamAsync( + streamName, + new ReadStreamOptions(), + cancellationToken + ); + } +} diff --git a/src/Kurrent.Client/Streams/KurrentClient.Subscriptions.cs b/src/Kurrent.Client/Streams/KurrentClient.Subscriptions.cs new file mode 100644 index 000000000..fca4e5275 --- /dev/null +++ b/src/Kurrent.Client/Streams/KurrentClient.Subscriptions.cs @@ -0,0 +1,670 @@ +using System.Threading.Channels; +using EventStore.Client.Diagnostics; +using EventStore.Client.Streams; +using Grpc.Core; +using Kurrent.Client.Core.Serialization; +using static EventStore.Client.Streams.ReadResp.ContentOneofCase; + +namespace EventStore.Client { + /// + /// Subscribes to all events options. + /// + public class SubscribeToAllOptions { + /// + /// A (exclusive of) to start the subscription from. + /// + public FromAll Start { get; set; } = FromAll.Start; + + /// + /// Whether to resolve LinkTo events automatically. + /// + public bool ResolveLinkTos { get; set; } + + /// + /// The optional to apply. + /// + public SubscriptionFilterOptions? FilterOptions { get; set; } + + /// + /// The optional to apply. + /// + public IEventFilter Filter { set => FilterOptions = new SubscriptionFilterOptions(value); } + + /// + /// The optional user credentials to perform operation with. + /// + public UserCredentials? UserCredentials { get; set; } + + /// + /// Allows to customize or disable the automatic deserialization + /// + public OperationSerializationSettings? SerializationSettings { get; set; } + } + + /// + /// Subscribes to all events options. + /// + public class SubscribeToStreamOptions { + /// + /// A (exclusive of) to start the subscription from. + /// + public FromStream Start { get; set; } = FromStream.Start; + + /// + /// Whether to resolve LinkTo events automatically. + /// + public bool ResolveLinkTos { get; set; } + + /// + /// The optional user credentials to perform operation with. + /// + public UserCredentials? UserCredentials { get; set; } + + /// + /// Allows to customize or disable the automatic deserialization + /// + public OperationSerializationSettings? SerializationSettings { get; set; } + } + + public class SubscriptionListener { +#if NET48 + /// + /// A handler called when a new event is received over the subscription. + /// + public Func EventAppeared { get; set; } = null!; +#else + public required Func EventAppeared { get; set; } +#endif + /// + /// A handler called if the subscription is dropped. + /// + public Action? SubscriptionDropped { get; set; } + + /// + /// A handler called when a checkpoint is reached. + /// Set the checkpointInterval in subscription filter options to define how often this method is called. + /// + public Func? CheckpointReached { get; set; } + + /// + /// Returns the subscription listener with configured handlers + /// + /// Handler invoked when a new event is received over the subscription. + /// A handler invoked if the subscription is dropped. + /// A handler called when a checkpoint is reached. + /// Set the checkpointInterval in subscription filter options to define how often this method is called. + /// + /// + public static SubscriptionListener Handle( + Func eventAppeared, + Action? subscriptionDropped = null, + Func? checkpointReached = null + ) => + new SubscriptionListener { + EventAppeared = eventAppeared, + SubscriptionDropped = subscriptionDropped, + CheckpointReached = checkpointReached + }; + } + + public partial class KurrentClient { + /// + /// Subscribes to all events. + /// + /// A (exclusive of) to start the subscription from. + /// A Task invoked and awaited when a new event is received over the subscription. + /// Whether to resolve LinkTo events automatically. + /// An action invoked if the subscription is dropped. + /// The optional to apply. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public Task SubscribeToAllAsync( + FromAll start, + Func eventAppeared, + bool resolveLinkTos = false, + Action? subscriptionDropped = default, + SubscriptionFilterOptions? filterOptions = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) { + var listener = SubscriptionListener.Handle( + eventAppeared, + subscriptionDropped, + filterOptions?.CheckpointReached + ); + + var options = new SubscribeToAllOptions { + Start = start, + FilterOptions = filterOptions, + ResolveLinkTos = resolveLinkTos, + UserCredentials = userCredentials, + SerializationSettings = OperationSerializationSettings.Disabled, + }; + + return SubscribeToAllAsync(listener, options, cancellationToken); + } + + /// + /// Subscribes to all events. + /// + /// Listener configured to receive notifications about new events and subscription state change. + /// Optional settings like: Position from which to read, to apply, etc. + /// The optional . + /// + public Task SubscribeToAllAsync( + SubscriptionListener listener, + SubscribeToAllOptions options, + CancellationToken cancellationToken = default + ) { + listener.CheckpointReached ??= options.FilterOptions?.CheckpointReached; + + return StreamSubscription.Confirm( + SubscribeToAll(options, cancellationToken), + listener, + _log, + cancellationToken + ); + } + + /// + /// Subscribes to all events. + /// + /// Optional settings like: Position from which to read, to apply, etc. + /// The optional . + /// + public StreamSubscriptionResult SubscribeToAll( + SubscribeToAllOptions options, + CancellationToken cancellationToken = default + ) => new( + async _ => await GetChannelInfo(cancellationToken).ConfigureAwait(false), + new ReadReq { + Options = new ReadReq.Types.Options { + ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, + ResolveLinks = options.ResolveLinkTos, + All = ReadReq.Types.Options.Types.AllOptions.FromSubscriptionPosition(options.Start), + Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), + Filter = GetFilterOptions(options.FilterOptions)!, + UuidOption = new() { Structured = new() } + } + }, + Settings, + options.UserCredentials, + _messageSerializer.With(Settings.Serialization, options.SerializationSettings), + cancellationToken + ); + + /// + /// Subscribes to all events. + /// + /// A (exclusive of) to start the subscription from. + /// Whether to resolve LinkTo events automatically. + /// The optional to apply. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public StreamSubscriptionResult SubscribeToAll( + FromAll start, + bool resolveLinkTos = false, + SubscriptionFilterOptions? filterOptions = null, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) => + SubscribeToAll( + new SubscribeToAllOptions { + Start = start, + ResolveLinkTos = resolveLinkTos, + FilterOptions = filterOptions, + UserCredentials = userCredentials, + SerializationSettings = OperationSerializationSettings.Disabled + }, + cancellationToken + ); + + /// + /// Subscribes to a stream from a checkpoint. + /// + /// A (exclusive of) to start the subscription from. + /// The name of the stream to subscribe for notifications about new events. + /// A Task invoked and awaited when a new event is received over the subscription. + /// Whether to resolve LinkTo events automatically. + /// An action invoked if the subscription is dropped. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public Task SubscribeToStreamAsync( + string streamName, + FromStream start, + Func eventAppeared, + bool resolveLinkTos = false, + Action? subscriptionDropped = default, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) => + SubscribeToStreamAsync( + streamName, + SubscriptionListener.Handle(eventAppeared, subscriptionDropped), + new SubscribeToStreamOptions { + Start = start, + ResolveLinkTos = resolveLinkTos, + UserCredentials = userCredentials, + SerializationSettings = OperationSerializationSettings.Disabled + }, + cancellationToken + ); + + /// + /// Subscribes to a stream from a checkpoint. + /// + /// The name of the stream to subscribe for notifications about new events. + /// Listener configured to receive notifications about new events and subscription state change. + /// Optional settings like: Position from which to read, etc. + /// The optional . + /// + public Task SubscribeToStreamAsync( + string streamName, + SubscriptionListener listener, + SubscribeToStreamOptions options, + CancellationToken cancellationToken = default + ) { + return StreamSubscription.Confirm( + SubscribeToStream(streamName, options, cancellationToken), + listener, + _log, + cancellationToken + ); + } + + /// + /// Subscribes to a stream from a checkpoint. + /// + /// A (exclusive of) to start the subscription from. + /// The name of the stream to subscribe for notifications about new events. + /// Whether to resolve LinkTo events automatically. + /// The optional user credentials to perform operation with. + /// The optional . + /// + public StreamSubscriptionResult SubscribeToStream( + string streamName, + FromStream start, + bool resolveLinkTos = false, + UserCredentials? userCredentials = null, + CancellationToken cancellationToken = default + ) => + SubscribeToStream( + streamName, + new SubscribeToStreamOptions { + Start = start, + ResolveLinkTos = resolveLinkTos, + UserCredentials = userCredentials, + SerializationSettings = OperationSerializationSettings.Disabled + }, + cancellationToken + ); + + /// + /// Subscribes to a stream from a checkpoint. + /// + /// The name of the stream to subscribe for notifications about new events. + /// Optional settings like: Position from which to read, etc. + /// The optional . + /// + public StreamSubscriptionResult SubscribeToStream( + string streamName, + SubscribeToStreamOptions options, + CancellationToken cancellationToken = default + ) => new( + async _ => await GetChannelInfo(cancellationToken).ConfigureAwait(false), + new ReadReq { + Options = new ReadReq.Types.Options { + ReadDirection = ReadReq.Types.Options.Types.ReadDirection.Forwards, + ResolveLinks = options.ResolveLinkTos, + Stream = ReadReq.Types.Options.Types.StreamOptions.FromSubscriptionPosition( + streamName, + options.Start + ), + Subscription = new ReadReq.Types.Options.Types.SubscriptionOptions(), + UuidOption = new() { Structured = new() } + } + }, + Settings, + options.UserCredentials, + _messageSerializer.With(Settings.Serialization, options.SerializationSettings), + cancellationToken + ); + + /// + /// A class that represents the result of a subscription operation. You may either enumerate this instance directly or . Do not enumerate more than once. + /// + public class StreamSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable { + private readonly ReadReq _request; + private readonly Channel _channel; + private readonly CancellationTokenSource _cts; + private readonly CallOptions _callOptions; + private readonly KurrentClientSettings _settings; + private AsyncServerStreamingCall? _call; + + private int _messagesEnumerated; + + /// + /// The server-generated unique identifier for the subscription. + /// + public string? SubscriptionId { get; private set; } + + /// + /// An . Do not enumerate more than once. + /// + public IAsyncEnumerable Messages { + get { + if (Interlocked.Exchange(ref _messagesEnumerated, 1) == 1) + throw new InvalidOperationException("Messages may only be enumerated once."); + + return GetMessages(); + + async IAsyncEnumerable GetMessages() { + try { + await foreach (var message in _channel.Reader.ReadAllAsync(_cts.Token)) { + if (message is StreamMessage.SubscriptionConfirmation(var subscriptionId)) + SubscriptionId = subscriptionId; + + yield return message; + } + } finally { +#if NET8_0_OR_GREATER + await _cts.CancelAsync().ConfigureAwait(false); +#else + _cts.Cancel(); +#endif + } + } + } + } + + internal StreamSubscriptionResult( + Func> selectChannelInfo, + ReadReq request, + KurrentClientSettings settings, + UserCredentials? userCredentials, + IMessageSerializer messageSerializer, + CancellationToken cancellationToken + ) { + _request = request; + _settings = settings; + + _callOptions = KurrentCallOptions.CreateStreaming( + settings, + userCredentials: userCredentials, + cancellationToken: cancellationToken + ); + + _channel = Channel.CreateBounded(ReadBoundedChannelOptions); + + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + if (_request.Options.FilterOptionCase == ReadReq.Types.Options.FilterOptionOneofCase.None) { + _request.Options.NoFilter = new(); + } + + _ = PumpMessages(); + + return; + + async Task PumpMessages() { + try { + var channelInfo = await selectChannelInfo(_cts.Token).ConfigureAwait(false); + var client = new Streams.Streams.StreamsClient(channelInfo.CallInvoker); + _call = client.Read(_request, _callOptions); + await foreach (var response in _call.ResponseStream.ReadAllAsync(_cts.Token) + .ConfigureAwait(false)) { + StreamMessage subscriptionMessage = + response.ContentCase switch { + Confirmation => new StreamMessage.SubscriptionConfirmation( + response.Confirmation.SubscriptionId + ), + Event => new StreamMessage.Event( + ConvertToResolvedEvent(response.Event, messageSerializer) + ), + FirstStreamPosition => new StreamMessage.FirstStreamPosition( + new StreamPosition(response.FirstStreamPosition) + ), + LastStreamPosition => new StreamMessage.LastStreamPosition( + new StreamPosition(response.LastStreamPosition) + ), + LastAllStreamPosition => new StreamMessage.LastAllStreamPosition( + new Position( + response.LastAllStreamPosition.CommitPosition, + response.LastAllStreamPosition.PreparePosition + ) + ), + Checkpoint => new StreamMessage.AllStreamCheckpointReached( + new Position( + response.Checkpoint.CommitPosition, + response.Checkpoint.PreparePosition + ) + ), + CaughtUp => StreamMessage.CaughtUp.Instance, + FellBehind => StreamMessage.FellBehind.Instance, + _ => StreamMessage.Unknown.Instance + }; + + if (subscriptionMessage is StreamMessage.Event evt) + KurrentClientDiagnostics.ActivitySource.TraceSubscriptionEvent( + SubscriptionId, + evt.ResolvedEvent, + channelInfo, + _settings, + userCredentials + ); + + await _channel.Writer + .WriteAsync(subscriptionMessage, _cts.Token) + .ConfigureAwait(false); + } + + _channel.Writer.Complete(); + } catch (Exception ex) { + _channel.Writer.TryComplete(ex); + } + } + } + + /// + public async ValueTask DisposeAsync() { + //TODO SS: Check if `CastAndDispose` is still relevant + await CastAndDispose(_cts).ConfigureAwait(false); + await CastAndDispose(_call).ConfigureAwait(false); + + return; + + static async ValueTask CastAndDispose(IDisposable? resource) { + switch (resource) { + case null: + return; + + case IAsyncDisposable disposable: + await disposable.DisposeAsync().ConfigureAwait(false); + break; + + default: + resource.Dispose(); + break; + } + } + } + + /// + public void Dispose() { + _cts.Dispose(); + _call?.Dispose(); + } + + /// + public async IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default + ) { + try { + await foreach (var message in + _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { + if (message is not StreamMessage.Event e) + continue; + + yield return e.ResolvedEvent; + } + } finally { +#if NET8_0_OR_GREATER + await _cts.CancelAsync().ConfigureAwait(false); +#else + _cts.Cancel(); +#endif + } + } + } + } + + public static class KurrentClientSubscribeToAllExtensions { + /// + /// Subscribes to all events. + /// + /// + /// Listener configured to receive notifications about new events and subscription state change. + /// The optional . + /// + public static Task SubscribeToAllAsync( + this KurrentClient kurrentClient, + SubscriptionListener listener, + CancellationToken cancellationToken = default + ) => + kurrentClient.SubscribeToAllAsync(listener, new SubscribeToAllOptions(), cancellationToken); + + /// + /// Subscribes to all events. + /// + /// + /// + /// The optional . + /// + public static Task SubscribeToAllAsync( + this KurrentClient kurrentClient, + Func eventAppeared, + CancellationToken cancellationToken = default + ) => + kurrentClient.SubscribeToAllAsync( + eventAppeared, + new SubscribeToAllOptions(), + cancellationToken + ); + + /// + /// Subscribes to all events. + /// + /// + /// Handler invoked when a new event is received over the subscription. + /// Optional settings like: Position from which to read, to apply, etc. + /// The optional . + /// + public static Task SubscribeToAllAsync( + this KurrentClient kurrentClient, + Func eventAppeared, + SubscribeToAllOptions options, + CancellationToken cancellationToken = default + ) => + kurrentClient.SubscribeToAllAsync( + SubscriptionListener.Handle(eventAppeared), + options, + cancellationToken + ); + + /// + /// Subscribes to all events. + /// + /// + /// The optional . + /// + public static KurrentClient.StreamSubscriptionResult SubscribeToAll( + this KurrentClient kurrentClient, + CancellationToken cancellationToken = default + ) => + kurrentClient.SubscribeToAll(new SubscribeToAllOptions(), cancellationToken); + } + + public static class KurrentClientSubscribeToStreamExtensions { + /// + /// Subscribes to messages from a specific stream + /// + /// + /// The name of the stream to subscribe for notifications about new events. + /// Listener configured to receive notifications about new events and subscription state change. + /// The optional . + /// + public static Task SubscribeToStreamAsync( + this KurrentClient kurrentClient, + string streamName, + SubscriptionListener listener, + CancellationToken cancellationToken = default + ) => + kurrentClient.SubscribeToStreamAsync( + streamName, + listener, + new SubscribeToStreamOptions(), + cancellationToken + ); + + /// + /// Subscribes to messages from a specific stream + /// + /// + /// The name of the stream to subscribe for notifications about new events. + /// + /// The optional . + /// + public static Task SubscribeToStreamAsync( + this KurrentClient kurrentClient, + string streamName, + Func eventAppeared, + CancellationToken cancellationToken = default + ) => + kurrentClient.SubscribeToStreamAsync( + streamName, + eventAppeared, + new SubscribeToStreamOptions(), + cancellationToken + ); + + /// + /// Subscribes to messages from a specific stream + /// + /// + /// The name of the stream to subscribe for notifications about new events. + /// Handler invoked when a new event is received over the subscription. + /// Optional settings like: Position from which to read, to apply, etc. + /// The optional . + /// + public static Task SubscribeToStreamAsync( + this KurrentClient kurrentClient, + string streamName, + Func eventAppeared, + SubscribeToStreamOptions options, + CancellationToken cancellationToken = default + ) => + kurrentClient.SubscribeToStreamAsync( + streamName, + SubscriptionListener.Handle(eventAppeared), + options, + cancellationToken + ); + + /// + /// Subscribes to messages from a specific stream + /// + /// + /// The name of the stream to subscribe for notifications about new events. + /// The optional . + /// + public static KurrentClient.StreamSubscriptionResult SubscribeToStream( + this KurrentClient kurrentClient, + string streamName, + CancellationToken cancellationToken = default + ) => + kurrentClient.SubscribeToStream(streamName, new SubscribeToStreamOptions(), cancellationToken); + } +} diff --git a/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs b/src/Kurrent.Client/Streams/KurrentClient.Tombstone.cs similarity index 95% rename from src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs rename to src/Kurrent.Client/Streams/KurrentClient.Tombstone.cs index 9fcddfeb4..26d62dad9 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs +++ b/src/Kurrent.Client/Streams/KurrentClient.Tombstone.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; namespace EventStore.Client { - public partial class EventStoreClient { + public partial class KurrentClient { /// /// Tombstones a stream asynchronously. Note: Tombstoned streams can never be recreated. /// @@ -54,7 +54,7 @@ private async Task TombstoneInternal(TombstoneReq request, TimeSpa var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Streams.Streams.StreamsClient( channelInfo.CallInvoker).TombstoneAsync(request, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); var result = await call.ResponseAsync.ConfigureAwait(false); return new DeleteResult(new Position(result.Position.CommitPosition, result.Position.PreparePosition)); diff --git a/src/EventStore.Client.Streams/EventStoreClient.cs b/src/Kurrent.Client/Streams/KurrentClient.cs similarity index 84% rename from src/EventStore.Client.Streams/EventStoreClient.cs rename to src/Kurrent.Client/Streams/KurrentClient.cs index 9474ff653..524500c06 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.cs +++ b/src/Kurrent.Client/Streams/KurrentClient.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Threading.Channels; using Grpc.Core; +using Kurrent.Client.Core.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -10,7 +11,7 @@ namespace EventStore.Client { /// /// The client used for operations on streams. /// - public sealed partial class EventStoreClient : EventStoreClientBase { + public sealed partial class KurrentClient : KurrentClientBase { static readonly JsonSerializerOptions StreamMetadataJsonSerializerOptions = new() { Converters = { StreamMetadataJsonConverter.Instance @@ -23,10 +24,11 @@ public sealed partial class EventStoreClient : EventStoreClientBase { AllowSynchronousContinuations = true }; - readonly ILogger _log; - Lazy _batchAppenderLazy; - StreamAppender BatchAppender => _batchAppenderLazy.Value; - readonly CancellationTokenSource _disposedTokenSource; + readonly ILogger _log; + Lazy _batchAppenderLazy; + StreamAppender BatchAppender => _batchAppenderLazy.Value; + readonly CancellationTokenSource _disposedTokenSource; + readonly IMessageSerializer _messageSerializer; static readonly Dictionary> ExceptionMap = new() { [Constants.Exceptions.InvalidTransaction] = ex => new InvalidTransactionException(ex.Message, ex), @@ -56,19 +58,21 @@ public sealed partial class EventStoreClient : EventStoreClientBase { }; /// - /// Constructs a new . This is not intended to be called directly from your code. + /// Constructs a new . This is not intended to be called directly from your code. /// /// - public EventStoreClient(IOptions options) : this(options.Value) { } + public KurrentClient(IOptions options) : this(options.Value) { } /// - /// Constructs a new . + /// Constructs a new . /// /// - public EventStoreClient(EventStoreClientSettings? settings = null) : base(settings, ExceptionMap) { - _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); + public KurrentClient(KurrentClientSettings? settings = null) : base(settings, ExceptionMap) { + _log = Settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); _disposedTokenSource = new CancellationTokenSource(); - _batchAppenderLazy = new Lazy(CreateStreamAppender); + _batchAppenderLazy = new Lazy(CreateStreamAppender); + + _messageSerializer = MessageSerializer.From(settings?.Serialization); } void SwapStreamAppender(Exception ex) => @@ -163,4 +167,4 @@ public override async ValueTask DisposeAsync() { await base.DisposeAsync().ConfigureAwait(false); } } -} \ No newline at end of file +} diff --git a/src/EventStore.Client.Streams/EventStoreClientExtensions.cs b/src/Kurrent.Client/Streams/KurrentClientExtensions.cs similarity index 94% rename from src/EventStore.Client.Streams/EventStoreClientExtensions.cs rename to src/Kurrent.Client/Streams/KurrentClientExtensions.cs index 85a78dbe5..690fa89bd 100644 --- a/src/EventStore.Client.Streams/EventStoreClientExtensions.cs +++ b/src/Kurrent.Client/Streams/KurrentClientExtensions.cs @@ -6,9 +6,9 @@ namespace EventStore.Client { /// - /// A set of extension methods for an . + /// A set of extension methods for an . /// - public static class EventStoreClientExtensions { + public static class KurrentClientExtensions { private static readonly JsonSerializerOptions SystemSettingsJsonSerializerOptions = new JsonSerializerOptions { Converters = { SystemSettingsJsonConverter.Instance @@ -26,7 +26,7 @@ public static class EventStoreClientExtensions { /// /// public static Task SetSystemSettingsAsync( - this EventStoreClient client, + this KurrentClient client, SystemSettings settings, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { @@ -51,7 +51,7 @@ public static Task SetSystemSettingsAsync( /// /// public static async Task ConditionalAppendToStreamAsync( - this EventStoreClient client, + this KurrentClient client, string streamName, StreamRevision expectedRevision, IEnumerable eventData, @@ -84,7 +84,7 @@ public static async Task ConditionalAppendToStreamAsync( /// /// public static async Task ConditionalAppendToStreamAsync( - this EventStoreClient client, + this KurrentClient client, string streamName, StreamState expectedState, IEnumerable eventData, diff --git a/src/EventStore.Client.Streams/EventStoreClientServiceCollectionExtensions.cs b/src/Kurrent.Client/Streams/KurrentClientServiceCollectionExtensions.cs similarity index 54% rename from src/EventStore.Client.Streams/EventStoreClientServiceCollectionExtensions.cs rename to src/Kurrent.Client/Streams/KurrentClientServiceCollectionExtensions.cs index 5278f4a6d..b2832fa56 100644 --- a/src/EventStore.Client.Streams/EventStoreClientServiceCollectionExtensions.cs +++ b/src/Kurrent.Client/Streams/KurrentClientServiceCollectionExtensions.cs @@ -9,116 +9,116 @@ namespace Microsoft.Extensions.DependencyInjection { /// - /// A set of extension methods for which provide support for an . + /// A set of extension methods for which provide support for an . /// - public static class EventStoreClientServiceCollectionExtensions { + public static class KurrentClientServiceCollectionExtensions { /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// /// - public static IServiceCollection AddEventStoreClient(this IServiceCollection services, Uri address, + public static IServiceCollection AddKurrentClient(this IServiceCollection services, Uri address, Func? createHttpMessageHandler = null) - => services.AddEventStoreClient(options => { + => services.AddKurrentClient(options => { options.ConnectivitySettings.Address = address; options.CreateHttpMessageHandler = createHttpMessageHandler; }); /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// /// - public static IServiceCollection AddEventStoreClient(this IServiceCollection services, + public static IServiceCollection AddKurrentClient(this IServiceCollection services, Func addressFactory, Func? createHttpMessageHandler = null) - => services.AddEventStoreClient(provider => options => { + => services.AddKurrentClient(provider => options => { options.ConnectivitySettings.Address = addressFactory(provider); options.CreateHttpMessageHandler = createHttpMessageHandler; }); /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// - public static IServiceCollection AddEventStoreClient(this IServiceCollection services, - Action? configureSettings = null) => - services.AddEventStoreClient(new EventStoreClientSettings(), configureSettings); + public static IServiceCollection AddKurrentClient(this IServiceCollection services, + Action? configureSettings = null) => + services.AddKurrentClient(new KurrentClientSettings(), configureSettings); /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// - public static IServiceCollection AddEventStoreClient(this IServiceCollection services, - Func> configureSettings) => - services.AddEventStoreClient(new EventStoreClientSettings(), + public static IServiceCollection AddKurrentClient(this IServiceCollection services, + Func> configureSettings) => + services.AddKurrentClient(new KurrentClientSettings(), configureSettings); /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// /// - public static IServiceCollection AddEventStoreClient(this IServiceCollection services, - string connectionString, Action? configureSettings = null) { + public static IServiceCollection AddKurrentClient(this IServiceCollection services, + string connectionString, Action? configureSettings = null) { if (services == null) { throw new ArgumentNullException(nameof(services)); } - return services.AddEventStoreClient(EventStoreClientSettings.Create(connectionString), configureSettings); + return services.AddKurrentClient(KurrentClientSettings.Create(connectionString), configureSettings); } /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// /// - public static IServiceCollection AddEventStoreClient(this IServiceCollection services, + public static IServiceCollection AddKurrentClient(this IServiceCollection services, Func connectionStringFactory, - Action? configureSettings = null) { + Action? configureSettings = null) { if (services == null) { throw new ArgumentNullException(nameof(services)); } - return services.AddEventStoreClient(provider => EventStoreClientSettings.Create(connectionStringFactory(provider)), configureSettings); + return services.AddKurrentClient(provider => KurrentClientSettings.Create(connectionStringFactory(provider)), configureSettings); } - private static IServiceCollection AddEventStoreClient(this IServiceCollection services, - EventStoreClientSettings settings, - Action? configureSettings) { + private static IServiceCollection AddKurrentClient(this IServiceCollection services, + KurrentClientSettings settings, + Action? configureSettings) { configureSettings?.Invoke(settings); services.TryAddSingleton(provider => { settings.LoggerFactory ??= provider.GetService(); settings.Interceptors ??= provider.GetServices(); - return new EventStoreClient(settings); + return new KurrentClient(settings); }); return services; } - private static IServiceCollection AddEventStoreClient(this IServiceCollection services, - Func settingsFactory, - Action? configureSettings = null) { + private static IServiceCollection AddKurrentClient(this IServiceCollection services, + Func settingsFactory, + Action? configureSettings = null) { services.TryAddSingleton(provider => { var settings = settingsFactory(provider); @@ -127,15 +127,15 @@ private static IServiceCollection AddEventStoreClient(this IServiceCollection se settings.LoggerFactory ??= provider.GetService(); settings.Interceptors ??= provider.GetServices(); - return new EventStoreClient(settings); + return new KurrentClient(settings); }); return services; } - private static IServiceCollection AddEventStoreClient(this IServiceCollection services, - EventStoreClientSettings settings, - Func> configureSettingsFactory) { + private static IServiceCollection AddKurrentClient(this IServiceCollection services, + KurrentClientSettings settings, + Func> configureSettingsFactory) { services.TryAddSingleton(provider => { configureSettingsFactory(provider).Invoke(settings); @@ -143,7 +143,7 @@ private static IServiceCollection AddEventStoreClient(this IServiceCollection se settings.LoggerFactory ??= provider.GetService(); settings.Interceptors ??= provider.GetServices(); - return new EventStoreClient(settings); + return new KurrentClient(settings); }); return services; diff --git a/src/EventStore.Client.Streams/MaximumAppendSizeExceededException.cs b/src/Kurrent.Client/Streams/MaximumAppendSizeExceededException.cs similarity index 100% rename from src/EventStore.Client.Streams/MaximumAppendSizeExceededException.cs rename to src/Kurrent.Client/Streams/MaximumAppendSizeExceededException.cs diff --git a/src/EventStore.Client.Streams/ReadState.cs b/src/Kurrent.Client/Streams/ReadState.cs similarity index 100% rename from src/EventStore.Client.Streams/ReadState.cs rename to src/Kurrent.Client/Streams/ReadState.cs diff --git a/src/EventStore.Client.Streams/StreamAcl.cs b/src/Kurrent.Client/Streams/StreamAcl.cs similarity index 100% rename from src/EventStore.Client.Streams/StreamAcl.cs rename to src/Kurrent.Client/Streams/StreamAcl.cs diff --git a/src/EventStore.Client.Streams/StreamAclJsonConverter.cs b/src/Kurrent.Client/Streams/StreamAclJsonConverter.cs similarity index 100% rename from src/EventStore.Client.Streams/StreamAclJsonConverter.cs rename to src/Kurrent.Client/Streams/StreamAclJsonConverter.cs diff --git a/src/EventStore.Client.Streams/StreamMessage.cs b/src/Kurrent.Client/Streams/StreamMessage.cs similarity index 57% rename from src/EventStore.Client.Streams/StreamMessage.cs rename to src/Kurrent.Client/Streams/StreamMessage.cs index 4f1a87559..54aaffad9 100644 --- a/src/EventStore.Client.Streams/StreamMessage.cs +++ b/src/Kurrent.Client/Streams/StreamMessage.cs @@ -4,9 +4,9 @@ namespace EventStore.Client { /// public abstract record StreamMessage { /// - /// A that represents a . + /// A that represents a . /// - /// The . + /// The . public record Event(ResolvedEvent ResolvedEvent) : StreamMessage; /// @@ -24,57 +24,57 @@ public record Ok : StreamMessage { }; /// - /// A indicating the first position of a stream. + /// A indicating the first position of a stream. /// - /// The . + /// The . public record FirstStreamPosition(StreamPosition StreamPosition) : StreamMessage; /// - /// A indicating the last position of a stream. + /// A indicating the last position of a stream. /// - /// The . + /// The . public record LastStreamPosition(StreamPosition StreamPosition) : StreamMessage; /// - /// A indicating the last position of the $all stream. + /// A indicating the last position of the $all stream. /// - /// The . + /// The . public record LastAllStreamPosition(Position Position) : StreamMessage; /// - /// A indicating that the subscription is ready to send additional messages. + /// A indicating that the subscription is ready to send additional messages. /// /// The unique identifier of the subscription. public record SubscriptionConfirmation(string SubscriptionId) : StreamMessage; /// - /// A indicating that a checkpoint has been reached. + /// A indicating that a checkpoint has been reached. /// /// The . public record AllStreamCheckpointReached(Position Position) : StreamMessage; /// - /// A indicating that a checkpoint has been reached. + /// A indicating that a checkpoint has been reached. /// /// The . public record StreamCheckpointReached(StreamPosition StreamPosition) : StreamMessage; /// - /// A indicating that the subscription is live. + /// A indicating that the subscription is live. /// public record CaughtUp : StreamMessage { internal static readonly CaughtUp Instance = new(); } /// - /// A indicating that the subscription has switched to catch up mode. + /// A indicating that the subscription has switched to catch up mode. /// public record FellBehind : StreamMessage { internal static readonly FellBehind Instance = new(); } /// - /// A that could not be identified, usually indicating a lower client compatibility level than the server supports. + /// A that could not be identified, usually indicating a lower client compatibility level than the server supports. /// public record Unknown : StreamMessage { internal static readonly Unknown Instance = new(); diff --git a/src/EventStore.Client.Streams/StreamMetadata.cs b/src/Kurrent.Client/Streams/StreamMetadata.cs similarity index 100% rename from src/EventStore.Client.Streams/StreamMetadata.cs rename to src/Kurrent.Client/Streams/StreamMetadata.cs diff --git a/src/EventStore.Client.Streams/StreamMetadataJsonConverter.cs b/src/Kurrent.Client/Streams/StreamMetadataJsonConverter.cs similarity index 100% rename from src/EventStore.Client.Streams/StreamMetadataJsonConverter.cs rename to src/Kurrent.Client/Streams/StreamMetadataJsonConverter.cs diff --git a/src/EventStore.Client.Streams/StreamMetadataResult.cs b/src/Kurrent.Client/Streams/StreamMetadataResult.cs similarity index 100% rename from src/EventStore.Client.Streams/StreamMetadataResult.cs rename to src/Kurrent.Client/Streams/StreamMetadataResult.cs diff --git a/src/EventStore.Client.Streams/StreamSubscription.cs b/src/Kurrent.Client/Streams/StreamSubscription.cs similarity index 82% rename from src/EventStore.Client.Streams/StreamSubscription.cs rename to src/Kurrent.Client/Streams/StreamSubscription.cs index f70080c17..0b8f2bc9d 100644 --- a/src/EventStore.Client.Streams/StreamSubscription.cs +++ b/src/Kurrent.Client/Streams/StreamSubscription.cs @@ -6,7 +6,7 @@ namespace EventStore.Client { /// A class representing a . /// public class StreamSubscription : IDisposable { - private readonly EventStoreClient.StreamSubscriptionResult _subscription; + private readonly KurrentClient.StreamSubscriptionResult _subscription; private readonly IAsyncEnumerator _messages; private readonly Func _eventAppeared; private readonly Func _checkpointReached; @@ -21,11 +21,9 @@ public class StreamSubscription : IDisposable { public string SubscriptionId { get; } internal static async Task Confirm( - EventStoreClient.StreamSubscriptionResult subscription, - Func eventAppeared, - Action? subscriptionDropped, + KurrentClient.StreamSubscriptionResult subscription, + SubscriptionListener subscriptionListener, ILogger log, - Func? checkpointReached = null, CancellationToken cancellationToken = default ) { var messages = subscription.Messages; @@ -40,29 +38,26 @@ enumerator.Current is not StreamMessage.SubscriptionConfirmation(var subscriptio subscription, enumerator, subscriptionId, - eventAppeared, - subscriptionDropped, + subscriptionListener, log, - checkpointReached, cancellationToken ); } private StreamSubscription( - EventStoreClient.StreamSubscriptionResult subscription, - IAsyncEnumerator messages, string subscriptionId, - Func eventAppeared, - Action? subscriptionDropped, + KurrentClient.StreamSubscriptionResult subscription, + IAsyncEnumerator messages, + string subscriptionId, + SubscriptionListener subscriptionListener, ILogger log, - Func? checkpointReached, CancellationToken cancellationToken = default ) { _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _subscription = subscription; _messages = messages; - _eventAppeared = eventAppeared; - _checkpointReached = checkpointReached ?? ((_, _, _) => Task.CompletedTask); - _subscriptionDropped = subscriptionDropped; + _eventAppeared = subscriptionListener.EventAppeared; + _checkpointReached = subscriptionListener.CheckpointReached ?? ((_, _, _) => Task.CompletedTask); + _subscriptionDropped = subscriptionListener.SubscriptionDropped; _log = log; _subscriptionDroppedInvoked = 0; SubscriptionId = subscriptionId; diff --git a/src/EventStore.Client.Streams/Streams/AppendReq.cs b/src/Kurrent.Client/Streams/Streams/AppendReq.cs similarity index 100% rename from src/EventStore.Client.Streams/Streams/AppendReq.cs rename to src/Kurrent.Client/Streams/Streams/AppendReq.cs diff --git a/src/EventStore.Client.Streams/Streams/BatchAppendReq.cs b/src/Kurrent.Client/Streams/Streams/BatchAppendReq.cs similarity index 100% rename from src/EventStore.Client.Streams/Streams/BatchAppendReq.cs rename to src/Kurrent.Client/Streams/Streams/BatchAppendReq.cs diff --git a/src/EventStore.Client.Streams/Streams/BatchAppendResp.cs b/src/Kurrent.Client/Streams/Streams/BatchAppendResp.cs similarity index 98% rename from src/EventStore.Client.Streams/Streams/BatchAppendResp.cs rename to src/Kurrent.Client/Streams/Streams/BatchAppendResp.cs index 926dcee25..4be031057 100644 --- a/src/EventStore.Client.Streams/Streams/BatchAppendResp.cs +++ b/src/Kurrent.Client/Streams/Streams/BatchAppendResp.cs @@ -1,5 +1,5 @@ -using System; using Grpc.Core; +using EventStore.Client; using static EventStore.Client.WrongExpectedVersion.CurrentStreamRevisionOptionOneofCase; using static EventStore.Client.WrongExpectedVersion.ExpectedStreamPositionOptionOneofCase; diff --git a/src/EventStore.Client.Streams/Streams/DeleteReq.cs b/src/Kurrent.Client/Streams/Streams/DeleteReq.cs similarity index 100% rename from src/EventStore.Client.Streams/Streams/DeleteReq.cs rename to src/Kurrent.Client/Streams/Streams/DeleteReq.cs diff --git a/src/EventStore.Client.Streams/Streams/ReadReq.cs b/src/Kurrent.Client/Streams/Streams/ReadReq.cs similarity index 100% rename from src/EventStore.Client.Streams/Streams/ReadReq.cs rename to src/Kurrent.Client/Streams/Streams/ReadReq.cs diff --git a/src/EventStore.Client.Streams/Streams/TombstoneReq.cs b/src/Kurrent.Client/Streams/Streams/TombstoneReq.cs similarity index 100% rename from src/EventStore.Client.Streams/Streams/TombstoneReq.cs rename to src/Kurrent.Client/Streams/Streams/TombstoneReq.cs diff --git a/src/EventStore.Client.Streams/SubscriptionFilterOptions.cs b/src/Kurrent.Client/Streams/SubscriptionFilterOptions.cs similarity index 100% rename from src/EventStore.Client.Streams/SubscriptionFilterOptions.cs rename to src/Kurrent.Client/Streams/SubscriptionFilterOptions.cs diff --git a/src/EventStore.Client.Streams/SuccessResult.cs b/src/Kurrent.Client/Streams/SuccessResult.cs similarity index 100% rename from src/EventStore.Client.Streams/SuccessResult.cs rename to src/Kurrent.Client/Streams/SuccessResult.cs diff --git a/src/EventStore.Client.Streams/SystemEventTypes.cs b/src/Kurrent.Client/Streams/SystemEventTypes.cs similarity index 100% rename from src/EventStore.Client.Streams/SystemEventTypes.cs rename to src/Kurrent.Client/Streams/SystemEventTypes.cs diff --git a/src/EventStore.Client.Streams/SystemMetadata.cs b/src/Kurrent.Client/Streams/SystemMetadata.cs similarity index 100% rename from src/EventStore.Client.Streams/SystemMetadata.cs rename to src/Kurrent.Client/Streams/SystemMetadata.cs diff --git a/src/EventStore.Client.Streams/SystemSettings.cs b/src/Kurrent.Client/Streams/SystemSettings.cs similarity index 100% rename from src/EventStore.Client.Streams/SystemSettings.cs rename to src/Kurrent.Client/Streams/SystemSettings.cs diff --git a/src/EventStore.Client.Streams/SystemSettingsJsonConverter.cs b/src/Kurrent.Client/Streams/SystemSettingsJsonConverter.cs similarity index 100% rename from src/EventStore.Client.Streams/SystemSettingsJsonConverter.cs rename to src/Kurrent.Client/Streams/SystemSettingsJsonConverter.cs diff --git a/src/EventStore.Client.Streams/WriteResultExtensions.cs b/src/Kurrent.Client/Streams/WriteResultExtensions.cs similarity index 91% rename from src/EventStore.Client.Streams/WriteResultExtensions.cs rename to src/Kurrent.Client/Streams/WriteResultExtensions.cs index ed83c532e..7b5005e02 100644 --- a/src/EventStore.Client.Streams/WriteResultExtensions.cs +++ b/src/Kurrent.Client/Streams/WriteResultExtensions.cs @@ -1,7 +1,7 @@ namespace EventStore.Client { internal static class WriteResultExtensions { public static IWriteResult OptionallyThrowWrongExpectedVersionException(this IWriteResult writeResult, - EventStoreClientOperationOptions options) => + KurrentClientOperationOptions options) => (options.ThrowOnAppendFailure, writeResult) switch { (true, WrongExpectedVersionResult wrongExpectedVersionResult) => throw new WrongExpectedVersionException(wrongExpectedVersionResult.StreamName, diff --git a/src/EventStore.Client.Streams/WrongExpectedVersionResult.cs b/src/Kurrent.Client/Streams/WrongExpectedVersionResult.cs similarity index 100% rename from src/EventStore.Client.Streams/WrongExpectedVersionResult.cs rename to src/Kurrent.Client/Streams/WrongExpectedVersionResult.cs diff --git a/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs b/src/Kurrent.Client/UserManagement/KurrentUserManagementClient.cs similarity index 90% rename from src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs rename to src/Kurrent.Client/UserManagement/KurrentUserManagementClient.cs index 6b86e81b4..ce18d40a0 100644 --- a/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs +++ b/src/Kurrent.Client/UserManagement/KurrentUserManagementClient.cs @@ -8,17 +8,17 @@ namespace EventStore.Client { /// /// The client used for operations on internal users. /// - public sealed class EventStoreUserManagementClient : EventStoreClientBase { + public sealed class KurrentUserManagementClient : KurrentClientBase { private readonly ILogger _log; /// - /// Constructs a new . + /// Constructs a new . /// /// - public EventStoreUserManagementClient(EventStoreClientSettings? settings = null) : + public KurrentUserManagementClient(KurrentClientSettings? settings = null) : base(settings, ExceptionMap) { - _log = Settings.LoggerFactory?.CreateLogger() ?? - new NullLogger(); + _log = Settings.LoggerFactory?.CreateLogger() ?? + new NullLogger(); } /// @@ -54,7 +54,7 @@ public async Task CreateUserAsync(string loginName, string fullName, string[] gr Password = password, Groups = {groups} } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -84,7 +84,7 @@ public async Task GetUserAsync(string loginName, TimeSpan? deadline Options = new DetailsReq.Types.Options { LoginName = loginName } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseStream.MoveNext().ConfigureAwait(false); var userDetails = call.ResponseStream.Current.UserDetails; @@ -121,7 +121,7 @@ public async Task DeleteUserAsync(string loginName, TimeSpan? deadline = null, Options = new DeleteReq.Types.Options { LoginName = loginName } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -151,7 +151,7 @@ public async Task EnableUserAsync(string loginName, TimeSpan? deadline = null, Options = new EnableReq.Types.Options { LoginName = loginName } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -174,7 +174,7 @@ public async Task DisableUserAsync(string loginName, TimeSpan? deadline = null, Options = new DisableReq.Types.Options { LoginName = loginName } - }, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + }, KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -191,7 +191,7 @@ public async IAsyncEnumerable ListAllAsync(TimeSpan? deadline = nul var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Users.Users.UsersClient( channelInfo.CallInvoker).Details(new DetailsReq(), - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await foreach (var userDetail in call.ResponseStream .ReadAllAsync(cancellationToken) @@ -234,7 +234,7 @@ public async Task ChangePasswordAsync(string loginName, string currentPassword, LoginName = loginName } }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } @@ -266,7 +266,7 @@ public async Task ResetPasswordAsync(string loginName, string newPassword, LoginName = loginName } }, - EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); + KurrentCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); await call.ResponseAsync.ConfigureAwait(false); } diff --git a/src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs b/src/Kurrent.Client/UserManagement/KurrentUserManagementClientCollectionExtensions.cs similarity index 53% rename from src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs rename to src/Kurrent.Client/UserManagement/KurrentUserManagementClientCollectionExtensions.cs index 2b25f816c..220f99740 100644 --- a/src/EventStore.Client.UserManagement/EventStoreUserManagementClientCollectionExtensions.cs +++ b/src/Kurrent.Client/UserManagement/KurrentUserManagementClientCollectionExtensions.cs @@ -8,51 +8,51 @@ namespace Microsoft.Extensions.DependencyInjection { /// - /// A set of extension methods for which provide support for an . + /// A set of extension methods for which provide support for an . /// - public static class EventStoreUserManagementClientCollectionExtensions { + public static class KurrentUserManagementClientCollectionExtensions { /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// /// - public static IServiceCollection AddEventStoreUserManagementClient(this IServiceCollection services, + public static IServiceCollection AddKurrentUserManagementClient(this IServiceCollection services, Uri address, Func? createHttpMessageHandler = null) - => services.AddEventStoreUserManagementClient(options => { + => services.AddKurrentUserManagementClient(options => { options.ConnectivitySettings.Address = address; options.CreateHttpMessageHandler = createHttpMessageHandler; }); /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// /// - public static IServiceCollection AddEventStoreUserManagementClient(this IServiceCollection services, - string connectionString, Action? configureSettings = null) - => services.AddEventStoreUserManagementClient(EventStoreClientSettings.Create(connectionString), + public static IServiceCollection AddKurrentUserManagementClient(this IServiceCollection services, + string connectionString, Action? configureSettings = null) + => services.AddKurrentUserManagementClient(KurrentClientSettings.Create(connectionString), configureSettings); /// - /// Adds an to the . + /// Adds an to the . /// /// /// /// /// - public static IServiceCollection AddEventStoreUserManagementClient(this IServiceCollection services, - Action? configureSettings = null) => - services.AddEventStoreUserManagementClient(new EventStoreClientSettings(), configureSettings); + public static IServiceCollection AddKurrentUserManagementClient(this IServiceCollection services, + Action? configureSettings = null) => + services.AddKurrentUserManagementClient(new KurrentClientSettings(), configureSettings); - private static IServiceCollection AddEventStoreUserManagementClient(this IServiceCollection services, - EventStoreClientSettings settings, Action? configureSettings = null) { + private static IServiceCollection AddKurrentUserManagementClient(this IServiceCollection services, + KurrentClientSettings settings, Action? configureSettings = null) { configureSettings?.Invoke(settings); if (services == null) { throw new ArgumentNullException(nameof(services)); @@ -62,7 +62,7 @@ private static IServiceCollection AddEventStoreUserManagementClient(this IServic settings.LoggerFactory ??= provider.GetService(); settings.Interceptors ??= provider.GetServices(); - return new EventStoreUserManagementClient(settings); + return new KurrentUserManagementClient(settings); }); return services; diff --git a/src/EventStore.Client.UserManagement/EventStoreUserManagerClientExtensions.cs b/src/Kurrent.Client/UserManagement/KurrentUserManagerClientExtensions.cs similarity index 75% rename from src/EventStore.Client.UserManagement/EventStoreUserManagerClientExtensions.cs rename to src/Kurrent.Client/UserManagement/KurrentUserManagerClientExtensions.cs index a43ae43d7..be365fdd2 100644 --- a/src/EventStore.Client.UserManagement/EventStoreUserManagerClientExtensions.cs +++ b/src/Kurrent.Client/UserManagement/KurrentUserManagerClientExtensions.cs @@ -1,9 +1,9 @@ -namespace EventStore.Client; +namespace EventStore.Client; /// -/// A set of extension methods for an . +/// A set of extension methods for an . /// -public static class EventStoreUserManagerClientExtensions { +public static class KurrentUserManagerClientExtensions { /// /// Gets the of the internal user specified by the supplied . /// @@ -13,11 +13,11 @@ public static class EventStoreUserManagerClientExtensions { /// /// public static Task GetCurrentUserAsync( - this EventStoreUserManagementClient users, + this KurrentUserManagementClient users, UserCredentials userCredentials, TimeSpan? deadline = null, CancellationToken cancellationToken = default ) => users.GetUserAsync( userCredentials.Username!, deadline, userCredentials, cancellationToken ); -} \ No newline at end of file +} diff --git a/src/EventStore.Client.UserManagement/UserDetails.cs b/src/Kurrent.Client/UserManagement/UserDetails.cs similarity index 99% rename from src/EventStore.Client.UserManagement/UserDetails.cs rename to src/Kurrent.Client/UserManagement/UserDetails.cs index 92c345eea..b7414dd36 100644 --- a/src/EventStore.Client.UserManagement/UserDetails.cs +++ b/src/Kurrent.Client/UserManagement/UserDetails.cs @@ -1,4 +1,4 @@ -namespace EventStore.Client; +namespace EventStore.Client; /// /// Provides the details for a user. @@ -94,4 +94,4 @@ public override string ToString() => LoginName, Groups = string.Join(",", Groups) }?.ToString()!; -} \ No newline at end of file +} diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 151f65d09..6630ffb1b 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -3,38 +3,42 @@ true - xUnit1031 + xUnit1031;NU1903 + false + false - - - + + + - - + + all runtime; build; native; contentfiles; analyzers + + - + - + - + diff --git a/test/EventStore.Client.Operations.Tests/AssemblyInfo.cs b/test/EventStore.Client.Operations.Tests/AssemblyInfo.cs deleted file mode 100644 index b0b47aa73..000000000 --- a/test/EventStore.Client.Operations.Tests/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj b/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj deleted file mode 100644 index d43bdceb5..000000000 --- a/test/EventStore.Client.Operations.Tests/EventStore.Client.Operations.Tests.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/test/EventStore.Client.Operations.Tests/MergeIndexesTests.cs b/test/EventStore.Client.Operations.Tests/MergeIndexesTests.cs deleted file mode 100644 index c4cf7bfb0..000000000 --- a/test/EventStore.Client.Operations.Tests/MergeIndexesTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace EventStore.Client.Operations.Tests; - -public class MergeIndexesTests : IClassFixture { - public MergeIndexesTests(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] - public async Task merge_indexes_does_not_throw() => - await Fixture.Operations - .MergeIndexesAsync(userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - - [Fact] - public async Task merge_indexes_without_credentials_throws() => - await Fixture.Operations - .MergeIndexesAsync() - .ShouldThrowAsync(); -} \ No newline at end of file diff --git a/test/EventStore.Client.Operations.Tests/ResignNodeTests.cs b/test/EventStore.Client.Operations.Tests/ResignNodeTests.cs deleted file mode 100644 index 6012ae943..000000000 --- a/test/EventStore.Client.Operations.Tests/ResignNodeTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace EventStore.Client.Operations.Tests; - -public class ResignNodeTests : IClassFixture { - public ResignNodeTests(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] - public async Task resign_node_does_not_throw() => - await Fixture.Operations - .ResignNodeAsync(userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - - [Fact] - public async Task resign_node_without_credentials_throws() => - await Fixture.Operations - .ResignNodeAsync() - .ShouldThrowAsync(); -} \ No newline at end of file diff --git a/test/EventStore.Client.Operations.Tests/RestartPersistentSubscriptionsTests.cs b/test/EventStore.Client.Operations.Tests/RestartPersistentSubscriptionsTests.cs deleted file mode 100644 index 1d04d1ca4..000000000 --- a/test/EventStore.Client.Operations.Tests/RestartPersistentSubscriptionsTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace EventStore.Client.Operations.Tests; - -public class RestartPersistentSubscriptionsTests : IClassFixture { - public RestartPersistentSubscriptionsTests(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] - public async Task restart_persistent_subscriptions_does_not_throw() => - await Fixture.Operations - .RestartPersistentSubscriptions(userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - - [Fact] - public async Task restart_persistent_subscriptions_without_credentials_throws() => - await Fixture.Operations - .RestartPersistentSubscriptions() - .ShouldThrowAsync(); -} \ No newline at end of file diff --git a/test/EventStore.Client.Operations.Tests/ShutdownNodeAuthenticationTests.cs b/test/EventStore.Client.Operations.Tests/ShutdownNodeAuthenticationTests.cs deleted file mode 100644 index 4219f616d..000000000 --- a/test/EventStore.Client.Operations.Tests/ShutdownNodeAuthenticationTests.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace EventStore.Client.Operations.Tests; - -public class ShutdownNodeAuthenticationTests : IClassFixture { - public ShutdownNodeAuthenticationTests(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] - public async Task shutdown_without_credentials_throws() => - await Fixture.Operations.ShutdownAsync().ShouldThrowAsync(); -} \ No newline at end of file diff --git a/test/EventStore.Client.Operations.Tests/ShutdownNodeTests.cs b/test/EventStore.Client.Operations.Tests/ShutdownNodeTests.cs deleted file mode 100644 index 4c73f52fa..000000000 --- a/test/EventStore.Client.Operations.Tests/ShutdownNodeTests.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace EventStore.Client.Operations.Tests; - -public class ShutdownNodeTests : IClassFixture { - public ShutdownNodeTests(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } - - [Fact] - public async Task shutdown_does_not_throw() => - await Fixture.Operations.ShutdownAsync(userCredentials: TestCredentials.Root).ShouldNotThrowAsync(); -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/AssemblyInfo.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/AssemblyInfo.cs deleted file mode 100644 index b0b47aa73..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs deleted file mode 100644 index c94429ca3..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/Bugs/Issue_1125.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.Bugs; - -public class Issue_1125 : IClassFixture { - readonly Fixture _fixture; - - public Issue_1125(Fixture fixture) => _fixture = fixture; - - public static IEnumerable TestCases() => Enumerable.Range(0, 50).Select(i => new object[] { i }); - - [Theory] - [MemberData(nameof(TestCases))] - public async Task persistent_subscription_delivers_all_events(int iteration) { - const int eventCount = 250; - const int totalEvents = eventCount * 2; - - var hitCount = 0; - - var userCredentials = new UserCredentials("admin", "changeit"); - - var streamName = $"stream_{iteration}"; - var subscriptionName = $"subscription_{iteration}"; - - for (var i = 0; i < eventCount; i++) - await _fixture.StreamsClient.AppendToStreamAsync( - streamName, - StreamState.Any, - _fixture.CreateTestEvents() - ); - - await _fixture.Client.CreateToStreamAsync( - streamName, - subscriptionName, - new( - true, - StreamPosition.Start, - readBatchSize: 10, - historyBufferSize: 20 - ), - userCredentials: userCredentials - ); - - await using var subscription = - _fixture.Client.SubscribeToStream(streamName, subscriptionName, userCredentials: userCredentials); - - await Task.WhenAll(Subscribe(), Append()).WithTimeout(); - - Assert.Equal(totalEvents, hitCount); - - return; - - async Task Subscribe() { - await foreach (var message in subscription.Messages) { - if (message is not PersistentSubscriptionMessage.Event(var resolvedEvent, var retryCount)) { - continue; - } - - if (retryCount is 0 or null) { - var result = Interlocked.Increment(ref hitCount); - - await subscription.Ack(resolvedEvent); - - if (totalEvents == result) - return; - } else { - // This is a retry - await subscription.Ack(resolvedEvent); - } - } - } - - async Task Append() { - for (var i = 0; i < eventCount; i++) - await _fixture.StreamsClient.AppendToStreamAsync( - streamName, - StreamState.Any, - _fixture.CreateTestEvents()); - } - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/Diagnostics/PersistentSubscriptionsTracingInstrumentationTests.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/Diagnostics/PersistentSubscriptionsTracingInstrumentationTests.cs deleted file mode 100644 index 904f74f70..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/Diagnostics/PersistentSubscriptionsTracingInstrumentationTests.cs +++ /dev/null @@ -1,155 +0,0 @@ -using EventStore.Diagnostics.Tracing; - -namespace EventStore.Client.PersistentSubscriptions.Tests.Diagnostics; - -[Trait("Category", "Diagnostics:Tracing")] -public class PersistentSubscriptionsTracingInstrumentationTests(ITestOutputHelper output, DiagnosticsFixture fixture) - : EventStoreTests(output, fixture) { - [Fact] - public async Task PersistentSubscriptionIsInstrumentedWithTracingAndRestoresRemoteAppendContextAsExpected() { - var stream = Fixture.GetStreamName(); - var events = Fixture.CreateTestEvents(2, metadata: Fixture.CreateTestJsonMetadata()).ToArray(); - - var groupName = $"{stream}-group"; - await Fixture.Subscriptions.CreateToStreamAsync( - stream, - groupName, - new() - ); - - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - events - ); - - string? subscriptionId = null; - await Subscribe().WithTimeout(); - - var appendActivity = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Append, stream) - .SingleOrDefault() - .ShouldNotBeNull(); - - var subscribeActivities = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Subscribe, stream) - .ToArray(); - - subscriptionId.ShouldNotBeNull(); - subscribeActivities.Length.ShouldBe(events.Length); - - for (var i = 0; i < subscribeActivities.Length; i++) { - subscribeActivities[i].TraceId.ShouldBe(appendActivity.Context.TraceId); - subscribeActivities[i].ParentSpanId.ShouldBe(appendActivity.Context.SpanId); - subscribeActivities[i].HasRemoteParent.ShouldBeTrue(); - - Fixture.AssertSubscriptionActivityHasExpectedTags( - subscribeActivities[i], - stream, - events[i].EventId.ToString(), - subscriptionId - ); - } - - return; - - async Task Subscribe() { - await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, groupName); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - int eventsAppeared = 0; - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is PersistentSubscriptionMessage.SubscriptionConfirmation(var sid)) - subscriptionId = sid; - - if (enumerator.Current is not PersistentSubscriptionMessage.Event(_, _)) - continue; - - eventsAppeared++; - if (eventsAppeared >= events.Length) - return; - } - } - } - - [Fact] - public async Task PersistentSubscriptionDoesNotThrowWhenInstrumentedWithTracingAndReceivesNonJsonEvents() { - var stream = Fixture.GetStreamName(); - var events = Fixture.CreateTestEvents( - 2, - metadata: Fixture.CreateTestJsonMetadata(), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - ).ToArray(); - - var groupName = $"{stream}-group"; - await Fixture.Subscriptions.CreateToStreamAsync( - stream, - groupName, - new() - ); - - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - events - ); - - await Subscribe().WithTimeout(); - - return; - - async Task Subscribe() { - await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, groupName); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - var eventsAppeared = 0; - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is PersistentSubscriptionMessage.Event(_, _)) - eventsAppeared++; - - if (eventsAppeared >= events.Length) - return; - } - } - } - - [Fact] - public async Task PersistentSubscriptionDoesNotThrowWhenInstrumentedWithTracingAndReceivesEventsWithInvalidJsonMetadata() { - var stream = Fixture.GetStreamName(); - var events = Fixture.CreateTestEvents( - 2, - metadata: "clearlynotavalidjsonobject"u8.ToArray() - ).ToArray(); - - var groupName = $"{stream}-group"; - await Fixture.Subscriptions.CreateToStreamAsync( - stream, - groupName, - new() - ); - - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - events - ); - - await Subscribe().WithTimeout(); - - return; - - async Task Subscribe() { - await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, groupName); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - var eventsAppeared = 0; - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is PersistentSubscriptionMessage.Event(_, _)) - eventsAppeared++; - - if (eventsAppeared >= events.Length) - return; - } - } - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj b/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj deleted file mode 100644 index 04cf6634b..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj.DotSettings b/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj.DotSettings deleted file mode 100644 index a456beab9..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStore.Client.PersistentSubscriptions.Tests.csproj.DotSettings +++ /dev/null @@ -1,4 +0,0 @@ - - False - False - False \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStoreClientFixture.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/EventStoreClientFixture.cs deleted file mode 100644 index babe29328..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/EventStoreClientFixture.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests; - -public abstract class EventStoreClientFixture : EventStoreClientFixtureBase { - readonly bool _skipPsWarmUp; - - protected EventStoreClientFixture(EventStoreClientSettings? settings = null, bool skipPSWarmUp = false, bool noDefaultCredentials = false) - : base(settings, noDefaultCredentials: noDefaultCredentials) { - _skipPsWarmUp = skipPSWarmUp; - - Client = new(Settings); - StreamsClient = new(Settings); - UserManagementClient = new(Settings); - } - - public EventStorePersistentSubscriptionsClient Client { get; } - public EventStoreClient StreamsClient { get; } - public EventStoreUserManagementClient UserManagementClient { get; } - - protected override async Task OnServerUpAsync() { - await StreamsClient.WarmUp(); - await UserManagementClient.WarmUp(); - - if (!_skipPsWarmUp) - await Client.WarmUp(); - - await UserManagementClient.CreateUserWithRetry( - TestCredentials.TestUser1.Username!, - TestCredentials.TestUser1.Username!, - Array.Empty(), - TestCredentials.TestUser1.Password!, - TestCredentials.Root - ); - } - - public override async Task DisposeAsync() { - await UserManagementClient.DisposeAsync(); - await StreamsClient.DisposeAsync(); - await Client.DisposeAsync(); - await base.DisposeAsync(); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/PersistentSubscriptionSettingsTests.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/PersistentSubscriptionSettingsTests.cs deleted file mode 100644 index de32c4160..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/PersistentSubscriptionSettingsTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests; - -public class PersistentSubscriptionSettingsTests { - [Fact] - public void LargeCheckpointAfterThrows() => - Assert.Throws(() => new PersistentSubscriptionSettings(checkPointAfter: TimeSpan.FromDays(25 * 365))); - - [Fact] - public void LargeMessageTimeoutThrows() => - Assert.Throws(() => new PersistentSubscriptionSettings(messageTimeout: TimeSpan.FromDays(25 * 365))); -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs deleted file mode 100644 index 3c39e3598..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_max_one_client_obsolete : IClassFixture { - const string Group = "maxoneclient"; - - readonly Fixture _fixture; - - public connect_to_existing_with_max_one_client_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_second_subscription_fails_to_connect() { - using var first = await _fixture.Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ).WithTimeout(); - - var ex = await Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ); - } - ).WithTimeout(); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToAllAsync( - Group, - new(maxSubscriberCount: 1), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_permissions_obsolete.cs deleted file mode 100644 index 890dacf49..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_permissions_obsolete.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_permissions_obsolete - : IClassFixture { - const string Group = "connectwithpermissions"; - - readonly Fixture _fixture; - - public connect_to_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_succeeds() { - var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>(); - using var subscription = await _fixture.Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - (s, reason, ex) => dropped.TrySetResult((reason, ex)), - TestCredentials.Root - ).WithTimeout(); - - Assert.NotNull(subscription); - - await Assert.ThrowsAsync(() => dropped.Task.WithTimeout()); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_beginning_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_beginning_obsolete.cs deleted file mode 100644 index 10b06a965..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_beginning_obsolete.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_beginning_obsolete : IClassFixture { - const string Group = "startfrombeginning"; - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_beginning_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_event_zero_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(_fixture.Events![0].Event.EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - - PersistentSubscription? _subscription; - - public Fixture() => _firstEventSource = new(); - - public ResolvedEvent[]? Events { get; set; } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - //append 10 events to random streams to make sure we have at least 10 events in the transaction file - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, new[] { @event }); - - Events = await StreamsClient.ReadAllAsync( - Direction.Forwards, - Position.Start, - 10, - userCredentials: TestCredentials.Root - ).ToArrayAsync(); - - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_obsolete.cs deleted file mode 100644 index 5446e1eef..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_obsolete.cs +++ /dev/null @@ -1,64 +0,0 @@ - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_not_set_obsolete : IClassFixture { - const string Group = "startfromend1"; - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_not_set_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_no_non_system_events() => - await Assert.ThrowsAsync(() => _fixture.FirstNonSystemEvent.WithTimeout()); - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstNonSystemEventSource; - PersistentSubscription? _subscription; - - public Fixture() => _firstNonSystemEventSource = new(); - - public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync( - "non-system-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { - @event - } - ); - - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { - await subscription.Ack(e); - return; - } - - _firstNonSystemEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstNonSystemEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs deleted file mode 100644 index 1ee2b6fae..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_not_set_then_event_written_obsolete.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_not_set_then_event_written_obsolete - : IClassFixture { - const string Group = "startfromnotset2"; - - readonly Fixture _fixture; - - public - connect_to_existing_with_start_from_not_set_then_event_written_obsolete(Fixture fixture) => - _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event2() { - var resolvedEvent = await _fixture.FirstNonSystemEvent.WithTimeout(); - Assert.Equal(_fixture.ExpectedEvent!.EventId, resolvedEvent.Event.EventId); - Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstNonSystemEventSource; - - public readonly EventData? ExpectedEvent; - public readonly string ExpectedStreamId; - - PersistentSubscription? _subscription; - - public Fixture() { - _firstNonSystemEventSource = new(); - ExpectedEvent = CreateTestEvents(1).First(); - ExpectedStreamId = Guid.NewGuid().ToString(); - } - - public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync( - "non-system-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { @event } - ); - - await Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { - await subscription.Ack(e); - return; - } - - _firstNonSystemEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstNonSystemEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - } - - protected override async Task When() => await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent! }); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs deleted file mode 100644 index 7057b1489..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_obsolete.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_set_to_end_position_obsolete: IClassFixture { - const string Group = "startfromend1"; - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_no_non_system_events() => - await Assert.ThrowsAsync(() => _fixture.FirstNonSystemEvent.WithTimeout()); - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstNonSystemEventSource; - - PersistentSubscription? _subscription; - - public Fixture() => _firstNonSystemEventSource = new(); - - public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync( - "non-system-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { @event } - ); - - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.End), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { - await subscription.Ack(e); - return; - } - - _firstNonSystemEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstNonSystemEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs deleted file mode 100644 index 7083cb78f..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class - connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete - : IClassFixture { - const string Group = "startfromnotset2"; - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position_then_event_written_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event() { - var resolvedEvent = await _fixture.FirstNonSystemEvent.WithTimeout(); - Assert.Equal(_fixture.ExpectedEvent.EventId, resolvedEvent.Event.EventId); - Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstNonSystemEventSource; - public readonly EventData ExpectedEvent; - public readonly string ExpectedStreamId; - PersistentSubscription? _subscription; - - public Fixture() { - _firstNonSystemEventSource = new(); - ExpectedEvent = CreateTestEvents(1).First(); - ExpectedStreamId = Guid.NewGuid().ToString(); - } - - public Task FirstNonSystemEvent => _firstNonSystemEventSource.Task; - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync( - "non-system-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { @event } - ); - - await Client.CreateToAllAsync(Group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { - await subscription.Ack(e); - return; - } - - _firstNonSystemEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstNonSystemEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - } - - protected override async Task When() => await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent }); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs deleted file mode 100644 index f1d317b76..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete - : IClassFixture { - const string Group = "startfrominvalid1"; - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_invalid_middle_position_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_is_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - Assert.IsType(exception); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped; - - PersistentSubscription? _subscription; - - public Fixture() => _dropped = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task; - - protected override async Task Given() { - var invalidPosition = new Position(1L, 1L); - await Client.CreateToAllAsync( - Group, - new(startFrom: invalidPosition), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => await subscription.Ack(e), - (subscription, reason, ex) => { _dropped.TrySetResult((reason, ex)); }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs deleted file mode 100644 index 69230d695..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete - : IClassFixture { - const string Group = "startfromvalid"; - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_valid_middle_position_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_the_event_at_the_specified_start_position_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(_fixture.ExpectedEvent.OriginalPosition, resolvedEvent.Event.Position); - Assert.Equal(_fixture.ExpectedEvent.Event.EventId, resolvedEvent.Event.EventId); - Assert.Equal(_fixture.ExpectedEvent.Event.EventStreamId, resolvedEvent.Event.EventStreamId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - PersistentSubscription? _subscription; - - public Fixture() => _firstEventSource = new(); - - public Task FirstEvent => _firstEventSource.Task; - public ResolvedEvent ExpectedEvent { get; private set; } - - protected override async Task Given() { - var events = await StreamsClient.ReadAllAsync( - Direction.Forwards, - Position.Start, - 10, - userCredentials: TestCredentials.Root - ).ToArrayAsync(); - - ExpectedEvent = events[events.Length / 2]; //just a random event in the middle of the results - - await Client.CreateToAllAsync( - Group, - new(startFrom: ExpectedEvent.OriginalPosition), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_permissions_obsolete.cs deleted file mode 100644 index b09dbc46a..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_permissions_obsolete.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_without_permissions_obsolete - : IClassFixture { - readonly Fixture _fixture; - public connect_to_existing_without_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task throws_access_denied() => - Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToAllAsync( - "agroupname55", - delegate { return Task.CompletedTask; } - ); - } - ).WithTimeout(); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToAllAsync( - "agroupname55", - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_read_all_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_read_all_permissions_obsolete.cs deleted file mode 100644 index da6f19f72..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_existing_without_read_all_permissions_obsolete.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_without_read_all_permissions_obsolete - : IClassFixture { - readonly Fixture _fixture; - public connect_to_existing_without_read_all_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task throws_access_denied() => - Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToAllAsync( - "agroupname55", - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.TestUser1 - ); - } - ).WithTimeout(); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToAllAsync( - "agroupname55", - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs deleted file mode 100644 index 5f8c885c1..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_non_existing_with_permissions_obsolete - : IClassFixture { - const string Group = "foo"; - - readonly Fixture _fixture; - - public connect_to_non_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task throws_persistent_subscription_not_found() { - var ex = await Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ); - } - ).WithTimeout(); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_with_retries_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_with_retries_obsolete.cs deleted file mode 100644 index 085eecc3e..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/connect_with_retries_obsolete.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_with_retries_obsolete - : IClassFixture { - const string Group = "retries"; - readonly Fixture _fixture; - - public connect_with_retries_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task events_are_retried_until_success() => Assert.Equal(5, await _fixture.RetryCount.WithTimeout()); - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _retryCountSource; - PersistentSubscription? _subscription; - - public Fixture() => _retryCountSource = new(); - - public Task RetryCount => _retryCountSource.Task; - - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, r, ct) => { - if (r > 4) { - _retryCountSource.TrySetResult(r.Value); - await subscription.Ack(e.Event.EventId); - } - else { - await subscription.Nack( - PersistentSubscriptionNakEventAction.Retry, - "Not yet tried enough times", - e - ); - } - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _retryCountSource.TrySetException(ex!); - }, - TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/deleting_existing_with_subscriber_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/deleting_existing_with_subscriber_obsolete.cs deleted file mode 100644 index fdca510d5..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/deleting_existing_with_subscriber_obsolete.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class deleting_existing_with_subscriber_obsolete - : IClassFixture { - readonly Fixture _fixture; - - public deleting_existing_with_subscriber_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_is_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal("groupname123", ex.GroupName); - } - - [Fact(Skip = "Isn't this how it should work?")] - public async Task the_subscription_is_dropped_with_not_found() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal("groupname123", ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped; - PersistentSubscription? _subscription; - - public Fixture() => _dropped = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task; - - protected override async Task Given() { - await Client.CreateToAllAsync( - "groupname123", - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - "groupname123", - async (s, e, i, ct) => await s.Ack(e), - (s, r, e) => _dropped.TrySetResult((r, e)), - TestCredentials.Root - ); - - // todo: investigate why this test is flaky without this delay - await Task.Delay(500); - } - - protected override Task When() => - Client.DeleteToAllAsync( - "groupname123", - userCredentials: TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs deleted file mode 100644 index cfccd2de2..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Text; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_catching_up_to_link_to_events_manual_ack_obsolete - : IClassFixture { - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public happy_case_catching_up_to_link_to_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount) - .Select( - (e, i) => new EventData( - e.EventId, - SystemEventTypes.LinkTo, - Encoding.UTF8.GetBytes($"{i}@test"), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - ) - ) - .ToArray(); - - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs deleted file mode 100644 index cd1f69ee4..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_catching_up_to_normal_events_manual_ack_obsolete : IClassFixture { - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public happy_case_catching_up_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_obsolete.cs deleted file mode 100644 index f9169733b..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_obsolete.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_filtered_obsolete : IClassFixture { - readonly Fixture _fixture; - - public happy_case_filtered_obsolete(Fixture fixture) => _fixture = fixture; - - public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); - - [SupportsPSToAll.Theory] - [MemberData(nameof(FilterCases))] - public async Task reads_all_existing_filtered_events(string filterName) { - var streamPrefix = $"{filterName}-{_fixture.GetStreamName()}"; - var (getFilter, prepareEvent) = Filters.GetFilter(filterName); - var filter = getFilter(streamPrefix); - - var appeared = new TaskCompletionSource(); - var appearedEvents = new List(); - var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); - - foreach (var e in events) - await _fixture.StreamsClient.AppendToStreamAsync( - $"{streamPrefix}_{Guid.NewGuid():n}", - StreamState.NoStream, - new[] { e } - ); - - await _fixture.Client.CreateToAllAsync( - filterName, - filter, - new(startFrom: Position.Start), - userCredentials: TestCredentials.Root - ); - - using var subscription = await _fixture.Client.SubscribeToAllAsync( - filterName, - async (s, e, r, ct) => { - appearedEvents.Add(e.Event); - if (appearedEvents.Count >= events.Length) - appeared.TrySetResult(true); - - await s.Ack(e); - }, - userCredentials: TestCredentials.Root - ) - .WithTimeout(); - - await Task.WhenAll(appeared.Task).WithTimeout(); - - Assert.Equal(events.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); - } - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync( - Guid.NewGuid().ToString(), - StreamState.NoStream, - CreateTestEvents(256) - ); - - await StreamsClient.SetStreamMetadataAsync( - SystemStreams.AllStream, - StreamState.Any, - new(acl: new(SystemRoles.All)), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_with_start_from_set_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_with_start_from_set_obsolete.cs deleted file mode 100644 index 111ca4e19..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_filtered_with_start_from_set_obsolete.cs +++ /dev/null @@ -1,86 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_filtered_with_start_from_set_obsolete : IClassFixture { - readonly Fixture _fixture; - - public happy_case_filtered_with_start_from_set_obsolete(Fixture fixture) => _fixture = fixture; - - public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); - - [SupportsPSToAll.Theory] - [MemberData(nameof(FilterCases))] - public async Task reads_all_existing_filtered_events_from_specified_start(string filterName) { - var streamPrefix = $"{filterName}-{_fixture.GetStreamName()}"; - var (getFilter, prepareEvent) = Filters.GetFilter(filterName); - var filter = getFilter(streamPrefix); - - var appeared = new TaskCompletionSource(); - var appearedEvents = new List(); - var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); - var eventsToSkip = events.Take(10).ToArray(); - var eventsToCapture = events.Skip(10).ToArray(); - - IWriteResult? eventToCaptureResult = null; - - foreach (var e in eventsToSkip) - await _fixture.StreamsClient.AppendToStreamAsync( - $"{streamPrefix}_{Guid.NewGuid():n}", - StreamState.NoStream, - new[] { e } - ); - - foreach (var e in eventsToCapture) { - var result = await _fixture.StreamsClient.AppendToStreamAsync( - $"{streamPrefix}_{Guid.NewGuid():n}", - StreamState.NoStream, - new[] { e } - ); - - eventToCaptureResult ??= result; - } - - await _fixture.Client.CreateToAllAsync( - filterName, - filter, - new(startFrom: eventToCaptureResult!.LogPosition), - userCredentials: TestCredentials.Root - ); - - using var subscription = await _fixture.Client.SubscribeToAllAsync( - filterName, - async (s, e, r, ct) => { - appearedEvents.Add(e.Event); - if (appearedEvents.Count >= eventsToCapture.Length) - appeared.TrySetResult(true); - - await s.Ack(e); - }, - userCredentials: TestCredentials.Root - ) - .WithTimeout(); - - await Task.WhenAll(appeared.Task).WithTimeout(); - - Assert.Equal(eventsToCapture.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); - } - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync( - Guid.NewGuid().ToString(), - StreamState.NoStream, - CreateTestEvents(256) - ); - - await StreamsClient.SetStreamMetadataAsync( - SystemStreams.AllStream, - StreamState.Any, - new(acl: new(SystemRoles.All)), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs deleted file mode 100644 index 2563c488d..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete - : IClassFixture { - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.End, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_check_point_filtered_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_check_point_filtered_obsolete.cs deleted file mode 100644 index 0e9e2be21..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_check_point_filtered_obsolete.cs +++ /dev/null @@ -1,120 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class update_existing_with_check_point_filtered_obsolete - : IClassFixture { - const string Group = "existing-with-check-point-filtered"; - - readonly Fixture _fixture; - - public update_existing_with_check_point_filtered_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task resumes_from_check_point() { - var resumedEvent = await _fixture.Resumed.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.True(resumedEvent.Event.Position > _fixture.CheckPoint); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _appeared; - readonly List _appearedEvents; - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - readonly EventData[] _events; - readonly TaskCompletionSource _resumedSource; - - PersistentSubscription? _firstSubscription; - PersistentSubscription? _secondSubscription; - - public Fixture() { - _droppedSource = new(); - _resumedSource = new(); - _appeared = new(); - _appearedEvents = new(); - _events = CreateTestEvents(5).ToArray(); - } - - public Task Resumed => _resumedSource.Task; - - public Position CheckPoint { get; private set; } - - protected override async Task Given() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); - - await Client.CreateToAllAsync( - Group, - StreamFilter.Prefix("test"), - new( - checkPointLowerBound: 5, - checkPointAfter: TimeSpan.FromSeconds(1), - startFrom: Position.Start - ), - userCredentials: TestCredentials.Root - ); - - _firstSubscription = await Client.SubscribeToAllAsync( - Group, - async (s, e, r, ct) => { - _appearedEvents.Add(e); - - if (_appearedEvents.Count == _events.Length) - _appeared.TrySetResult(true); - - await s.Ack(e); - }, - (subscription, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); - - await Task.WhenAll(_appeared.Task, Checkpointed()).WithTimeout(); - - return; - - async Task Checkpointed() { - await using var subscription = StreamsClient.SubscribeToStream( - $"$persistentsubscription-$all::{Group}-checkpoint", - FromStream.Start, - userCredentials: TestCredentials.Root); - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - CheckPoint = resolvedEvent.Event.Data.ParsePosition(); - return; - } - } - } - - protected override async Task When() { - // Force restart of the subscription - await Client.UpdateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - - await _droppedSource.Task.WithTimeout(); - - _secondSubscription = await Client.SubscribeToAllAsync( - Group, - async (s, e, r, ct) => { - _resumedSource.TrySetResult(e); - await s.Ack(e); - s.Dispose(); - }, - (_, reason, ex) => { - if (ex is not null) - _resumedSource.TrySetException(ex); - else - _resumedSource.TrySetResult(default); - }, - TestCredentials.Root - ); - - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); - } - - public override Task DisposeAsync() { - _firstSubscription?.Dispose(); - _secondSubscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_subscribers_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_subscribers_obsolete.cs deleted file mode 100644 index 17a77063c..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/update_existing_with_subscribers_obsolete.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class update_existing_with_subscribers_obsolete - : IClassFixture { - const string Group = "existing"; - - readonly Fixture _fixture; - - public update_existing_with_subscribers_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task existing_subscriptions_are_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - PersistentSubscription? _subscription; - - public Fixture() => _droppedSource = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _droppedSource.Task; - - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - delegate { return Task.CompletedTask; }, - (subscription, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); - - // todo: investigate why this test is flaky without this delay - await Task.Delay(500); - } - - protected override Task When() => - Client.UpdateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_filtering_out_events_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_filtering_out_events_obsolete.cs deleted file mode 100644 index 302ac221a..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_filtering_out_events_obsolete.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class when_writing_and_filtering_out_events_obsolete - : IClassFixture { - const string Group = "filtering-out-events"; - - readonly Fixture _fixture; - - public when_writing_and_filtering_out_events_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task it_should_write_a_check_point() { - await Task.Yield(); - Assert.True(_fixture.SecondCheckPoint > _fixture.FirstCheckPoint); - Assert.Equal( - _fixture.Events.Select(e => e.EventId), - _fixture.AppearedEvents.Select(e => e.Event.EventId) - ); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _appeared; - readonly List _appearedEvents, _checkPoints; - readonly EventData[] _events; - - PersistentSubscription? _subscription; - - public Fixture() { - _appeared = new(); - _appearedEvents = new(); - _checkPoints = new(); - _events = CreateTestEvents(10).ToArray(); - } - - public Position SecondCheckPoint { get; private set; } - public Position FirstCheckPoint { get; private set; } - public EventData[] Events => _events.ToArray(); - public ResolvedEvent[] AppearedEvents => _appearedEvents.ToArray(); - - protected override async Task Given() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - - await Client.CreateToAllAsync( - Group, - StreamFilter.Prefix("test"), - new( - checkPointLowerBound: 1, - checkPointUpperBound: 5, - checkPointAfter: TimeSpan.FromSeconds(1), - startFrom: Position.Start - ), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (s, e, r, ct) => { - _appearedEvents.Add(e); - - if (_appearedEvents.Count == _events.Length) - _appeared.TrySetResult(true); - - await s.Ack(e); - }, - userCredentials: TestCredentials.Root - ); - - await Task.WhenAll(_appeared.Task, Checkpointed()).WithTimeout(); - - async Task Checkpointed() { - bool firstCheckpointSet = false; - await using var subscription = StreamsClient.SubscribeToStream( - $"$persistentsubscription-$all::{Group}-checkpoint", - FromStream.Start, - userCredentials: TestCredentials.Root); - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - if (!firstCheckpointSet) { - FirstCheckPoint = resolvedEvent.Event.Data.ParsePosition(); - firstCheckpointSet = true; - } else { - SecondCheckPoint = resolvedEvent.Event.Data.ParsePosition(); - return; - } - } - } - } - - protected override async Task When() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync( - "filtered-out-stream-" + Guid.NewGuid(), - StreamState.Any, - new[] { e } - ); - } - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs deleted file mode 100644 index f18d6ad96..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class when_writing_and_subscribing_to_normal_events_manual_nack_obsolete - : IClassFixture { - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public when_writing_and_subscribing_to_normal_events_manual_nack_obsolete(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount) - .ToArray(); - - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToAllAsync( - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e); - - if (e.OriginalStreamId.StartsWith("test-") - && Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/can_create_duplicate_name_on_different_streams.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/can_create_duplicate_name_on_different_streams.cs deleted file mode 100644 index d23e7c911..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/can_create_duplicate_name_on_different_streams.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class can_create_duplicate_name_on_different_streams : IClassFixture { - readonly Fixture _fixture; - - public can_create_duplicate_name_on_different_streams(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task the_completion_succeeds() => - _fixture.Client.CreateToStreamAsync("someother", "group3211", new(), userCredentials: TestCredentials.Root); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => - Client.CreateToAllAsync( - "group3211", - new(), - userCredentials: TestCredentials.Root - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs deleted file mode 100644 index db2818abf..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_max_one_client.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_max_one_client : IClassFixture { - private const string Group = "maxoneclient"; - - private readonly Fixture _fixture; - - public connect_to_existing_with_max_one_client(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_second_subscription_fails_to_connect2() { - var ex = await Assert.ThrowsAsync( - () => Task.WhenAll(Subscribe().WithTimeout(), Subscribe().WithTimeout())); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - return; - - async Task Subscribe() { - await using var subscription = _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - await subscription.Messages.AnyAsync(); - } - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToAllAsync(Group, new(maxSubscriberCount: 1), userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs deleted file mode 100644 index e10ed9909..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_permissions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_permissions : IClassFixture { - private const string Group = "connectwithpermissions"; - private readonly Fixture _fixture; - - public connect_to_existing_with_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_succeeds() { - await using var subscription = - _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - - Assert.True(await subscription.Messages - .FirstAsync().AsTask().WithTimeout() is PersistentSubscriptionMessage.SubscriptionConfirmation); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs deleted file mode 100644 index 47e36c620..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_beginning.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_start_from_beginning - : IClassFixture { - private const string Group = "startfrombeginning"; - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_beginning(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_event_zero_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(_fixture.Events[0].Event.EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public ResolvedEvent[] Events { get; private set; } = Array.Empty(); - - protected override async Task Given() { - //append 10 events to random streams to make sure we have at least 10 events in the transaction file - foreach (var @event in CreateTestEvents(10)) { - await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, - new[] { @event }); - } - - Events = await StreamsClient - .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) - .ToArrayAsync(); - - await Client.CreateToAllAsync(Group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); - } - - protected override Task When() { - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs deleted file mode 100644 index 17a28d1d6..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_start_from_not_set - : IClassFixture { - private const string Group = "startfromend1"; - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_not_set(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_no_non_system_events() { - await Assert.ThrowsAsync(() => _fixture.Subscription!.Messages - .OfType() - .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) - .AnyAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(250))); - } - - public class Fixture : EventStoreClientFixture { - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) - await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any, - new[] { @event }); - - await Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - } - - protected override Task When() { - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs deleted file mode 100644 index bbd8ef16e..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_not_set_then_event_written.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_start_from_not_set_then_event_written - : IClassFixture { - private const string Group = "startfromnotset2"; - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_not_set_then_event_written(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .Where(resolvedEvent => !SystemStreams.IsSystemStream(resolvedEvent.OriginalStreamId)) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(_fixture.ExpectedEvent.EventId, resolvedEvent.Event.EventId); - Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData ExpectedEvent; - public readonly string ExpectedStreamId; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - ExpectedEvent = CreateTestEvents(1).First(); - ExpectedStreamId = Guid.NewGuid().ToString(); - } - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) { - await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any, - new[] { @event }); - } - - await Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - } - - protected override async Task When() => - await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent }); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs deleted file mode 100644 index e7f3c9913..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_start_from_set_to_end_position - : IClassFixture { - private const string Group = "startfromend1"; - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_no_non_system_events() { - await Assert.ThrowsAsync(() => _fixture.Subscription!.Messages - .OfType() - .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) - .AnyAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(250))); - } - - public class Fixture : EventStoreClientFixture { - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) { - await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any, - new[] { @event }); - } - - await Client.CreateToAllAsync(Group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); - } - - protected override Task When() { - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs deleted file mode 100644 index 835c8538c..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_end_position_then_event_written.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class - connect_to_existing_with_start_from_set_to_end_position_then_event_written - : IClassFixture { - private const string Group = "startfromnotset2"; - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position_then_event_written(Fixture fixture) => - _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_non_system_event() { - var resolvedEvent = await _fixture.Subscription!.Messages - .OfType() - .Select(e => e.ResolvedEvent) - .Where(resolvedEvent => !SystemStreams.IsSystemStream(resolvedEvent.OriginalStreamId)) - .FirstAsync() - .AsTask() - .WithTimeout(); - Assert.Equal(_fixture.ExpectedEvent.EventId, resolvedEvent.Event.EventId); - Assert.Equal(_fixture.ExpectedStreamId, resolvedEvent.Event.EventStreamId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData ExpectedEvent; - public readonly string ExpectedStreamId; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - ExpectedEvent = CreateTestEvents(1).First(); - ExpectedStreamId = Guid.NewGuid().ToString(); - } - - protected override async Task Given() { - foreach (var @event in CreateTestEvents(10)) { - await StreamsClient.AppendToStreamAsync("non-system-stream-" + Guid.NewGuid(), StreamState.Any, - new[] { @event }); - } - - await Client.CreateToAllAsync(Group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - } - - protected override async Task When() => - await StreamsClient.AppendToStreamAsync(ExpectedStreamId, StreamState.NoStream, new[] { ExpectedEvent }); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs deleted file mode 100644 index b45330a9e..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_invalid_middle_position.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_start_from_set_to_invalid_middle_position - : IClassFixture { - private const string Group = "startfrominvalid1"; - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_invalid_middle_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_is_dropped() { - var ex = await Assert.ThrowsAsync(async () => - await _fixture.Enumerator!.MoveNextAsync()); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - public IAsyncEnumerator? Enumerator { get; private set; } - - protected override async Task Given() { - var invalidPosition = new Position(1L, 1L); - await Client.CreateToAllAsync(Group, new(startFrom: invalidPosition), - userCredentials: TestCredentials.Root); - } - - protected override async Task When() { - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - Enumerator = Subscription.Messages.GetAsyncEnumerator(); - - await Enumerator.MoveNextAsync(); - } - - public override async Task DisposeAsync() { - if (Enumerator is not null) { - await Enumerator.DisposeAsync(); - } - - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs deleted file mode 100644 index daaf838a9..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_with_start_from_set_to_valid_middle_position.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_with_start_from_set_to_valid_middle_position - : IClassFixture { - private const string Group = "startfromvalid"; - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_valid_middle_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_gets_the_event_at_the_specified_start_position_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstAsync() - .AsTask() - .WithTimeout(); - Assert.Equal(_fixture.ExpectedEvent.OriginalPosition, resolvedEvent.Event.Position); - Assert.Equal(_fixture.ExpectedEvent.Event.EventId, resolvedEvent.Event.EventId); - Assert.Equal(_fixture.ExpectedEvent.Event.EventStreamId, resolvedEvent.Event.EventStreamId); - } - - public class Fixture : EventStoreClientFixture { - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - public ResolvedEvent ExpectedEvent { get; private set; } - - protected override async Task Given() { - var events = await StreamsClient - .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) - .ToArrayAsync(); - - ExpectedEvent = events[events.Length / 2]; //just a random event in the middle of the results - - await Client.CreateToAllAsync(Group, new(startFrom: ExpectedEvent.OriginalPosition), - userCredentials: TestCredentials.Root); - } - - protected override Task When() { - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs deleted file mode 100644 index 865281ec0..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_permissions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_without_permissions : IClassFixture { - private readonly Fixture _fixture; - public connect_to_existing_without_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task throws_access_denied() => - Assert.ThrowsAsync( - async () => { - await using var subscription = _fixture.Client.SubscribeToAll("agroupname55"); - await subscription.AnyAsync().AsTask().WithTimeout(); - }); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToAllAsync("agroupname55", new(), userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs deleted file mode 100644 index 56eccd0fe..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_existing_without_read_all_permissions.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_existing_without_read_all_permissions - : IClassFixture { - private readonly Fixture _fixture; - public connect_to_existing_without_read_all_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task throws_access_denied() => - Assert.ThrowsAsync( - async () => { - await using var subscription = - _fixture.Client.SubscribeToAll("agroupname55", userCredentials: TestCredentials.TestUser1); - await subscription.Messages.AnyAsync().AsTask().WithTimeout(); - }); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToAllAsync("agroupname55", new(), userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs deleted file mode 100644 index 40a541040..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_to_non_existing_with_permissions.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_to_non_existing_with_permissions - : IClassFixture { - private const string Group = "foo"; - - private readonly Fixture _fixture; - - public connect_to_non_existing_with_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task throws_persistent_subscription_not_found() { - await using var subscription = _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - - Assert.True(await subscription.Messages.OfType().AnyAsync() - .AsTask() - .WithTimeout()); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs deleted file mode 100644 index ea4236278..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/connect_with_retries.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class connect_with_retries : IClassFixture { - private const string Group = "retries"; - private readonly Fixture _fixture; - - public connect_with_retries(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task events_are_retried_until_success() { - var retryCount = await _fixture.Subscription!.Messages.OfType() - .SelectAwait(async e => { - if (e.RetryCount > 4) { - await _fixture.Subscription.Ack(e.ResolvedEvent); - } else { - await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Retry, - "Not yet tried enough times", e.ResolvedEvent); - } - - return e.RetryCount; - }) - .Where(retryCount => retryCount > 4) - .FirstOrDefaultAsync() - .AsTask() - .WithTimeout(); - - Assert.Equal(5, retryCount); - } - - public class Fixture : EventStoreClientFixture { - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - protected override async Task Given() { - await Client.CreateToAllAsync(Group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_after_deleting_the_same.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_after_deleting_the_same.cs deleted file mode 100644 index a3c673f48..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_after_deleting_the_same.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_after_deleting_the_same : IClassFixture { - readonly Fixture _fixture; - - public create_after_deleting_the_same(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.CreateToAllAsync( - "existing", - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override async Task When() { - await Client.CreateToAllAsync( - "existing", - new(), - userCredentials: TestCredentials.Root - ); - - await Client.DeleteToAllAsync( - "existing", - userCredentials: TestCredentials.Root - ); - } - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_duplicate.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_duplicate.cs deleted file mode 100644 index abe410c26..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_duplicate.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_duplicate : IClassFixture { - readonly Fixture _fixture; - - public create_duplicate(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_fails() { - var ex = await Assert.ThrowsAsync( - () => _fixture.Client.CreateToAllAsync( - "group32", - new(), - userCredentials: TestCredentials.Root - ) - ); - - Assert.Equal(StatusCode.AlreadyExists, ex.StatusCode); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => - Client.CreateToAllAsync( - "group32", - new(), - userCredentials: TestCredentials.Root - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_filtered.cs deleted file mode 100644 index 8eff2a425..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_filtered.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_filtered : IClassFixture { - readonly Fixture _fixture; - - public create_filtered(Fixture fixture) => _fixture = fixture; - - public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); - - [SupportsPSToAll.Theory] - [MemberData(nameof(FilterCases))] - public async Task the_completion_succeeds(string filterName) { - var streamPrefix = _fixture.GetStreamName(); - var (getFilter, _) = Filters.GetFilter(filterName); - var filter = getFilter(streamPrefix); - - await _fixture.Client.CreateToAllAsync( - filterName, - filter, - new(), - userCredentials: TestCredentials.Root - ); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_on_all_stream.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_on_all_stream.cs deleted file mode 100644 index 92aa86e1d..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_on_all_stream.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_on_all_stream : IClassFixture { - readonly Fixture _fixture; - - public create_on_all_stream(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task the_completion_succeeds() => _fixture.Client.CreateToAllAsync("existing", new(), userCredentials: TestCredentials.Root); - - [SupportsPSToAll.Fact] - public Task throws_argument_exception_if_wrong_start_from_type_passed() => - Assert.ThrowsAsync( - () => _fixture.Client.CreateToAllAsync("existing", new(startFrom: StreamPosition.End), userCredentials: TestCredentials.Root) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_persistent_subscription_with_dont_timeout.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_persistent_subscription_with_dont_timeout.cs deleted file mode 100644 index f44c830f0..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_persistent_subscription_with_dont_timeout.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_with_dont_timeout - : IClassFixture { - readonly Fixture _fixture; - - public create_with_dont_timeout(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task the_subscription_is_created_without_error() => - _fixture.Client.CreateToAllAsync( - "dont-timeout", - new(messageTimeout: TimeSpan.Zero), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_equal_to_last_indexed_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_equal_to_last_indexed_position.cs deleted file mode 100644 index e6db02901..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_equal_to_last_indexed_position.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_with_commit_position_equal_to_last_indexed_position : IClassFixture { - readonly Fixture _fixture; - - public create_with_commit_position_equal_to_last_indexed_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.CreateToAllAsync( - "group57", - new(startFrom: new Position(_fixture.LastCommitPosition, _fixture.LastCommitPosition)), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - public ulong LastCommitPosition; - - protected override async Task Given() { - var lastEvent = await StreamsClient.ReadAllAsync( - Direction.Backwards, - Position.End, - 1, - userCredentials: TestCredentials.Root - ).FirstAsync(); - - LastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_larger_than_last_indexed_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_larger_than_last_indexed_position.cs deleted file mode 100644 index 7769f8b70..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_commit_position_larger_than_last_indexed_position.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_with_commit_position_larger_than_last_indexed_position - : IClassFixture { - readonly Fixture _fixture; - - public create_with_commit_position_larger_than_last_indexed_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task fails() { - var ex = await Assert.ThrowsAsync( - () => - _fixture.Client.CreateToAllAsync( - "group57", - new(startFrom: new Position(_fixture.LastCommitPosition + 1, _fixture.LastCommitPosition)), - userCredentials: TestCredentials.Root - ) - ); - - Assert.Equal(StatusCode.Internal, ex.StatusCode); - } - - public class Fixture : EventStoreClientFixture { - public ulong LastCommitPosition; - - protected override async Task Given() { - var lastEvent = await StreamsClient.ReadAllAsync( - Direction.Backwards, - Position.End, - 1, - userCredentials: TestCredentials.Root - ).FirstAsync(); - - LastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_prepare_position_larger_than_commit_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_prepare_position_larger_than_commit_position.cs deleted file mode 100644 index 434e90d7a..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_with_prepare_position_larger_than_commit_position.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_with_prepare_position_larger_than_commit_position - : IClassFixture { - readonly Fixture _fixture; - - public create_with_prepare_position_larger_than_commit_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task fails_with_argument_out_of_range_exception() => - Assert.ThrowsAsync( - () => - _fixture.Client.CreateToAllAsync( - "group57", - new(startFrom: new Position(0, 1)), - userCredentials: TestCredentials.Root - ) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_without_permissions.cs deleted file mode 100644 index 71d87a128..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/create_without_permissions.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class create_without_permissions - : IClassFixture { - readonly Fixture _fixture; - - public create_without_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task the_completion_fails_with_access_denied() => - Assert.ThrowsAsync( - () => - _fixture.Client.CreateToAllAsync( - "group57", - new() - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_permissions.cs deleted file mode 100644 index a81180092..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_permissions.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class deleting_existing_with_permissions - : IClassFixture { - readonly Fixture _fixture; - - public deleting_existing_with_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task the_delete_of_group_succeeds() => - _fixture.Client.DeleteToAllAsync( - "groupname123", - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => - Client.CreateToAllAsync( - "groupname123", - new(), - userCredentials: TestCredentials.Root - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs deleted file mode 100644 index 4db83e251..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_existing_with_subscriber.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class deleting_existing_with_subscriber : IClassFixture { - public const string Group = "groupname123"; - private readonly Fixture _fixture; - - public deleting_existing_with_subscriber(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_subscription_is_dropped_with_not_found() { - await using var subscription = _fixture.Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - - Assert.True(await subscription.Messages.OfType().AnyAsync() - .AsTask() - .WithTimeout()); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Client.CreateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - - protected override Task When() => Client.DeleteToAllAsync(Group, userCredentials: TestCredentials.Root); - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_filtered.cs deleted file mode 100644 index 7a461ebf9..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_filtered.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class deleting_filtered - : IClassFixture { - const string Group = "to-be-deleted"; - readonly Fixture _fixture; - - public deleting_filtered(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_succeeds() => await _fixture.Client.DeleteToAllAsync(Group, userCredentials: TestCredentials.Root); - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() => - await Client.CreateToAllAsync( - Group, - EventTypeFilter.Prefix("prefix-filter-"), - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_nonexistent.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_nonexistent.cs deleted file mode 100644 index 804850e89..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_nonexistent.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class deleting_nonexistent - : IClassFixture { - readonly Fixture _fixture; - - public deleting_nonexistent(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_delete_fails_with_argument_exception() => - await Assert.ThrowsAsync( - () => _fixture.Client.DeleteToAllAsync(Guid.NewGuid().ToString(), userCredentials: TestCredentials.Root) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_without_permissions.cs deleted file mode 100644 index 13e1339a9..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/deleting_without_permissions.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class deleting_without_permissions - : IClassFixture { - readonly Fixture _fixture; - - public deleting_without_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_delete_fails_with_access_denied() => - await Assert.ThrowsAsync(() => _fixture.Client.DeleteToAllAsync(Guid.NewGuid().ToString())); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs deleted file mode 100644 index ab748a5d5..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/get_info.cs +++ /dev/null @@ -1,181 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class get_info : IClassFixture { - private const string GroupName = nameof(get_info); - - private static readonly PersistentSubscriptionSettings Settings = new( - resolveLinkTos: true, - startFrom: Position.Start, - extraStatistics: true, - messageTimeout: TimeSpan.FromSeconds(9), - maxRetryCount: 11, - liveBufferSize: 303, - readBatchSize: 30, - historyBufferSize: 909, - checkPointAfter: TimeSpan.FromSeconds(1), - checkPointLowerBound: 1, - checkPointUpperBound: 1, - maxSubscriberCount: 500, - consumerStrategyName: SystemConsumerStrategies.Pinned); - - private readonly Fixture _fixture; - - public get_info(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task throws_when_not_supported() { - if (SupportsPSToAll.No) { - await Assert.ThrowsAsync(async () => - await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root)); - } - } - - [SupportsPSToAll.Fact] - public async Task returns_expected_result() { - var result = await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root); - - Assert.Equal("$all", result.EventSource); - Assert.Equal(GroupName, result.GroupName); - Assert.Equal("Live", result.Status); - - Assert.NotNull(Settings.StartFrom); - Assert.True(result.Stats.TotalItems > 0); - Assert.True(result.Stats.OutstandingMessagesCount > 0); - Assert.True(result.Stats.AveragePerSecond >= 0); - Assert.True(result.Stats.ParkedMessageCount > 0); - Assert.True(result.Stats.AveragePerSecond >= 0); - Assert.True(result.Stats.CountSinceLastMeasurement >= 0); - Assert.True(result.Stats.TotalInFlightMessages >= 0); - Assert.NotNull(result.Stats.LastKnownEventPosition); - Assert.NotNull(result.Stats.LastCheckpointedEventPosition); - Assert.True(result.Stats.LiveBufferCount >= 0); - - Assert.NotNull(result.Connections); - Assert.NotEmpty(result.Connections); - - var connection = result.Connections.First(); - Assert.NotNull(connection.From); - Assert.Equal(TestCredentials.Root.Username, connection.Username); - Assert.NotEmpty(connection.ConnectionName); - Assert.True(connection.AverageItemsPerSecond >= 0); - Assert.True(connection.TotalItems > 0); - Assert.True(connection.CountSinceLastMeasurement >= 0); - Assert.True(connection.AvailableSlots >= 0); - Assert.True(connection.InFlightMessages >= 0); - Assert.NotNull(connection.ExtraStatistics); - Assert.NotEmpty(connection.ExtraStatistics); - - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Highest); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Mean); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Median); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Fastest); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile1); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile2); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile3); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile4); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile5); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyPercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyFivePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointFivePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointNinePercent); - - Assert.NotNull(result.Settings); - Assert.Equal(Settings.StartFrom, result.Settings!.StartFrom); - Assert.Equal(Settings.ResolveLinkTos, result.Settings!.ResolveLinkTos); - Assert.Equal(Settings.ExtraStatistics, result.Settings!.ExtraStatistics); - Assert.Equal(Settings.MessageTimeout, result.Settings!.MessageTimeout); - Assert.Equal(Settings.MaxRetryCount, result.Settings!.MaxRetryCount); - Assert.Equal(Settings.LiveBufferSize, result.Settings!.LiveBufferSize); - Assert.Equal(Settings.ReadBatchSize, result.Settings!.ReadBatchSize); - Assert.Equal(Settings.HistoryBufferSize, result.Settings!.HistoryBufferSize); - Assert.Equal(Settings.CheckPointAfter, result.Settings!.CheckPointAfter); - Assert.Equal(Settings.CheckPointLowerBound, result.Settings!.CheckPointLowerBound); - Assert.Equal(Settings.CheckPointUpperBound, result.Settings!.CheckPointUpperBound); - Assert.Equal(Settings.MaxSubscriberCount, result.Settings!.MaxSubscriberCount); - Assert.Equal(Settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName); - } - - [SupportsPSToAll.Fact] - public async Task throws_with_non_existing_subscription() => - await Assert.ThrowsAsync( - async () => await _fixture.Client.GetInfoToAllAsync("NonExisting", userCredentials: TestCredentials.Root)); - - [SupportsPSToAll.Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync(async () => - await _fixture.Client.GetInfoToAllAsync("NonExisting")); - - [SupportsPSToAll.Fact] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync(async () => - await _fixture.Client.GetInfoToAllAsync("NonExisting", userCredentials: TestCredentials.TestBadUser)); - - [SupportsPSToAll.Fact] - public async Task returns_result_with_normal_user_credentials() { - var result = await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.TestUser1); - - Assert.Equal("$all", result.EventSource); - } - - private void AssertKeyAndValue(IDictionary items, string key) { - Assert.True(items.ContainsKey(key)); - Assert.True(items[key] > 0); - } - - public class Fixture : EventStoreClientFixture { - private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; - private IAsyncEnumerator? _enumerator; - public Fixture() : base(noDefaultCredentials: true) { } - - protected override async Task Given() { - if (SupportsPSToAll.No) - return; - - await Client.CreateToAllAsync(GroupName, get_info.Settings, userCredentials: TestCredentials.Root); - - foreach (var eventData in CreateTestEvents(20)) { - await StreamsClient.AppendToStreamAsync($"test-{Guid.NewGuid():n}", StreamState.NoStream, - new[] { eventData }, userCredentials: TestCredentials.Root); - } - } - - protected override async Task When() { - if (SupportsPSToAll.No) - return; - - var counter = 0; - - _subscription = Client.SubscribeToAll(GroupName, userCredentials: TestCredentials.Root); - _enumerator = _subscription.Messages.GetAsyncEnumerator(); - - while (await _enumerator.MoveNextAsync()) { - if (_enumerator.Current is not PersistentSubscriptionMessage.Event (var resolvedEvent, _)) { - continue; - } - - counter++; - - if (counter == 1) { - await _subscription.Nack(PersistentSubscriptionNakEventAction.Park, "Test", resolvedEvent); - } - - if (counter > 10) { - return; - } - } - } - - public override async Task DisposeAsync() { - if (_enumerator is not null) { - await _enumerator.DisposeAsync(); - } - - if (_subscription is not null) { - await _subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs deleted file mode 100644 index dbcc44b43..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_link_to_events_manual_ack.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Text; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class happy_case_catching_up_to_link_to_events_manual_ack - : IClassFixture { - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public happy_case_catching_up_to_link_to_events_manual_ack(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .Take(_fixture.Events.Length) - .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) - .WithTimeout(); - - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(EventWriteCount) - .Select((e, i) => new EventData(e.EventId, SystemEventTypes.LinkTo, Encoding.UTF8.GetBytes($"{i}@test"), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream)) - .ToArray(); - } - - protected override async Task Given() { - foreach (var e in Events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - - await Client.CreateToAllAsync(Group, new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs deleted file mode 100644 index 05612443d..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_catching_up_to_normal_events_manual_ack.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class happy_case_catching_up_to_normal_events_manual_ack - : IClassFixture { - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public happy_case_catching_up_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .Take(_fixture.Events.Length) - .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) - .WithTimeout(); - } - - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(EventWriteCount).ToArray(); - } - - protected override async Task Given() { - foreach (var e in Events) - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - - await Client.CreateToAllAsync(Group, new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs deleted file mode 100644 index 9d89fddb1..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class happy_case_filtered : IClassFixture { - private readonly Fixture _fixture; - - public happy_case_filtered(Fixture fixture) => _fixture = fixture; - - public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); - - [SupportsPSToAll.Theory] - [MemberData(nameof(FilterCases))] - public async Task reads_all_existing_filtered_events(string filterName) { - var streamPrefix = $"{filterName}-{_fixture.GetStreamName()}"; - var (getFilter, prepareEvent) = Filters.GetFilter(filterName); - var filter = getFilter(streamPrefix); - - var appearedEvents = new List(); - var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); - - foreach (var e in events) { - await _fixture.StreamsClient.AppendToStreamAsync($"{streamPrefix}_{Guid.NewGuid():n}", StreamState.NoStream, - new[] { e }); - } - - await _fixture.Client.CreateToAllAsync(filterName, filter, new(startFrom: Position.Start), - userCredentials: TestCredentials.Root); - - await using var subscription = - _fixture.Client.SubscribeToAll(filterName, userCredentials: TestCredentials.Root); - await subscription.Messages - .OfType() - .Take(events.Length) - .ForEachAwaitAsync(async e => { - var (resolvedEvent, _) = e; - appearedEvents.Add(resolvedEvent.Event); - await subscription.Ack(resolvedEvent); - }) - .WithTimeout(); - - Assert.Equal(events.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); - } - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, - CreateTestEvents(256)); - - await StreamsClient.SetStreamMetadataAsync(SystemStreams.AllStream, StreamState.Any, - new(acl: new(SystemRoles.All)), userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set.cs deleted file mode 100644 index 065782783..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_filtered_with_start_from_set.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class happy_case_filtered_with_start_from_set : IClassFixture { - private readonly Fixture _fixture; - - public happy_case_filtered_with_start_from_set(Fixture fixture) => _fixture = fixture; - - public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); - - [SupportsPSToAll.Theory] - [MemberData(nameof(FilterCases))] - public async Task reads_all_existing_filtered_events_from_specified_start(string filterName) { - var streamPrefix = $"{filterName}-{_fixture.GetStreamName()}"; - var (getFilter, prepareEvent) = Filters.GetFilter(filterName); - var filter = getFilter(streamPrefix); - - var events = _fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); - var eventsToSkip = events.Take(10).ToArray(); - var eventsToCapture = events.Skip(10).ToArray(); - - IWriteResult? eventToCaptureResult = null; - - foreach (var e in eventsToSkip) { - await _fixture.StreamsClient.AppendToStreamAsync($"{streamPrefix}_{Guid.NewGuid():n}", StreamState.NoStream, - new[] { e }); - } - - foreach (var e in eventsToCapture) { - var result = await _fixture.StreamsClient.AppendToStreamAsync($"{streamPrefix}_{Guid.NewGuid():n}", - StreamState.NoStream, new[] { e }); - - eventToCaptureResult ??= result; - } - - await _fixture.Client.CreateToAllAsync(filterName, filter, new(startFrom: eventToCaptureResult!.LogPosition), - userCredentials: TestCredentials.Root); - - await using var subscription = - _fixture.Client.SubscribeToAll(filterName, userCredentials: TestCredentials.Root); - - var appearedEvents = await subscription.Messages.OfType() - .Take(10) - .Select(e => e.ResolvedEvent.Event) - .ToArrayAsync() - .AsTask() - .WithTimeout(); - - Assert.Equal(eventsToCapture.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); - } - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Guid.NewGuid().ToString(), StreamState.NoStream, - CreateTestEvents(256)); - - await StreamsClient.SetStreamMetadataAsync(SystemStreams.AllStream, StreamState.Any, - new(acl: new(SystemRoles.All)), userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs deleted file mode 100644 index 100e49f27..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class happy_case_writing_and_subscribing_to_normal_events_manual_ack - : IClassFixture { - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public happy_case_writing_and_subscribing_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .SelectAwait(async e => { - await _fixture.Subscription.Ack(e.ResolvedEvent); - return e; - }) - .Where(e => e.ResolvedEvent.OriginalStreamId.StartsWith("test-")) - .Take(_fixture.Events.Length) - .ToArrayAsync() - .AsTask() - .WithTimeout(); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(EventWriteCount).ToArray(); - } - - protected override async Task Given() { - await Client.CreateToAllAsync(Group, new(startFrom: Position.End, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); - } - - protected override async Task When() { - foreach (var e in Events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_with_persistent_subscriptions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_with_persistent_subscriptions.cs deleted file mode 100644 index 443a9d04e..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_with_persistent_subscriptions.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class list_with_persistent_subscriptions : IClassFixture { - const int AllStreamSubscriptionCount = 3; - const int StreamSubscriptionCount = 4; - const string GroupName = nameof(list_with_persistent_subscriptions); - const string StreamName = nameof(list_with_persistent_subscriptions); - - readonly Fixture _fixture; - - public list_with_persistent_subscriptions(Fixture fixture) => _fixture = fixture; - - int TotalSubscriptionCount => - SupportsPSToAll.No - ? StreamSubscriptionCount - : StreamSubscriptionCount + AllStreamSubscriptionCount; - - [Fact] - public async Task throws_when_not_supported() { - if (SupportsPSToAll.No) - await Assert.ThrowsAsync(async () => { await _fixture.Client.ListToAllAsync(userCredentials: TestCredentials.Root); }); - } - - [SupportsPSToAll.Fact] - public async Task returns_subscriptions_to_all_stream() { - var result = (await _fixture.Client.ListToAllAsync(userCredentials: TestCredentials.Root)).ToList(); - Assert.Equal(AllStreamSubscriptionCount, result.Count); - Assert.All(result, s => Assert.Equal("$all", s.EventSource)); - } - - [Fact] - public async Task returns_all_subscriptions() { - var result = (await _fixture.Client.ListAllAsync(userCredentials: TestCredentials.Root)).ToList(); - Assert.Equal(TotalSubscriptionCount, result.Count()); - } - - [SupportsPSToAll.Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListToAllAsync() - ); - - [SupportsPSToAll.Fact] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListToAllAsync(userCredentials: TestCredentials.TestBadUser) - ); - - [SupportsPSToAll.Fact] - public async Task returns_result_with_normal_user_credentials() { - var result = await _fixture.Client.ListToAllAsync(userCredentials: TestCredentials.TestUser1); - Assert.Equal(AllStreamSubscriptionCount, result.Count()); - } - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(skipPSWarmUp: true, noDefaultCredentials: true) { } - - protected override async Task Given() { - for (var i = 0; i < StreamSubscriptionCount; i++) - await Client.CreateToStreamAsync( - StreamName, - GroupName + i, - new(), - userCredentials: TestCredentials.Root - ); - - if (SupportsPSToAll.No) - return; - - for (var i = 0; i < AllStreamSubscriptionCount; i++) - await Client.CreateToAllAsync( - GroupName + i, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_without_persistent_subscriptions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_without_persistent_subscriptions.cs deleted file mode 100644 index 0a9c435be..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/list_without_persistent_subscriptions.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class list_without_persistent_subscriptions : IClassFixture { - readonly Fixture _fixture; - - public list_without_persistent_subscriptions(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task throws() { - if (SupportsPSToAll.No) { - await Assert.ThrowsAsync(async () => { await _fixture.Client.ListToAllAsync(userCredentials: TestCredentials.Root); }); - - return; - } - - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListToAllAsync(userCredentials: TestCredentials.Root) - ); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/replay_parked.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/replay_parked.cs deleted file mode 100644 index 501500abd..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/replay_parked.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class replay_parked : IClassFixture { - const string GroupName = nameof(replay_parked); - - readonly Fixture _fixture; - - public replay_parked(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task throws_when_not_supported() { - if (SupportsPSToAll.No) - await Assert.ThrowsAsync( - async () => { - await _fixture.Client.ReplayParkedMessagesToAllAsync( - GroupName, - userCredentials: TestCredentials.Root - ); - } - ); - } - - [SupportsPSToAll.Fact] - public async Task does_not_throw() { - await _fixture.Client.ReplayParkedMessagesToAllAsync( - GroupName, - userCredentials: TestCredentials.Root - ); - - await _fixture.Client.ReplayParkedMessagesToAllAsync( - GroupName, - 100, - userCredentials: TestCredentials.Root - ); - } - - [SupportsPSToAll.Fact] - public async Task throws_when_given_non_existing_subscription() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToAllAsync( - "NonExisting", - userCredentials: TestCredentials.Root - ) - ); - - [SupportsPSToAll.Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToAllAsync(GroupName) - ); - - [SupportsPSToAll.Fact] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToAllAsync( - GroupName, - userCredentials: TestCredentials.TestBadUser - ) - ); - - [SupportsPSToAll.Fact] - public async Task throws_with_normal_user_credentials() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToAllAsync( - GroupName, - userCredentials: TestCredentials.TestUser1 - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override async Task Given() { - if (SupportsPSToAll.No) - return; - - await Client.CreateToAllAsync( - GroupName, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing.cs deleted file mode 100644 index c1fac1c1f..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing : IClassFixture { - const string Group = "existing"; - - readonly Fixture _fixture; - - public update_existing(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.UpdateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() => - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_filtered.cs deleted file mode 100644 index ff0f5ab5d..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_filtered.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_filtered - : IClassFixture { - const string Group = "existing-filtered"; - - readonly Fixture _fixture; - - public update_existing_filtered(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.UpdateToAllAsync( - Group, - new(true), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() => - await Client.CreateToAllAsync( - Group, - EventTypeFilter.Prefix("prefix-filter-"), - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point.cs deleted file mode 100644 index 05d6a70e3..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_with_check_point : IClassFixture { - private const string Group = "existing-with-check-point"; - - private readonly Fixture _fixture; - - public update_existing_with_check_point(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public void resumes_from_check_point() { - Assert.True(_fixture.Resumed.Event.Position > _fixture.CheckPoint); - } - - public class Fixture : EventStoreClientFixture { - private readonly List _appearedEvents; - private readonly EventData[] _events; - - private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; - private IAsyncEnumerator? _enumerator; - - public ResolvedEvent Resumed { get; private set; } - public Position CheckPoint { get; private set; } - - public Fixture() { - _appearedEvents = new(); - _events = CreateTestEvents(5).ToArray(); - } - - protected override async Task Given() { - foreach (var e in _events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - - await Client.CreateToAllAsync(Group, - new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: Position.Start), - userCredentials: TestCredentials.Root); - - _subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - _enumerator = _subscription.Messages.GetAsyncEnumerator(); - await _enumerator.MoveNextAsync(); - - await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); - - return; - - async Task Subscribe() { - while (await _enumerator.MoveNextAsync()) { - if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { - continue; - } - - _appearedEvents.Add(resolvedEvent); - await _subscription.Ack(resolvedEvent); - if (_appearedEvents.Count == _events.Length) { - break; - } - } - } - - async Task WaitForCheckpoint() { - await using var subscription = StreamsClient.SubscribeToStream( - $"$persistentsubscription-$all::{Group}-checkpoint", FromStream.Start, - userCredentials: TestCredentials.Root); - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - CheckPoint = resolvedEvent.Event.Data.ParsePosition(); - return; - } - } } - - protected override async Task When() { - // Force restart of the subscription - await Client.UpdateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - - try { - while (await _enumerator!.MoveNextAsync()) { } - } catch (PersistentSubscriptionDroppedByServerException) { } - - foreach (var e in _events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); - } - - await using var subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - Resumed = await subscription.Messages.OfType() - .Select(e => e.ResolvedEvent) - .Take(1) - .FirstAsync() - .AsTask() - .WithTimeout(); - } - - public override async Task DisposeAsync() { - if (_enumerator is not null) { - await _enumerator.DisposeAsync(); - } - - if (_subscription is not null) { - await _subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered.cs deleted file mode 100644 index 829aca768..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_check_point_filtered.cs +++ /dev/null @@ -1,111 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_with_check_point_filtered - : IClassFixture { - private const string Group = "existing-with-check-point-filtered"; - - private readonly Fixture _fixture; - - public update_existing_with_check_point_filtered(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public void resumes_from_check_point() { - Assert.True(_fixture.Resumed.Event.Position > _fixture.CheckPoint); - } - - public class Fixture : EventStoreClientFixture { - private readonly List _appearedEvents; - private readonly EventData[] _events; - - private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; - private IAsyncEnumerator? _enumerator; - - public ResolvedEvent Resumed { get; private set; } - public Position CheckPoint { get; private set; } - - public Fixture() { - _appearedEvents = new(); - _events = CreateTestEvents(5).ToArray(); - } - - - protected override async Task Given() { - foreach (var e in _events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); - } - - await Client.CreateToAllAsync(Group, StreamFilter.Prefix("test"), - new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: Position.Start), - userCredentials: TestCredentials.Root); - - _subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - _enumerator = _subscription.Messages.GetAsyncEnumerator(); - await _enumerator.MoveNextAsync(); - - await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); - - return; - - async Task Subscribe() { - while (await _enumerator.MoveNextAsync()) { - if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { - continue; - } - - _appearedEvents.Add(resolvedEvent); - await _subscription.Ack(resolvedEvent); - if (_appearedEvents.Count == _events.Length) { - break; - } - } - } - - async Task WaitForCheckpoint() { - await using var subscription = StreamsClient.SubscribeToStream( - $"$persistentsubscription-$all::{Group}-checkpoint", FromStream.Start, - userCredentials: TestCredentials.Root); - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - CheckPoint = resolvedEvent.Event.Data.ParsePosition(); - return; - } - } - } - - protected override async Task When() { - // Force restart of the subscription - await Client.UpdateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - - try { - while (await _enumerator!.MoveNextAsync()) { } - } catch (PersistentSubscriptionDroppedByServerException) { } - - foreach (var e in _events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); - } - - await using var subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - Resumed = await subscription.Messages.OfType() - .Select(e => e.ResolvedEvent) - .Take(1) - .FirstAsync() - .AsTask() - .WithTimeout(); - } - - public override async Task DisposeAsync() { - if (_enumerator is not null) { - await _enumerator.DisposeAsync(); - } - - if (_subscription is not null) { - await _subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_equal_to_last_indexed_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_equal_to_last_indexed_position.cs deleted file mode 100644 index 983349b85..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_equal_to_last_indexed_position.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_with_commit_position_equal_to_last_indexed_position - : IClassFixture { - const string Group = "existing"; - - readonly Fixture _fixture; - - public update_existing_with_commit_position_equal_to_last_indexed_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.UpdateToAllAsync( - Group, - new(startFrom: new Position(_fixture.LastCommitPosition, _fixture.LastCommitPosition)), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - public ulong LastCommitPosition; - - protected override async Task Given() { - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - var lastEvent = await StreamsClient.ReadAllAsync( - Direction.Backwards, - Position.End, - 1, - userCredentials: TestCredentials.Root - ).FirstAsync(); - - LastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_larger_than_last_indexed_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_larger_than_last_indexed_position.cs deleted file mode 100644 index 864e6a806..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_commit_position_larger_than_last_indexed_position.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_with_commit_position_larger_than_last_indexed_position - : IClassFixture { - const string Group = "existing"; - - readonly Fixture _fixture; - - public update_existing_with_commit_position_larger_than_last_indexed_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task fails() { - var ex = await Assert.ThrowsAsync( - () => - _fixture.Client.UpdateToAllAsync( - Group, - new(startFrom: new Position(_fixture.LastCommitPosition + 1, _fixture.LastCommitPosition)), - userCredentials: TestCredentials.Root - ) - ); - - Assert.Equal(StatusCode.Internal, ex.StatusCode); - } - - public class Fixture : EventStoreClientFixture { - public ulong LastCommitPosition; - - protected override async Task When() { - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - var lastEvent = await StreamsClient.ReadAllAsync( - Direction.Backwards, - Position.End, - 1, - userCredentials: TestCredentials.Root - ).FirstAsync(); - - LastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); - } - - protected override Task Given() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs deleted file mode 100644 index 71e89d260..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_with_subscribers.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_with_subscribers : IClassFixture { - private const string Group = "existing"; - - private readonly Fixture _fixture; - - public update_existing_with_subscribers(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task existing_subscriptions_are_dropped() { - var ex = await Assert.ThrowsAsync(async () => { - while (await _fixture.Enumerator!.MoveNextAsync()) { - } - }).WithTimeout(); - - Assert.Equal(SystemStreams.AllStream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; - public IAsyncEnumerator? Enumerator { get; private set; } - - protected override async Task Given() { - await Client.CreateToAllAsync(Group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); - - _subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - - Enumerator = _subscription.Messages.GetAsyncEnumerator(); - - await Enumerator.MoveNextAsync(); - } - - protected override Task When() => Client.UpdateToAllAsync(Group, new(), userCredentials: TestCredentials.Root); - - public override async Task DisposeAsync() { - if (Enumerator is not null) { - await Enumerator.DisposeAsync(); - } - - if (_subscription is not null) { - await _subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_without_permissions.cs deleted file mode 100644 index fa72629ca..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_existing_without_permissions.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_existing_without_permissions - : IClassFixture { - const string Group = "existing"; - - readonly Fixture _fixture; - - public update_existing_without_permissions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_fails_with_access_denied() => - await Assert.ThrowsAsync( - () => _fixture.Client.UpdateToAllAsync( - Group, - new() - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override async Task Given() => - await Client.CreateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_non_existent.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_non_existent.cs deleted file mode 100644 index 5082e86d0..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_non_existent.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_non_existent - : IClassFixture { - const string Group = "nonexistent"; - - readonly Fixture _fixture; - - public update_non_existent(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task the_completion_fails_with_not_found() => - await Assert.ThrowsAsync( - () => _fixture.Client.UpdateToAllAsync( - Group, - new(), - userCredentials: TestCredentials.Root - ) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_with_prepare_position_larger_than_commit_position.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_with_prepare_position_larger_than_commit_position.cs deleted file mode 100644 index ce53d81ae..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/update_with_prepare_position_larger_than_commit_position.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class update_with_prepare_position_larger_than_commit_position - : IClassFixture { - const string Group = "existing"; - - readonly Fixture _fixture; - - public update_with_prepare_position_larger_than_commit_position(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public Task fails_with_argument_out_of_range_exception() => - Assert.ThrowsAsync( - () => - _fixture.Client.UpdateToAllAsync( - Group, - new(startFrom: new Position(0, 1)), - userCredentials: TestCredentials.Root - ) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events.cs deleted file mode 100644 index cda94d8b3..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_filtering_out_events.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class when_writing_and_filtering_out_events : IClassFixture { - private const string Group = "filtering-out-events"; - - private readonly Fixture _fixture; - - public when_writing_and_filtering_out_events(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public void it_should_write_a_check_point() { - Assert.True(_fixture.SecondCheckPoint > _fixture.FirstCheckPoint); - Assert.Equal(_fixture.Events.Select(e => e.EventId), _fixture.AppearedEvents.Select(e => e.Event.EventId)); - } - - public class Fixture : EventStoreClientFixture { - private readonly List _appearedEvents; - - public Fixture() { - Events = CreateTestEvents(10).ToArray(); - _appearedEvents = new(); - } - - public Position SecondCheckPoint { get; private set; } - public Position FirstCheckPoint { get; private set; } - public EventData[] Events { get; } - public ResolvedEvent[] AppearedEvents => _appearedEvents.ToArray(); - - protected override async Task Given() { - foreach (var e in Events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - - await Client.CreateToAllAsync(Group, StreamFilter.Prefix("test"), - new(checkPointLowerBound: 1, checkPointUpperBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), - startFrom: Position.Start), userCredentials: TestCredentials.Root); - - await using var subscription = Client.SubscribeToAll(Group, userCredentials: TestCredentials.Root); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoints().WithTimeout()); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { - continue; - } - - _appearedEvents.Add(resolvedEvent); - await subscription.Ack(resolvedEvent); - if (_appearedEvents.Count == Events.Length) { - break; - } - } - } - - async Task WaitForCheckpoints() { - bool firstCheckpointSet = false; - await using var subscription = StreamsClient.SubscribeToStream( - $"$persistentsubscription-$all::{Group}-checkpoint", FromStream.Start, - userCredentials: TestCredentials.Root); - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - - if (!firstCheckpointSet) { - FirstCheckPoint = resolvedEvent.Event.Data.ParsePosition(); - firstCheckpointSet = true; - } else { - SecondCheckPoint = resolvedEvent.Event.Data.ParsePosition(); - return; - } - } - } - } - - protected override async Task When() { - foreach (var e in Events) { - await StreamsClient.AppendToStreamAsync("filtered-out-stream-" + Guid.NewGuid(), StreamState.Any, - new[] { e }); - } - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack.cs deleted file mode 100644 index 72e0cfd00..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/when_writing_and_subscribing_to_normal_events_manual_nack.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll; - -public class when_writing_and_subscribing_to_normal_events_manual_nack - : IClassFixture { - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public when_writing_and_subscribing_to_normal_events_manual_nack(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .SelectAwait(async e => { - await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e.ResolvedEvent); - return e; - }) - .Where(e => e.ResolvedEvent.OriginalStreamId.StartsWith("test-")) - .Take(_fixture.Events.Length) - .ToArrayAsync() - .AsTask() - .WithTimeout(); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(EventWriteCount).ToArray(); - } - - protected override async Task Given() { - await Client.CreateToAllAsync(Group, new(startFrom: Position.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToAll(Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); - } - - protected override async Task When() { - foreach (var e in Events) { - await StreamsClient.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); - } - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs deleted file mode 100644 index 8f290088c..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_max_one_client_obsolete.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_max_one_client_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - const string Stream = nameof(connect_to_existing_with_max_one_client_obsolete); - readonly Fixture _fixture; - - public connect_to_existing_with_max_one_client_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_second_subscription_fails_to_connect() { - using var first = await _fixture.Client.SubscribeToStreamAsync( - Stream, - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ).WithTimeout(); - - var ex = await Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToStreamAsync( - Stream, - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ); - } - ).WithTimeout(); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToStreamAsync( - Stream, - Group, - new(maxSubscriberCount: 1), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_permissions_obsolete.cs deleted file mode 100644 index 70ada6f17..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_permissions_obsolete.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_permissions_obsolete - : IClassFixture { - const string Stream = nameof(connect_to_existing_with_permissions_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_succeeds() { - var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>(); - using var subscription = await _fixture.Client.SubscribeToStreamAsync( - Stream, - "agroupname17", - delegate { return Task.CompletedTask; }, - (s, reason, ex) => dropped.TrySetResult((reason, ex)), - TestCredentials.Root - ).WithTimeout(); - - Assert.NotNull(subscription); - - await Assert.ThrowsAsync(() => dropped.Task.WithTimeout()); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToStreamAsync( - Stream, - "agroupname17", - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete.cs deleted file mode 100644 index c023db1f8..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - - const string Stream = - nameof(connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_beginning_and_events_in_it_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_event_zero_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events[0].EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(10).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_no_stream_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_no_stream_obsolete.cs deleted file mode 100644 index 037ec52e5..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_beginning_and_no_stream_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_beginning_and_no_stream_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - - const string Stream = - nameof(connect_to_existing_with_start_from_beginning_and_no_stream_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_beginning_and_no_stream_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_event_zero_as_its_first_event() { - var firstEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(StreamPosition.Start, firstEvent.Event.EventNumber); - Assert.Equal(_fixture.EventId, firstEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents().ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - public Uuid EventId => Events.Single().EventId; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete.cs deleted file mode 100644 index f0bd49b06..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - - const string Stream = - nameof(connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_not_set_and_events_in_it_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_no_events() => await Assert.ThrowsAsync(() => _fixture.FirstEvent.WithTimeout()); - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(10).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete.cs deleted file mode 100644 index ea1987ceb..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - const string Stream = nameof(connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete); - - readonly Fixture _fixture; - - public - connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written_obsolete(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(new(10), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - - public readonly EventData[] Events; - - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(11).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete.cs deleted file mode 100644 index b25765019..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - - const string Stream = - nameof(connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_no_events() => await Assert.ThrowsAsync(() => _fixture.FirstEvent.WithTimeout()); - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(10).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.End), - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() => - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete.cs deleted file mode 100644 index 9bc45141b..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete - : IClassFixture< - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete - .Fixture> { - const string Group = "startinbeginning1"; - - const string Stream = - nameof( - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete - ); - - readonly Fixture _fixture; - - public - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written_obsolete(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(new(10), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(11).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.End), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_two_and_no_stream_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_two_and_no_stream_obsolete.cs deleted file mode 100644 index 09d6e4f21..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_two_and_no_stream_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_two_and_no_stream_obsolete - : IClassFixture { - const string Group = "startinbeginning1"; - - const string Stream = - nameof(connect_to_existing_with_start_from_two_and_no_stream_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_two_and_no_stream_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_event_two_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(new(2), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(3).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - public Uuid EventId => Events.Last().EventId; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(2)), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete.cs deleted file mode 100644 index c74f8b45f..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete - : IClassFixture { - const string Group = "startinx2"; - - const string Stream = - nameof(connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_x_set_and_events_in_it_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(new(4), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Skip(4).First().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(10).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(4)), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete.cs deleted file mode 100644 index a3fae30a1..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete - : IClassFixture< - connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete. - Fixture> { - const string Group = "startinbeginning1"; - - const string Stream = - nameof(connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete - ); - - readonly Fixture _fixture; - - public connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(new(10), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(11).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(10)), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete.cs deleted file mode 100644 index 59198a660..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete - : IClassFixture< - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete - .Fixture> { - const string Group = "startinbeginning1"; - - const string Stream = - nameof( - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete - ); - - readonly Fixture _fixture; - - public - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written_obsolete(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(new(11), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(12).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(11)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(11)), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_without_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_without_permissions_obsolete.cs deleted file mode 100644 index 25942cd05..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_existing_without_permissions_obsolete.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_existing_without_permissions_obsolete - : IClassFixture { - const string Stream = "$" + nameof(connect_to_existing_without_permissions_obsolete); - readonly Fixture _fixture; - public connect_to_existing_without_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task throws_access_denied() => - Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToStreamAsync( - Stream, - "agroupname55", - delegate { return Task.CompletedTask; } - ); - } - ).WithTimeout(); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToStreamAsync( - Stream, - "agroupname55", - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs deleted file mode 100644 index dd9d74900..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_to_non_existing_with_permissions_obsolete.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_to_non_existing_with_permissions_obsolete - : IClassFixture { - const string Stream = nameof(connect_to_non_existing_with_permissions_obsolete); - const string Group = "foo"; - - readonly Fixture _fixture; - - public connect_to_non_existing_with_permissions_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task throws_persistent_subscription_not_found() { - var ex = await Assert.ThrowsAsync( - async () => { - using var _ = await _fixture.Client.SubscribeToStreamAsync( - Stream, - Group, - delegate { return Task.CompletedTask; }, - userCredentials: TestCredentials.Root - ); - } - ).WithTimeout(); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_with_retries_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_with_retries_obsolete.cs deleted file mode 100644 index d31fcc961..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connect_with_retries_obsolete.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class connect_with_retries_obsolete : IClassFixture { - const string Group = "retries"; - const string Stream = nameof(connect_with_retries_obsolete); - - readonly Fixture _fixture; - - public connect_with_retries_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task events_are_retried_until_success() => Assert.Equal(5, await _fixture.RetryCount.WithTimeout()); - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _retryCountSource; - - public readonly EventData[] Events; - - PersistentSubscription? _subscription; - - public Fixture() { - _retryCountSource = new(); - - Events = CreateTestEvents().ToArray(); - } - - public Task RetryCount => _retryCountSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - if (r > 4) { - _retryCountSource.TrySetResult(r.Value); - await subscription.Ack(e.Event.EventId); - } - else { - await subscription.Nack( - PersistentSubscriptionNakEventAction.Retry, - "Not yet tried enough times", - e - ); - } - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _retryCountSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connecting_to_a_persistent_subscription_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connecting_to_a_persistent_subscription_obsolete.cs deleted file mode 100644 index af09f9867..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/connecting_to_a_persistent_subscription_obsolete.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class - connecting_to_a_persistent_subscription_obsolete - : IClassFixture< - connecting_to_a_persistent_subscription_obsolete - .Fixture> { - const string Group = "startinbeginning1"; - - const string Stream = - nameof( - connecting_to_a_persistent_subscription_obsolete - ); - - readonly Fixture _fixture; - - public - connecting_to_a_persistent_subscription_obsolete(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.FirstEvent.WithTimeout(); - Assert.Equal(new(11), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _firstEventSource; - public readonly EventData[] Events; - PersistentSubscription? _subscription; - - public Fixture() { - _firstEventSource = new(); - Events = CreateTestEvents(12).ToArray(); - } - - public Task FirstEvent => _firstEventSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(11)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(11)), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, r, ct) => { - _firstEventSource.TrySetResult(e); - await subscription.Ack(e); - }, - (subscription, reason, ex) => { - if (reason != SubscriptionDroppedReason.Disposed) - _firstEventSource.TrySetException(ex!); - }, - TestCredentials.TestUser1 - ); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/deleting_existing_with_subscriber_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/deleting_existing_with_subscriber_obsolete.cs deleted file mode 100644 index d684c76af..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/deleting_existing_with_subscriber_obsolete.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class deleting_existing_with_subscriber_obsolete - : IClassFixture { - const string Stream = nameof(deleting_existing_with_subscriber_obsolete); - readonly Fixture _fixture; - - public deleting_existing_with_subscriber_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_is_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal("groupname123", ex.GroupName); - } - - [Fact(Skip = "Isn't this how it should work?")] - public async Task the_subscription_is_dropped_with_not_found() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - Assert.Equal(Stream, ex.StreamName); - Assert.Equal("groupname123", ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped; - PersistentSubscription? _subscription; - - public Fixture() => _dropped = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _dropped.Task; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - "groupname123", - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - "groupname123", - (_, _, _, _) => Task.CompletedTask, - (_, r, e) => _dropped.TrySetResult((r, e)), - TestCredentials.Root - ); - } - - protected override Task When() => - Client.DeleteToStreamAsync( - Stream, - "groupname123", - userCredentials: TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs deleted file mode 100644 index e69e88fb2..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_link_to_events_manual_ack_obsolete.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Text; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_catching_up_to_link_to_events_manual_ack_obsolete - : IClassFixture { - const string Stream = nameof(happy_case_catching_up_to_link_to_events_manual_ack_obsolete); - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public happy_case_catching_up_to_link_to_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount) - .Select( - (e, i) => new EventData( - e.EventId, - SystemEventTypes.LinkTo, - Encoding.UTF8.GetBytes($"{i}@{Stream}"), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - ) - ) - .ToArray(); - - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - - if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs deleted file mode 100644 index 6973087cc..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_catching_up_to_normal_events_manual_ack_obsolete.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_catching_up_to_normal_events_manual_ack_obsolete : IClassFixture { - const string Stream = nameof(happy_case_catching_up_to_normal_events_manual_ack_obsolete); - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public happy_case_catching_up_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - - if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs deleted file mode 100644 index af07bcb4c..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete - : IClassFixture { - const string Stream = nameof(happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete); - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public happy_case_writing_and_subscribing_to_normal_events_manual_ack_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.End, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Ack(e); - if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - } - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_check_point_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_check_point_obsolete.cs deleted file mode 100644 index f5922c746..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_check_point_obsolete.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class update_existing_with_check_point_obsolete - : IClassFixture { - const string Stream = nameof(update_existing_with_check_point_obsolete); - const string Group = "existing-with-check-point"; - readonly Fixture _fixture; - - public update_existing_with_check_point_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task resumes_from_check_point() { - var resumedEvent = await _fixture.Resumed.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(_fixture.CheckPoint.Next(), resumedEvent.Event.EventNumber); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource _appeared; - readonly List _appearedEvents; - - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - readonly EventData[] _events; - readonly TaskCompletionSource _resumedSource; - PersistentSubscription? _firstSubscription; - PersistentSubscription? _secondSubscription; - - public Fixture() { - _droppedSource = new(); - _resumedSource = new(); - _appeared = new(); - _appearedEvents = new(); - _events = CreateTestEvents(5).ToArray(); - } - - public Task Resumed => _resumedSource.Task; - public StreamPosition CheckPoint { get; private set; } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, _events); - - await Client.CreateToStreamAsync( - Stream, - Group, - new( - checkPointLowerBound: 5, - checkPointAfter: TimeSpan.FromSeconds(1), - startFrom: StreamPosition.Start - ), - userCredentials: TestCredentials.Root - ); - - var checkPointStream = $"$persistentsubscription-{Stream}::{Group}-checkpoint"; - - _firstSubscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (s, e, _, _) => { - _appearedEvents.Add(e); - await s.Ack(e); - - if (_appearedEvents.Count == _events.Length) - _appeared.TrySetResult(true); - }, - (_, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); - - await Task.WhenAll(_appeared.Task.WithTimeout(), Checkpointed()); - - return; - - async Task Checkpointed() { - await using var subscription = StreamsClient.SubscribeToStream(checkPointStream, FromStream.Start, - userCredentials: TestCredentials.Root); - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event(var resolvedEvent)) { - continue; - } - CheckPoint = resolvedEvent.Event.Data.ParseStreamPosition(); - return; - } - - throw new InvalidOperationException(); - } - } - - protected override async Task When() { - // Force restart of the subscription - await Client.UpdateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - await _droppedSource.Task.WithTimeout(); - - _secondSubscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (s, e, _, _) => { - _resumedSource.TrySetResult(e); - await s.Ack(e); - }, - (_, reason, ex) => { - if (ex is not null) - _resumedSource.TrySetException(ex); - else - _resumedSource.TrySetResult(default); - }, - TestCredentials.Root - ); - - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, CreateTestEvents(1)); - } - - public override Task DisposeAsync() { - _firstSubscription?.Dispose(); - _secondSubscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_subscribers_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_subscribers_obsolete.cs deleted file mode 100644 index 7ff215206..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/update_existing_with_subscribers_obsolete.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class update_existing_with_subscribers_obsolete - : IClassFixture { - const string Stream = nameof(update_existing_with_subscribers_obsolete); - const string Group = "existing"; - readonly Fixture _fixture; - - public update_existing_with_subscribers_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task existing_subscriptions_are_dropped() { - var (reason, exception) = await _fixture.Dropped.WithTimeout(TimeSpan.FromSeconds(10)); - Assert.Equal(SubscriptionDroppedReason.ServerError, reason); - var ex = Assert.IsType(exception); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - readonly TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _droppedSource; - PersistentSubscription? _subscription; - - public Fixture() => _droppedSource = new(); - - public Task<(SubscriptionDroppedReason, Exception?)> Dropped => _droppedSource.Task; - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, CreateTestEvents()); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - delegate { return Task.CompletedTask; }, - (_, reason, ex) => _droppedSource.TrySetResult((reason, ex)), - TestCredentials.Root - ); - } - - protected override Task When() => - Client.UpdateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs deleted file mode 100644 index 96a2d0af0..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/when_writing_and_subscribing_to_normal_events_manual_nack_obsolete.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class when_writing_and_subscribing_to_normal_events_manual_nack_obsolete - : IClassFixture { - const string Stream = nameof(when_writing_and_subscribing_to_normal_events_manual_nack_obsolete); - const string Group = nameof(Group); - const int BufferCount = 10; - const int EventWriteCount = BufferCount * 2; - - readonly Fixture _fixture; - - public when_writing_and_subscribing_to_normal_events_manual_nack_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() => await _fixture.EventsReceived.WithTimeout(); - - public class Fixture : EventStoreClientFixture { - readonly EventData[] _events; - readonly TaskCompletionSource _eventsReceived; - int _eventReceivedCount; - - PersistentSubscription? _subscription; - - public Fixture() { - _events = CreateTestEvents(EventWriteCount) - .ToArray(); - - _eventsReceived = new(); - } - - public Task EventsReceived => _eventsReceived.Task; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root - ); - - _subscription = await Client.SubscribeToStreamAsync( - Stream, - Group, - async (subscription, e, retryCount, ct) => { - await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e); - - if (Interlocked.Increment(ref _eventReceivedCount) == _events.Length) - _eventsReceived.TrySetResult(true); - }, - (s, r, e) => { - if (e != null) - _eventsReceived.TrySetException(e); - }, - bufferSize: BufferCount, - userCredentials: TestCredentials.Root - ); - } - - protected override async Task When() { - foreach (var e in _events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - } - - public override Task DisposeAsync() { - _subscription?.Dispose(); - return base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/can_create_duplicate_name_on_different_streams.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/can_create_duplicate_name_on_different_streams.cs deleted file mode 100644 index cb9af1bd6..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/can_create_duplicate_name_on_different_streams.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class can_create_duplicate_name_on_different_streams - : IClassFixture { - const string Stream = - nameof(can_create_duplicate_name_on_different_streams); - - readonly Fixture _fixture; - - public can_create_duplicate_name_on_different_streams(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task the_completion_succeeds() => - _fixture.Client.CreateToStreamAsync( - "someother" + Stream, - "group3211", - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => - Client.CreateToStreamAsync( - Stream, - "group3211", - new(), - userCredentials: TestCredentials.Root - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client.cs deleted file mode 100644 index eb5e442ae..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_max_one_client.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_max_one_client - : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connect_to_existing_with_max_one_client); - private readonly Fixture _fixture; - - public connect_to_existing_with_max_one_client(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_second_subscription_fails_to_connect() { - var ex = await Assert.ThrowsAsync( - () => Task.WhenAll(Subscribe().WithTimeout(), Subscribe().WithTimeout())); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - return; - - async Task Subscribe() { - await using var subscription = - _fixture.Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); - await subscription.Messages.AnyAsync(); - } - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Client.CreateToStreamAsync(Stream, Group, new(maxSubscriberCount: 1), - userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions.cs deleted file mode 100644 index 70f4b51f0..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_permissions.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_permissions - : IClassFixture { - private const string Stream = nameof(connect_to_existing_with_permissions); - - private readonly Fixture _fixture; - - public connect_to_existing_with_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_succeeds() { - await using var subscription = - _fixture.Client.SubscribeToStream(Stream, "agroupname17", userCredentials: TestCredentials.Root); - - Assert.True(await subscription.Messages - .FirstAsync().AsTask().WithTimeout() is PersistentSubscriptionMessage.SubscriptionConfirmation); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateToStreamAsync(Stream, "agroupname17", new(), userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it.cs deleted file mode 100644 index 2c0f5225e..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_events_in_it.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_beginning_and_events_in_it - : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connect_to_existing_with_start_from_beginning_and_events_in_it); - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_beginning_and_events_in_it(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_event_zero_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events[0].EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(10).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root); - } - - protected override Task When() { - Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); - - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream.cs deleted file mode 100644 index a6e9cb26b..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_beginning_and_no_stream.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_beginning_and_no_stream - : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connect_to_existing_with_start_from_beginning_and_no_stream); - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_beginning_and_no_stream(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_event_zero_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents().ToArray(); - } - - public Uuid EventId => Events.Single().EventId; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it.cs deleted file mode 100644 index cef6a28a7..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -[Obsolete] -public class connect_to_existing_with_start_from_not_set_and_events_in_it - : IClassFixture { - private const string Group = "startinbeginning1"; - - private const string Stream = nameof(connect_to_existing_with_start_from_not_set_and_events_in_it); - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_not_set_and_events_in_it(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_no_events() => - await Assert.ThrowsAsync( - () => _fixture.Subscription!.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event) - .AsTask().WithTimeout(TimeSpan.FromMilliseconds(250))); - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(10).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() { - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written.cs deleted file mode 100644 index 08a2172d4..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written - : IClassFixture { - private const string Group = "startinbeginning1"; - - private const string Stream = - nameof(connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written); - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(new(10), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(11).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => - StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it.cs deleted file mode 100644 index f2bf5e807..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_set_to_end_position_and_events_in_it - : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connect_to_existing_with_start_from_set_to_end_position_and_events_in_it); - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_set_to_end_position_and_events_in_it(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_no_events() => - await Assert.ThrowsAsync( - () => _fixture.Subscription!.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event) - .AsTask().WithTimeout(TimeSpan.FromMilliseconds(250))); - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(10).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.End), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() { - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - return Task.CompletedTask; - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written.cs deleted file mode 100644 index 2e7b8a4c1..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written - : IClassFixture { - private const string Group = "startinbeginning1"; - - private const string Stream = - nameof(connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written); - - private readonly Fixture _fixture; - - public - connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(new(10), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(11).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: StreamPosition.End), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => - StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream.cs deleted file mode 100644 index 0199eaa3b..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_two_and_no_stream.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_two_and_no_stream - : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connect_to_existing_with_start_from_two_and_no_stream); - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_two_and_no_stream(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_event_two_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - - Assert.Equal(new(2), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(3).ToArray(); - } - - public Uuid EventId => Events.Last().EventId; - - protected override async Task Given() { - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(2)), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it.cs deleted file mode 100644 index 3a7edafba..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_x_set_and_events_in_it - : IClassFixture { - private const string Group = "startinx2"; - private const string Stream = nameof(connect_to_existing_with_start_from_x_set_and_events_in_it); - - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_x_set_and_events_in_it(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages - .OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - Assert.Equal(new(4), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Skip(4).First().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(10).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(4)), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => - StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written.cs deleted file mode 100644 index 276bae9aa..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written - : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written); - private readonly Fixture _fixture; - - public connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - Assert.Equal(new(10), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(11).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(10)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(10)), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => - StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(9), Events.Skip(10)); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written.cs deleted file mode 100644 index f4b249372..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written : - IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = - nameof(connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written); - - private readonly Fixture _fixture; - - public - connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written(Fixture fixture) => - _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - Assert.Equal(new(11), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(12).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(11)); - await Client.CreateToStreamAsync( - Stream, - Group, - new(startFrom: new StreamPosition(11)), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream( - Stream, - Group, - userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => - StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions.cs deleted file mode 100644 index 756de2519..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_existing_without_permissions.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_existing_without_permissions : IClassFixture { - private const string Stream = $"${nameof(connect_to_existing_without_permissions)}"; - private readonly Fixture _fixture; - public connect_to_existing_without_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task throws_access_denied() => - Assert.ThrowsAsync( - async () => { - await using var subscription = _fixture.Client.SubscribeToStream(Stream, "agroupname55"); - await subscription.Messages.AnyAsync(); - }).WithTimeout(); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToStreamAsync( - Stream, - "agroupname55", - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions.cs deleted file mode 100644 index 3e13c3983..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_to_non_existing_with_permissions.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_to_non_existing_with_permissions : IClassFixture { - private const string Stream = nameof(connect_to_non_existing_with_permissions); - private const string Group = "foo"; - - private readonly Fixture _fixture; - - public connect_to_non_existing_with_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task throws_persistent_subscription_not_found() { - await using var subscription = _fixture.Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); - Assert.True(await subscription.Messages.OfType() - .AnyAsync() - .AsTask() - .WithTimeout()); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries.cs deleted file mode 100644 index 89995fedf..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connect_with_retries.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connect_with_retries : IClassFixture { - private const string Group = "retries"; - private const string Stream = nameof(connect_with_retries); - - private readonly Fixture _fixture; - - public connect_with_retries(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task events_are_retried_until_success() { - var retryCount = await _fixture.Subscription!.Messages.OfType() - .SelectAwait(async e => { - if (e.RetryCount > 4) { - await _fixture.Subscription.Ack(e.ResolvedEvent); - } else { - await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Retry, - "Not yet tried enough times", e.ResolvedEvent); - } - - return e.RetryCount; - }) - .Where(retryCount => retryCount > 4) - .FirstOrDefaultAsync() - .AsTask() - .WithTimeout(); - - Assert.Equal(5, retryCount); - } - - public class Fixture : EventStoreClientFixture { - - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents().ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription.cs deleted file mode 100644 index a9dbb4b59..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/connecting_to_a_persistent_subscription.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class connecting_to_a_persistent_subscription : IClassFixture { - private const string Group = "startinbeginning1"; - private const string Stream = nameof(connecting_to_a_persistent_subscription); - - private readonly Fixture _fixture; - - public connecting_to_a_persistent_subscription(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_gets_the_written_event_as_its_first_event() { - var resolvedEvent = await _fixture.Subscription!.Messages.OfType() - .Select(e => e.ResolvedEvent) - .FirstOrDefaultAsync().AsTask().WithTimeout(); - Assert.Equal(new(11), resolvedEvent.Event.EventNumber); - Assert.Equal(_fixture.Events.Last().EventId, resolvedEvent.Event.EventId); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(12).ToArray(); - } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, Events.Take(11)); - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: new StreamPosition(11)), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.TestUser1); - } - - protected override Task When() => - StreamsClient.AppendToStreamAsync(Stream, new StreamRevision(10), Events.Skip(11)); - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_after_deleting_the_same.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_after_deleting_the_same.cs deleted file mode 100644 index 3e8e0a6c5..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_after_deleting_the_same.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class create_after_deleting_the_same - : IClassFixture { - const string Stream = nameof(create_after_deleting_the_same); - readonly Fixture _fixture; - - public create_after_deleting_the_same(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.CreateToStreamAsync( - Stream, - "existing", - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override async Task When() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, CreateTestEvents()); - await Client.CreateToStreamAsync( - Stream, - "existing", - new(), - userCredentials: TestCredentials.Root - ); - - await Client.DeleteToStreamAsync( - Stream, - "existing", - userCredentials: TestCredentials.Root - ); - } - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_duplicate.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_duplicate.cs deleted file mode 100644 index c67748a7a..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_duplicate.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class create_duplicate - : IClassFixture { - const string Stream = nameof(create_duplicate); - readonly Fixture _fixture; - - public create_duplicate(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_completion_fails() { - var ex = await Assert.ThrowsAsync( - () => _fixture.Client.CreateToStreamAsync( - Stream, - "group32", - new(), - userCredentials: TestCredentials.Root - ) - ); - - Assert.Equal(StatusCode.AlreadyExists, ex.StatusCode); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => - Client.CreateToStreamAsync( - Stream, - "group32", - new(), - userCredentials: TestCredentials.Root - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_existing_stream.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_existing_stream.cs deleted file mode 100644 index 6ad81a6db..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_existing_stream.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class create_on_existing_stream - : IClassFixture { - const string Stream = nameof(create_on_existing_stream); - readonly Fixture _fixture; - - public create_on_existing_stream(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task the_completion_succeeds() => - _fixture.Client.CreateToStreamAsync( - Stream, - "existing", - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override async Task When() => await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, CreateTestEvents()); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_non_existing_stream.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_non_existing_stream.cs deleted file mode 100644 index f0d7e0fba..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_on_non_existing_stream.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class create_on_non_existing_stream - : IClassFixture { - const string Stream = nameof(create_on_non_existing_stream); - readonly Fixture _fixture; - - public create_on_non_existing_stream(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.CreateToStreamAsync( - Stream, - "nonexistinggroup", - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_persistent_subscription_with_dont_timeout.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_persistent_subscription_with_dont_timeout.cs deleted file mode 100644 index d32c65b4c..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_persistent_subscription_with_dont_timeout.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class create_with_dont_timeout - : IClassFixture { - const string Stream = nameof(create_with_dont_timeout); - readonly Fixture _fixture; - - public create_with_dont_timeout(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task the_subscription_is_created_without_error() => - _fixture.Client.CreateToStreamAsync( - Stream, - "dont-timeout", - new(messageTimeout: TimeSpan.Zero), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_without_permissions.cs deleted file mode 100644 index d2838cf34..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/create_without_permissions.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class create_without_permissions - : IClassFixture { - const string Stream = nameof(create_without_permissions); - readonly Fixture _fixture; - - public create_without_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task the_completion_fails_with_access_denied() => - Assert.ThrowsAsync( - () => - _fixture.Client.CreateToStreamAsync( - Stream, - "group57", - new() - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_permissions.cs deleted file mode 100644 index 4d3a0f17d..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_permissions.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class deleting_existing_with_permissions - : IClassFixture { - const string Stream = nameof(deleting_existing_with_permissions); - readonly Fixture _fixture; - - public deleting_existing_with_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public Task the_delete_of_group_succeeds() => - _fixture.Client.DeleteToStreamAsync( - Stream, - "groupname123", - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => - Client.CreateToStreamAsync( - Stream, - "groupname123", - new(), - userCredentials: TestCredentials.Root - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs deleted file mode 100644 index dae83cec6..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_existing_with_subscriber.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class deleting_existing_with_subscriber : IClassFixture { - private const string Stream = nameof(deleting_existing_with_subscriber); - private readonly Fixture _fixture; - - public deleting_existing_with_subscriber(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_subscription_is_dropped_with_not_found() { - await using var subscription = _fixture.Client.SubscribeToStream(Stream, "groupname123", userCredentials: TestCredentials.Root); - - Assert.True(await subscription.Messages.OfType().AnyAsync() - .AsTask() - .WithTimeout()); - } - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() { - await Client.CreateToStreamAsync(Stream, "groupname123", new(), userCredentials: TestCredentials.Root); - } - - protected override Task When() => - Client.DeleteToStreamAsync(Stream, "groupname123", userCredentials: TestCredentials.Root); - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_nonexistent.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_nonexistent.cs deleted file mode 100644 index 58ad6bc13..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_nonexistent.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class deleting_nonexistent - : IClassFixture { - const string Stream = nameof(deleting_nonexistent); - readonly Fixture _fixture; - - public deleting_nonexistent(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_delete_fails_with_argument_exception() => - await Assert.ThrowsAsync( - () => _fixture.Client.DeleteToStreamAsync( - Stream, - Guid.NewGuid().ToString(), - userCredentials: TestCredentials.Root - ) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_without_permissions.cs deleted file mode 100644 index 31dab0ba4..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/deleting_without_permissions.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class deleting_without_permissions - : IClassFixture { - const string Stream = nameof(deleting_without_permissions); - readonly Fixture _fixture; - - public deleting_without_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_delete_fails_with_access_denied() => - await Assert.ThrowsAsync( - () => _fixture.Client.DeleteToStreamAsync( - Stream, - Guid.NewGuid().ToString() - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info.cs deleted file mode 100644 index bec3c4118..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/get_info.cs +++ /dev/null @@ -1,198 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class get_info : IClassFixture { - private const string GroupName = nameof(get_info); - private const string StreamName = nameof(get_info); - - private static readonly PersistentSubscriptionSettings _settings = new( - true, - StreamPosition.Start, - true, - TimeSpan.FromSeconds(9), - 11, - 303, - 30, - 909, - TimeSpan.FromSeconds(1), - 1, - 1, - 500, - SystemConsumerStrategies.RoundRobin - ); - - private readonly Fixture _fixture; - - public get_info(Fixture fixture) => _fixture = fixture; - - public static IEnumerable AllowedUsers() { - yield return new object[] { TestCredentials.Root }; - yield return new object[] { TestCredentials.TestUser1 }; - } - - [Theory] - [MemberData(nameof(AllowedUsers))] - public async Task returns_expected_result(UserCredentials credentials) { - var result = await _fixture.Client.GetInfoToStreamAsync(StreamName, GroupName, userCredentials: credentials); - - Assert.Equal(StreamName, result.EventSource); - Assert.Equal(GroupName, result.GroupName); - Assert.NotNull(_settings.StartFrom); - Assert.True(result.Stats.TotalItems > 0); - Assert.True(result.Stats.OutstandingMessagesCount > 0); - Assert.True(result.Stats.AveragePerSecond >= 0); - Assert.True(result.Stats.ParkedMessageCount >= 0); - Assert.True(result.Stats.AveragePerSecond >= 0); - Assert.True(result.Stats.ReadBufferCount >= 0); - Assert.True(result.Stats.RetryBufferCount >= 0); - Assert.True(result.Stats.CountSinceLastMeasurement >= 0); - Assert.True(result.Stats.TotalInFlightMessages >= 0); - Assert.NotNull(result.Stats.LastKnownEventPosition); - Assert.NotNull(result.Stats.LastCheckpointedEventPosition); - Assert.True(result.Stats.LiveBufferCount >= 0); - - Assert.NotNull(result.Connections); - Assert.NotEmpty(result.Connections); - var connection = result.Connections.First(); - Assert.NotNull(connection.From); - Assert.Equal(TestCredentials.Root.Username, connection.Username); - Assert.NotEmpty(connection.ConnectionName); - Assert.True(connection.AverageItemsPerSecond >= 0); - Assert.True(connection.TotalItems >= 0); - Assert.True(connection.CountSinceLastMeasurement >= 0); - Assert.True(connection.AvailableSlots >= 0); - Assert.True(connection.InFlightMessages >= 0); - Assert.NotNull(connection.ExtraStatistics); - Assert.NotEmpty(connection.ExtraStatistics); - - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Highest); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Mean); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Median); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Fastest); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile1); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile2); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile3); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile4); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.Quintile5); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyPercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyFivePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointFivePercent); - AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointNinePercent); - - Assert.NotNull(result.Settings); - Assert.Equal(_settings.StartFrom, result.Settings!.StartFrom); - Assert.Equal(_settings.ResolveLinkTos, result.Settings!.ResolveLinkTos); - Assert.Equal(_settings.ExtraStatistics, result.Settings!.ExtraStatistics); - Assert.Equal(_settings.MessageTimeout, result.Settings!.MessageTimeout); - Assert.Equal(_settings.MaxRetryCount, result.Settings!.MaxRetryCount); - Assert.Equal(_settings.LiveBufferSize, result.Settings!.LiveBufferSize); - Assert.Equal(_settings.ReadBatchSize, result.Settings!.ReadBatchSize); - Assert.Equal(_settings.HistoryBufferSize, result.Settings!.HistoryBufferSize); - Assert.Equal(_settings.CheckPointAfter, result.Settings!.CheckPointAfter); - Assert.Equal(_settings.CheckPointLowerBound, result.Settings!.CheckPointLowerBound); - Assert.Equal(_settings.CheckPointUpperBound, result.Settings!.CheckPointUpperBound); - Assert.Equal(_settings.MaxSubscriberCount, result.Settings!.MaxSubscriberCount); - Assert.Equal(_settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName); - } - - [Fact] - public async Task throws_when_given_non_existing_subscription() => - await Assert.ThrowsAsync( - async () => { - await _fixture.Client.GetInfoToStreamAsync( - "NonExisting", - "NonExisting", - userCredentials: TestCredentials.Root - ); - } - ); - - [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - async () => { - await _fixture.Client.GetInfoToStreamAsync( - "NonExisting", - "NonExisting", - userCredentials: TestCredentials.TestBadUser - ); - } - ); - - [Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - async () => { - await _fixture.Client.GetInfoToStreamAsync( - "NonExisting", - "NonExisting" - ); - } - ); - - [Fact] - public async Task returns_result_for_normal_user() { - var result = await _fixture.Client.GetInfoToStreamAsync( - StreamName, - GroupName, - userCredentials: TestCredentials.TestUser1 - ); - - Assert.NotNull(result); - } - - private void AssertKeyAndValue(IDictionary items, string key) { - Assert.True(items.ContainsKey(key)); - Assert.True(items[key] > 0); - } - - public class Fixture : EventStoreClientFixture { - private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; - private IAsyncEnumerator? _enumerator; - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToStreamAsync(groupName: GroupName, streamName: StreamName, settings: _settings, - userCredentials: TestCredentials.Root); - - protected override async Task When() { - var counter = 0; - _subscription = Client.SubscribeToStream(StreamName, GroupName, userCredentials: TestCredentials.Root); - _enumerator = _subscription.Messages.GetAsyncEnumerator(); - - for (var i = 0; i < 15; i++) { - await StreamsClient.AppendToStreamAsync(StreamName, StreamState.Any, - new[] { new EventData(Uuid.NewUuid(), "test-event", ReadOnlyMemory.Empty) }, - userCredentials: TestCredentials.Root); - } - - while (await _enumerator.MoveNextAsync()) { - if (_enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { - continue; - } - - counter++; - - if (counter == 1) { - await _subscription.Nack(PersistentSubscriptionNakEventAction.Park, "Test", resolvedEvent); - } - - if (counter > 10) { - return; - } - } - } - - public override async Task DisposeAsync() { - if (_enumerator is not null) { - await _enumerator.DisposeAsync(); - } - - if (_subscription is not null) { - await _subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack.cs deleted file mode 100644 index 864eccd8a..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_link_to_events_manual_ack.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Text; - -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class happy_case_catching_up_to_link_to_events_manual_ack - : IClassFixture { - private const string Stream = nameof(happy_case_catching_up_to_link_to_events_manual_ack); - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public happy_case_catching_up_to_link_to_events_manual_ack(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .Take(_fixture.Events.Length) - .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) - .WithTimeout(); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(EventWriteCount) - .Select((e, i) => new EventData( - e.EventId, - SystemEventTypes.LinkTo, - Encoding.UTF8.GetBytes($"{i}@{Stream}"), - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - )).ToArray(); - } - - protected override async Task Given() { - foreach (var e in Events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, bufferSize: BufferCount, - userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack.cs deleted file mode 100644 index 5bcf18171..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_catching_up_to_normal_events_manual_ack.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class happy_case_catching_up_to_normal_events_manual_ack - : IClassFixture { - private const string Stream = nameof(happy_case_catching_up_to_normal_events_manual_ack); - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public happy_case_catching_up_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .Take(_fixture.Events.Length) - .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) - .WithTimeout(); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - - public Fixture() { - Events = CreateTestEvents(EventWriteCount).ToArray(); - } - - protected override async Task Given() { - foreach (var e in Events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, bufferSize: BufferCount, - userCredentials: TestCredentials.Root); - } - - protected override Task When() => Task.CompletedTask; - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs deleted file mode 100644 index b4f957d47..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/happy_case_writing_and_subscribing_to_normal_events_manual_ack.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class happy_case_writing_and_subscribing_to_normal_events_manual_ack - : IClassFixture { - private const string Stream = nameof(happy_case_writing_and_subscribing_to_normal_events_manual_ack); - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public happy_case_writing_and_subscribing_to_normal_events_manual_ack(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .Take(_fixture.Events.Length) - .ForEachAwaitAsync(e => _fixture.Subscription.Ack(e.ResolvedEvent)) - .WithTimeout(); - } - - public class Fixture : EventStoreClientFixture { - public readonly EventData[] Events; - - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - Events = CreateTestEvents(EventWriteCount).ToArray(); - } - - protected override async Task Given() { - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.End, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, bufferSize: BufferCount, - userCredentials: TestCredentials.Root); - } - - protected override async Task When() { - foreach (var e in Events) - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_with_persistent_subscriptions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_with_persistent_subscriptions.cs deleted file mode 100644 index 61f93cb11..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_with_persistent_subscriptions.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class list_with_persistent_subscriptions : IClassFixture { - const int AllStreamSubscriptionCount = 4; - const int StreamSubscriptionCount = 3; - const string GroupName = nameof(list_with_persistent_subscriptions); - const string StreamName = nameof(list_with_persistent_subscriptions); - readonly Fixture _fixture; - - public list_with_persistent_subscriptions(Fixture fixture) => _fixture = fixture; - - int TotalSubscriptionCount => - SupportsPSToAll.No - ? StreamSubscriptionCount - : AllStreamSubscriptionCount + StreamSubscriptionCount; - - [Fact] - public async Task returns_subscriptions_to_stream() { - var result = (await _fixture.Client.ListToStreamAsync(StreamName, userCredentials: TestCredentials.Root)).ToList(); - Assert.Equal(StreamSubscriptionCount, result.Count); - Assert.All(result, p => Assert.Equal(StreamName, p.EventSource)); - } - - [Fact] - public async Task returns_all_subscriptions() { - var result = (await _fixture.Client.ListAllAsync(userCredentials: TestCredentials.Root)).ToList(); - Assert.Equal(TotalSubscriptionCount, result.Count); - } - - [Fact] - public async Task throws_for_non_existing() => - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListToStreamAsync("NonExistingStream", userCredentials: TestCredentials.Root) - ); - - [Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListToStreamAsync("NonExistingStream") - ); - - [Fact] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListAllAsync(userCredentials: TestCredentials.TestBadUser) - ); - - [Fact] - public async Task returns_result_with_normal_user_credentials() { - var result = await _fixture.Client.ListAllAsync(userCredentials: TestCredentials.TestUser1); - Assert.Equal(TotalSubscriptionCount, result.Count()); - } - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(skipPSWarmUp: true, noDefaultCredentials: true) { } - - protected override async Task Given() { - for (var i = 0; i < StreamSubscriptionCount; i++) - await Client.CreateToStreamAsync( - StreamName, - GroupName + i, - new(), - userCredentials: TestCredentials.Root - ); - - if (SupportsPSToAll.No) - return; - - for (var i = 0; i < AllStreamSubscriptionCount; i++) - await Client.CreateToAllAsync( - GroupName + i, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_without_persistent_subscriptions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_without_persistent_subscriptions.cs deleted file mode 100644 index 7e3ec3ad6..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/list_without_persistent_subscriptions.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class list_without_persistent_subscriptions : IClassFixture { - readonly Fixture _fixture; - - public list_without_persistent_subscriptions(Fixture fixture) => _fixture = fixture; - - [SupportsPSToAll.Fact] - public async Task throws() { - if (SupportsPSToAll.No) - return; - - await Assert.ThrowsAsync( - async () => - await _fixture.Client.ListToStreamAsync("stream", userCredentials: TestCredentials.Root) - ); - } - - [Fact] - public async Task returns_empty_collection() { - if (SupportsPSToAll.No) - return; - - var result = await _fixture.Client.ListAllAsync(userCredentials: TestCredentials.Root); - - Assert.Empty(result); - } - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(skipPSWarmUp: true) { } - - protected override Task Given() => Task.CompletedTask; - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/replay_parked.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/replay_parked.cs deleted file mode 100644 index 562449e52..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/replay_parked.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class replay_parked : IClassFixture { - const string GroupName = nameof(replay_parked); - const string StreamName = nameof(replay_parked); - - readonly Fixture _fixture; - - public replay_parked(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task does_not_throw() { - await _fixture.Client.ReplayParkedMessagesToStreamAsync( - StreamName, - GroupName, - userCredentials: TestCredentials.Root - ); - - await _fixture.Client.ReplayParkedMessagesToStreamAsync( - StreamName, - GroupName, - 100, - userCredentials: TestCredentials.Root - ); - } - - [Fact] - public async Task throws_when_given_non_existing_subscription() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToStreamAsync( - "NonExisting", - "NonExisting", - userCredentials: TestCredentials.Root - ) - ); - - [Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToStreamAsync(StreamName, GroupName) - ); - - [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToStreamAsync( - StreamName, - GroupName, - userCredentials: TestCredentials.TestBadUser - ) - ); - - [Fact] - public async Task throws_with_normal_user_credentials() => - await Assert.ThrowsAsync( - () => - _fixture.Client.ReplayParkedMessagesToStreamAsync( - StreamName, - GroupName, - userCredentials: TestCredentials.TestUser1 - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToStreamAsync( - StreamName, - GroupName, - new(), - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing.cs deleted file mode 100644 index b76d5b189..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class update_existing - : IClassFixture { - const string Stream = nameof(update_existing); - const string Group = "existing"; - readonly Fixture _fixture; - - public update_existing(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_completion_succeeds() => - await _fixture.Client.UpdateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, CreateTestEvents()); - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point.cs deleted file mode 100644 index 09020a8df..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_check_point.cs +++ /dev/null @@ -1,95 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class update_existing_with_check_point - : IClassFixture { - private const string Stream = nameof(update_existing_with_check_point); - private const string Group = "existing-with-check-point"; - private readonly Fixture _fixture; - - public update_existing_with_check_point(Fixture fixture) { - _fixture = fixture; - } - - [Fact] - public async Task resumes_from_check_point() { - await using var subscription = - _fixture.Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); - - var resolvedEvent = await subscription.Messages - .OfType() - .Select(e => e.ResolvedEvent) - .FirstAsync() - .AsTask() - .WithTimeout(); - - Assert.Equal(_fixture.CheckPoint.Next(), resolvedEvent.Event.EventNumber); - } - - public class Fixture : EventStoreClientFixture { - private readonly EventData[] _events; - - public Fixture() { - _events = CreateTestEvents(5).ToArray(); - } - - public StreamPosition CheckPoint { get; private set; } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, _events); - - await Client.CreateToStreamAsync(Stream, Group, - new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: StreamPosition.Start), - userCredentials: TestCredentials.Root); - - await using var subscription = - Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); - - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - await enumerator.MoveNextAsync(); - - await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); - - return; - - async Task Subscribe() { - var count = 0; - - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { - continue; - } - - count++; - - await subscription.Ack(resolvedEvent); - if (count >= _events.Length) { - break; - } - } - } - - async Task WaitForCheckpoint() { - await using var subscription = StreamsClient.SubscribeToStream( - $"$persistentsubscription-{Stream}::{Group}-checkpoint", FromStream.Start, - userCredentials: TestCredentials.Root); - - await foreach (var message in subscription.Messages) { - if (message is not StreamMessage.Event (var resolvedEvent)) { - continue; - } - - CheckPoint = resolvedEvent.Event.Data.ParseStreamPosition(); - return; - } - } - } - - protected override async Task When() { - // Force restart of the subscription - await Client.UpdateToStreamAsync(Stream, Group, new(), userCredentials: TestCredentials.Root); - - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, CreateTestEvents(1)); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs deleted file mode 100644 index e472aec9d..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_with_subscribers.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class update_existing_with_subscribers : IClassFixture { - private const string Stream = nameof(update_existing_with_subscribers); - private const string Group = "existing"; - private readonly Fixture _fixture; - - public update_existing_with_subscribers(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task existing_subscriptions_are_dropped() { - var ex = await Assert.ThrowsAsync(async () => { - while (await _fixture.Enumerator!.MoveNextAsync()) { - } - }).WithTimeout(); - - Assert.Equal(Stream, ex.StreamName); - Assert.Equal(Group, ex.GroupName); - } - - public class Fixture : EventStoreClientFixture { - private EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? _subscription; - public IAsyncEnumerator? Enumerator { get; private set; } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.NoStream, CreateTestEvents()); - await Client.CreateToStreamAsync(Stream, Group, new(), userCredentials: TestCredentials.Root); - - _subscription = Client.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); - Enumerator = _subscription.Messages.GetAsyncEnumerator(); - - await Enumerator.MoveNextAsync(); - } - - protected override Task When() => - Client.UpdateToStreamAsync(Stream, Group, new(), userCredentials: TestCredentials.Root); - - public override async Task DisposeAsync() { - if (Enumerator is not null) { - await Enumerator.DisposeAsync(); - } - - if (_subscription is not null) { - await _subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_without_permissions.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_without_permissions.cs deleted file mode 100644 index 021aa47b9..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_existing_without_permissions.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class update_existing_without_permissions - : IClassFixture { - const string Stream = nameof(update_existing_without_permissions); - const string Group = "existing"; - readonly Fixture _fixture; - - public update_existing_without_permissions(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task the_completion_fails_with_access_denied() => - await Assert.ThrowsAsync( - () => _fixture.Client.UpdateToStreamAsync( - Stream, - Group, - new() - ) - ); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override async Task Given() { - await StreamsClient.AppendToStreamAsync( - Stream, - StreamState.NoStream, - CreateTestEvents(), - userCredentials: TestCredentials.Root - ); - - await Client.CreateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ); - } - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_non_existent.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_non_existent.cs deleted file mode 100644 index 9e1f37e86..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/update_non_existent.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -public class update_non_existent - : IClassFixture { - const string Stream = nameof(update_non_existent); - const string Group = "nonexistent"; - readonly Fixture _fixture; - - public update_non_existent(Fixture fixture) => _fixture = fixture; - - [Regression.Fact(21, "20.x returns the wrong exception")] - public async Task the_completion_fails_with_not_found() => - await Assert.ThrowsAsync( - () => _fixture.Client.UpdateToStreamAsync( - Stream, - Group, - new(), - userCredentials: TestCredentials.Root - ) - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack.cs deleted file mode 100644 index 267baa684..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/when_writing_and_subscribing_to_normal_events_manual_nack.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream; - -[Obsolete] -public class when_writing_and_subscribing_to_normal_events_manual_nack - : IClassFixture { - private const string Stream = nameof(when_writing_and_subscribing_to_normal_events_manual_nack); - private const string Group = nameof(Group); - private const int BufferCount = 10; - private const int EventWriteCount = BufferCount * 2; - - private readonly Fixture _fixture; - - public when_writing_and_subscribing_to_normal_events_manual_nack(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task Test() { - await _fixture.Subscription!.Messages.OfType() - .Take(1) - .ForEachAwaitAsync(async message => - await _fixture.Subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", - message.ResolvedEvent)) - .WithTimeout(); - } - - public class Fixture : EventStoreClientFixture { - private readonly EventData[] _events; - public EventStorePersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription { get; private set; } - - public Fixture() { - _events = CreateTestEvents(EventWriteCount).ToArray(); - } - - protected override async Task Given() { - await Client.CreateToStreamAsync(Stream, Group, new(startFrom: StreamPosition.Start, resolveLinkTos: true), - userCredentials: TestCredentials.Root); - - Subscription = Client.SubscribeToStream(Stream, Group, bufferSize: BufferCount, userCredentials: TestCredentials.Root); - } - - protected override async Task When() { - foreach (var e in _events) { - await StreamsClient.AppendToStreamAsync(Stream, StreamState.Any, new[] { e }); - } - } - - public override async Task DisposeAsync() { - if (Subscription is not null) { - await Subscription.DisposeAsync(); - } - - await base.DisposeAsync(); - } - } -} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/restart_subsystem.cs b/test/EventStore.Client.PersistentSubscriptions.Tests/restart_subsystem.cs deleted file mode 100644 index cc6047e00..000000000 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/restart_subsystem.cs +++ /dev/null @@ -1,74 +0,0 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests; - -public class restart_subsystem : IClassFixture { - readonly Fixture _fixture; - - public restart_subsystem(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task does_not_throw() => await _fixture.Client.RestartSubsystemAsync(userCredentials: TestCredentials.Root); - - [Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync( - async () => - await _fixture.Client.RestartSubsystemAsync() - ); - - [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] - public async Task throws_with_non_existing_user() => - await Assert.ThrowsAsync( - async () => await _fixture.Client.RestartSubsystemAsync(userCredentials: TestCredentials.TestBadUser) - ); - - [Fact] - public async Task throws_with_normal_user_credentials() => - await Assert.ThrowsAsync(async () => await _fixture.Client.RestartSubsystemAsync(userCredentials: TestCredentials.TestUser1)); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} - -// namespace EventStore.Client.PersistentSubscriptions.Tests; -// -// public class restart_subsystem : IClassFixture { -// readonly InsecureClientTestFixture _fixture; -// -// public restart_subsystem(InsecureClientTestFixture fixture) => _fixture = fixture; -// -// [Fact] -// public async Task does_not_throw() => -// await _fixture.PersistentSubscriptions.RestartSubsystemAsync(userCredentials: TestCredentials.Root); -// -// [Fact] -// public async Task throws_with_no_credentials() => -// await Assert.ThrowsAsync( -// async () => -// await _fixture.PersistentSubscriptions.RestartSubsystemAsync() -// ); -// -// [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] -// public async Task throws_with_non_existing_user() => -// await Assert.ThrowsAsync( -// async () => await _fixture.PersistentSubscriptions.RestartSubsystemAsync(userCredentials: TestCredentials.TestBadUser) -// ); -// -// [Fact] -// public async Task throws_with_normal_user_credentials() { -// await _fixture.Users.CreateUserWithRetry( -// TestCredentials.TestUser1.Username!, -// TestCredentials.TestUser1.Username!, -// Array.Empty(), -// TestCredentials.TestUser1.Password!, -// TestCredentials.Root -// ); -// -// await Assert.ThrowsAsync( -// async () => await _fixture.PersistentSubscriptions.RestartSubsystemAsync(userCredentials: TestCredentials.TestUser1) -// ); -// } -// } \ No newline at end of file diff --git a/test/EventStore.Client.Plugins.Tests/ClientCertificateTests.cs b/test/EventStore.Client.Plugins.Tests/ClientCertificateTests.cs deleted file mode 100644 index e50fde0a5..000000000 --- a/test/EventStore.Client.Plugins.Tests/ClientCertificateTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace EventStore.Client.Plugins.Tests; - -[Trait("Category", "Target:Plugins")] -[Trait("Category", "Type:UserCertificate")] -public class ClientCertificateTests(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { - [Theory, BadClientCertificatesTestCases] - async Task bad_certificates_combinations_should_return_authentication_error(string userCertFile, string userKeyFile, string tlsCaFile) { - var stream = Fixture.GetStreamName(); - var seedEvents = Fixture.CreateTestEvents(); - var connectionString = $"esdb://localhost:2113/?tls=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; - - var settings = EventStoreClientSettings.Create(connectionString); - settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); - - await using var client = new EventStoreClient(settings); - - await client.AppendToStreamAsync(stream, StreamState.NoStream, seedEvents).ShouldThrowAsync(); - } - - [Theory, ValidClientCertificatesTestCases] - async Task valid_certificates_combinations_should_write_to_stream(string userCertFile, string userKeyFile, string tlsCaFile) { - var stream = Fixture.GetStreamName(); - var seedEvents = Fixture.CreateTestEvents(); - var connectionString = $"esdb://localhost:2113/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; - - var settings = EventStoreClientSettings.Create(connectionString); - settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); - - await using var client = new EventStoreClient(settings); - - var result = await client.AppendToStreamAsync(stream, StreamState.NoStream, seedEvents); - result.ShouldNotBeNull(); - } - - [Theory, BadClientCertificatesTestCases] - async Task basic_authentication_should_take_precedence(string userCertFile, string userKeyFile, string tlsCaFile) { - var stream = Fixture.GetStreamName(); - var seedEvents = Fixture.CreateTestEvents(); - var connectionString = $"esdb://admin:changeit@localhost:2113/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; - - var settings = EventStoreClientSettings.Create(connectionString); - settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); - - await using var client = new EventStoreClient(settings); - - var result = await client.AppendToStreamAsync(stream, StreamState.NoStream, seedEvents); - result.ShouldNotBeNull(); - } - - class BadClientCertificatesTestCases : TestCaseGenerator { - protected override IEnumerable Data() { - yield return [Certificates.Invalid.CertAbsolute, Certificates.Invalid.KeyAbsolute, Certificates.TlsCa.Absolute]; - yield return [Certificates.Invalid.CertRelative, Certificates.Invalid.KeyRelative, Certificates.TlsCa.Absolute]; - yield return [Certificates.Invalid.CertAbsolute, Certificates.Invalid.KeyAbsolute, Certificates.TlsCa.Relative]; - yield return [Certificates.Invalid.CertRelative, Certificates.Invalid.KeyRelative, Certificates.TlsCa.Relative]; - } - } - - class ValidClientCertificatesTestCases : TestCaseGenerator { - protected override IEnumerable Data() { - yield return [Certificates.Admin.CertAbsolute, Certificates.Admin.KeyAbsolute, Certificates.TlsCa.Absolute]; - yield return [Certificates.Admin.CertRelative, Certificates.Admin.KeyRelative, Certificates.TlsCa.Absolute]; - yield return [Certificates.Admin.CertAbsolute, Certificates.Admin.KeyAbsolute, Certificates.TlsCa.Relative]; - yield return [Certificates.Admin.CertRelative, Certificates.Admin.KeyRelative, Certificates.TlsCa.Relative]; - } - } -} diff --git a/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj b/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj deleted file mode 100644 index ee7d48512..000000000 --- a/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/test/EventStore.Client.ProjectionManagement.Tests/AssemblyInfo.cs b/test/EventStore.Client.ProjectionManagement.Tests/AssemblyInfo.cs deleted file mode 100644 index b0b47aa73..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj b/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj deleted file mode 100644 index 31381e212..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/EventStore.Client.ProjectionManagement.Tests.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/EventStoreClientFixture.cs b/test/EventStore.Client.ProjectionManagement.Tests/EventStoreClientFixture.cs deleted file mode 100644 index 62d9ba053..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/EventStoreClientFixture.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public abstract class EventStoreClientFixture : EventStoreClientFixtureBase { - protected EventStoreClientFixture(EventStoreClientSettings? settings = null, bool noDefaultCredentials = false) : - base( - settings, - new Dictionary { - ["EVENTSTORE_RUN_PROJECTIONS"] = "ALL", - ["EVENTSTORE_START_STANDARD_PROJECTIONS"] = "True" - }, - noDefaultCredentials - ) { - Client = new(Settings); - UserManagementClient = new(Settings); - StreamsClient = new(Settings); - } - - public EventStoreUserManagementClient UserManagementClient { get; } - public EventStoreClient StreamsClient { get; } - public EventStoreProjectionManagementClient Client { get; } - - protected virtual bool RunStandardProjections => true; - - protected override async Task OnServerUpAsync() { - await StreamsClient.WarmUp(); - await UserManagementClient.WarmUp(); - await Client.WarmUp(); - await UserManagementClient.CreateUserWithRetry( - TestCredentials.TestUser1.Username!, - TestCredentials.TestUser1.Username!, - Array.Empty(), - TestCredentials.TestUser1.Password!, - TestCredentials.Root - ).WithTimeout(); - - await StandardProjections.Created(Client).WithTimeout(TimeSpan.FromMinutes(2)); - - if (RunStandardProjections) - await Task - .WhenAll(StandardProjections.Names.Select(name => Client.EnableAsync(name, userCredentials: TestCredentials.Root))) - .WithTimeout(TimeSpan.FromMinutes(2)); - } - - public override async Task DisposeAsync() { - await StreamsClient.DisposeAsync(); - await UserManagementClient.DisposeAsync(); - await Client.DisposeAsync(); - await base.DisposeAsync(); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/StandardProjections.cs b/test/EventStore.Client.ProjectionManagement.Tests/StandardProjections.cs deleted file mode 100644 index 4f57a0ae2..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/StandardProjections.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -static class StandardProjections { - public static readonly string[] Names = { - "$streams", - "$stream_by_category", - "$by_category", - "$by_event_type", - "$by_correlation_id" - }; - - public static Task Created(EventStoreProjectionManagementClient client) { - var systemProjectionsReady = Names.Select( - async name => { - var ready = false; - - while (!ready) { - var result = await client.GetStatusAsync(name, userCredentials: TestCredentials.Root); - - if (result?.Status.Contains("Running") ?? false) - ready = true; - else - await Task.Delay(100); - } - } - ); - - return Task.WhenAll(systemProjectionsReady); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/abort.cs b/test/EventStore.Client.ProjectionManagement.Tests/abort.cs deleted file mode 100644 index 1983a79b6..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/abort.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class @abort : IClassFixture { - readonly Fixture _fixture; - - public abort(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task status_is_aborted() { - var name = StandardProjections.Names.First(); - await _fixture.Client.AbortAsync(name, userCredentials: TestCredentials.Root); - var result = await _fixture.Client.GetStatusAsync(name, userCredentials: TestCredentials.Root); - Assert.NotNull(result); - Assert.Contains(new[] { "Aborted/Stopped", "Stopped" }, x => x == result!.Status); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/create.cs b/test/EventStore.Client.ProjectionManagement.Tests/create.cs deleted file mode 100644 index e8f9c21d2..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/create.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class @create : IClassFixture { - readonly Fixture _fixture; - - public create(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task one_time() => - await _fixture.Client.CreateOneTimeAsync("fromAll().when({$init: function (state, ev) {return {};}});", userCredentials: TestCredentials.Root); - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task continuous(bool trackEmittedStreams) => - await _fixture.Client.CreateContinuousAsync( - $"{nameof(continuous)}_{trackEmittedStreams}", - "fromAll().when({$init: function (state, ev) {return {};}});", - trackEmittedStreams, - userCredentials: TestCredentials.Root - ); - - [Fact] - public async Task transient() => - await _fixture.Client.CreateTransientAsync( - nameof(transient), - "fromAll().when({$init: function (state, ev) {return {};}});", - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/disable.cs b/test/EventStore.Client.ProjectionManagement.Tests/disable.cs deleted file mode 100644 index bd03a2847..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/disable.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class @disable : IClassFixture { - readonly Fixture _fixture; - - public disable(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task status_is_stopped() { - var name = StandardProjections.Names.First(); - await _fixture.Client.DisableAsync(name, userCredentials: TestCredentials.Root); - var result = await _fixture.Client.GetStatusAsync(name, userCredentials: TestCredentials.Root); - Assert.NotNull(result); - Assert.Contains(new[] { "Aborted/Stopped", "Stopped" }, x => x == result!.Status); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/enable.cs b/test/EventStore.Client.ProjectionManagement.Tests/enable.cs deleted file mode 100644 index 0afecaaa7..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/enable.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class @enable : IClassFixture { - readonly Fixture _fixture; - - public enable(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task status_is_running() { - var name = StandardProjections.Names.First(); - await _fixture.Client.EnableAsync(name, userCredentials: TestCredentials.Root); - var result = await _fixture.Client.GetStatusAsync(name, userCredentials: TestCredentials.Root); - Assert.NotNull(result); - Assert.Equal("Running", result!.Status); - } - - public class Fixture : EventStoreClientFixture { - protected override bool RunStandardProjections => false; - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/get_result.cs b/test/EventStore.Client.ProjectionManagement.Tests/get_result.cs deleted file mode 100644 index d664862b5..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/get_result.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class get_result : IClassFixture { - readonly Fixture _fixture; - - public get_result(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task returns_expected_result() { - Result? result = null; - - await AssertEx.IsOrBecomesTrue( - async () => { - result = await _fixture.Client - .GetResultAsync(nameof(get_result), userCredentials: TestCredentials.TestUser1); - - return result.Count > 0; - } - ); - - Assert.NotNull(result); - Assert.Equal(1, result!.Count); - } - - class Result { - public int Count { get; set; } - } - - public class Fixture : EventStoreClientFixture { - static readonly string Projection = $@" -fromStream('{nameof(get_result)}').when({{ - ""$init"": function() {{ return {{ Count: 0 }}; }}, - ""$any"": function(s, e) {{ s.Count++; return s; }} -}}); -"; - - protected override Task Given() => - Client.CreateContinuousAsync( - nameof(get_result), - Projection, - userCredentials: TestCredentials.Root - ); - - protected override async Task When() => - await StreamsClient.AppendToStreamAsync( - nameof(get_result), - StreamState.NoStream, - CreateTestEvents() - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/get_state.cs b/test/EventStore.Client.ProjectionManagement.Tests/get_state.cs deleted file mode 100644 index 2295ed722..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/get_state.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class get_state : IClassFixture { - readonly Fixture _fixture; - - public get_state(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task returns_expected_result() { - Result? result = null; - - await AssertEx.IsOrBecomesTrue( - async () => { - result = await _fixture.Client - .GetStateAsync(nameof(get_state), userCredentials: TestCredentials.TestUser1); - - return result.Count > 0; - } - ); - - Assert.NotNull(result); - Assert.Equal(1, result!.Count); - } - - class Result { - public int Count { get; set; } - } - - public class Fixture : EventStoreClientFixture { - static readonly string Projection = $@" -fromStream('{nameof(get_state)}').when({{ - ""$init"": function() {{ return {{ Count: 0 }}; }}, - ""$any"": function(s, e) {{ s.Count++; return s; }} -}}); -"; - - protected override Task Given() => - Client.CreateContinuousAsync( - nameof(get_state), - Projection, - userCredentials: TestCredentials.Root - ); - - protected override Task When() => - StreamsClient.AppendToStreamAsync( - nameof(get_state), - StreamState.NoStream, - CreateTestEvents() - ); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/get_status.cs b/test/EventStore.Client.ProjectionManagement.Tests/get_status.cs deleted file mode 100644 index 7382a758b..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/get_status.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class get_status : IClassFixture { - readonly Fixture _fixture; - - public get_status(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task returns_expected_result() { - var name = StandardProjections.Names.First(); - var result = await _fixture.Client.GetStatusAsync(name, userCredentials: TestCredentials.TestUser1); - - Assert.NotNull(result); - Assert.Equal(name, result!.Name); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/list_all_projections.cs b/test/EventStore.Client.ProjectionManagement.Tests/list_all_projections.cs deleted file mode 100644 index cac2e85da..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/list_all_projections.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class list_all_projections : IClassFixture { - readonly Fixture _fixture; - - public list_all_projections(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task returns_expected_result() { - var result = await _fixture.Client.ListAllAsync(userCredentials: TestCredentials.Root) - .ToArrayAsync(); - - Assert.Equal(result.Select(x => x.Name).OrderBy(x => x), StandardProjections.Names.OrderBy(x => x)); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/list_continuous_projections.cs b/test/EventStore.Client.ProjectionManagement.Tests/list_continuous_projections.cs deleted file mode 100644 index c96ed825f..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/list_continuous_projections.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class list_continuous_projections : IClassFixture { - readonly Fixture _fixture; - - public list_continuous_projections(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task returns_expected_result() { - var result = await _fixture.Client.ListContinuousAsync(userCredentials: TestCredentials.Root) - .ToArrayAsync(); - - Assert.Equal( - result.Select(x => x.Name).OrderBy(x => x), - StandardProjections.Names.Concat(new[] { nameof(list_continuous_projections) }).OrderBy(x => x) - ); - - Assert.True(result.All(x => x.Mode == "Continuous")); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateContinuousAsync( - nameof(list_continuous_projections), - "fromAll().when({$init: function (state, ev) {return {};}});", - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/list_one_time_projections.cs b/test/EventStore.Client.ProjectionManagement.Tests/list_one_time_projections.cs deleted file mode 100644 index d88d68b5c..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/list_one_time_projections.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class list_one_time_projections : IClassFixture { - readonly Fixture _fixture; - - public list_one_time_projections(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task returns_expected_result() { - var result = await _fixture.Client.ListOneTimeAsync(userCredentials: TestCredentials.Root) - .ToArrayAsync(); - - var details = Assert.Single(result); - Assert.Equal("OneTime", details.Mode); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateOneTimeAsync("fromAll().when({$init: function (state, ev) {return {};}});", userCredentials: TestCredentials.Root); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/reset.cs b/test/EventStore.Client.ProjectionManagement.Tests/reset.cs deleted file mode 100644 index 3dbfc15a5..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/reset.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class @reset : IClassFixture { - readonly Fixture _fixture; - - public reset(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task status_is_running() { - var name = StandardProjections.Names.First(); - await _fixture.Client.ResetAsync(name, userCredentials: TestCredentials.Root); - var result = await _fixture.Client.GetStatusAsync(name, userCredentials: TestCredentials.Root); - - Assert.NotNull(result); - Assert.Equal("Running", result!.Status); - } - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/restart_subsystem.cs b/test/EventStore.Client.ProjectionManagement.Tests/restart_subsystem.cs deleted file mode 100644 index d10ba1545..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/restart_subsystem.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class restart_subsystem : IClassFixture { - readonly Fixture _fixture; - - public restart_subsystem(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task does_not_throw() => await _fixture.Client.RestartSubsystemAsync(userCredentials: TestCredentials.Root); - - [Fact] - public async Task throws_when_given_no_credentials() => await Assert.ThrowsAsync(() => _fixture.Client.RestartSubsystemAsync()); - - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => Task.CompletedTask; - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.ProjectionManagement.Tests/update.cs b/test/EventStore.Client.ProjectionManagement.Tests/update.cs deleted file mode 100644 index ba8b682ad..000000000 --- a/test/EventStore.Client.ProjectionManagement.Tests/update.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace EventStore.Client.ProjectionManagement.Tests; - -public class @update : IClassFixture { - readonly Fixture _fixture; - - public update(Fixture fixture) => _fixture = fixture; - - [Theory] - [InlineData(true)] - [InlineData(false)] - [InlineData(null)] - public async Task returns_expected_result(bool? emitEnabled) => - await _fixture.Client.UpdateAsync( - nameof(update), - "fromAll().when({$init: function (s, e) {return {};}});", - emitEnabled, - userCredentials: TestCredentials.Root - ); - - public class Fixture : EventStoreClientFixture { - protected override Task Given() => - Client.CreateContinuousAsync( - nameof(update), - "fromAll().when({$init: function (state, ev) {return {};}});", - userCredentials: TestCredentials.Root - ); - - protected override Task When() => Task.CompletedTask; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Append/ShouldThrowAsyncExtensions.cs b/test/EventStore.Client.Streams.Tests/Append/ShouldThrowAsyncExtensions.cs deleted file mode 100644 index 80f983ce0..000000000 --- a/test/EventStore.Client.Streams.Tests/Append/ShouldThrowAsyncExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Append; - -public static class ShouldThrowAsyncExtensions { - public static Task ShouldThrowAsync(this EventStoreClient.ReadStreamResult source) where TException : Exception => - source - .ToArrayAsync() - .AsTask() - .ShouldThrowAsync(); - - public static async Task ShouldThrowAsync(this EventStoreClient.ReadStreamResult source, Action handler) where TException : Exception { - var ex = await source.ShouldThrowAsync(); - handler(ex); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Append/append_to_stream_limits.cs b/test/EventStore.Client.Streams.Tests/Append/append_to_stream_limits.cs deleted file mode 100644 index 6bdd3fbb8..000000000 --- a/test/EventStore.Client.Streams.Tests/Append/append_to_stream_limits.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Append; - -[Trait("Category", "Target:Stream")] -[Trait("Category", "Operation:Append")] -public class append_to_stream_limits(ITestOutputHelper output, StreamLimitsFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task succeeds_when_size_is_less_than_max_append_size() { - var stream = Fixture.GetStreamName(); - - var (events, size) = Fixture.CreateTestEventsUpToMaxSize(StreamLimitsFixture.MaxAppendSize - 1); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - } - - [Fact] - public async Task fails_when_size_exceeds_max_append_size() { - var stream = Fixture.GetStreamName(); - - var eventsAppendSize = StreamLimitsFixture.MaxAppendSize * 2; - - // beware of the size of the events... - var (events, size) = Fixture.CreateTestEventsUpToMaxSize(eventsAppendSize); - - size.ShouldBeGreaterThan(StreamLimitsFixture.MaxAppendSize); - - var ex = await Fixture.Streams - .AppendToStreamAsync(stream, StreamState.NoStream, events) - .ShouldThrowAsync(); - - ex.MaxAppendSize.ShouldBe(StreamLimitsFixture.MaxAppendSize); - } -} - -public class StreamLimitsFixture() : EventStoreFixture(x => x.WithMaxAppendSize(MaxAppendSize)) { - public const uint MaxAppendSize = 64; - - public (IEnumerable Events, uint size) CreateTestEventsUpToMaxSize(uint maxSize) { - var size = 0; - var events = new List(); - - foreach (var evt in CreateTestEvents(int.MaxValue)) { - size += evt.Data.Length; - - if (size >= maxSize) { - size -= evt.Data.Length; - break; - } - - events.Add(evt); - } - - return (events, (uint)size); - } -} diff --git a/test/EventStore.Client.Streams.Tests/Append/append_to_stream_retry.cs b/test/EventStore.Client.Streams.Tests/Append/append_to_stream_retry.cs deleted file mode 100644 index d67240c49..000000000 --- a/test/EventStore.Client.Streams.Tests/Append/append_to_stream_retry.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Polly; -using Polly.Contrib.WaitAndRetry; - -namespace EventStore.Client.Streams.Tests.Append; - -[Trait("Category", "Target:Stream")] -[Trait("Category", "Operation:Append")] -public class append_to_stream_retry(ITestOutputHelper output, StreamRetryFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task can_retry() { - var stream = Fixture.GetStreamName(); - - // can definitely write without throwing - var result = await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, Fixture.CreateTestEvents()); - - result.NextExpectedStreamRevision.ShouldBe(new(0)); - - await Fixture.Service.Restart(); - - // write can be retried - var writeResult = await Policy - .Handle() - .WaitAndRetryAsync( - Backoff.LinearBackoff(TimeSpan.FromMilliseconds(250), 10), - (ex, ts) => Fixture.Log.Debug("Error writing events to stream. Retrying. Reason: {Message}.", ex.Message) - ) - .ExecuteAsync(() => Fixture.Streams.AppendToStreamAsync(stream, result.NextExpectedStreamRevision, Fixture.CreateTestEvents())); - - Fixture.Log.Information("Successfully wrote events to stream {Stream}.", stream); - - writeResult.NextExpectedStreamRevision.ShouldBe(new(1)); - } -} - -public class StreamRetryFixture() : EventStoreFixture( - x => x.RunInMemory(false).With(o => o.ClientSettings.ConnectivitySettings.MaxDiscoverAttempts = 2) -); \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Append/append_to_stream_with_tls_ca_file.cs b/test/EventStore.Client.Streams.Tests/Append/append_to_stream_with_tls_ca_file.cs deleted file mode 100644 index 2fdc983ba..000000000 --- a/test/EventStore.Client.Streams.Tests/Append/append_to_stream_with_tls_ca_file.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Append; - -[Trait("Category", "Target:Stream")] -[Trait("Category", "Operation:Append")] -public class append_to_stream_with_tls_ca_file(ITestOutputHelper output, EventStoreFixture fixture) - : EventStoreTests(output, fixture) { - public static IEnumerable CertPaths => - new List { - new object[] { Path.Combine("certs", "ca", "ca.crt") }, - new object[] { Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "certs", "ca", "ca.crt") }, - }; - - [Theory] - [MemberData(nameof(CertPaths))] - private async Task TestAppendWithCaFile(string certificateFilePath) { - Fixture.Log.Information($"Using certificate: {certificateFilePath}"); - - var connectionString = - $"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={certificateFilePath}"; - - var settings = EventStoreClientSettings.Create(connectionString); - - var client = new EventStoreClient(settings); - - var appendResult = await client.AppendToStreamAsync( - "some-stream", - StreamState.Any, - new[] { new EventData(Uuid.NewUuid(), "some-event", default) } - ); - - appendResult.ShouldNotBeNull(); - - await client.DisposeAsync(); - } -} diff --git a/test/EventStore.Client.Streams.Tests/Append/appending_to_implicitly_created_stream.cs b/test/EventStore.Client.Streams.Tests/Append/appending_to_implicitly_created_stream.cs deleted file mode 100644 index 59ab89df0..000000000 --- a/test/EventStore.Client.Streams.Tests/Append/appending_to_implicitly_created_stream.cs +++ /dev/null @@ -1,266 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Append; - -[Trait("Category", "Target:Stream")] -[Trait("Category", "Operation:Append")] -public class appending_to_implicitly_created_stream(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0em1_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_4e4_0any_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e5_non_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(5), events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 2).CountAsync(); - - Assert.Equal(events.Length + 1, count); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e6_throws_wev() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - - await Assert.ThrowsAsync(() => Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(6), events.Take(1))); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e6_returns_wev() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - - var writeResult = await Fixture.Streams.AppendToStreamAsync( - stream, - new StreamRevision(6), - events.Take(1), - options => options.ThrowOnAppendFailure = false - ); - - Assert.IsType(writeResult); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e4_throws_wev() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - - await Assert.ThrowsAsync(() => Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(4), events.Take(1))); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e4_returns_wev() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(6).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - - var writeResult = await Fixture.Streams.AppendToStreamAsync( - stream, - new StreamRevision(4), - events.Take(1), - options => options.ThrowOnAppendFailure = false - ); - - Assert.IsType(writeResult); - } - - [Fact] - public async Task sequence_0em1_0e0_non_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents().ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(0), events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 2).CountAsync(); - - Assert.Equal(events.Length + 1, count); - } - - [Fact] - public async Task sequence_0em1_0any_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents().ToArray(); - - await Task.Delay(TimeSpan.FromSeconds(30)); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_0em1_0em1_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents().ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_0em1_1e0_2e1_1any_1any_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(3).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Skip(1).Take(1)); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Skip(1).Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_S_0em1_1em1_E_S_0em1_E_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(2).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_S_0em1_1em1_E_S_0any_E_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(2).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_S_0em1_1em1_E_S_1e0_E_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(2).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - - await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(0), events.Skip(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_S_0em1_1em1_E_S_1any_E_idempotent() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(2).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Skip(1).Take(1)); - - var count = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); - - Assert.Equal(events.Length, count); - } - - [Fact] - public async Task sequence_S_0em1_1em1_E_S_0em1_1em1_2em1_E_idempotancy_fail_throws() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(3).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(2)); - - await Assert.ThrowsAsync( - () => Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - events - ) - ); - } - - [Fact] - public async Task sequence_S_0em1_1em1_E_S_0em1_1em1_2em1_E_idempotancy_fail_returns() { - var stream = Fixture.GetStreamName(); - - var events = Fixture.CreateTestEvents(3).ToArray(); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(2)); - - var writeResult = await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - events, - options => options.ThrowOnAppendFailure = false - ); - - Assert.IsType(writeResult); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Append/sending_and_receiving_large_messages.cs b/test/EventStore.Client.Streams.Tests/Append/sending_and_receiving_large_messages.cs deleted file mode 100644 index 099ece45c..000000000 --- a/test/EventStore.Client.Streams.Tests/Append/sending_and_receiving_large_messages.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Grpc.Core; - -namespace EventStore.Client.Streams.Tests.Append; - -[Trait("Category", "Target:Stream")] -[Trait("Category", "Operation:Append")] -public class sending_and_receiving_large_messages(ITestOutputHelper output, sending_and_receiving_large_messages.CustomFixture fixture) - : EventStoreTests(output, fixture) { - [Fact] - public async Task over_the_hard_limit() { - var streamName = Fixture.GetStreamName(); - var largeEvent = Fixture.CreateTestEvents() - .Select(e => new EventData(e.EventId, "-", new byte[CustomFixture.MaximumSize + 1])); - - var ex = await Assert.ThrowsAsync( - () => Fixture.Streams.AppendToStreamAsync( - streamName, - StreamState.NoStream, - largeEvent - ) - ); - - Assert.Equal(StatusCode.ResourceExhausted, ex.StatusCode); - } - - public class CustomFixture() : EventStoreFixture(x => x.WithMaxAppendSize(MaximumSize)) { - public const int MaximumSize = 16 * 1024 * 1024 - 10000; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/AssemblyInfo.cs b/test/EventStore.Client.Streams.Tests/AssemblyInfo.cs deleted file mode 100644 index b0b47aa73..000000000 --- a/test/EventStore.Client.Streams.Tests/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/DependencyInjectionTests.cs b/test/EventStore.Client.Streams.Tests/DependencyInjectionTests.cs deleted file mode 100644 index 96512557b..000000000 --- a/test/EventStore.Client.Streams.Tests/DependencyInjectionTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Grpc.Core.Interceptors; -using Microsoft.Extensions.DependencyInjection; - -namespace EventStore.Client.Streams.Tests; - -[Trait("Category", "UnitTest")] -public class DependencyInjectionTests { - [Fact] - public void Register() => - new ServiceCollection() - .AddEventStoreClient() - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterWithConnectionString() => - new ServiceCollection() - .AddEventStoreClient("esdb://localhost:2113?tls=false") - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterWithConnectionStringFactory() => - new ServiceCollection() - .AddEventStoreClient(provider => "esdb://localhost:2113?tls=false") - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterWithUri() => - new ServiceCollection() - .AddEventStoreClient(new Uri("https://localhost:1234")) - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterWithUriFactory() => - new ServiceCollection() - .AddEventStoreClient(provider => new Uri("https://localhost:1234")) - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterWithSettings() => - new ServiceCollection() - .AddEventStoreClient(settings => { }) - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterWithSettingsFactory() => - new ServiceCollection() - .AddEventStoreClient(provider => settings => { }) - .BuildServiceProvider() - .GetRequiredService(); - - [Fact] - public void RegisterInterceptors() { - var interceptorResolved = false; - new ServiceCollection() - .AddEventStoreClient() - .AddSingleton(() => interceptorResolved = true) - .AddSingleton() - .BuildServiceProvider() - .GetRequiredService(); - - Assert.True(interceptorResolved); - } - - delegate void ConstructorInvoked(); - - class TestInterceptor : Interceptor { - public TestInterceptor(ConstructorInvoked invoked) => invoked.Invoke(); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs b/test/EventStore.Client.Streams.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs deleted file mode 100644 index 5a19c6b0d..000000000 --- a/test/EventStore.Client.Streams.Tests/Diagnostics/StreamsTracingInstrumentationTests.cs +++ /dev/null @@ -1,221 +0,0 @@ -// ReSharper disable ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - -using EventStore.Client.Diagnostics; -using EventStore.Diagnostics.Tracing; - -namespace EventStore.Client.Streams.Tests.Diagnostics; - -[Trait("Category", "Diagnostics:Tracing")] -public class StreamsTracingInstrumentationTests(ITestOutputHelper output, DiagnosticsFixture fixture) - : EventStoreTests(output, fixture) { - [Fact] - public async Task AppendIsInstrumentedWithTracingAsExpected() { - var stream = Fixture.GetStreamName(); - - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - Fixture.CreateTestEvents() - ); - - var activity = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Append, stream) - .SingleOrDefault() - .ShouldNotBeNull(); - - Fixture.AssertAppendActivityHasExpectedTags(activity, stream); - } - - [Fact] - public async Task AppendTraceIsTaggedWithErrorStatusOnException() { - var stream = Fixture.GetStreamName(); - - var actualException = await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - Fixture.CreateTestEventsThatThrowsException() - ).ShouldThrowAsync(); - - var activity = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Append, stream) - .SingleOrDefault() - .ShouldNotBeNull(); - - Fixture.AssertErroneousAppendActivityHasExpectedTags(activity, actualException); - } - - [Fact] - public async Task TracingContextIsInjectedWhenUserMetadataIsValidJsonObject() { - var stream = Fixture.GetStreamName(); - - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - Fixture.CreateTestEvents(1, metadata: Fixture.CreateTestJsonMetadata()) - ); - - var activity = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Append, stream) - .SingleOrDefault() - .ShouldNotBeNull(); - - var readResult = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start) - .ToListAsync(); - - var tracingMetadata = readResult[0].OriginalEvent.Metadata.ExtractTracingMetadata(); - - tracingMetadata.ShouldNotBe(TracingMetadata.None); - tracingMetadata.TraceId.ShouldBe(activity.TraceId.ToString()); - tracingMetadata.SpanId.ShouldBe(activity.SpanId.ToString()); - } - - [Fact] - public async Task TracingContextIsNotInjectedWhenUserMetadataIsNotValidJsonObject() { - var stream = Fixture.GetStreamName(); - - var inputMetadata = "clearlynotavalidjsonobject"u8.ToArray(); - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - Fixture.CreateTestEvents(1, metadata: inputMetadata) - ); - - var readResult = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start) - .ToListAsync(); - - var outputMetadata = readResult[0].OriginalEvent.Metadata.ToArray(); - outputMetadata.ShouldBe(inputMetadata); - } - - [Fact] - public async Task TracingContextIsInjectedWhenEventIsNotJsonButHasJsonMetadata() { - var stream = Fixture.GetStreamName(); - - var inputMetadata = Fixture.CreateTestJsonMetadata().ToArray(); - await Fixture.Streams.AppendToStreamAsync( - stream, - StreamState.NoStream, - Fixture.CreateTestEvents( - metadata: inputMetadata, - contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream - ) - ); - - var readResult = await Fixture.Streams - .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start) - .ToListAsync(); - - var outputMetadata = readResult[0].OriginalEvent.Metadata.ToArray(); - outputMetadata.ShouldNotBe(inputMetadata); - - var appendActivities = Fixture.GetActivitiesForOperation(TracingConstants.Operations.Append, stream); - - appendActivities.ShouldNotBeEmpty(); - } - - [Fact] - public async Task json_metadata_event_is_traced_and_non_json_metadata_event_is_not_traced() { - var streamName = Fixture.GetStreamName(); - - var seedEvents = new[] { - Fixture.CreateTestEvent(metadata: Fixture.CreateTestJsonMetadata()), - Fixture.CreateTestEvent(metadata: Fixture.CreateTestNonJsonMetadata()) - }; - - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - await using var subscription = Fixture.Streams.SubscribeToStream(streamName, FromStream.Start); - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - var appendActivities = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Append, streamName) - .ShouldNotBeNull(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - await Subscribe(enumerator).WithTimeout(); - - var subscribeActivities = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Subscribe, streamName) - .ToArray(); - - appendActivities.ShouldHaveSingleItem(); - - subscribeActivities.ShouldHaveSingleItem(); - - subscribeActivities.First().ParentId.ShouldBe(appendActivities.First().Id); - - var jsonMetadataEvent = seedEvents.First(); - - Fixture.AssertSubscriptionActivityHasExpectedTags( - subscribeActivities.First(), - streamName, - jsonMetadataEvent.EventId.ToString() - ); - - return; - - async Task Subscribe(IAsyncEnumerator internalEnumerator) { - while (await internalEnumerator.MoveNextAsync()) { - if (internalEnumerator.Current is not StreamMessage.Event(var resolvedEvent)) - continue; - - availableEvents.Remove(resolvedEvent.Event.EventId); - - if (availableEvents.Count == 0) - return; - } - } - } - - [Fact] - [Trait("Category", "Special cases")] - public async Task should_not_trace_when_event_is_null() { - var category = Guid.NewGuid().ToString("N"); - var streamName = category + "-123"; - - var seedEvents = Fixture.CreateTestEvents(type: $"{category}-{Fixture.GetStreamName()}").ToArray(); - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - await Fixture.Streams.DeleteAsync(streamName, StreamState.StreamExists); - - await using var subscription = Fixture.Streams.SubscribeToStream("$ce-" + category, FromStream.Start, resolveLinkTos: true); - - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - await Subscribe().WithTimeout(); - - var appendActivities = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Append, streamName) - .ShouldNotBeNull(); - - var subscribeActivities = Fixture - .GetActivitiesForOperation(TracingConstants.Operations.Subscribe, "$ce-" + category) - .ToArray(); - - appendActivities.ShouldHaveSingleItem(); - subscribeActivities.ShouldBeEmpty(); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not StreamMessage.Event(var resolvedEvent)) - continue; - - if (resolvedEvent.Event?.EventType is "$metadata") - return; - } - } - } -} diff --git a/test/EventStore.Client.Streams.Tests/EventDataTests.cs b/test/EventStore.Client.Streams.Tests/EventDataTests.cs deleted file mode 100644 index 87ed00191..000000000 --- a/test/EventStore.Client.Streams.Tests/EventDataTests.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace EventStore.Client.Streams.Tests; - -[Trait("Category", "UnitTest")] -public class EventDataTests { - [Fact] - public void EmptyEventIdThrows() { - var ex = Assert.Throws( - () => new EventData(Uuid.Empty, "-", Array.Empty()) - ); - - Assert.Equal("eventId", ex.ParamName); - } - - [Fact] - public void MalformedContentTypeThrows() => - Assert.Throws(() => new EventData(Uuid.NewUuid(), "-", Array.Empty(), contentType: "application")); - - [Fact] - public void InvalidContentTypeThrows() { - var ex = Assert.Throws(() => new EventData(Uuid.NewUuid(), "-", Array.Empty(), contentType: "application/xml")); - Assert.Equal("contentType", ex.ParamName); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj b/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj deleted file mode 100644 index e0cf4e59d..000000000 --- a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - CS0612;xUnit1031 - - - - - diff --git a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj.DotSettings b/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj.DotSettings deleted file mode 100644 index 9176e378d..000000000 --- a/test/EventStore.Client.Streams.Tests/EventStore.Client.Streams.Tests.csproj.DotSettings +++ /dev/null @@ -1,12 +0,0 @@ - - False - True - False - False - True - False - True - True - True - True - False \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/SecurityFixture_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/SecurityFixture_obsolete.cs deleted file mode 100644 index 18b52be82..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/SecurityFixture_obsolete.cs +++ /dev/null @@ -1,321 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class SecurityFixture_obsolete : EventStoreFixture { - public const string NoAclStream = nameof(NoAclStream); - public const string ReadStream = nameof(ReadStream); - public const string WriteStream = nameof(WriteStream); - public const string MetaReadStream = nameof(MetaReadStream); - public const string MetaWriteStream = nameof(MetaWriteStream); - public const string AllStream = SystemStreams.AllStream; - public const string NormalAllStream = nameof(NormalAllStream); - public const string SystemAllStream = $"${nameof(SystemAllStream)}"; - public const string SystemAdminStream = $"${nameof(SystemAdminStream)}"; - public const string SystemAclStream = $"${nameof(SystemAclStream)}"; - - const int TimeoutMs = 1000; - - public SecurityFixture_obsolete() : base(x => x.WithoutDefaultCredentials()) { - OnSetup = async () => { - await Users.CreateUserWithRetry( - TestCredentials.TestUser1.Username!, - nameof(TestCredentials.TestUser1), - Array.Empty(), - TestCredentials.TestUser1.Password!, - TestCredentials.Root - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Users.CreateUserWithRetry( - TestCredentials.TestUser2.Username!, - nameof(TestCredentials.TestUser2), - Array.Empty(), - TestCredentials.TestUser2.Password!, - TestCredentials.Root - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Users.CreateUserWithRetry( - TestCredentials.TestAdmin.Username!, - nameof(TestCredentials.TestAdmin), - new[] { SystemRoles.Admins }, - TestCredentials.TestAdmin.Password!, - TestCredentials.Root - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Given(); - await When(); - }; - } - - protected virtual async Task Given() { - await Streams.SetStreamMetadataAsync( - NoAclStream, - StreamState.NoStream, - new(), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - ReadStream, - StreamState.NoStream, - new(acl: new(TestCredentials.TestUser1.Username)), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - WriteStream, - StreamState.NoStream, - new(acl: new(writeRole: TestCredentials.TestUser1.Username)), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - MetaReadStream, - StreamState.NoStream, - new(acl: new(metaReadRole: TestCredentials.TestUser1.Username)), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - MetaWriteStream, - StreamState.NoStream, - new(acl: new(metaWriteRole: TestCredentials.TestUser1.Username)), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - AllStream, - StreamState.Any, - new(acl: new(TestCredentials.TestUser1.Username)), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - SystemAclStream, - StreamState.NoStream, - new( - acl: new( - writeRole: TestCredentials.TestUser1.Username, - readRole: TestCredentials.TestUser1.Username, - metaWriteRole: TestCredentials.TestUser1.Username, - metaReadRole: TestCredentials.TestUser1.Username - ) - ), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - SystemAdminStream, - StreamState.NoStream, - new( - acl: new( - writeRole: SystemRoles.Admins, - readRole: SystemRoles.Admins, - metaWriteRole: SystemRoles.Admins, - metaReadRole: SystemRoles.Admins - ) - ), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - NormalAllStream, - StreamState.NoStream, - new( - acl: new( - writeRole: SystemRoles.All, - readRole: SystemRoles.All, - metaWriteRole: SystemRoles.All, - metaReadRole: SystemRoles.All - ) - ), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - await Streams.SetStreamMetadataAsync( - SystemAllStream, - StreamState.NoStream, - new( - acl: new( - writeRole: SystemRoles.All, - readRole: SystemRoles.All, - metaWriteRole: SystemRoles.All, - metaReadRole: SystemRoles.All - ) - ), - userCredentials: TestCredentials.TestAdmin - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - } - - protected virtual Task When() => Task.CompletedTask; - - public Task ReadEvent(string streamId, UserCredentials? userCredentials = default) => - Streams.ReadStreamAsync( - Direction.Forwards, - streamId, - StreamPosition.Start, - 1, - false, - userCredentials: userCredentials - ) - .ToArrayAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task ReadStreamForward(string streamId, UserCredentials? userCredentials = default) => - Streams.ReadStreamAsync( - Direction.Forwards, - streamId, - StreamPosition.Start, - 1, - false, - userCredentials: userCredentials - ) - .ToArrayAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task ReadStreamBackward(string streamId, UserCredentials? userCredentials = default) => - Streams.ReadStreamAsync( - Direction.Backwards, - streamId, - StreamPosition.Start, - 1, - false, - userCredentials: userCredentials - ) - .ToArrayAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task AppendStream(string streamId, UserCredentials? userCredentials = default) => - Streams.AppendToStreamAsync( - streamId, - StreamState.Any, - CreateTestEvents(3), - userCredentials: userCredentials - ) - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task ReadAllForward(UserCredentials? userCredentials = default) => - Streams.ReadAllAsync( - Direction.Forwards, - Position.Start, - 1, - false, - userCredentials: userCredentials - ) - .ToArrayAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task ReadAllBackward(UserCredentials? userCredentials = default) => - Streams - .ReadAllAsync( - Direction.Backwards, - Position.End, - 1, - false, - userCredentials: userCredentials - ) - .ToArrayAsync() - .AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task ReadMeta(string streamId, UserCredentials? userCredentials = default) => - Streams.GetStreamMetadataAsync(streamId, userCredentials: userCredentials) - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - public Task WriteMeta(string streamId, UserCredentials? userCredentials = default, string? role = default) => - Streams.SetStreamMetadataAsync( - streamId, - StreamState.Any, - new( - acl: new( - writeRole: role, - readRole: role, - metaWriteRole: role, - metaReadRole: role - ) - ), - userCredentials: userCredentials - ) - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - [Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client", false)] - public async Task SubscribeToStreamObsolete(string streamId, UserCredentials? userCredentials = default) { - var source = new TaskCompletionSource(); - using (await Streams.SubscribeToStreamAsync( - streamId, - FromStream.Start, - (_, _, _) => { - source.TrySetResult(true); - return Task.CompletedTask; - }, - subscriptionDropped: (_, _, ex) => { - if (ex == null) - source.TrySetResult(true); - else - source.TrySetException(ex); - }, - userCredentials: userCredentials - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs))) { - await source.Task.WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - } - } - - public async Task SubscribeToStream(string streamId, UserCredentials? userCredentials = default) { - await using var subscription = - Streams.SubscribeToStream(streamId, FromStream.Start, userCredentials: userCredentials); - await subscription - .Messages.OfType().AnyAsync().AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - } - - [Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client", false)] - public async Task SubscribeToAllObsolete(UserCredentials? userCredentials = default) { - var source = new TaskCompletionSource(); - using (await Streams.SubscribeToAllAsync( - FromAll.Start, - (_, _, _) => { - source.TrySetResult(true); - return Task.CompletedTask; - }, - false, - (_, _, ex) => { - if (ex == null) - source.TrySetResult(true); - else - source.TrySetException(ex); - }, - userCredentials: userCredentials - ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs))) { - await source.Task.WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - } - } - - public async Task SubscribeToAll(UserCredentials? userCredentials = default) { - await using var subscription = - Streams.SubscribeToAll(FromAll.Start, userCredentials: userCredentials); - await subscription - .Messages.OfType().AnyAsync().AsTask() - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - } - - public async Task CreateStreamWithMeta(StreamMetadata metadata, [CallerMemberName] string streamId = "") { - await Streams.SetStreamMetadataAsync( - streamId, - StreamState.NoStream, - metadata, - userCredentials: TestCredentials.TestAdmin - ) - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - - return streamId; - } - - public Task DeleteStream(string streamId, UserCredentials? userCredentials = default) => - Streams.TombstoneAsync(streamId, StreamState.Any, userCredentials: userCredentials) - .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); -} diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/all_stream_with_no_acl_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/all_stream_with_no_acl_security_obsolete.cs deleted file mode 100644 index 40cdd81ef..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/all_stream_with_no_acl_security_obsolete.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class all_stream_with_no_acl_security_obsolete(ITestOutputHelper output, all_stream_with_no_acl_security_obsolete.CustomFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task reading_and_subscribing_is_not_allowed_when_no_credentials_are_passed() { - await Assert.ThrowsAsync(() => Fixture.ReadAllForward()); - await Assert.ThrowsAsync(() => Fixture.ReadAllBackward()); - await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture_obsolete.AllStream)); - await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete()); - } - - [Fact] - public async Task reading_and_subscribing_is_not_allowed_for_usual_user() { - await Assert.ThrowsAsync(() => Fixture.ReadAllForward(TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.ReadAllBackward(TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture_obsolete.AllStream, TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete(TestCredentials.TestUser1)); - } - - [Fact] - public async Task reading_and_subscribing_is_allowed_for_admin_user() { - await Fixture.ReadAllForward(TestCredentials.TestAdmin); - await Fixture.ReadAllBackward(TestCredentials.TestAdmin); - await Fixture.ReadMeta(SecurityFixture_obsolete.AllStream, TestCredentials.TestAdmin); - await Fixture.SubscribeToAllObsolete(TestCredentials.TestAdmin); - } - - public class CustomFixture : SecurityFixture_obsolete { - protected override async Task Given() { - await base.Given(); - - await Streams.SetStreamMetadataAsync(AllStream, StreamState.Any, new(), userCredentials: TestCredentials.Root); - } - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_for_all_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_for_all_obsolete.cs deleted file mode 100644 index 4593e39e0..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_for_all_obsolete.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class overriden_system_stream_security_for_all_obsolete(ITestOutputHelper output, overriden_system_stream_security_for_all_obsolete.CustomFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task operations_on_system_stream_succeeds_for_user() { - var stream = $"${Fixture.GetStreamName()}"; - await Fixture.AppendStream(stream, TestCredentials.TestUser1); - await Fixture.ReadEvent(stream, TestCredentials.TestUser1); - await Fixture.ReadStreamForward(stream, TestCredentials.TestUser1); - await Fixture.ReadStreamBackward(stream, TestCredentials.TestUser1); - - await Fixture.ReadMeta(stream, TestCredentials.TestUser1); - await Fixture.WriteMeta(stream, TestCredentials.TestUser1); - - await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser1); - - await Fixture.DeleteStream(stream, TestCredentials.TestUser1); - } - - [AnonymousAccess.Fact] - public async Task operations_on_system_stream_fail_for_anonymous_user() { - var stream = $"${Fixture.GetStreamName()}"; - await Fixture.AppendStream(stream); - await Fixture.ReadEvent(stream); - await Fixture.ReadStreamForward(stream); - await Fixture.ReadStreamBackward(stream); - - await Fixture.ReadMeta(stream); - await Fixture.WriteMeta(stream); - - await Fixture.SubscribeToStreamObsolete(stream); - - await Fixture.DeleteStream(stream); - } - - [Fact] - public async Task operations_on_system_stream_succeed_for_admin() { - var stream = $"${Fixture.GetStreamName()}"; - await Fixture.AppendStream(stream, TestCredentials.TestAdmin); - - await Fixture.ReadEvent(stream, TestCredentials.TestAdmin); - await Fixture.ReadStreamForward(stream, TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward(stream, TestCredentials.TestAdmin); - - await Fixture.ReadMeta(stream, TestCredentials.TestAdmin); - await Fixture.WriteMeta(stream, TestCredentials.TestAdmin); - - await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestAdmin); - - await Fixture.DeleteStream(stream, TestCredentials.TestAdmin); - } - - public class CustomFixture : SecurityFixture_obsolete { - protected override Task When() { - var settings = new SystemSettings( - systemStreamAcl: new( - SystemRoles.All, - SystemRoles.All, - SystemRoles.All, - SystemRoles.All, - SystemRoles.All - ) - ); - - return Streams.SetSystemSettingsAsync(settings, userCredentials: TestCredentials.TestAdmin); - } - } -} diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_obsolete.cs deleted file mode 100644 index f36646be1..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_system_stream_security_obsolete.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class overriden_system_stream_security_obsolete(ITestOutputHelper output, overriden_system_stream_security_obsolete.CustomFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task operations_on_system_stream_succeed_for_authorized_user() { - var stream = $"${Fixture.GetStreamName()}"; - await Fixture.AppendStream(stream, TestCredentials.TestUser1); - - await Fixture.ReadEvent(stream, TestCredentials.TestUser1); - await Fixture.ReadStreamForward(stream, TestCredentials.TestUser1); - await Fixture.ReadStreamBackward(stream, TestCredentials.TestUser1); - - await Fixture.ReadMeta(stream, TestCredentials.TestUser1); - await Fixture.WriteMeta(stream, TestCredentials.TestUser1); - - await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser1); - - await Fixture.DeleteStream(stream, TestCredentials.TestUser1); - } - - [Fact] - public async Task operations_on_system_stream_fail_for_not_authorized_user() { - var stream = $"${Fixture.GetStreamName()}"; - await Assert.ThrowsAsync(() => Fixture.ReadEvent(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync( - () => - Fixture.ReadStreamBackward(stream, TestCredentials.TestUser2) - ); - - await Assert.ThrowsAsync(() => Fixture.AppendStream(stream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.ReadMeta(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta(stream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.DeleteStream(stream, TestCredentials.TestUser2)); - } - - [Fact] - public async Task operations_on_system_stream_fail_for_anonymous_user() { - var stream = $"${Fixture.GetStreamName()}"; - await Assert.ThrowsAsync(() => Fixture.ReadEvent(stream)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward(stream)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamBackward(stream)); - - await Assert.ThrowsAsync(() => Fixture.AppendStream(stream)); - - await Assert.ThrowsAsync(() => Fixture.ReadMeta(stream)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta(stream)); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(stream)); - - await Assert.ThrowsAsync(() => Fixture.DeleteStream(stream)); - } - - [Fact] - public async Task operations_on_system_stream_succeed_for_admin() { - var stream = $"${Fixture.GetStreamName()}"; - await Fixture.AppendStream(stream, TestCredentials.TestAdmin); - - await Fixture.ReadEvent(stream, TestCredentials.TestAdmin); - await Fixture.ReadStreamForward(stream, TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward(stream, TestCredentials.TestAdmin); - - await Fixture.ReadMeta(stream, TestCredentials.TestAdmin); - await Fixture.WriteMeta(stream, TestCredentials.TestAdmin); - - await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestAdmin); - - await Fixture.DeleteStream(stream, TestCredentials.TestAdmin); - } - - public class CustomFixture : SecurityFixture_obsolete { - protected override Task When() { - var settings = new SystemSettings( - systemStreamAcl: new("user1", "user1", "user1", "user1", "user1"), - userStreamAcl: default - ); - - return Streams.SetSystemSettingsAsync(settings, userCredentials: TestCredentials.TestAdmin); - } - } -} diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_user_stream_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_user_stream_security_obsolete.cs deleted file mode 100644 index dcbd88fa1..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/overriden_user_stream_security_obsolete.cs +++ /dev/null @@ -1,78 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class overriden_user_stream_security_obsolete(ITestOutputHelper output, overriden_user_stream_security_obsolete.CustomFixture fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task operations_on_user_stream_succeeds_for_authorized_user() { - var stream = Fixture.GetStreamName(); - await Fixture.AppendStream(stream, TestCredentials.TestUser1); - - await Fixture.ReadEvent(stream, TestCredentials.TestUser1); - await Fixture.ReadStreamForward(stream, TestCredentials.TestUser1); - await Fixture.ReadStreamBackward(stream, TestCredentials.TestUser1); - - await Fixture.ReadMeta(stream, TestCredentials.TestUser1); - await Fixture.WriteMeta(stream, TestCredentials.TestUser1); - - await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser1); - - await Fixture.DeleteStream(stream, TestCredentials.TestUser1); - } - - [Fact] - public async Task operations_on_user_stream_fail_for_not_authorized_user() { - var stream = Fixture.GetStreamName(); - await Assert.ThrowsAsync(() => Fixture.ReadEvent(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamBackward(stream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.AppendStream(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.ReadMeta(stream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta(stream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.DeleteStream(stream, TestCredentials.TestUser2)); - } - - [Fact] - public async Task operations_on_user_stream_fail_for_anonymous_user() { - var stream = Fixture.GetStreamName(); - await Assert.ThrowsAsync(() => Fixture.ReadEvent(stream)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward(stream)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamBackward(stream)); - - await Assert.ThrowsAsync(() => Fixture.AppendStream(stream)); - await Assert.ThrowsAsync(() => Fixture.ReadMeta(stream)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta(stream)); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(stream)); - - await Assert.ThrowsAsync(() => Fixture.DeleteStream(stream)); - } - - [Fact] - public async Task operations_on_user_stream_succeed_for_admin() { - var stream = Fixture.GetStreamName(); - await Fixture.AppendStream(stream, TestCredentials.TestAdmin); - - await Fixture.ReadEvent(stream, TestCredentials.TestAdmin); - await Fixture.ReadStreamForward(stream, TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward(stream, TestCredentials.TestAdmin); - - await Fixture.ReadMeta(stream, TestCredentials.TestAdmin); - await Fixture.WriteMeta(stream, TestCredentials.TestAdmin); - - await Fixture.SubscribeToStreamObsolete(stream, TestCredentials.TestAdmin); - - await Fixture.DeleteStream(stream, TestCredentials.TestAdmin); - } - - public class CustomFixture : SecurityFixture_obsolete { - protected override Task When() { - var settings = new SystemSettings(new("user1", "user1", "user1", "user1", "user1")); - return Streams.SetSystemSettingsAsync(settings, userCredentials: TestCredentials.TestAdmin); - } - } -} diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_all_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_all_security_obsolete.cs deleted file mode 100644 index b21cf66c6..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_all_security_obsolete.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class subscribe_to_all_security_obsolete(ITestOutputHelper output, SecurityFixture_obsolete fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task subscribing_to_all_with_not_existing_credentials_is_not_authenticated() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete(TestCredentials.TestBadUser)); - - [Fact] - public async Task subscribing_to_all_with_no_credentials_is_denied() => await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete()); - - [Fact] - public async Task subscribing_to_all_with_not_authorized_user_credentials_is_denied() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToAllObsolete(TestCredentials.TestUser2)); - - [Fact] - public async Task subscribing_to_all_with_authorized_user_credentials_succeeds() => await Fixture.SubscribeToAllObsolete(TestCredentials.TestUser1); - - [Fact] - public async Task subscribing_to_all_with_admin_user_credentials_succeeds() => await Fixture.SubscribeToAllObsolete(TestCredentials.TestAdmin); -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_stream_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_stream_security_obsolete.cs deleted file mode 100644 index 409256650..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/subscribe_to_stream_security_obsolete.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class subscribe_to_stream_security_obsolete(ITestOutputHelper output, SecurityFixture_obsolete fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task subscribing_to_stream_with_not_existing_credentials_is_not_authenticated() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.ReadStream, TestCredentials.TestBadUser)); - - [Fact] - public async Task subscribing_to_stream_with_no_credentials_is_denied() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.ReadStream)); - - [Fact] - public async Task subscribing_to_stream_with_not_authorized_user_credentials_is_denied() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.ReadStream, TestCredentials.TestUser2)); - - [Fact] - public async Task reading_stream_with_authorized_user_credentials_succeeds() { - await Fixture.AppendStream(SecurityFixture_obsolete.ReadStream, TestCredentials.TestUser1); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.ReadStream, TestCredentials.TestUser1); - } - - [Fact] - public async Task reading_stream_with_admin_user_credentials_succeeds() { - await Fixture.AppendStream(SecurityFixture_obsolete.ReadStream, TestCredentials.TestAdmin); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.ReadStream, TestCredentials.TestAdmin); - } - - [AnonymousAccess.Fact] - public async Task subscribing_to_no_acl_stream_succeeds_when_no_credentials_are_passed() { - await Fixture.AppendStream(SecurityFixture_obsolete.NoAclStream); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NoAclStream); - } - - [Fact] - public async Task subscribing_to_no_acl_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NoAclStream, TestCredentials.TestBadUser)); - - [Fact] - public async Task subscribing_to_no_acl_stream_succeeds_when_any_existing_user_credentials_are_passed() { - await Fixture.AppendStream(SecurityFixture_obsolete.NoAclStream, TestCredentials.TestUser1); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NoAclStream, TestCredentials.TestUser1); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NoAclStream, TestCredentials.TestUser2); - } - - [Fact] - public async Task subscribing_to_no_acl_stream_succeeds_when_admin_user_credentials_are_passed() { - await Fixture.AppendStream(SecurityFixture_obsolete.NoAclStream, TestCredentials.TestAdmin); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NoAclStream, TestCredentials.TestAdmin); - } - - [AnonymousAccess.Fact] - public async Task subscribing_to_all_access_normal_stream_succeeds_when_no_credentials_are_passed() { - await Fixture.AppendStream(SecurityFixture_obsolete.NormalAllStream); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NormalAllStream); - } - - [Fact] - public async Task subscribing_to_all_access_normal_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NormalAllStream, TestCredentials.TestBadUser)); - - [Fact] - public async Task subscribing_to_all_access_normal_stream_succeeds_when_any_existing_user_credentials_are_passed() { - await Fixture.AppendStream(SecurityFixture_obsolete.NormalAllStream, TestCredentials.TestUser1); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NormalAllStream, TestCredentials.TestUser1); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NormalAllStream, TestCredentials.TestUser2); - } - - [Fact] - public async Task subscribing_to_all_access_normal_streamm_succeeds_when_admin_user_credentials_are_passed() { - await Fixture.AppendStream(SecurityFixture_obsolete.NormalAllStream, TestCredentials.TestAdmin); - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.NormalAllStream, TestCredentials.TestAdmin); - } -} diff --git a/test/EventStore.Client.Streams.Tests/Security/Obsolete/system_stream_security_obsolete.cs b/test/EventStore.Client.Streams.Tests/Security/Obsolete/system_stream_security_obsolete.cs deleted file mode 100644 index 462696ff2..000000000 --- a/test/EventStore.Client.Streams.Tests/Security/Obsolete/system_stream_security_obsolete.cs +++ /dev/null @@ -1,146 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Obsolete; - -[Trait("Category", "Security")] -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class system_stream_security_obsolete(ITestOutputHelper output, SecurityFixture_obsolete fixture) : EventStoreTests(output, fixture) { - [Fact] - public async Task operations_on_system_stream_with_no_acl_set_fail_for_non_admin() { - await Assert.ThrowsAsync(() => Fixture.ReadEvent("$system-no-acl", TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward("$system-no-acl", TestCredentials.TestUser1)); - - await Assert.ThrowsAsync(() => Fixture.ReadStreamBackward("$system-no-acl", TestCredentials.TestUser1)); - - await Assert.ThrowsAsync(() => Fixture.AppendStream("$system-no-acl", TestCredentials.TestUser1)); - - await Assert.ThrowsAsync(() => Fixture.ReadMeta("$system-no-acl", TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta("$system-no-acl", TestCredentials.TestUser1)); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete("$system-no-acl", TestCredentials.TestUser1)); - } - - [Fact] - public async Task operations_on_system_stream_with_no_acl_set_succeed_for_admin() { - await Fixture.AppendStream("$system-no-acl", TestCredentials.TestAdmin); - - await Fixture.ReadEvent("$system-no-acl", TestCredentials.TestAdmin); - await Fixture.ReadStreamForward("$system-no-acl", TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward("$system-no-acl", TestCredentials.TestAdmin); - - await Fixture.ReadMeta("$system-no-acl", TestCredentials.TestAdmin); - await Fixture.WriteMeta("$system-no-acl", TestCredentials.TestAdmin); - - await Fixture.SubscribeToStreamObsolete("$system-no-acl", TestCredentials.TestAdmin); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_usual_user_fail_for_not_authorized_user() { - await Assert.ThrowsAsync(() => Fixture.ReadEvent(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.AppendStream(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2)); - - await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2, TestCredentials.TestUser1.Username)); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser2)); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_usual_user_succeed_for_that_user() { - await Fixture.AppendStream(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1); - await Fixture.ReadEvent(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1); - await Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1); - await Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1); - - await Fixture.ReadMeta(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1); - await Fixture.WriteMeta(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1, TestCredentials.TestUser1.Username); - - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestUser1); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_usual_user_succeed_for_admin() { - await Fixture.AppendStream(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin); - await Fixture.ReadEvent(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin); - await Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin); - - await Fixture.ReadMeta(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin); - await Fixture.WriteMeta(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin, TestCredentials.TestUser1.Username); - - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAclStream, TestCredentials.TestAdmin); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_admins_fail_for_usual_user() { - await Assert.ThrowsAsync(() => Fixture.ReadEvent(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1)); - await Assert.ThrowsAsync(() => Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1)); - await Assert.ThrowsAsync( - () => - Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1) - ); - - await Assert.ThrowsAsync(() => Fixture.AppendStream(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1)); - - await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1)); - await Assert.ThrowsAsync( - () => - Fixture.WriteMeta(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1, SystemRoles.Admins) - ); - - await Assert.ThrowsAsync(() => Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestUser1)); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_admins_succeed_for_admin() { - await Fixture.AppendStream(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin); - await Fixture.ReadEvent(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin); - await Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin); - - await Fixture.ReadMeta(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin); - await Fixture.WriteMeta(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin, SystemRoles.Admins); - - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAdminStream, TestCredentials.TestAdmin); - } - - [AnonymousAccess.Fact] - public async Task operations_on_system_stream_with_acl_set_to_all_succeed_for_not_authenticated_user() { - await Fixture.AppendStream(SecurityFixture_obsolete.SystemAllStream); - await Fixture.ReadEvent(SecurityFixture_obsolete.SystemAllStream); - await Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAllStream); - await Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAllStream); - - await Fixture.ReadMeta(SecurityFixture_obsolete.SystemAllStream); - await Fixture.WriteMeta(SecurityFixture_obsolete.SystemAllStream, role: SystemRoles.All); - - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAllStream); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_all_succeed_for_usual_user() { - await Fixture.AppendStream(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1); - await Fixture.ReadEvent(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1); - await Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1); - await Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1); - - await Fixture.ReadMeta(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1); - await Fixture.WriteMeta(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1, SystemRoles.All); - - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestUser1); - } - - [Fact] - public async Task operations_on_system_stream_with_acl_set_to_all_succeed_for_admin() { - await Fixture.AppendStream(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin); - await Fixture.ReadEvent(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin); - await Fixture.ReadStreamForward(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin); - await Fixture.ReadStreamBackward(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin); - - await Fixture.ReadMeta(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin); - await Fixture.WriteMeta(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin, SystemRoles.All); - - await Fixture.SubscribeToStreamObsolete(SecurityFixture_obsolete.SystemAllStream, TestCredentials.TestAdmin); - } -} diff --git a/test/EventStore.Client.Streams.Tests/Serialization/is_json.cs b/test/EventStore.Client.Streams.Tests/Serialization/is_json.cs deleted file mode 100644 index d2b44a918..000000000 --- a/test/EventStore.Client.Streams.Tests/Serialization/is_json.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Text; - -namespace EventStore.Client.Streams.Tests.Serialization; - -[Trait("Category", "Serialization")] -public class is_json(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { - public static IEnumerable TestCases() { - var json = @"{""some"":""json""}"; - - yield return new object?[] { true, json, string.Empty }; - yield return new object?[] { true, string.Empty, json }; - yield return new object?[] { true, json, json }; - yield return new object?[] { false, json, string.Empty }; - yield return new object?[] { false, string.Empty, json }; - yield return new object?[] { false, json, json }; - } - - [Theory] - [MemberData(nameof(TestCases))] - public async Task is_preserved(bool isJson, string data, string metadata) { - var stream = GetStreamName(isJson, data, metadata); - var encoding = Encoding.UTF8; - var eventData = new EventData( - Uuid.NewUuid(), - "-", - encoding.GetBytes(data), - encoding.GetBytes(metadata), - isJson - ? Constants.Metadata.ContentTypes.ApplicationJson - : Constants.Metadata.ContentTypes.ApplicationOctetStream - ); - - await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, new[] { eventData }); - - var @event = await Fixture.Streams - .ReadStreamAsync( - Direction.Forwards, - stream, - StreamPosition.Start, - 1, - true - ) - .FirstOrDefaultAsync(); - - Assert.Equal( - isJson - ? Constants.Metadata.ContentTypes.ApplicationJson - : Constants.Metadata.ContentTypes.ApplicationOctetStream, - @event.Event.ContentType - ); - - Assert.Equal(data, encoding.GetString(@event.Event.Data.ToArray())); - Assert.Equal(metadata, encoding.GetString(@event.Event.Metadata.ToArray())); - } - - string GetStreamName(bool isJson, string data, string metadata, [CallerMemberName] string? testMethod = default) => - $"{Fixture.GetStreamName(testMethod)}_{isJson}_{(data == string.Empty ? "no_data" : "data")}_{(metadata == string.Empty ? "no_metadata" : "metadata")}"; -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionsFixture.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionsFixture.cs deleted file mode 100644 index 582344f98..000000000 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionsFixture.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions; - - -[Trait("Category", "Subscriptions")] -public class SubscriptionsFixture : EventStoreFixture { - public SubscriptionsFixture(): base(x => x.RunProjections()) { - OnSetup = async () => { - await Streams.SetStreamMetadataAsync( - SystemStreams.AllStream, - StreamState.NoStream, - new(acl: new(SystemRoles.All)), - userCredentials: TestCredentials.Root - ); - - await Streams.AppendToStreamAsync($"SubscriptionsFixture-Noise-{Guid.NewGuid():N}", StreamState.NoStream, CreateTestEvents(10)); - }; - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs b/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs deleted file mode 100644 index 2dc9912e5..000000000 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/reconnection.cs +++ /dev/null @@ -1,178 +0,0 @@ -using Grpc.Core; -using static System.TimeSpan; - -namespace EventStore.Client.Streams.Tests.Subscriptions; - -[Trait("Category", "Subscriptions")] -[Obsolete] -public class @reconnection(ITestOutputHelper output, ReconnectionFixture fixture) : EventStoreTests(output, fixture) { - [Theory] - [InlineData(4, 5000, 0, 30000)] - public async Task when_the_connection_is_lost(int expectedNumberOfEvents, int reconnectDelayMs, int serviceRestartDelayMs, int testTimeoutMs) { - using var cancellator = new CancellationTokenSource().With(x => x.CancelAfter(testTimeoutMs)); - - var streamName = Fixture.GetStreamName(); - - // create backpressure by producing half of the events - await Fixture.ProduceEvents(streamName, expectedNumberOfEvents / 2, cancellationToken: cancellator.Token); - - // create subscription that will actually receive the first event and - // then wait for the service to be restarted - // but we are evil and will force the drop of the subscription muah ah ah - var consumeEvents = Fixture.ConsumeEvents( - streamName, - expectedNumberOfEvents, - FromMilliseconds(reconnectDelayMs), - cancellator.Token - ); - - // create chaos by pausing the service - await Fixture.RestartService(FromMilliseconds(serviceRestartDelayMs)); - - // produce the rest of the events to make it more interesting - await Fixture.ProduceEvents(streamName, expectedNumberOfEvents / 2, cancellationToken: cancellator.Token); - - // wait for the subscription to receive all events or timeout - await consumeEvents.ShouldNotThrowAsync(); - } -} - -public class ReconnectionFixture() - : EventStoreFixture( - x => x.RunInMemory(false) - .With(o => o.ClientSettings.ConnectivitySettings.DiscoveryInterval = FromMilliseconds(100)) - .With(o => o.ClientSettings.ConnectivitySettings.GossipTimeout = FromMilliseconds(100)) - ) -{ - public async Task ProduceEvents(string streamName, int numberOfEvents, StreamState? streamState = null, CancellationToken cancellationToken = default) { - while (!cancellationToken.IsCancellationRequested) { - try { - var result = await Streams.AppendToStreamAsync( - streamName, - streamState.GetValueOrDefault(StreamState.Any), - CreateTestEvents(numberOfEvents), - cancellationToken: cancellationToken - ); - - if (result is SuccessResult success) { - Log.Information( - "{NumberOfEvents} events produced to {StreamName}.", numberOfEvents, streamName - ); - - return; - } - - Log.Error( - "Failed to produce {NumberOfEvents} events to {StreamName}.", numberOfEvents, streamName - ); - - await Task.Delay(250); - } - catch (Exception ex) when ( ex is not OperationCanceledException) { - Log.Error( - ex, "Failed to produce {NumberOfEvents} events to {StreamName}.", numberOfEvents, streamName - ); - - await Task.Delay(250); - } - } - } - - public Task ConsumeEvents( - string streamName, - int expectedNumberOfEvents, - TimeSpan reconnectDelay, - CancellationToken cancellationToken - ) { - var receivedAllEvents = new TaskCompletionSource(); - - var receivedEventsCount = 0; - - _ = SubscribeToStream( - streamName, - checkpoint: null, - OnReceive(), - OnDrop(), - cancellationToken - ); - - return receivedAllEvents.Task; - - Func OnReceive() { - return re => { - receivedEventsCount++; - Log.Debug("{ReceivedEventsCount}/{ExpectedNumberOfEvents} events received.", receivedEventsCount, expectedNumberOfEvents); - - if (receivedEventsCount == expectedNumberOfEvents) { - Log.Information("Test complete. {ReceivedEventsCount}/{ExpectedNumberOfEvents} events received.", receivedEventsCount, expectedNumberOfEvents); - receivedAllEvents.TrySetResult(); - } - - return Task.CompletedTask; - }; - } - - Func> OnDrop() { - return async (reason, ex) => { - if (ex is RpcException { StatusCode: StatusCode.Unavailable or StatusCode.DeadlineExceeded }) { - Log.Warning("Transitive exception detected. Retrying connection in {reconnectDelayMs}ms.", reconnectDelay.TotalMilliseconds); - await Task.Delay(reconnectDelay); - return true; - } - - if (reason == SubscriptionDroppedReason.Disposed || ex is OperationCanceledException || ex is TaskCanceledException || ex is null) { - if (receivedEventsCount != expectedNumberOfEvents) - receivedAllEvents.TrySetException(new TimeoutException($"Test timeout detected. {receivedEventsCount}/{expectedNumberOfEvents} events received.", ex)); - else { - Log.Information("Test cancellation requested. {ReceivedEventsCount}/{ExpectedNumberOfEvents} events received.", receivedEventsCount, expectedNumberOfEvents); - receivedAllEvents.TrySetCanceled(cancellationToken); - } - - return false; - } - - Log.Fatal(ex, "Fatal exception detected. This is the end..."); - receivedAllEvents.SetException(ex); - - return false; - }; - } - } - - [Obsolete] - async Task SubscribeToStream( - string stream, - StreamPosition? checkpoint, - Func onReceive, - Func> onDrop, - CancellationToken cancellationToken - ) { - var start = checkpoint == null ? FromStream.Start : FromStream.After(checkpoint.Value); - - Log.Verbose("Attempting to start from checkpoint: {Checkpoint}.", checkpoint); - - try { - var sub = await Streams.SubscribeToStreamAsync( - streamName: stream, - start: start, - eventAppeared: async (s, re, ct) => { - await onReceive(re); - checkpoint = re.OriginalEventNumber; - Log.Verbose("Checkpoint Set: {Checkpoint}.", checkpoint); - }, - subscriptionDropped: async (s, reason, ex) => { - var resubscribe = await onDrop(reason, ex); - if (resubscribe) _ = SubscribeToStream(stream, checkpoint, onReceive, onDrop, cancellationToken); - }, - cancellationToken: cancellationToken - ); - } catch (Exception ex) { - var reason = ex is OperationCanceledException or TaskCanceledException - ? SubscriptionDroppedReason.Disposed - : SubscriptionDroppedReason.SubscriberError; - - var resubscribe = await onDrop(reason, ex); - if (resubscribe) _ = SubscribeToStream(stream, checkpoint, onReceive, onDrop, cancellationToken); - } - } -} diff --git a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj b/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj deleted file mode 100644 index f666f3871..000000000 --- a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj +++ /dev/null @@ -1,72 +0,0 @@ - - - EventStore.Client.Tests - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - certs\%(RecursiveDir)/%(FileName)%(Extension) - Always - - - - - - Always - - - Always - - - Always - - - PreserveNewest - - - Always - - - Always - - - - - - - diff --git a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj.DotSettings b/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj.DotSettings deleted file mode 100644 index 6ba62e1cd..000000000 --- a/test/EventStore.Client.Tests.Common/EventStore.Client.Tests.Common.csproj.DotSettings +++ /dev/null @@ -1,5 +0,0 @@ - - True - True - True - True \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/Facts/SupportsPSToAllFact.cs b/test/EventStore.Client.Tests.Common/Facts/SupportsPSToAllFact.cs deleted file mode 100644 index 3f5d993fe..000000000 --- a/test/EventStore.Client.Tests.Common/Facts/SupportsPSToAllFact.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ReSharper disable InconsistentNaming - -namespace EventStore.Client.Tests; - -[PublicAPI] -public class SupportsPSToAll { - const int SupportedFromMajorVersion = 21; - - static readonly string SkipMessage = $"Persistent Subscriptions to $all are not supported on" - + $" {EventStoreTestServer.Version?.ToString(3) ?? "unknown"}"; - - public static bool No => !Yes; - public static bool Yes => (EventStoreTestServer.Version?.Major ?? int.MaxValue) >= SupportedFromMajorVersion; - - public class FactAttribute() : Regression.FactAttribute(SupportedFromMajorVersion, SkipMessage); - - public class TheoryAttribute() : Regression.TheoryAttribute(SupportedFromMajorVersion, SkipMessage); -} \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreClientFixtureBase.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreClientFixtureBase.cs deleted file mode 100644 index 141a2a8da..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreClientFixtureBase.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Diagnostics; -using System.Net; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using System.Runtime.CompilerServices; -using System.Text; -using Serilog; -using Serilog.Events; -using Serilog.Extensions.Logging; -using Serilog.Formatting.Display; - -namespace EventStore.Client; - -public abstract class EventStoreClientFixtureBase : IAsyncLifetime { - public const string TestEventType = "-"; - - const string ConnectionStringSingle = "esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=false"; - const string ConnectionStringCluster = "esdb://admin:changeit@localhost:2113,localhost:2112,localhost:2111?tls=true&tlsVerifyCert=false"; - - static readonly Subject LogEventSubject = new(); - - readonly IList _disposables; - - static EventStoreClientFixtureBase() => ConfigureLogging(); - - protected EventStoreClientFixtureBase( - EventStoreClientSettings? clientSettings, - IDictionary? env = null, bool noDefaultCredentials = false - ) { - _disposables = new List(); - - ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; - - var connectionString = GlobalEnvironment.UseCluster ? ConnectionStringCluster : ConnectionStringSingle; - Settings = clientSettings ?? EventStoreClientSettings.Create(connectionString); - - if (noDefaultCredentials) - Settings.DefaultCredentials = null; - - Settings.DefaultDeadline = Debugger.IsAttached - ? new TimeSpan?() - : TimeSpan.FromSeconds(30); - - var hostCertificatePath = Path.Combine( - Environment.CurrentDirectory, - GlobalEnvironment.UseCluster ? "certs-cluster" : "certs" - ); - - Settings.LoggerFactory ??= new SerilogLoggerFactory(); - - Settings.ConnectivitySettings.MaxDiscoverAttempts = 20; - Settings.ConnectivitySettings.DiscoveryInterval = TimeSpan.FromSeconds(1); - - if (GlobalEnvironment.UseExternalServer) - TestServer = new EventStoreTestServerExternal(); - else - TestServer = GlobalEnvironment.UseCluster - ? new EventStoreTestServerCluster(hostCertificatePath, Settings.ConnectivitySettings.ResolvedAddressOrDefault, env) - : new EventStoreTestServer(hostCertificatePath, Settings.ConnectivitySettings.ResolvedAddressOrDefault, env); - } - - public IEventStoreTestServer TestServer { get; } - protected EventStoreClientSettings Settings { get; } - - public Faker Faker { get; } = new(); - - public virtual async Task InitializeAsync() { - await TestServer.StartAsync().WithTimeout(TimeSpan.FromMinutes(5)); - await OnServerUpAsync().WithTimeout(TimeSpan.FromMinutes(5)); - await Given().WithTimeout(TimeSpan.FromMinutes(5)); - await When().WithTimeout(TimeSpan.FromMinutes(5)); - } - - public virtual Task DisposeAsync() { - foreach (var disposable in _disposables) - disposable.Dispose(); - - return TestServer.DisposeAsync().AsTask().WithTimeout(TimeSpan.FromMinutes(5)); - } - - static void ConfigureLogging() { - var loggerConfiguration = new LoggerConfiguration() - .Enrich.FromLogContext() - .MinimumLevel.Is(LogEventLevel.Verbose) - .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) - .MinimumLevel.Override("Grpc", LogEventLevel.Verbose) - .WriteTo.Observers(observable => observable.Subscribe(LogEventSubject.OnNext)) - .WriteTo.Seq("http://localhost:5341/", period: TimeSpan.FromMilliseconds(1)); - - Log.Logger = loggerConfiguration.CreateLogger(); - AppDomain.CurrentDomain.DomainUnload += (_, e) => Log.CloseAndFlush(); - } - - protected abstract Task OnServerUpAsync(); - protected abstract Task Given(); - protected abstract Task When(); - - public IEnumerable CreateTestEvents(int count = 1, string? type = null, int metadataSize = 1) => - Enumerable.Range(0, count).Select(index => CreateTestEvent(index, type ?? TestEventType, metadataSize)); - - protected static EventData CreateTestEvent(int index) => CreateTestEvent(index, TestEventType, 1); - - protected static EventData CreateTestEvent(int index, string type, int metadataSize) => - new( - Uuid.NewUuid(), - type, - Encoding.UTF8.GetBytes($@"{{""x"":{index}}}"), - Encoding.UTF8.GetBytes("\"" + new string('$', metadataSize) + "\"") - ); - - public string GetStreamName([CallerMemberName] string? testMethod = null) { - var type = GetType(); - - return $"{type.DeclaringType?.Name}.{testMethod ?? "unknown"}"; - } - - public void CaptureLogs(ITestOutputHelper testOutputHelper) { - const string captureCorrelationId = nameof(captureCorrelationId); - - var captureId = Guid.NewGuid(); - - var callContextData = new AsyncLocal<(string, Guid)> { - Value = (captureCorrelationId, captureId) - }; - - bool Filter(LogEvent logEvent) => callContextData.Value.Item2.Equals(captureId); - - var formatter = new MessageTemplateTextFormatter("{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message}"); - - var formatterWithException = - new MessageTemplateTextFormatter("{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}"); - - var subscription = LogEventSubject.Where(Filter).Subscribe( - logEvent => { - using var writer = new StringWriter(); - if (logEvent.Exception != null) - formatterWithException.Format(logEvent, writer); - else - formatter.Format(logEvent, writer); - - testOutputHelper.WriteLine(writer.ToString()); - } - ); - - _disposables.Add(subscription); - } -} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServer.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServer.cs deleted file mode 100644 index 20c8c0c93..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServer.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System.Net; -using System.Net.Http; -using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Extensions; -using Ductus.FluentDocker.Model.Builders; -using Ductus.FluentDocker.Services; -using Ductus.FluentDocker.Services.Extensions; -using Polly; - -namespace EventStore.Client.Tests; - -public class EventStoreTestServer : IEventStoreTestServer { - static readonly string ContainerName = "es-client-dotnet-test"; - - static Version? _version; - readonly IContainerService _eventStore; - readonly string _hostCertificatePath; - readonly HttpClient _httpClient; - - public EventStoreTestServer( - string hostCertificatePath, - Uri address, - IDictionary? envOverrides - ) { - _hostCertificatePath = hostCertificatePath; - VerifyCertificatesExist(); - -#if NET - _httpClient = new HttpClient(new SocketsHttpHandler { - SslOptions = {RemoteCertificateValidationCallback = delegate { return true; }} - }) { - BaseAddress = address, - }; -#else - _httpClient = new HttpClient(new WinHttpHandler { - ServerCertificateValidationCallback = delegate { return true; } - }) { - BaseAddress = address, - }; -#endif - - var env = new Dictionary { - ["EVENTSTORE_DB_LOG_FORMAT"] = "V2", - ["EVENTSTORE_MEM_DB"] = "true", - ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), - ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", - ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", - ["EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH"] = "/etc/eventstore/certs/ca", - ["EVENTSTORE_LOG_LEVEL"] = "Verbose", - ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", - ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", - ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "false", - ["EVENTSTORE_DISABLE_LOG_FILE"] = "true" - }; - - foreach (var val in envOverrides ?? Enumerable.Empty>()) - env[val.Key] = val.Value; - - _eventStore = new Builder() - .UseContainer() - .UseImage(GlobalEnvironment.DockerImage) - .WithEnvironment(env.Select(pair => $"{pair.Key}={pair.Value}").ToArray()) - .WithName(ContainerName) - .MountVolume(_hostCertificatePath, "/etc/eventstore/certs", MountType.ReadOnly) - .ExposePort(2113, 2113) - //.WaitForHealthy(TimeSpan.FromSeconds(120)) - //.KeepContainer() - //.KeepRunning() - .Build(); - } - - public static Version Version => _version ??= GetVersion(); - - public async Task StartAsync(CancellationToken cancellationToken = default) { - _eventStore.Start(); - try { - await Policy.Handle() - .WaitAndRetryAsync(200, retryCount => TimeSpan.FromMilliseconds(100)) - .ExecuteAsync( - async () => { - using var response = await _httpClient.GetAsync("/health/live", cancellationToken); - if (response.StatusCode >= HttpStatusCode.BadRequest) - throw new($"Health check failed with status code: {response.StatusCode}."); - } - ); - } - catch (Exception) { - _eventStore.Dispose(); - throw; - } - } - - public void Stop() => _eventStore.Stop(); - - public ValueTask DisposeAsync() { - _httpClient?.Dispose(); - _eventStore?.Dispose(); - - return new ValueTask(Task.CompletedTask); - } - - static Version GetVersion() { - const string versionPrefix = "EventStoreDB version"; - - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - using var eventstore = new Builder().UseContainer() - .UseImage(GlobalEnvironment.DockerImage) - .Command("--version") - .Build() - .Start(); - - using var log = eventstore.Logs(true, cts.Token); - foreach (var line in log.ReadToEnd()) { - if (line.StartsWith(versionPrefix) && - Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { - return version; - } - } - - throw new InvalidOperationException("Could not determine server version."); - - IEnumerable ReadVersion(string s) { - foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { - yield return c; - } - } - } - - void VerifyCertificatesExist() { - var certificateFiles = new[] { - Path.Combine("ca", "ca.crt"), - Path.Combine("ca", "ca.key"), - Path.Combine("node", "node.crt"), - Path.Combine("node", "node.key") - }.Select(path => Path.Combine(_hostCertificatePath, path)); - - foreach (var file in certificateFiles) - if (!File.Exists(file)) - throw new InvalidOperationException( - $"Could not locate the certificates file {file} needed to run EventStoreDB. Please run the 'gencert' tool at the root of the repository." - ); - } -} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerCluster.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerCluster.cs deleted file mode 100644 index ceb263e15..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerCluster.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Net; -using System.Net.Http; -using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Common; -using Ductus.FluentDocker.Services; -using Polly; - -namespace EventStore.Client; - -// [Obsolete("Use EventStoreTestCluster instead.", false)] -public class EventStoreTestServerCluster : IEventStoreTestServer { - readonly ICompositeService _eventStoreCluster; - readonly HttpClient _httpClient; - - public EventStoreTestServerCluster( - string hostCertificatePath, - Uri address, - IDictionary? envOverrides - ) { - envOverrides ??= new Dictionary(); - envOverrides["ES_CERTS_CLUSTER"] = hostCertificatePath; - - _eventStoreCluster = BuildCluster(envOverrides); - -#if NET - _httpClient = new HttpClient(new SocketsHttpHandler { - SslOptions = {RemoteCertificateValidationCallback = delegate { return true; }} - }) { - BaseAddress = address, - }; -#else - _httpClient = new HttpClient(new WinHttpHandler { - ServerCertificateValidationCallback = delegate { return true; } - }) { - BaseAddress = address, - }; -#endif - } - - public async Task StartAsync(CancellationToken cancellationToken = default) { - try { - // don't know why, sometimes the default network (e.g. net50_default) remains - // from previous cluster and prevents docker-compose up from executing successfully - Policy.Handle() - .WaitAndRetry( - 10, - retryCount => TimeSpan.FromSeconds(2), - (ex, _) => { - BuildCluster().Dispose(); - _eventStoreCluster.Start(); - } - ) - .Execute(() => { _eventStoreCluster.Start(); }); - - await Policy.Handle() - .WaitAndRetryAsync(200, retryCount => TimeSpan.FromMilliseconds(100)) - .ExecuteAsync( - async () => { - using var response = await _httpClient.GetAsync("/health/live", cancellationToken); - if (response.StatusCode >= HttpStatusCode.BadRequest) - throw new($"Health check failed with status code: {response.StatusCode}."); - } - ); - } - catch (Exception) { - _eventStoreCluster.Dispose(); - throw; - } - } - - public void Stop() => _eventStoreCluster.Stop(); - - public ValueTask DisposeAsync() { - _eventStoreCluster.Dispose(); - return new(Task.CompletedTask); - } - - ICompositeService BuildCluster(IDictionary? envOverrides = null) { - var env = GlobalEnvironment - .GetEnvironmentVariables(envOverrides) - .Select(pair => $"{pair.Key}={pair.Value}") - .ToArray(); - - return new Builder() - .UseContainer() - .UseCompose() - .WithEnvironment(env) - .FromFile("docker-compose.yml") - .ForceRecreate() - .RemoveOrphans() - .Build(); - } -} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerExternal.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerExternal.cs deleted file mode 100644 index 19b866a63..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/EventStoreTestServerExternal.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace EventStore.Client; - -public class EventStoreTestServerExternal : IEventStoreTestServer { - public Task StartAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; - public void Stop() { } - - public ValueTask DisposeAsync() => new ValueTask(Task.CompletedTask); -} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/Base/IEventStoreTestServer.cs b/test/EventStore.Client.Tests.Common/Fixtures/Base/IEventStoreTestServer.cs deleted file mode 100644 index 2d467835d..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/Base/IEventStoreTestServer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace EventStore.Client; - -public interface IEventStoreTestServer : IAsyncDisposable { - Task StartAsync(CancellationToken cancellationToken = default); - void Stop(); -} \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/Fixtures/DiagnosticsFixture.cs b/test/EventStore.Client.Tests.Common/Fixtures/DiagnosticsFixture.cs deleted file mode 100644 index 549093654..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/DiagnosticsFixture.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using EventStore.Client.Diagnostics; -using EventStore.Diagnostics; -using EventStore.Diagnostics.Telemetry; -using EventStore.Diagnostics.Tracing; - -namespace EventStore.Client.Tests; - -[PublicAPI] -public class DiagnosticsFixture : EventStoreFixture { - readonly ConcurrentDictionary<(string Operation, string Stream), List> _activities = []; - - public DiagnosticsFixture() : base(x => x.RunProjections()) { - var diagnosticActivityListener = new ActivityListener { - ShouldListenTo = source => source.Name == EventStoreClientDiagnostics.InstrumentationName, - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStopped = activity => { - var operation = (string?)activity.GetTagItem(TelemetryTags.Database.Operation); - var stream = (string?)activity.GetTagItem(TelemetryTags.EventStore.Stream); - - if (operation is null || stream is null) - return; - - _activities.AddOrUpdate( - (operation, stream), - _ => [activity], - (_, activities) => { - activities.Add(activity); - return activities; - } - ); - } - }; - - OnSetup = () => { - ActivitySource.AddActivityListener(diagnosticActivityListener); - return Task.CompletedTask; - }; - - OnTearDown = () => { - diagnosticActivityListener.Dispose(); - return Task.CompletedTask; - }; - } - - public List GetActivitiesForOperation(string operation, string stream) => - _activities.TryGetValue((operation, stream), out var activities) ? activities : []; - - public void AssertAppendActivityHasExpectedTags(Activity activity, string stream) { - var expectedTags = new Dictionary { - { TelemetryTags.Database.System, EventStoreClientDiagnostics.InstrumentationName }, - { TelemetryTags.Database.Operation, TracingConstants.Operations.Append }, - { TelemetryTags.EventStore.Stream, stream }, - { TelemetryTags.Database.User, TestCredentials.Root.Username }, - { TelemetryTags.Otel.StatusCode, ActivityStatusCodeHelper.OkStatusCodeTagValue } - }; - - foreach (var tag in expectedTags) - activity.Tags.ShouldContain(tag); - } - - public void AssertErroneousAppendActivityHasExpectedTags(Activity activity, Exception actualException) { - var expectedTags = new Dictionary { - { TelemetryTags.Otel.StatusCode, ActivityStatusCodeHelper.ErrorStatusCodeTagValue } - }; - - foreach (var tag in expectedTags) - activity.Tags.ShouldContain(tag); - - var actualEvent = activity.Events.ShouldHaveSingleItem(); - - actualEvent.Name.ShouldBe(TelemetryTags.Exception.EventName); - actualEvent.Tags.ShouldContain( - new KeyValuePair(TelemetryTags.Exception.Type, actualException.GetType().FullName) - ); - - actualEvent.Tags.ShouldContain( - new KeyValuePair(TelemetryTags.Exception.Message, actualException.Message) - ); - - actualEvent.Tags.Any(x => x.Key == TelemetryTags.Exception.Stacktrace).ShouldBeTrue(); - } - - public void AssertSubscriptionActivityHasExpectedTags( - Activity activity, - string stream, - string eventId, - string? subscriptionId = null - ) { - var expectedTags = new Dictionary { - { TelemetryTags.Database.System, EventStoreClientDiagnostics.InstrumentationName }, - { TelemetryTags.Database.Operation, TracingConstants.Operations.Subscribe }, - { TelemetryTags.EventStore.Stream, stream }, - { TelemetryTags.EventStore.EventId, eventId }, - { TelemetryTags.EventStore.EventType, TestEventType }, - { TelemetryTags.Database.User, TestCredentials.Root.Username } - }; - - if (subscriptionId != null) - expectedTags[TelemetryTags.EventStore.SubscriptionId] = subscriptionId; - - foreach (var tag in expectedTags) { - activity.Tags.ShouldContain(tag); - } - } -} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreFixture.cs b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreFixture.cs deleted file mode 100644 index 50faebf7a..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreFixture.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System.Net; -using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Extensions; -using Ductus.FluentDocker.Services.Extensions; -using EventStore.Client.Tests.FluentDocker; -using Serilog; -using static System.TimeSpan; - -namespace EventStore.Client.Tests; - -public record EventStoreFixtureOptions( - EventStoreClientSettings ClientSettings, - IDictionary Environment -) { - public EventStoreFixtureOptions RunInMemory(bool runInMemory = true) => - this with { Environment = Environment.With(x => x["EVENTSTORE_MEM_DB"] = runInMemory.ToString()) }; - - public EventStoreFixtureOptions RunProjections(bool runProjections = true) => - this with { - Environment = Environment.With( - x => { - x["EVENTSTORE_START_STANDARD_PROJECTIONS"] = runProjections.ToString(); - x["EVENTSTORE_RUN_PROJECTIONS"] = runProjections ? "All" : "None"; - } - ) - }; - - public EventStoreFixtureOptions WithoutDefaultCredentials() => - this with { ClientSettings = ClientSettings.With(x => x.DefaultCredentials = null) }; - - public EventStoreFixtureOptions WithMaxAppendSize(uint maxAppendSize) => - this with { Environment = Environment.With(x => x["EVENTSTORE_MAX_APPEND_SIZE"] = $"{maxAppendSize}") }; -} - -public delegate EventStoreFixtureOptions ConfigureFixture(EventStoreFixtureOptions options); - -[PublicAPI] -public partial class EventStoreFixture : IAsyncLifetime, IAsyncDisposable { - static readonly ILogger Logger; - - static EventStoreFixture() { - Logging.Initialize(); - Logger = Serilog.Log.ForContext(); - - ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; - } - - public EventStoreFixture() : this(options => options) { } - - protected EventStoreFixture(ConfigureFixture configure) { - // TODO SS: should I verify the certificates exist here? - if (GlobalEnvironment.UseExternalServer) { - Options = new(new(), new Dictionary()); - Service = new TestBypassService(); - } - - if (GlobalEnvironment.UseCluster) { - Options = configure(EventStoreTestCluster.DefaultOptions()); - Service = new EventStoreTestCluster(Options); - } else { - Options = configure(EventStoreTestNode.DefaultOptions()); - Service = new EventStoreTestNode(Options); - } - } - - List TestRuns { get; } = new(); - - public ILogger Log => Logger; - - public ITestService Service { get; } - public EventStoreFixtureOptions Options { get; } - public Faker Faker { get; } = new Faker(); - - public Version EventStoreVersion { get; private set; } = null!; - public bool EventStoreHasLastStreamPosition { get; private set; } - - public EventStoreClient Streams { get; private set; } = null!; - public EventStoreUserManagementClient Users { get; private set; } = null!; - public EventStoreProjectionManagementClient Projections { get; private set; } = null!; - public EventStorePersistentSubscriptionsClient Subscriptions { get; private set; } = null!; - public EventStoreOperationsClient Operations { get; private set; } = null!; - - public Func OnSetup { get; init; } = () => Task.CompletedTask; - public Func OnTearDown { get; init; } = () => Task.CompletedTask; - - /// - /// must test this - /// - public EventStoreClientSettings ClientSettings => - new() { - Interceptors = Options.ClientSettings.Interceptors, - ConnectionName = Options.ClientSettings.ConnectionName, - CreateHttpMessageHandler = Options.ClientSettings.CreateHttpMessageHandler, - LoggerFactory = Options.ClientSettings.LoggerFactory, - ChannelCredentials = Options.ClientSettings.ChannelCredentials, - OperationOptions = Options.ClientSettings.OperationOptions, - ConnectivitySettings = Options.ClientSettings.ConnectivitySettings, - DefaultCredentials = Options.ClientSettings.DefaultCredentials, - DefaultDeadline = Options.ClientSettings.DefaultDeadline - }; - - InterlockedBoolean WarmUpCompleted { get; } = new InterlockedBoolean(); - SemaphoreSlim WarmUpGatekeeper { get; } = new(1, 1); - - public void CaptureTestRun(ITestOutputHelper outputHelper) { - var testRunId = Logging.CaptureLogs(outputHelper); - TestRuns.Add(testRunId); - Logger.Information(">>> Test Run {TestRunId} {Operation} <<<", testRunId, "starting"); - Service.ReportStatus(); - } - - public async Task InitializeAsync() { - await Service.Start(); - - EventStoreVersion = GetEventStoreVersion(); - EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; - - await WarmUpGatekeeper.WaitAsync(); - - try { - if (!WarmUpCompleted.CurrentValue) { - Logger.Warning("*** Warmup started ***"); - - await Task.WhenAll( - InitClient(async x => Users = await x.WarmUp()), - InitClient(async x => Streams = await x.WarmUp()), - InitClient( - async x => Projections = await x.WarmUp(), - Options.Environment["EVENTSTORE_RUN_PROJECTIONS"] != "None" - ), - InitClient(async x => Subscriptions = await x.WarmUp()), - InitClient(async x => Operations = await x.WarmUp()) - ); - - WarmUpCompleted.EnsureCalledOnce(); - - Logger.Warning("*** Warmup completed ***"); - } else { - Logger.Information("*** Warmup skipped ***"); - } - } finally { - WarmUpGatekeeper.Release(); - } - - await OnSetup(); - - return; - - async Task InitClient(Func action, bool execute = true) where T : EventStoreClientBase { - if (!execute) return default(T)!; - - var client = (Activator.CreateInstance(typeof(T), new object?[] { ClientSettings }) as T)!; - await action(client); - return client; - } - - static Version GetEventStoreVersion() { - const string versionPrefix = "EventStoreDB version"; - - using var cancellator = new CancellationTokenSource(FromSeconds(30)); - using var eventstore = new Builder() - .UseContainer() - .UseImage(GlobalEnvironment.DockerImage) - .Command("--version") - .Build() - .Start(); - - using var log = eventstore.Logs(true, cancellator.Token); - foreach (var line in log.ReadToEnd()) { - if (line.StartsWith(versionPrefix) && - Version.TryParse( - new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), - out var version - )) { - return version; - } - } - - throw new InvalidOperationException("Could not determine server version."); - - IEnumerable ReadVersion(string s) { - foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { - yield return c; - } - } - } - } - - public async Task DisposeAsync() { - try { - await OnTearDown(); - } catch { - // ignored - } - - await Service.DisposeAsync().AsTask().WithTimeout(FromMinutes(5)); - - foreach (var testRunId in TestRuns) - Logging.ReleaseLogs(testRunId); - } - - async ValueTask IAsyncDisposable.DisposeAsync() => await DisposeAsync(); -} - -[CollectionDefinition(nameof(EventStoreSharedDatabaseFixture))] -public class EventStoreSharedDatabaseFixture : ICollectionFixture { - // This class has no code, and is never created. Its purpose is simply - // to be the place to apply [CollectionDefinition] and all the - // ICollectionFixture<> interfaces. -} - -public abstract class EventStoreTests : IClassFixture where TFixture : EventStoreFixture { - protected EventStoreTests(ITestOutputHelper output, TFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - protected TFixture Fixture { get; } -} - -[Collection(nameof(EventStoreSharedDatabaseFixture))] -public abstract class EventStoreSharedDatabaseTests(ITestOutputHelper output, TFixture fixture) - : EventStoreTests(output, fixture) - where TFixture : EventStoreFixture; diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestCluster.cs b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestCluster.cs deleted file mode 100644 index ad8246843..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestCluster.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Ductus.FluentDocker.Builders; -using EventStore.Client.Tests.FluentDocker; -using Serilog; -using Serilog.Extensions.Logging; - -namespace EventStore.Client.Tests; - -public class EventStoreTestCluster(EventStoreFixtureOptions options) : TestCompositeService { - EventStoreFixtureOptions Options { get; } = options; - - public static EventStoreFixtureOptions DefaultOptions() { - const string connString = "esdb://localhost:2113,localhost:2112,localhost:2111?tls=true&tlsVerifyCert=false"; - - var defaultSettings = EventStoreClientSettings - .Create(connString) - .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) - .With(x => x.DefaultDeadline = Application.DebuggerIsAttached ? new TimeSpan?() : TimeSpan.FromSeconds(30)) - .With(x => x.ConnectivitySettings.MaxDiscoverAttempts = 30) - .With(x => x.ConnectivitySettings.DiscoveryInterval = TimeSpan.FromSeconds(1)); - - var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { - ["ES_CERTS_CLUSTER"] = Path.Combine(Environment.CurrentDirectory, "certs-cluster"), - ["EVENTSTORE_CLUSTER_SIZE"] = "3", - ["EVENTSTORE_INT_TCP_PORT"] = "1112", - ["EVENTSTORE_HTTP_PORT"] = "2113", - ["EVENTSTORE_DISCOVER_VIA_DNS"] = "false", - ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", - ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000" - }; - - return new(defaultSettings, defaultEnvironment); - } - - protected override CompositeBuilder Configure() { - var env = Options.Environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray(); - - var builder = new Builder() - .UseContainer() - .FromComposeFile("docker-compose.yml") - .ServiceName("esdb-test-cluster") - .WithEnvironment(env) - .RemoveOrphans() - .NoRecreate() - .KeepRunning(); - - return builder; - } - - protected override async Task OnServiceStarted() { - await Service.WaitUntilNodesAreHealthy("esdb-node", TimeSpan.FromSeconds(60)); - } -} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/InsecureClientTestFixture.cs b/test/EventStore.Client.Tests.Common/Fixtures/InsecureClientTestFixture.cs deleted file mode 100644 index 33407d177..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/InsecureClientTestFixture.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace EventStore.Client.Tests; - -/// -/// The clients dont have default credentials set. -/// -[PublicAPI] -public class InsecureClientTestFixture() : EventStoreFixture(x => x.WithoutDefaultCredentials()); \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/Fixtures/RunInMemoryTestFixture.cs b/test/EventStore.Client.Tests.Common/Fixtures/RunInMemoryTestFixture.cs deleted file mode 100644 index 61cfbc77c..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/RunInMemoryTestFixture.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace EventStore.Client.Tests; - -[PublicAPI] -public class RunInMemoryTestFixture() : EventStoreFixture(x => x.RunInMemory()); \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/Fixtures/RunProjectionsTestFixture.cs b/test/EventStore.Client.Tests.Common/Fixtures/RunProjectionsTestFixture.cs deleted file mode 100644 index cb42cc1db..000000000 --- a/test/EventStore.Client.Tests.Common/Fixtures/RunProjectionsTestFixture.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace EventStore.Client.Tests; - -[PublicAPI] -public class RunProjectionsTestFixture() : EventStoreFixture(x => x.RunProjections()); \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs b/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs deleted file mode 100644 index 782ab7696..000000000 --- a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Reflection; -using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Model.Compose; -using Ductus.FluentDocker.Services; -using Ductus.FluentDocker.Services.Impl; - -namespace EventStore.Client.Tests.FluentDocker; - -public static class FluentDockerBuilderExtensions { - public static CompositeBuilder OverrideConfiguration(this CompositeBuilder compositeBuilder, Action configure) { - configure(GetInternalConfig(compositeBuilder)); - return compositeBuilder; - - static DockerComposeConfig GetInternalConfig(CompositeBuilder compositeBuilder) => - (DockerComposeConfig)typeof(CompositeBuilder) - .GetField("_config", BindingFlags.NonPublic | BindingFlags.Instance)! - .GetValue(compositeBuilder)!; - } - - public static DockerComposeConfig Configuration(this ICompositeService service) => - (DockerComposeConfig)typeof(DockerComposeCompositeService) - .GetProperty("Config", BindingFlags.NonPublic | BindingFlags.Instance)! - .GetValue(service)!; -} \ No newline at end of file diff --git a/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj b/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj deleted file mode 100644 index 494e5e243..000000000 --- a/test/EventStore.Client.Tests/EventStore.Client.Tests.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - diff --git a/test/EventStore.Client.Tests/EventStoreClientOperationOptionsTests.cs b/test/EventStore.Client.Tests/EventStoreClientOperationOptionsTests.cs deleted file mode 100644 index baf9bf5b3..000000000 --- a/test/EventStore.Client.Tests/EventStoreClientOperationOptionsTests.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace EventStore.Client.Tests; - -public class EventStoreClientOperationOptionsTests { - [Fact] - public void setting_options_on_clone_should_not_modify_original() { - var options = EventStoreClientOperationOptions.Default; - - var clonedOptions = options.Clone(); - clonedOptions.BatchAppendSize = int.MaxValue; - - Assert.Equal(options.BatchAppendSize, EventStoreClientOperationOptions.Default.BatchAppendSize); - Assert.Equal(int.MaxValue, clonedOptions.BatchAppendSize); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs b/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs deleted file mode 100644 index 2b13cf13b..000000000 --- a/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -#if NET -using System.Net; -using EventStore.Client.ServerFeatures; -using Grpc.Core; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; - -namespace EventStore.Client.Tests; - -public class GrpcServerCapabilitiesClientTests { - public static IEnumerable ExpectedResultsCases() { - yield return new object?[] { new SupportedMethods(), new ServerCapabilities() }; - yield return new object?[] { - new SupportedMethods { - Methods = { - new SupportedMethod { - ServiceName = "event_store.client.streams.streams", - MethodName = "batchappend" - } - } - }, - new ServerCapabilities(true) - }; - - yield return new object?[] { - new SupportedMethods { - Methods = { - new SupportedMethod { - ServiceName = "event_store.client.persistent_subscriptions.persistentsubscriptions", - MethodName = "read", - Features = { - "all" - } - } - } - }, - new ServerCapabilities(SupportsPersistentSubscriptionsToAll: true) - }; - - yield return new object?[] { - new SupportedMethods { - Methods = { - new SupportedMethod { - ServiceName = "event_store.client.persistent_subscriptions.persistentsubscriptions", - MethodName = "read" - } - } - }, - new ServerCapabilities() - }; - } - - [Theory] - [MemberData(nameof(ExpectedResultsCases))] - internal async Task GetAsyncReturnsExpectedResults( - SupportedMethods supportedMethods, - ServerCapabilities expected - ) { - using var kestrel = new TestServer( - new WebHostBuilder() - .ConfigureServices( - services => services - .AddRouting() - .AddGrpc().Services - .AddSingleton(new FakeServerFeatures(supportedMethods)) - ) - .Configure( - app => app - .UseRouting() - .UseEndpoints(ep => ep.MapGrpcService()) - ) - ); - - var sut = new GrpcServerCapabilitiesClient(new()); - - var actual = - await sut.GetAsync( - ChannelFactory - .CreateChannel( - new() { - CreateHttpMessageHandler = kestrel.CreateHandler - }, - new DnsEndPoint("localhost", 80) - ) - .CreateCallInvoker(), - default - ); - - Assert.Equal(expected, actual); - } - - class FakeServerFeatures : ServerFeatures.ServerFeatures.ServerFeaturesBase { - readonly SupportedMethods _supportedMethods; - - public FakeServerFeatures(SupportedMethods supportedMethods) => _supportedMethods = supportedMethods; - - public override Task GetSupportedMethods(Empty request, ServerCallContext context) => Task.FromResult(_supportedMethods); - } -} -#endif diff --git a/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs b/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs deleted file mode 100644 index 5703c3bd7..000000000 --- a/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs +++ /dev/null @@ -1,224 +0,0 @@ -using EventStore.Client.Interceptors; -using Grpc.Core; -using Grpc.Core.Interceptors; - -namespace EventStore.Client.Tests.Interceptors; - -public class ReportLeaderInterceptorTests { - public delegate Task GrpcCall(Interceptor interceptor, Task? response = null); - - static readonly StatusCode[] ForcesRediscoveryStatusCodes = { - //StatusCode.Unknown, TODO: use RPC exceptions on server - StatusCode.Unavailable - }; - - static readonly Marshaller Marshaller = new(_ => Array.Empty(), _ => new()); - - static IEnumerable GrpcCalls() { - yield return MakeUnaryCall; - yield return MakeClientStreamingCall; - yield return MakeDuplexStreamingCall; - yield return MakeServerStreamingCall; - yield return MakeClientStreamingCallForWriting; - yield return MakeDuplexStreamingCallForWriting; - } - - public static IEnumerable ReportsNewLeaderCases() => GrpcCalls().Select(call => new object[] { call }); - - [Theory] - [MemberData(nameof(ReportsNewLeaderCases))] - public async Task ReportsNewLeader(GrpcCall call) { - ReconnectionRequired? actual = default; - - var sut = new ReportLeaderInterceptor(result => actual = result); - - var result = await Assert.ThrowsAsync(() => call(sut, Task.FromException(new NotLeaderException("a.host", 2112)))); - - Assert.Equal(new ReconnectionRequired.NewLeader(result.LeaderEndpoint), actual); - } - - public static IEnumerable ForcesRediscoveryCases() => - from call in GrpcCalls() - from statusCode in ForcesRediscoveryStatusCodes - select new object[] { call, statusCode }; - - [Theory] - [MemberData(nameof(ForcesRediscoveryCases))] - public async Task ForcesRediscovery(GrpcCall call, StatusCode statusCode) { - ReconnectionRequired? actual = default; - - var sut = new ReportLeaderInterceptor(result => actual = result); - - await Assert.ThrowsAsync(() => call(sut, Task.FromException(new RpcException(new(statusCode, "oops"))))); - - Assert.Equal(ReconnectionRequired.Rediscover.Instance, actual); - } - - public static IEnumerable DoesNotForceRediscoveryCases() => - from call in GrpcCalls() - from statusCode in Enum.GetValues(typeof(StatusCode)) - .OfType() - .Except(ForcesRediscoveryStatusCodes) - select new object[] { call, statusCode }; - - [Theory] - [MemberData(nameof(DoesNotForceRediscoveryCases))] - public async Task DoesNotForceRediscovery(GrpcCall call, StatusCode statusCode) { - ReconnectionRequired actual = ReconnectionRequired.None.Instance; - - var sut = new ReportLeaderInterceptor(result => actual = result); - - await Assert.ThrowsAsync(() => call(sut, Task.FromException(new RpcException(new(statusCode, "oops"))))); - - Assert.Equal(ReconnectionRequired.None.Instance, actual); - } - - static async Task MakeUnaryCall(Interceptor interceptor, Task? response = null) { - using var call = interceptor.AsyncUnaryCall( - new(), - CreateClientInterceptorContext(MethodType.Unary), - (_, context) => new( - response ?? Task.FromResult(new object()), - Task.FromResult(context.Options.Headers!), - GetSuccess, - GetTrailers, - OnDispose - ) - ); - - await call.ResponseAsync; - } - - static async Task MakeClientStreamingCall(Interceptor interceptor, Task? response = null) { - using var call = interceptor.AsyncClientStreamingCall( - CreateClientInterceptorContext(MethodType.ClientStreaming), - context => new( - null!, - response ?? Task.FromResult(new object()), - Task.FromResult(context.Options.Headers!), - GetSuccess, - GetTrailers, - OnDispose - ) - ); - - await call.ResponseAsync; - } - - static async Task MakeServerStreamingCall(Interceptor interceptor, Task? response = null) { - using var call = interceptor.AsyncServerStreamingCall( - new(), - CreateClientInterceptorContext(MethodType.ServerStreaming), - (_, context) => new( - new TestAsyncStreamReader(response), - Task.FromResult(context.Options.Headers!), - GetSuccess, - GetTrailers, - OnDispose - ) - ); - - await call.ResponseStream.ReadAllAsync().ToArrayAsync(); - } - - static async Task MakeDuplexStreamingCall(Interceptor interceptor, Task? response = null) { - using var call = interceptor.AsyncDuplexStreamingCall( - CreateClientInterceptorContext(MethodType.ServerStreaming), - context => new( - null!, - new TestAsyncStreamReader(response), - Task.FromResult(context.Options.Headers!), - GetSuccess, - GetTrailers, - OnDispose - ) - ); - - await call.ResponseStream.ReadAllAsync().ToArrayAsync(); - } - - // we might write to the server before listening to its response. if that write fails because - // the server is down then we will never listen to its response, so the failed write should - // trigger rediscovery itself - static async Task MakeClientStreamingCallForWriting(Interceptor interceptor, Task? response = null) { - using var call = interceptor.AsyncClientStreamingCall( - CreateClientInterceptorContext(MethodType.ClientStreaming), - context => new( - new TestAsyncStreamWriter(response), - Task.FromResult(new object()), - Task.FromResult(context.Options.Headers!), - GetSuccess, - GetTrailers, - OnDispose - ) - ); - - await call.RequestStream.WriteAsync(new()); - } - - static async Task MakeDuplexStreamingCallForWriting(Interceptor interceptor, Task? response = null) { - using var call = interceptor.AsyncDuplexStreamingCall( - CreateClientInterceptorContext(MethodType.ServerStreaming), - _ => new( - new TestAsyncStreamWriter(response), - null!, - null!, - GetSuccess, - GetTrailers, - OnDispose - ) - ); - - await call.RequestStream.WriteAsync(new()); - } - - static Status GetSuccess() => Status.DefaultSuccess; - - static Metadata GetTrailers() => Metadata.Empty; - - static void OnDispose() { } - - static ClientInterceptorContext CreateClientInterceptorContext(MethodType methodType) => - new( - new( - methodType, - string.Empty, - string.Empty, - Marshaller, - Marshaller - ), - null, - new(new()) - ); - - class TestAsyncStreamReader : IAsyncStreamReader { - readonly Task _response; - - public TestAsyncStreamReader(Task? response = null) => _response = response ?? Task.FromResult(new object()); - - public Task MoveNext(CancellationToken cancellationToken) => - _response.IsFaulted - ? Task.FromException(_response.Exception!.GetBaseException()) - : Task.FromResult(false); - - public object Current => _response.Result; - } - - class TestAsyncStreamWriter : IClientStreamWriter { - readonly Task _response; - - public TestAsyncStreamWriter(Task? response = null) => _response = response ?? Task.FromResult(new object()); - - public WriteOptions? WriteOptions { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - - public Task CompleteAsync() => throw new NotImplementedException(); - - public Task WriteAsync(object message) => - _response.IsFaulted - ? Task.FromException(_response.Exception!.GetBaseException()) - : Task.FromResult(false); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Tests/PrefixFilterExpressionTests.cs b/test/EventStore.Client.Tests/PrefixFilterExpressionTests.cs deleted file mode 100644 index ae2cbdcd6..000000000 --- a/test/EventStore.Client.Tests/PrefixFilterExpressionTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoFixture; - -namespace EventStore.Client.Tests; - -public class PrefixFilterExpressionTests : ValueObjectTests { - public PrefixFilterExpressionTests() : base(new ScenarioFixture()) { } - - class ScenarioFixture : Fixture { - public ScenarioFixture() => Customize(composer => composer.FromFactory(value => new(value))); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/AssemblyInfo.cs b/test/EventStore.Client.UserManagement.Tests/AssemblyInfo.cs deleted file mode 100644 index b0b47aa73..000000000 --- a/test/EventStore.Client.UserManagement.Tests/AssemblyInfo.cs +++ /dev/null @@ -1 +0,0 @@ -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj b/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj deleted file mode 100644 index abaf5e7bf..000000000 --- a/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - EventStore.Client.Tests - - - - - - - - \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj.DotSettings b/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj.DotSettings deleted file mode 100644 index 1183b3a73..000000000 --- a/test/EventStore.Client.UserManagement.Tests/EventStore.Client.UserManagement.Tests.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - True \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/changing_user_password.cs b/test/EventStore.Client.UserManagement.Tests/changing_user_password.cs deleted file mode 100644 index d3931c1ea..000000000 --- a/test/EventStore.Client.UserManagement.Tests/changing_user_password.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace EventStore.Client.Tests; - -public class changing_user_password : IClassFixture { - public changing_user_password(ITestOutputHelper output, EventStoreFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); - - EventStoreFixture Fixture { get; } - - public static IEnumerable NullInputCases() { - yield return Fakers.Users.Generate().WithResult(x => new object?[] { null, x.Password, x.Password, "loginName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, null, x.Password, "currentPassword" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, x.Password, null, "newPassword" }); - } - - [Theory] - [MemberData(nameof(NullInputCases))] - public async Task with_null_input_throws(string loginName, string currentPassword, string newPassword, string paramName) { - var ex = await Fixture.Users - .ChangePasswordAsync(loginName, currentPassword, newPassword, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe(paramName); - } - - public static IEnumerable EmptyInputCases() { - yield return Fakers.Users.Generate().WithResult(x => new object?[] { string.Empty, x.Password, x.Password, "loginName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, string.Empty, x.Password, "currentPassword" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, x.Password, string.Empty, "newPassword" }); - } - - [Theory] - [MemberData(nameof(EmptyInputCases))] - public async Task with_empty_input_throws(string loginName, string currentPassword, string newPassword, string paramName) { - var ex = await Fixture.Users - .ChangePasswordAsync(loginName, currentPassword, newPassword, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe(paramName); - } - - [Theory(Skip = "This can't be right")] - [ClassData(typeof(InvalidCredentialsTestCases))] - public async Task with_user_with_insufficient_credentials_throws(string loginName, UserCredentials userCredentials) { - await Fixture.Users.CreateUserAsync(loginName, "Full Name", Array.Empty(), "password", userCredentials: TestCredentials.Root); - - await Fixture.Users - .ChangePasswordAsync(loginName, "password", "newPassword", userCredentials: userCredentials) - .ShouldThrowAsync(); - } - - [Fact] - public async Task when_the_current_password_is_wrong_throws() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users - .ChangePasswordAsync(user.LoginName, "wrong-password", "new-password", userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - } - - [Fact] - public async Task with_correct_credentials() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users - .ChangePasswordAsync(user.LoginName, user.Password, "new-password", userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - } - - [Fact] - public async Task with_own_credentials() { - var user = await Fixture.CreateTestUser(); - - await Fixture.Users - .ChangePasswordAsync(user.LoginName, user.Password, "new-password", userCredentials: user.Credentials) - .ShouldNotThrowAsync(); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/creating_a_user.cs b/test/EventStore.Client.UserManagement.Tests/creating_a_user.cs deleted file mode 100644 index abd37bab1..000000000 --- a/test/EventStore.Client.UserManagement.Tests/creating_a_user.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace EventStore.Client.Tests; - -public class creating_a_user : IClassFixture { - public creating_a_user(ITestOutputHelper outputHelper, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(outputHelper)); - - InsecureClientTestFixture Fixture { get; } - - public static IEnumerable NullInputCases() { - yield return Fakers.Users.Generate().WithResult(x => new object?[] { null, x.FullName, x.Groups, x.Password, "loginName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, null, x.Groups, x.Password, "fullName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, x.FullName, null, x.Password, "groups" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, x.FullName, x.Groups, null, "password" }); - } - - [Theory] - [MemberData(nameof(NullInputCases))] - public async Task with_null_input_throws(string loginName, string fullName, string[] groups, string password, string paramName) { - var ex = await Fixture.Users - .CreateUserAsync(loginName, fullName, groups, password, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe(paramName); - } - - public static IEnumerable EmptyInputCases() { - yield return Fakers.Users.Generate().WithResult(x => new object?[] { string.Empty, x.FullName, x.Groups, x.Password, "loginName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, string.Empty, x.Groups, x.Password, "fullName" }); - yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, x.FullName, x.Groups, string.Empty, "password" }); - } - - [Theory] - [MemberData(nameof(EmptyInputCases))] - public async Task with_empty_input_throws(string loginName, string fullName, string[] groups, string password, string paramName) { - var ex = await Fixture.Users - .CreateUserAsync(loginName, fullName, groups, password, userCredentials: TestCredentials.Root) - .ShouldThrowAsync(); - - ex.ParamName.ShouldBe(paramName); - } - - [Fact] - public async Task with_password_containing_ascii_chars() { - var user = Fakers.Users.Generate(); - - await Fixture.Users - .CreateUserAsync(user.LoginName, user.FullName, user.Groups, user.Password, userCredentials: TestCredentials.Root) - .ShouldNotThrowAsync(); - } - - [Theory] - [ClassData(typeof(InvalidCredentialsTestCases))] - public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) => - await Fixture.Users - .CreateUserAsync(testCase.User.LoginName, testCase.User.FullName, testCase.User.Groups, testCase.User.Password, userCredentials: testCase.User.Credentials) - .ShouldThrowAsync(testCase.ExpectedException); - - [Fact] - public async Task can_be_read() { - var user = Fakers.Users.Generate(); - - await Fixture.Users - .CreateUserAsync( - user.LoginName, - user.FullName, - user.Groups, - user.Password, - userCredentials: TestCredentials.Root - ) - .ShouldNotThrowAsync(); - - var actual = await Fixture.Users.GetUserAsync(user.LoginName, userCredentials: TestCredentials.Root); - - var expected = new UserDetails( - user.Details.LoginName, - user.Details.FullName, - user.Details.Groups, - user.Details.Disabled, - actual.DateLastUpdated - ); - - actual.ShouldBeEquivalentTo(expected); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.UserManagement.Tests/getting_current_user.cs b/test/EventStore.Client.UserManagement.Tests/getting_current_user.cs deleted file mode 100644 index b0cb3af2d..000000000 --- a/test/EventStore.Client.UserManagement.Tests/getting_current_user.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace EventStore.Client.Tests; - -public class getting_current_user : IClassFixture { - public getting_current_user(ITestOutputHelper output, EventStoreFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - EventStoreFixture Fixture { get; } - - [Fact] - public async Task returns_the_current_user() { - var user = await Fixture.Users.GetCurrentUserAsync(TestCredentials.Root); - user.LoginName.ShouldBe(TestCredentials.Root.Username); - } -} \ No newline at end of file diff --git a/test/EventStore.Client.Tests.Common/.env b/test/Kurrent.Client.Tests.Common/.env similarity index 100% rename from test/EventStore.Client.Tests.Common/.env rename to test/Kurrent.Client.Tests.Common/.env diff --git a/test/EventStore.Client.Tests.Common/ApplicationInfo.cs b/test/Kurrent.Client.Tests.Common/ApplicationInfo.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/ApplicationInfo.cs rename to test/Kurrent.Client.Tests.Common/ApplicationInfo.cs index 0120c21b4..26e78938d 100644 --- a/test/EventStore.Client.Tests.Common/ApplicationInfo.cs +++ b/test/Kurrent.Client.Tests.Common/ApplicationInfo.cs @@ -9,7 +9,7 @@ using static System.Environment; using static System.StringComparison; -namespace EventStore.Client; +namespace Kurrent.Client; /// /// Loads configuration and provides information about the application environment. diff --git a/test/EventStore.Client.ProjectionManagement.Tests/AssertEx.cs b/test/Kurrent.Client.Tests.Common/AssertEx.cs similarity index 97% rename from test/EventStore.Client.ProjectionManagement.Tests/AssertEx.cs rename to test/Kurrent.Client.Tests.Common/AssertEx.cs index f7d846294..6750e66b5 100644 --- a/test/EventStore.Client.ProjectionManagement.Tests/AssertEx.cs +++ b/test/Kurrent.Client.Tests.Common/AssertEx.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; using Xunit.Sdk; -namespace EventStore.Client.ProjectionManagement.Tests; +namespace Kurrent.Client.Tests; public static class AssertEx { /// @@ -53,4 +53,4 @@ static async Task IsOrBecomesTrueImpl(Func> func, TimeSpan? tim return false; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Certificates.cs b/test/Kurrent.Client.Tests.Common/Certificates.cs similarity index 97% rename from test/EventStore.Client.Tests.Common/Certificates.cs rename to test/Kurrent.Client.Tests.Common/Certificates.cs index efd167d67..3b8671d9b 100644 --- a/test/EventStore.Client.Tests.Common/Certificates.cs +++ b/test/Kurrent.Client.Tests.Common/Certificates.cs @@ -1,6 +1,6 @@ // ReSharper disable InconsistentNaming -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public static class Certificates { static readonly string BaseDirectory = AppDomain.CurrentDomain.BaseDirectory; diff --git a/test/EventStore.Client.Tests.Common/Extensions/ConfigurationExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/ConfigurationExtensions.cs similarity index 89% rename from test/EventStore.Client.Tests.Common/Extensions/ConfigurationExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/ConfigurationExtensions.cs index 3e975fc23..87f06cd45 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/ConfigurationExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/ConfigurationExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Configuration; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public static class ConfigurationExtensions { public static void EnsureValue(this IConfiguration configuration, string key, string defaultValue) { @@ -9,4 +9,4 @@ public static void EnsureValue(this IConfiguration configuration, string key, st if (string.IsNullOrEmpty(value)) configuration[key] = defaultValue; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/KurrentClientExtensions.cs similarity index 69% rename from test/EventStore.Client.Tests.Common/Extensions/EventStoreClientExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/KurrentClientExtensions.cs index e1475bc16..0b0861cf3 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/KurrentClientExtensions.cs @@ -1,11 +1,12 @@ +using EventStore.Client; using Polly; using static System.TimeSpan; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; -public static class EventStoreClientExtensions { +public static class KurrentClientExtensions { public static Task CreateUserWithRetry( - this EventStoreUserManagementClient client, string loginName, string fullName, string[] groups, string password, + this KurrentUserManagementClient client, string loginName, string fullName, string[] groups, string password, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default ) => Policy.Handle() @@ -21,4 +22,4 @@ public static Task CreateUserWithRetry( ), cancellationToken ); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientWarmupExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/KurrentClientWarmupExtensions.cs similarity index 79% rename from test/EventStore.Client.Tests.Common/Extensions/EventStoreClientWarmupExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/KurrentClientWarmupExtensions.cs index dea2f4ef9..89045a44b 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/EventStoreClientWarmupExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/KurrentClientWarmupExtensions.cs @@ -1,11 +1,12 @@ +using EventStore.Client; using Grpc.Core; using Polly; using Polly.Contrib.WaitAndRetry; using static System.TimeSpan; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; -public static class EventStoreClientWarmupExtensions { +public static class KurrentClientWarmupExtensions { static readonly TimeSpan RediscoverTimeout = FromSeconds(5); /// @@ -14,7 +15,7 @@ public static class EventStoreClientWarmupExtensions { static readonly IEnumerable DefaultBackoffDelay = Backoff.ConstantBackoff(FromMilliseconds(100), 300); static async Task TryWarmUp(T client, Func action, CancellationToken cancellationToken = default) - where T : EventStoreClientBase { + where T : KurrentClientBase { await Policy .Handle(ex => ex.StatusCode != StatusCode.Unimplemented) .Or() @@ -39,7 +40,7 @@ await Policy return client; } - public static Task WarmUp(this EventStoreClient client, CancellationToken cancellationToken = default) => + public static Task WarmUp(this KurrentClient client, CancellationToken cancellationToken = default) => TryWarmUp( client, async ct => { @@ -72,7 +73,7 @@ public static Task WarmUp(this EventStoreClient client, Cancel cancellationToken ); - public static Task WarmUp(this EventStoreOperationsClient client, CancellationToken cancellationToken = default) => + public static Task WarmUp(this KurrentOperationsClient client, CancellationToken cancellationToken = default) => TryWarmUp( client, async ct => { @@ -84,8 +85,8 @@ await client.RestartPersistentSubscriptions( cancellationToken ); - public static Task WarmUp( - this EventStorePersistentSubscriptionsClient client, CancellationToken cancellationToken = default + public static Task WarmUp( + this KurrentPersistentSubscriptionsClient client, CancellationToken cancellationToken = default ) => TryWarmUp( client, @@ -102,8 +103,8 @@ await client.CreateToStreamAsync( cancellationToken ); - public static Task WarmUp( - this EventStoreProjectionManagementClient client, CancellationToken cancellationToken = default + public static Task WarmUp( + this KurrentProjectionManagementClient client, CancellationToken cancellationToken = default ) => TryWarmUp( client, @@ -118,7 +119,7 @@ public static Task WarmUp( cancellationToken ); - public static Task WarmUp(this EventStoreUserManagementClient client, CancellationToken cancellationToken = default) => + public static Task WarmUp(this KurrentUserManagementClient client, CancellationToken cancellationToken = default) => TryWarmUp( client, async ct => { @@ -129,4 +130,4 @@ public static Task WarmUp(this EventStoreUserMan }, cancellationToken ); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs similarity index 87% rename from test/EventStore.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs index 1889b084d..fbd53f8cc 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/OperatingSystemExtensions.cs @@ -1,7 +1,7 @@ -namespace EventStore.Client; +namespace Kurrent.Client; public static class OperatingSystemExtensions { public static bool IsWindows(this OperatingSystem operatingSystem) => operatingSystem.Platform != PlatformID.Unix && operatingSystem.Platform != PlatformID.MacOSX; -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs similarity index 93% rename from test/EventStore.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs index 49ffd1cd9..403665653 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/ReadOnlyMemoryExtensions.cs @@ -1,6 +1,7 @@ using System.Text.Json; +using EventStore.Client; -namespace EventStore.Client; +namespace Kurrent.Client; public static class ReadOnlyMemoryExtensions { public static Position ParsePosition(this ReadOnlyMemory json) { @@ -25,4 +26,4 @@ public static StreamPosition ParseStreamPosition(this ReadOnlyMemory json) return StreamPosition.FromInt64(int.Parse(checkPoint)); } -} \ No newline at end of file +} diff --git a/test/Kurrent.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs new file mode 100644 index 000000000..827133337 --- /dev/null +++ b/test/Kurrent.Client.Tests.Common/Extensions/ShouldThrowAsyncExtensions.cs @@ -0,0 +1,16 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +public static class ShouldThrowAsyncExtensions { + public static Task ShouldThrowAsync(this KurrentClient.ReadStreamResult source) where TException : Exception => + source + .ToArrayAsync() + .AsTask() + .ShouldThrowAsync(); + + public static async Task ShouldThrowAsync(this KurrentClient.ReadStreamResult source, Action handler) where TException : Exception { + var ex = await source.ShouldThrowAsync(); + handler(ex); + } +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/TaskExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/TaskExtensions.cs similarity index 97% rename from test/EventStore.Client.Tests.Common/Extensions/TaskExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/TaskExtensions.cs index d3775b700..9fb022726 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/TaskExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/TaskExtensions.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace EventStore.Client; +namespace Kurrent.Client; public static class TaskExtensions { public static Task WithTimeout(this Task task, TimeSpan timeout) diff --git a/test/EventStore.Client.Tests/TypeExtensions.cs b/test/Kurrent.Client.Tests.Common/Extensions/TypeExtensions.cs similarity index 96% rename from test/EventStore.Client.Tests/TypeExtensions.cs rename to test/Kurrent.Client.Tests.Common/Extensions/TypeExtensions.cs index 268109da8..355544e88 100644 --- a/test/EventStore.Client.Tests/TypeExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/TypeExtensions.cs @@ -1,8 +1,8 @@ using System.Reflection; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; -static class TypeExtensions { +public static class TypeExtensions { public static bool InvokeEqualityOperator(this Type type, object? left, object? right) => type.InvokeOperator("Equality", left, right); public static bool InvokeInequalityOperator(this Type type, object? left, object? right) => type.InvokeOperator("Inequality", left, right); @@ -36,4 +36,4 @@ static bool InvokeOperator(this Type type, string name, object? left, object? ri return (bool)op.Invoke(null, new[] { left, right })!; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Extensions/WithExtension.cs b/test/Kurrent.Client.Tests.Common/Extensions/WithExtension.cs similarity index 97% rename from test/EventStore.Client.Tests.Common/Extensions/WithExtension.cs rename to test/Kurrent.Client.Tests.Common/Extensions/WithExtension.cs index b2ea3dbac..b0c577e26 100644 --- a/test/EventStore.Client.Tests.Common/Extensions/WithExtension.cs +++ b/test/Kurrent.Client.Tests.Common/Extensions/WithExtension.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public static class WithExtension { [DebuggerStepThrough] @@ -59,4 +59,4 @@ public static T With(this T instance, Func action, Func when) { return when() ? action(instance) : instance; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Facts/AnonymousAccess.cs b/test/Kurrent.Client.Tests.Common/Facts/AnonymousAccess.cs similarity index 90% rename from test/EventStore.Client.Tests.Common/Facts/AnonymousAccess.cs rename to test/Kurrent.Client.Tests.Common/Facts/AnonymousAccess.cs index 575093085..101a67df0 100644 --- a/test/EventStore.Client.Tests.Common/Facts/AnonymousAccess.cs +++ b/test/Kurrent.Client.Tests.Common/Facts/AnonymousAccess.cs @@ -1,4 +1,4 @@ -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [PublicAPI] public class AnonymousAccess { @@ -8,4 +8,4 @@ public class AnonymousAccess { public class FactAttribute() : Deprecation.FactAttribute(LegacySince, SkipMessage); public class TheoryAttribute() : Deprecation.TheoryAttribute(LegacySince, SkipMessage); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Facts/Deprecation.cs b/test/Kurrent.Client.Tests.Common/Facts/Deprecation.cs similarity index 73% rename from test/EventStore.Client.Tests.Common/Facts/Deprecation.cs rename to test/Kurrent.Client.Tests.Common/Facts/Deprecation.cs index 01adee242..3b374ee91 100644 --- a/test/EventStore.Client.Tests.Common/Facts/Deprecation.cs +++ b/test/Kurrent.Client.Tests.Common/Facts/Deprecation.cs @@ -1,10 +1,10 @@ -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [PublicAPI] public class Deprecation { public class FactAttribute(Version since, string skipMessage) : Xunit.FactAttribute { public override string? Skip { - get => EventStoreTestServer.Version >= since ? skipMessage : null; + get => KurrentPermanentTestNode.Version >= since ? skipMessage : null; set => throw new NotSupportedException(); } } @@ -19,8 +19,8 @@ public TheoryAttribute(Version since, string skipMessage) { } public override string? Skip { - get => EventStoreTestServer.Version >= _legacySince ? _skipMessage : null; + get => KurrentPermanentTestNode.Version >= _legacySince ? _skipMessage : null; set => throw new NotSupportedException(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Facts/Regression.cs b/test/Kurrent.Client.Tests.Common/Facts/Regression.cs similarity index 62% rename from test/EventStore.Client.Tests.Common/Facts/Regression.cs rename to test/Kurrent.Client.Tests.Common/Facts/Regression.cs index 3c6a92f0c..09e944518 100644 --- a/test/EventStore.Client.Tests.Common/Facts/Regression.cs +++ b/test/Kurrent.Client.Tests.Common/Facts/Regression.cs @@ -1,18 +1,18 @@ -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [PublicAPI] public class Regression { public class FactAttribute(int major, string skipMessage) : Xunit.FactAttribute { public override string? Skip { - get => (EventStoreTestServer.Version?.Major ?? int.MaxValue) < major ? skipMessage : null; + get => (KurrentPermanentTestNode.Version?.Major ?? int.MaxValue) < major ? skipMessage : null; set => throw new NotSupportedException(); } } public class TheoryAttribute(int major, string skipMessage) : Xunit.TheoryAttribute { public override string? Skip { - get => (EventStoreTestServer.Version?.Major ?? int.MaxValue) < major ? skipMessage : null; + get => (KurrentPermanentTestNode.Version?.Major ?? int.MaxValue) < major ? skipMessage : null; set => throw new NotSupportedException(); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Fakers/TestUserFaker.cs b/test/Kurrent.Client.Tests.Common/Fakers/TestUserFaker.cs similarity index 96% rename from test/EventStore.Client.Tests.Common/Fakers/TestUserFaker.cs rename to test/Kurrent.Client.Tests.Common/Fakers/TestUserFaker.cs index 1aca4aac1..472aa1b15 100644 --- a/test/EventStore.Client.Tests.Common/Fakers/TestUserFaker.cs +++ b/test/Kurrent.Client.Tests.Common/Fakers/TestUserFaker.cs @@ -1,4 +1,6 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; + +namespace Kurrent.Client.Tests; public class TestUser { public UserDetails Details { get; set; } = default!; @@ -50,4 +52,4 @@ public TestUser WithNonAsciiPassword() => public static partial class Fakers { public static TestUserFaker Users => TestUserFaker.Instance; -} \ No newline at end of file +} diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/BaseTestNode.cs b/test/Kurrent.Client.Tests.Common/Fixtures/BaseTestNode.cs new file mode 100644 index 000000000..5ace2a507 --- /dev/null +++ b/test/Kurrent.Client.Tests.Common/Fixtures/BaseTestNode.cs @@ -0,0 +1,162 @@ +// // ReSharper disable InconsistentNaming +// +// using System.Globalization; +// using System.Net; +// using System.Net.Sockets; +// using Ductus.FluentDocker.Builders; +// using Ductus.FluentDocker.Extensions; +// using Ductus.FluentDocker.Services.Extensions; +// using Kurrent.Client.Tests.FluentDocker; +// using Humanizer; +// using Serilog; +// using Serilog.Extensions.Logging; +// using static System.TimeSpan; +// +// namespace Kurrent.Client.Tests; +// +// public abstract class BaseTestNode(EventStoreFixtureOptions? options = null) : TestContainerService { +// static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); +// +// public KurrentFixtureOptions Options { get; } = options ?? DefaultOptions(); +// +// static Version? _version; +// +// public static Version Version => _version ??= GetVersion(); +// +// public static EventStoreFixtureOptions DefaultOptions() { +// const string connString = "esdb://admin:changeit@localhost:{port}/?tlsVerifyCert=false"; +// +// var port = NetworkPortProvider.NextAvailablePort; +// +// var defaultSettings = EventStoreClientSettings +// .Create(connString.Replace("{port}", $"{port}")) +// .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) +// .With(x => x.DefaultDeadline = Application.DebuggerIsAttached ? new TimeSpan?() : FromSeconds(30)) +// .With(x => x.ConnectivitySettings.MaxDiscoverAttempts = 20) +// .With(x => x.ConnectivitySettings.DiscoveryInterval = FromSeconds(1)); +// +// var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { +// // ["EVENTSTORE_MEM_DB"] = "true", +// // ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", +// // ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", +// // ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", +// // ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", +// // ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", +// // ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings +// // ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", +// // ["EVENTSTORE_START_STANDARD_PROJECTIONS"] = "true", +// // ["EVENTSTORE_RUN_PROJECTIONS"] = "All", +// // ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), +// // ["EVENTSTORE_MAX_APPEND_SIZE"] = 100.Kilobytes().Bytes.ToString(CultureInfo.InvariantCulture), +// // ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" +// +// ["EVENTSTORE_MEM_DB"] = "true", +// ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", +// ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", +// ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", +// ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", +// ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", +// ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings +// ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", +// ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), +// ["EVENTSTORE_MAX_APPEND_SIZE"] = 100.Kilobytes().Bytes.ToString(CultureInfo.InvariantCulture), +// ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" +// }; +// +// if (port != NetworkPortProvider.DefaultEsdbPort) { +// if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") +// defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; +// else +// defaultEnvironment["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{port}"; +// } +// +// return new(defaultSettings, defaultEnvironment); +// } +// +// static Version GetVersion() { +// const string versionPrefix = "EventStoreDB version"; +// +// using var cts = new CancellationTokenSource(FromSeconds(30)); +// using var eventstore = new Builder().UseContainer() +// .UseImage(GlobalEnvironment.DockerImage) +// .Command("--version") +// .Build() +// .Start(); +// +// using var log = eventstore.Logs(true, cts.Token); +// foreach (var line in log.ReadToEnd()) { +// if (line.StartsWith(versionPrefix) && +// Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { +// return version; +// } +// } +// +// throw new InvalidOperationException("Could not determine server version."); +// +// IEnumerable ReadVersion(string s) { +// foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { +// yield return c; +// } +// } +// } +// +// string[] GetEnvironmentVariables() => +// Options.Environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray(); +// +// protected abstract ContainerBuilder ConfigureContainer(ContainerBuilder builder); +// +// protected override ContainerBuilder Configure() { +// var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); +// +// CertificatesManager.VerifyCertificatesExist(certsPath); +// +// var builder = new Builder().UseContainer().WithEnvironment(GetEnvironmentVariables()); +// +// return ConfigureContainer(builder); +// } +// } +// +// /// +// /// Using the default 2113 port assumes that the test is running sequentially. +// /// +// /// +// class NetworkPortProvider(int port = 2114) { +// public const int DefaultEsdbPort = 2113; +// +// static readonly SemaphoreSlim Semaphore = new(1, 1); +// +// async Task GetNextAvailablePort(TimeSpan delay = default) { +// if (port == DefaultEsdbPort) +// return port; +// +// await Semaphore.WaitAsync(); +// +// try { +// using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); +// +// while (true) { +// var nexPort = Interlocked.Increment(ref port); +// +// try { +// await socket.ConnectAsync(IPAddress.Any, nexPort); +// } catch (SocketException ex) { +// if (ex.SocketErrorCode is SocketError.ConnectionRefused or not SocketError.IsConnected) { +// return nexPort; +// } +// +// await Task.Delay(delay); +// } finally { +// #if NET +// if (socket.Connected) await socket.DisconnectAsync(true); +// #else +// if (socket.Connected) socket.Disconnect(true); +// #endif +// } +// } +// } finally { +// Semaphore.Release(); +// } +// } +// +// public int NextAvailablePort => GetNextAvailablePort(FromMilliseconds(100)).GetAwaiter().GetResult(); +// } diff --git a/test/EventStore.Client.Tests.Common/Fixtures/CertificatesManager.cs b/test/Kurrent.Client.Tests.Common/Fixtures/CertificatesManager.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/Fixtures/CertificatesManager.cs rename to test/Kurrent.Client.Tests.Common/Fixtures/CertificatesManager.cs index 487bfd340..f01d9c246 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/CertificatesManager.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/CertificatesManager.cs @@ -1,6 +1,6 @@ using Ductus.FluentDocker.Builders; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; static class CertificatesManager { static readonly DirectoryInfo CertificateDirectory; diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs new file mode 100644 index 000000000..db5ce022e --- /dev/null +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentFixtureOptions.cs @@ -0,0 +1,25 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +public record KurrentFixtureOptions( + KurrentClientSettings ClientSettings, + IDictionary Environment +) { + public KurrentFixtureOptions RunInMemory(bool runInMemory = true) => + this with { Environment = Environment.With(x => x["EVENTSTORE_MEM_DB"] = runInMemory.ToString()) }; + + public KurrentFixtureOptions WithoutDefaultCredentials() => this with { ClientSettings = ClientSettings.With(x => x.DefaultCredentials = null) }; + + public KurrentFixtureOptions RunProjections(bool runProjections = true) => + this with { + Environment = Environment.With( + x => { + x["EVENTSTORE_START_STANDARD_PROJECTIONS"] = runProjections.ToString(); + x["EVENTSTORE_RUN_PROJECTIONS"] = runProjections ? "All" : "None"; + } + ) + }; +} + +public delegate KurrentFixtureOptions ConfigureFixture(KurrentFixtureOptions options); diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs new file mode 100644 index 000000000..50c50a599 --- /dev/null +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.Helpers.cs @@ -0,0 +1,122 @@ +using System.Runtime.CompilerServices; +using System.Text; +using EventStore.Client; +using Kurrent.Client.Core.Serialization; + +namespace Kurrent.Client.Tests; + +public partial class KurrentPermanentFixture { + public const string TestEventType = "test-event-type"; + public const string AnotherTestEventTypePrefix = "another"; + public const string AnotherTestEventType = $"{AnotherTestEventTypePrefix}-test-event-type"; + + public T NewClient(Action configure) where T : KurrentClientBase, new() => + (T)Activator.CreateInstance(typeof(T), [ClientSettings.With(configure)])!; + + public string GetStreamName([CallerMemberName] string? testMethod = null) => + $"stream-{testMethod}-{Guid.NewGuid():N}"; + + public string GetGroupName([CallerMemberName] string? testMethod = null) => + $"group-{testMethod}-{Guid.NewGuid():N}"; + + public UserCredentials GetUserCredentials([CallerMemberName] string? testMethod = null) => new UserCredentials( + $"user-{testMethod}-{Guid.NewGuid():N}", "pa$$word" + ); + + public string GetProjectionName([CallerMemberName] string? testMethod = null) => + $"projection-{testMethod}-{Guid.NewGuid():N}"; + + public ReadOnlyMemory CreateMetadataOfSize(int metadataSize) => + Encoding.UTF8.GetBytes($"\"{new string('$', metadataSize)}\""); + + public ReadOnlyMemory CreateTestJsonMetadata() => "{\"Foo\": \"Bar\"}"u8.ToArray(); + + public ReadOnlyMemory CreateTestNonJsonMetadata() => "non-json-metadata"u8.ToArray(); + + public (IEnumerable Events, uint size) CreateTestEventsUpToMaxSize(uint maxSize) { + var size = 0; + + var events = CreateTestEvents(int.MaxValue) + .TakeWhile(evt => (size += evt.Data.Length) < maxSize) + .ToList(); + + return (events, (uint)size); + } + + public IEnumerable CreateTestEvents( + int count = 1, string? type = null, ReadOnlyMemory? metadata = null, string? contentType = null + ) => + Enumerable.Range(0, count) + .Select(index => CreateTestEvent(index, type ?? TestEventType, metadata, contentType)); + + + public IEnumerable CreateTestMessages(int count = 1, object? metadata = null) => + Enumerable.Range(0, count) + .Select(index => CreateTestMessage(index, metadata)); + + public EventData CreateTestEvent( + string? type = null, ReadOnlyMemory? metadata = null, string? contentType = null + ) => + CreateTestEvent(0, type ?? TestEventType, metadata, contentType); + + public IEnumerable CreateTestEventsThatThrowsException() { + // Ensure initial IEnumerator.Current does not throw + yield return CreateTestEvent(1); + + // Throw after enumerator advances + throw new Exception(); + } + + protected static EventData CreateTestEvent(int index) => CreateTestEvent(index, TestEventType); + + protected static Message CreateTestMessage(int index, object? metadata = null) => + Message.From( + new DummyEvent(index), + metadata, + Uuid.NewUuid() + ); + + protected static EventData CreateTestEvent( + int index, string type, ReadOnlyMemory? metadata = null, string? contentType = null + ) => + new( + Uuid.NewUuid(), + type, + Encoding.UTF8.GetBytes($$"""{"x":{{index}}}"""), + metadata, + contentType ?? "application/json" + ); + + public async Task CreateTestUser(bool withoutGroups = true, bool useUserCredentials = false) { + var result = await CreateTestUsers(1, withoutGroups, useUserCredentials); + return result.First(); + } + + public Task CreateTestUsers( + int count = 3, bool withoutGroups = true, bool useUserCredentials = false + ) => + Fakers.Users + .RuleFor(x => x.Groups, f => withoutGroups ? Array.Empty() : f.Lorem.Words()) + .Generate(count) + .Select( + async user => { + await Users.CreateUserAsync( + user.LoginName, + user.FullName, + user.Groups, + user.Password, + userCredentials: useUserCredentials ? user.Credentials : TestCredentials.Root + ); + + return user; + } + ).WhenAll(); + + public async Task RestartService(TimeSpan delay) { + await Service.Restart(delay); + await Streams.WarmUp(); + Log.Information("Service restarted."); + } + + public record DummyEvent(int X); +} diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs new file mode 100644 index 000000000..9aa9294ef --- /dev/null +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentFixture.cs @@ -0,0 +1,184 @@ +// ReSharper disable InconsistentNaming + +using System.Net; +using Ductus.FluentDocker.Builders; +using Ductus.FluentDocker.Extensions; +using Ductus.FluentDocker.Services.Extensions; +using EventStore.Client; +using Kurrent.Client.Tests.FluentDocker; +using Serilog; +using static System.TimeSpan; +using KurrentClient = EventStore.Client.KurrentClient; + +namespace Kurrent.Client.Tests; + +[PublicAPI] +public partial class KurrentPermanentFixture : IAsyncLifetime, IAsyncDisposable { + static readonly ILogger Logger; + + static KurrentPermanentFixture() { + Logging.Initialize(); + Logger = Serilog.Log.ForContext(); + +#if NET9_0_OR_GREATER + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.ServerCertificateCustomValidationCallback = delegate { return true; }; +#else + ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; +#endif + } + + public KurrentPermanentFixture() : this(options => options) { } + + protected KurrentPermanentFixture(ConfigureFixture configure) { + Options = configure(KurrentPermanentTestNode.DefaultOptions()); + Service = new KurrentPermanentTestNode(Options); + } + + List TestRuns { get; } = new(); + + public ILogger Log => Logger; + + public ITestService Service { get; } + public KurrentFixtureOptions Options { get; } + public Faker Faker { get; } = new Faker(); + + public Version EventStoreVersion { get; private set; } = null!; + public bool EventStoreHasLastStreamPosition { get; private set; } + + public KurrentClient Streams { get; private set; } = null!; + public KurrentUserManagementClient Users { get; private set; } = null!; + public KurrentProjectionManagementClient Projections { get; private set; } = null!; + public KurrentPersistentSubscriptionsClient Subscriptions { get; private set; } = null!; + public KurrentOperationsClient Operations { get; private set; } = null!; + + public bool SkipPsWarmUp { get; set; } + + public Func OnSetup { get; init; } = () => Task.CompletedTask; + public Func OnTearDown { get; init; } = () => Task.CompletedTask; + + /// + /// must test this + /// + public KurrentClientSettings ClientSettings => + new() { + Interceptors = Options.ClientSettings.Interceptors, + ConnectionName = Options.ClientSettings.ConnectionName, + CreateHttpMessageHandler = Options.ClientSettings.CreateHttpMessageHandler, + LoggerFactory = Options.ClientSettings.LoggerFactory, + ChannelCredentials = Options.ClientSettings.ChannelCredentials, + OperationOptions = Options.ClientSettings.OperationOptions, + ConnectivitySettings = Options.ClientSettings.ConnectivitySettings, + DefaultCredentials = Options.ClientSettings.DefaultCredentials, + DefaultDeadline = Options.ClientSettings.DefaultDeadline, + Serialization = Options.ClientSettings.Serialization + }; + + InterlockedBoolean WarmUpCompleted { get; } = new InterlockedBoolean(); + static readonly SemaphoreSlim WarmUpGatekeeper = new(1, 1); + + public void CaptureTestRun(ITestOutputHelper outputHelper) { + var testRunId = Logging.CaptureLogs(outputHelper); + TestRuns.Add(testRunId); + Logger.Information(">>> Test Run {TestRunId} {Operation} <<<", testRunId, "starting"); + Service.ReportStatus(); + } + + public async Task InitializeAsync() { + await WarmUpGatekeeper.WaitAsync(); + + try { + await Service.Start(); + EventStoreVersion = GetKurrentVersion(); + EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; + + if (!WarmUpCompleted.CurrentValue) { + Logger.Warning("*** Warmup started ***"); + + await Task.WhenAll( + InitClient(async x => Users = await Task.FromResult(x)), + InitClient(async x => Streams = await Task.FromResult(x)), + InitClient( + async x => Projections = await Task.FromResult(x), + Options.Environment["EVENTSTORE_RUN_PROJECTIONS"] != "None" + ), + InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await Task.FromResult(x)), + InitClient(async x => Operations = await Task.FromResult(x)) + ); + + WarmUpCompleted.EnsureCalledOnce(); + + Logger.Warning("*** Warmup completed ***"); + } else { + Logger.Information("*** Warmup skipped ***"); + } + } finally { + WarmUpGatekeeper.Release(); + } + + await OnSetup(); + + return; + + async Task InitClient(Func action, bool execute = true) where T : KurrentClientBase { + if (!execute) return default(T)!; + + var client = (Activator.CreateInstance(typeof(T), ClientSettings) as T)!; + await action(client); + return client; + } + + static Version GetKurrentVersion() { + const string versionPrefix = "EventStoreDB version"; + + using var cancellator = new CancellationTokenSource(FromSeconds(30)); + using var eventstore = new Builder() + .UseContainer() + .UseImage(GlobalEnvironment.DockerImage) + .Command("--version") + .Build() + .Start(); + + using var log = eventstore.Logs(true, cancellator.Token); + foreach (var line in log.ReadToEnd()) { + Logger.Information("EventStoreDB: {Line}", line); + if (line.StartsWith(versionPrefix) && + Version.TryParse( + new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), + out var version + )) { + return version; + } + } + + throw new InvalidOperationException("Could not determine server version."); + + IEnumerable ReadVersion(string s) { + foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { + yield return c; + } + } + } + } + + public async Task DisposeAsync() { + try { + await OnTearDown(); + } catch { + // ignored + } + + await Service.DisposeAsync().AsTask().WithTimeout(FromMinutes(5)); + + foreach (var testRunId in TestRuns) + Logging.ReleaseLogs(testRunId); + } + + async ValueTask IAsyncDisposable.DisposeAsync() => await DisposeAsync(); +} + +public abstract class KurrentPermanentTests : IClassFixture where TFixture : KurrentPermanentFixture { + protected KurrentPermanentTests(ITestOutputHelper output, TFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); + + protected TFixture Fixture { get; } +} diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs similarity index 52% rename from test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs rename to test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs index 7f1da49d0..c39895cf1 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentPermanentTestNode.cs @@ -1,30 +1,68 @@ +// ReSharper disable InconsistentNaming + +// using Ductus.FluentDocker.Builders; +// using Ductus.FluentDocker.Model.Builders; +// using Kurrent.Client.Tests.FluentDocker; +// +// namespace Kurrent.Client.Tests; +// +// public class EventStorePermanentTestNode(EventStoreFixtureOptions? options = null) : BaseTestNode(options) { +// protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder) { +// var port = Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; +// var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); +// +// var containerName = "es-client-dotnet-test"; +// +// return builder +// .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) +// .WithName(containerName) +// .WithPublicEndpointResolver() +// .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) +// .ExposePort(port, 2113) +// .KeepContainer().KeepRunning().ReuseIfExists() +// .WaitUntilReadyWithConstantBackoff( +// 1_000, +// 60, +// service => { +// var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); +// if (!output.Success) +// throw new Exception(output.Error); +// } +// ); +// } +// } + +using System.Globalization; using System.Net; -using System.Net.Http; using System.Net.Sockets; using Ductus.FluentDocker.Builders; -using Ductus.FluentDocker.Common; +using Ductus.FluentDocker.Extensions; using Ductus.FluentDocker.Model.Builders; -using EventStore.Client.Tests.FluentDocker; -using Polly; -using Polly.Contrib.WaitAndRetry; +using Ductus.FluentDocker.Services.Extensions; +using EventStore.Client; +using Kurrent.Client; +using Kurrent.Client.Tests.FluentDocker; +using Humanizer; +using Kurrent.Client.Tests; using Serilog; using Serilog.Extensions.Logging; using static System.TimeSpan; -namespace EventStore.Client.Tests; - -public class EventStoreTestNode(EventStoreFixtureOptions? options = null) : TestContainerService { - +public class KurrentPermanentTestNode(KurrentFixtureOptions? options = null) : TestContainerService { static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); - - EventStoreFixtureOptions Options { get; } = options ?? DefaultOptions(); - public static EventStoreFixtureOptions DefaultOptions() { + KurrentFixtureOptions Options { get; } = options ?? DefaultOptions(); + + static Version? _version; + + public static Version Version => _version ??= GetVersion(); + + public static KurrentFixtureOptions DefaultOptions() { const string connString = "esdb://admin:changeit@localhost:{port}/?tlsVerifyCert=false"; - + var port = NetworkPortProvider.NextAvailablePort; - - var defaultSettings = EventStoreClientSettings + + var defaultSettings = KurrentClientSettings .Create(connString.Replace("{port}", $"{port}")) .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) .With(x => x.DefaultDeadline = Application.DebuggerIsAttached ? new TimeSpan?() : FromSeconds(30)) @@ -33,13 +71,17 @@ public static EventStoreFixtureOptions DefaultOptions() { var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { ["EVENTSTORE_MEM_DB"] = "true", - ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", + ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", + ["EVENTSTORE_START_STANDARD_PROJECTIONS"] = "true", + ["EVENTSTORE_RUN_PROJECTIONS"] = "All", + ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), + ["EVENTSTORE_MAX_APPEND_SIZE"] = 100.Kilobytes().Bytes.ToString(CultureInfo.InvariantCulture), ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" }; @@ -48,17 +90,43 @@ public static EventStoreFixtureOptions DefaultOptions() { defaultEnvironment["EventStore__Plugins__UserCertificates__Enabled"] = "true"; } - // TODO SS: must find a way to enable parallel tests on CI. It works locally. if (port != NetworkPortProvider.DefaultEsdbPort) { if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; else defaultEnvironment["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{port}"; } - + return new(defaultSettings, defaultEnvironment); } + static Version GetVersion() { + const string versionPrefix = "EventStoreDB version"; + + using var cts = new CancellationTokenSource(FromSeconds(30)); + using var eventstore = new Builder().UseContainer() + .UseImage(GlobalEnvironment.DockerImage) + .Command("--version") + .Build() + .Start(); + + using var log = eventstore.Logs(true, cts.Token); + foreach (var line in log.ReadToEnd()) { + if (line.StartsWith(versionPrefix) && + Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { + return version; + } + } + + throw new InvalidOperationException("Could not determine server version."); + + IEnumerable ReadVersion(string s) { + foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { + yield return c; + } + } + } + protected override ContainerBuilder Configure() { var env = Options.Environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray(); @@ -75,34 +143,18 @@ protected override ContainerBuilder Configure() { .UseContainer() .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) .WithName(containerName) + .WithPublicEndpointResolver() .WithEnvironment(env) .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) - .ExposePort(port, 2113); - } - - /// - /// max of 30 seconds (300 * 100ms) - /// - static readonly IEnumerable DefaultBackoffDelay = Backoff.ConstantBackoff(FromMilliseconds(100), 300); - - protected override async Task OnServiceStarted() { - using var http = new HttpClient( -#if NET - new SocketsHttpHandler { SslOptions = { RemoteCertificateValidationCallback = delegate { return true; } } } -#else - new WinHttpHandler { ServerCertificateValidationCallback = delegate { return true; } } -#endif - ) { - BaseAddress = Options.ClientSettings.ConnectivitySettings.Address - }; - - await Policy.Handle() - .WaitAndRetryAsync(DefaultBackoffDelay) - .ExecuteAsync( - async () => { - using var response = await http.GetAsync("/health/live", CancellationToken.None); - if (response.StatusCode >= HttpStatusCode.BadRequest) - throw new FluentDockerException($"Health check failed with status code: {response.StatusCode}."); + .ExposePort(port, 2113) + .KeepContainer().KeepRunning().ReuseIfExists() + .WaitUntilReadyWithConstantBackoff( + 1_000, + 60, + service => { + var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); + if (!output.Success) + throw new Exception(output.Error); } ); } @@ -114,42 +166,39 @@ await Policy.Handle() /// class NetworkPortProvider(int port = 2114) { public const int DefaultEsdbPort = 2113; - + static readonly SemaphoreSlim Semaphore = new(1, 1); - public async Task GetNextAvailablePort(TimeSpan delay = default) { + async Task GetNextAvailablePort(TimeSpan delay = default) { // TODO SS: find a way to enable parallel tests on CI if (port == DefaultEsdbPort) return port; await Semaphore.WaitAsync(); - + try { using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - + while (true) { var nexPort = Interlocked.Increment(ref port); - + try { await socket.ConnectAsync(IPAddress.Any, nexPort); - } - catch (SocketException ex) { + } catch (SocketException ex) { if (ex.SocketErrorCode is SocketError.ConnectionRefused or not SocketError.IsConnected) { return nexPort; } - + await Task.Delay(delay); - } - finally { + } finally { #if NET if (socket.Connected) await socket.DisconnectAsync(true); #else - if (socket.Connected) socket.Disconnect(true); + if (socket.Connected) socket.Disconnect(true); #endif } } - } - finally { + } finally { Semaphore.Release(); } } diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreFixture.Helpers.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs similarity index 74% rename from test/EventStore.Client.Tests.Common/Fixtures/EventStoreFixture.Helpers.cs rename to test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs index 1e8bb3d83..31da9f154 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreFixture.Helpers.cs +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.Helpers.cs @@ -1,18 +1,30 @@ using System.Runtime.CompilerServices; using System.Text; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests.TestNode; -public partial class EventStoreFixture { +public partial class KurrentTemporaryFixture { public const string TestEventType = "test-event-type"; public const string AnotherTestEventTypePrefix = "another"; public const string AnotherTestEventType = $"{AnotherTestEventTypePrefix}-test-event-type"; - public T NewClient(Action configure) where T : EventStoreClientBase, new() => + public T NewClient(Action configure) where T : KurrentClientBase, new() => (T)Activator.CreateInstance(typeof(T), [ClientSettings.With(configure)])!; public string GetStreamName([CallerMemberName] string? testMethod = null) => - $"{testMethod}-{Guid.NewGuid():N}"; + $"stream-{testMethod}-{Guid.NewGuid():N}"; + + public string GetGroupName([CallerMemberName] string? testMethod = null) => + $"group-{testMethod}-{Guid.NewGuid():N}"; + + public UserCredentials GetUserCredentials([CallerMemberName] string? testMethod = null) => new UserCredentials( + $"user-{testMethod}-{Guid.NewGuid():N}", + "pa$$word" + ); + + public string GetProjectionName([CallerMemberName] string? testMethod = null) => + $"projection-{testMethod}-{Guid.NewGuid():N}"; public ReadOnlyMemory CreateMetadataOfSize(int metadataSize) => Encoding.UTF8.GetBytes($"\"{new string('$', metadataSize)}\""); @@ -21,6 +33,16 @@ public ReadOnlyMemory CreateMetadataOfSize(int metadataSize) => public ReadOnlyMemory CreateTestNonJsonMetadata() => "non-json-metadata"u8.ToArray(); + public (IEnumerable Events, uint size) CreateTestEventsUpToMaxSize(uint maxSize) { + var size = 0; + + var events = CreateTestEvents(int.MaxValue) + .TakeWhile(evt => (size += evt.Data.Length) < maxSize) + .ToList(); + + return (events, (uint)size); + } + public IEnumerable CreateTestEvents( int count = 1, string? type = null, ReadOnlyMemory? metadata = null, string? contentType = null ) => diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs new file mode 100644 index 000000000..8a71891ca --- /dev/null +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryFixture.cs @@ -0,0 +1,185 @@ +// ReSharper disable InconsistentNaming + +using System.Net; +using Ductus.FluentDocker.Builders; +using Ductus.FluentDocker.Extensions; +using Ductus.FluentDocker.Services.Extensions; +using EventStore.Client; +using Kurrent.Client.Tests.FluentDocker; +using Serilog; +using static System.TimeSpan; +using KurrentClient = EventStore.Client.KurrentClient; + +namespace Kurrent.Client.Tests.TestNode; + +[PublicAPI] +public partial class KurrentTemporaryFixture : IAsyncLifetime, IAsyncDisposable { + static readonly ILogger Logger; + + static KurrentTemporaryFixture() { + Logging.Initialize(); + Logger = Serilog.Log.ForContext(); + +#if NET9_0_OR_GREATER + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.ServerCertificateCustomValidationCallback = delegate { return true; }; +#else + ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; +#endif + } + + public KurrentTemporaryFixture() : this(options => options) { } + + protected KurrentTemporaryFixture(ConfigureFixture configure) { + // Options = configure(EventStoreTemporaryTestNode.DefaultOptions()); + Options = configure(KurrentTemporaryTestNode.DefaultOptions()); + Service = new KurrentTemporaryTestNode(Options); + } + + List TestRuns { get; } = new(); + + public ILogger Log => Logger; + + public ITestService Service { get; } + public KurrentFixtureOptions Options { get; } + public Faker Faker { get; } = new Faker(); + + public Version EventStoreVersion { get; private set; } = null!; + public bool EventStoreHasLastStreamPosition { get; private set; } + + public KurrentClient Streams { get; private set; } = null!; + public KurrentUserManagementClient Users { get; private set; } = null!; + public KurrentProjectionManagementClient Projections { get; private set; } = null!; + public KurrentPersistentSubscriptionsClient Subscriptions { get; private set; } = null!; + public KurrentOperationsClient Operations { get; private set; } = null!; + + public bool SkipPsWarmUp { get; set; } + + public Func OnSetup { get; init; } = () => Task.CompletedTask; + public Func OnTearDown { get; init; } = () => Task.CompletedTask; + + /// + /// must test this + /// + public KurrentClientSettings ClientSettings => + new() { + Interceptors = Options.ClientSettings.Interceptors, + ConnectionName = Options.ClientSettings.ConnectionName, + CreateHttpMessageHandler = Options.ClientSettings.CreateHttpMessageHandler, + LoggerFactory = Options.ClientSettings.LoggerFactory, + ChannelCredentials = Options.ClientSettings.ChannelCredentials, + OperationOptions = Options.ClientSettings.OperationOptions, + ConnectivitySettings = Options.ClientSettings.ConnectivitySettings, + DefaultCredentials = Options.ClientSettings.DefaultCredentials, + DefaultDeadline = Options.ClientSettings.DefaultDeadline + }; + + InterlockedBoolean WarmUpCompleted { get; } = new InterlockedBoolean(); + static readonly SemaphoreSlim WarmUpGatekeeper = new(1, 1); + + public void CaptureTestRun(ITestOutputHelper outputHelper) { + var testRunId = Logging.CaptureLogs(outputHelper); + TestRuns.Add(testRunId); + Logger.Information(">>> Test Run {TestRunId} {Operation} <<<", testRunId, "starting"); + Service.ReportStatus(); + } + + public async Task InitializeAsync() { + await WarmUpGatekeeper.WaitAsync(); + + try { + await Service.Start(); + EventStoreVersion = GetKurrentVersion(); + EventStoreHasLastStreamPosition = (EventStoreVersion?.Major ?? int.MaxValue) >= 21; + + if (!WarmUpCompleted.CurrentValue) { + Logger.Warning("*** Warmup started ***"); + + await Task.WhenAll( + InitClient(async x => Users = await Task.FromResult(x)), + InitClient(async x => Streams = await Task.FromResult(x)), + InitClient( + async x => Projections = await Task.FromResult(x), + Options.Environment["EVENTSTORE_RUN_PROJECTIONS"] != "None" + ), + InitClient(async x => Subscriptions = SkipPsWarmUp ? x : await Task.FromResult(x)), + InitClient(async x => Operations = await Task.FromResult(x)) + ); + + WarmUpCompleted.EnsureCalledOnce(); + + Logger.Warning("*** Warmup completed ***"); + } else { + Logger.Information("*** Warmup skipped ***"); + } + } finally { + WarmUpGatekeeper.Release(); + } + + await OnSetup(); + + return; + + async Task InitClient(Func action, bool execute = true) where T : KurrentClientBase { + if (!execute) return default(T)!; + + var client = (Activator.CreateInstance(typeof(T), ClientSettings) as T)!; + await action(client); + return client; + } + + static Version GetKurrentVersion() { + const string versionPrefix = "EventStoreDB version"; + + using var cancellator = new CancellationTokenSource(FromSeconds(30)); + using var eventstore = new Builder() + .UseContainer() + .UseImage(GlobalEnvironment.DockerImage) + .Command("--version") + .Build() + .Start(); + + using var log = eventstore.Logs(true, cancellator.Token); + foreach (var line in log.ReadToEnd()) { + Logger.Information("EventStoreDB: {Line}", line); + if (line.StartsWith(versionPrefix) && + Version.TryParse( + new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), + out var version + )) { + return version; + } + } + + throw new InvalidOperationException("Could not determine server version."); + + IEnumerable ReadVersion(string s) { + foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { + yield return c; + } + } + } + } + + public async Task DisposeAsync() { + try { + await OnTearDown(); + } catch { + // ignored + } + + await Service.DisposeAsync().AsTask().WithTimeout(FromMinutes(5)); + + foreach (var testRunId in TestRuns) + Logging.ReleaseLogs(testRunId); + } + + async ValueTask IAsyncDisposable.DisposeAsync() => await DisposeAsync(); +} + +public abstract class KurrentTemporaryTests : IClassFixture where TFixture : KurrentTemporaryFixture { + protected KurrentTemporaryTests(ITestOutputHelper output, TFixture fixture) => + Fixture = fixture.With(x => x.CaptureTestRun(output)); + + protected TFixture Fixture { get; } +} diff --git a/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs new file mode 100644 index 000000000..7a6a98965 --- /dev/null +++ b/test/Kurrent.Client.Tests.Common/Fixtures/KurrentTemporaryTestNode.cs @@ -0,0 +1,199 @@ +// ReSharper disable InconsistentNaming + +// using Ductus.FluentDocker.Builders; +// using Ductus.FluentDocker.Model.Builders; +// using Kurrent.Client.Tests.FluentDocker; +// +// namespace Kurrent.Client.Tests.TestNode; +// +// public class EventStoreTemporaryTestNode(EventStoreFixtureOptions? options = null) : BaseTestNode(options) { +// protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder) { +// var port = Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; +// var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); +// +// var containerName = $"es-client-dotnet-test-{port}-{Guid.NewGuid().ToString()[30..]}"; +// +// return builder +// .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) +// .WithName(containerName) +// .WithPublicEndpointResolver() +// .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) +// .ExposePort(port, 2113) +// .WaitUntilReadyWithConstantBackoff( +// 1_000, +// 60, +// service => { +// var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); +// if (!output.Success) +// throw new Exception(output.Error); +// } +// ); +// } +// } + +using System.Globalization; +using System.Net; +using System.Net.Sockets; +using Ductus.FluentDocker.Builders; +using Ductus.FluentDocker.Extensions; +using Ductus.FluentDocker.Model.Builders; +using Ductus.FluentDocker.Services.Extensions; +using EventStore.Client; +using Kurrent.Client.Tests.FluentDocker; +using Humanizer; +using Serilog; +using Serilog.Extensions.Logging; +using static System.TimeSpan; + +namespace Kurrent.Client.Tests.TestNode; + +public class KurrentTemporaryTestNode(KurrentFixtureOptions? options = null) : TestContainerService { + static readonly NetworkPortProvider NetworkPortProvider = new(NetworkPortProvider.DefaultEsdbPort); + + KurrentFixtureOptions Options { get; } = options ?? DefaultOptions(); + + static Version? _version; + + public static Version Version => _version ??= GetVersion(); + + public static KurrentFixtureOptions DefaultOptions() { + const string connString = "esdb://admin:changeit@localhost:{port}/?tlsVerifyCert=false"; + + var port = NetworkPortProvider.NextAvailablePort; + + var defaultSettings = KurrentClientSettings + .Create(connString.Replace("{port}", $"{port}")) + .With(x => x.LoggerFactory = new SerilogLoggerFactory(Log.Logger)) + .With(x => x.DefaultDeadline = Application.DebuggerIsAttached ? new TimeSpan?() : FromSeconds(30)) + .With(x => x.ConnectivitySettings.MaxDiscoverAttempts = 20) + .With(x => x.ConnectivitySettings.DiscoveryInterval = FromSeconds(1)); + + var defaultEnvironment = new Dictionary(GlobalEnvironment.Variables) { + ["EVENTSTORE_MEM_DB"] = "true", + ["EVENTSTORE_CERTIFICATE_FILE"] = "/etc/eventstore/certs/node/node.crt", + ["EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE"] = "/etc/eventstore/certs/node/node.key", + ["EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE"] = "10000", + ["EVENTSTORE_STREAM_INFO_CACHE_CAPACITY"] = "10000", + ["EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP"] = "true", + ["EVENTSTORE_LOG_LEVEL"] = "Default", // required to use serilog settings + ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", + ["EVENTSTORE_CHUNK_SIZE"] = (1024 * 1024 * 1024).ToString(), + ["EVENTSTORE_MAX_APPEND_SIZE"] = 100.Kilobytes().Bytes.ToString(CultureInfo.InvariantCulture), + ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" + }; + + if (GlobalEnvironment.DockerImage.Contains("commercial")) { + defaultEnvironment["EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH"] = "/etc/eventstore/certs/ca"; + defaultEnvironment["EventStore__Plugins__UserCertificates__Enabled"] = "true"; + } + + if (port != NetworkPortProvider.DefaultEsdbPort) { + if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") + defaultEnvironment["EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS"] = $"{port}"; + else + defaultEnvironment["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{port}"; + } + + return new(defaultSettings, defaultEnvironment); + } + + static Version GetVersion() { + const string versionPrefix = "EventStoreDB version"; + + using var cts = new CancellationTokenSource(FromSeconds(30)); + using var eventstore = new Builder().UseContainer() + .UseImage(GlobalEnvironment.DockerImage) + .Command("--version") + .Build() + .Start(); + + using var log = eventstore.Logs(true, cts.Token); + foreach (var line in log.ReadToEnd()) { + if (line.StartsWith(versionPrefix) && + Version.TryParse(new string(ReadVersion(line[(versionPrefix.Length + 1)..]).ToArray()), out var version)) { + return version; + } + } + + throw new InvalidOperationException("Could not determine server version."); + + IEnumerable ReadVersion(string s) { + foreach (var c in s.TakeWhile(c => c == '.' || char.IsDigit(c))) { + yield return c; + } + } + } + + protected override ContainerBuilder Configure() { + var env = Options.Environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray(); + + var port = Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; + var certsPath = Path.Combine(Environment.CurrentDirectory, "certs"); + + var containerName = $"es-client-dotnet-test-{port}-{Guid.NewGuid().ToString()[30..]}"; + + CertificatesManager.VerifyCertificatesExist(certsPath); + + var builder = new Builder() + .UseContainer() + .UseImage(Options.Environment["ES_DOCKER_IMAGE"]) + .WithName(containerName) + .WithPublicEndpointResolver() + .WithEnvironment(env) + .MountVolume(certsPath, "/etc/eventstore/certs", MountType.ReadOnly) + .ExposePort(port, 2113) + .WaitUntilReadyWithConstantBackoff( + 1_000, + 60, + service => { + var output = service.ExecuteCommand("curl -u admin:changeit --cacert /etc/eventstore/certs/ca/ca.crt https://localhost:2113/health/live"); + if (!output.Success) + throw new Exception(output.Error); + } + ); + + return builder; + } +} + +/// +/// Using the default 2113 port assumes that the test is running sequentially. +/// +/// +class NetworkPortProvider(int port = 2114) { + public const int DefaultEsdbPort = 2113; + + static readonly SemaphoreSlim Semaphore = new(1, 1); + + async Task GetNextAvailablePort(TimeSpan delay = default) { + await Semaphore.WaitAsync(); + + try { + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + while (true) { + var nexPort = Interlocked.Increment(ref port); + + try { + await socket.ConnectAsync(IPAddress.Any, nexPort); + } catch (SocketException ex) { + if (ex.SocketErrorCode is SocketError.ConnectionRefused or not SocketError.IsConnected) { + return nexPort; + } + + await Task.Delay(delay); + } finally { +#if NET + if (socket.Connected) await socket.DisconnectAsync(true); +#else + if (socket.Connected) socket.Disconnect(true); +#endif + } + } + } finally { + Semaphore.Release(); + } + } + + public int NextAvailablePort => GetNextAvailablePort(FromMilliseconds(100)).GetAwaiter().GetResult(); +} diff --git a/test/Kurrent.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs b/test/Kurrent.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs new file mode 100644 index 000000000..506b4e98f --- /dev/null +++ b/test/Kurrent.Client.Tests.Common/FluentDocker/FluentDockerBuilderExtensions.cs @@ -0,0 +1,88 @@ +using System.Reflection; +using Ductus.FluentDocker.Builders; +using Ductus.FluentDocker.Common; +using Ductus.FluentDocker.Model.Compose; +using Ductus.FluentDocker.Services; +using Ductus.FluentDocker.Services.Impl; +using Polly; +using Polly.Contrib.WaitAndRetry; +using static System.TimeSpan; + +namespace Kurrent.Client.Tests.FluentDocker; + +public static class FluentDockerBuilderExtensions { + public static CompositeBuilder OverrideConfiguration(this CompositeBuilder compositeBuilder, Action configure) { + configure(GetInternalConfig(compositeBuilder)); + return compositeBuilder; + + static DockerComposeConfig GetInternalConfig(CompositeBuilder compositeBuilder) => + (DockerComposeConfig)typeof(CompositeBuilder) + .GetField("_config", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(compositeBuilder)!; + } + + public static DockerComposeConfig Configuration(this ICompositeService service) => + (DockerComposeConfig)typeof(DockerComposeCompositeService) + .GetProperty("Config", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(service)!; +} + +[PublicAPI] +public static class FluentDockerContainerBuilderExtensions { + public static ContainerBuilder WithEnvironment(this ContainerBuilder builder, Dictionary environment) => + builder.WithEnvironment(environment.Select(pair => $"{pair.Key}={pair.Value}").ToArray()); + + public static ContainerBuilder WaitUntilReady(this ContainerBuilder builder, IEnumerable sleepDurations, Action action) => + builder.Wait("", (service, _) => { + var result = Policy + .Handle() + .WaitAndRetry(sleepDurations) + .ExecuteAndCapture(() => action(service)); + + if (result.Outcome == OutcomeType.Successful) + return 0; + + throw result.FinalException is FluentDockerException + ? result.FinalException + : new FluentDockerException($"Service {service.Name} not ready: {result.FinalException.Message}"); + }); + + public static ContainerBuilder WaitUntilReadyWithConstantBackoff( + this ContainerBuilder builder, TimeSpan delay, int retryCount, Action action + ) => builder.WaitUntilReady(Backoff.ConstantBackoff(delay, retryCount), action); + + public static ContainerBuilder WaitUntilReadyWithExponentialBackoff( + this ContainerBuilder builder, TimeSpan delay, int retryCount, Action action + ) => builder.WaitUntilReady(Backoff.ExponentialBackoff(delay, retryCount), action); + + public static ContainerBuilder WaitUntilReadyWithConstantBackoff( + this ContainerBuilder builder, int delayMs, int retryCount, Action action + ) => builder.WaitUntilReadyWithConstantBackoff(FromMilliseconds(delayMs), retryCount, action); + + public static ContainerBuilder WaitUntilReadyWithExponentialBackoff( + this ContainerBuilder builder, int delayMs, int retryCount, Action action + ) => builder.WaitUntilReadyWithExponentialBackoff(FromMilliseconds(delayMs), retryCount, action); + + public static ContainerBuilder WaitUntilReadyAsync(this ContainerBuilder builder, IEnumerable sleepDurations, Func action) => + builder.WaitUntilReady(sleepDurations, service => { + var valueTask = action(service); + if (!valueTask.IsCompletedSuccessfully) + valueTask.AsTask().GetAwaiter().GetResult(); + }); + + public static ContainerBuilder WaitUntilReadyWithConstantBackoffAsync( + this ContainerBuilder builder, TimeSpan delay, int retryCount, Func action + ) => builder.WaitUntilReadyAsync(Backoff.ConstantBackoff(delay, retryCount, fastFirst: true), action); + + public static ContainerBuilder WaitUntilReadyWithExponentialBackoffAsync( + this ContainerBuilder builder, TimeSpan delay, int retryCount, Func action + ) => builder.WaitUntilReadyAsync(Backoff.ExponentialBackoff(delay, retryCount, fastFirst: true), action); + + public static ContainerBuilder WaitUntilReadyWithConstantBackoffAsync( + this ContainerBuilder builder, int delayMs, int retryCount, Func action + ) => builder.WaitUntilReadyWithConstantBackoffAsync(FromMilliseconds(delayMs), retryCount, action); + + public static ContainerBuilder WaitUntilReadyWithExponentialBackoffAsync( + this ContainerBuilder builder, int delayMs, int retryCount, Func action + ) => builder.WaitUntilReadyWithExponentialBackoffAsync(FromMilliseconds(delayMs), retryCount, action); +} diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs b/test/Kurrent.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs similarity index 62% rename from test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs rename to test/Kurrent.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs index c4773cede..b14bb7a43 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/FluentDocker/FluentDockerServiceExtensions.cs @@ -1,10 +1,14 @@ #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously +using System.Net; +using Ductus.FluentDocker.Builders; +using Ductus.FluentDocker.Commands; using Ductus.FluentDocker.Common; using Ductus.FluentDocker.Model.Containers; using Ductus.FluentDocker.Services; +using Ductus.FluentDocker.Services.Extensions; -namespace EventStore.Client.Tests.FluentDocker; +namespace Kurrent.Client.Tests.FluentDocker; public static class FluentDockerServiceExtensions { static readonly TimeSpan DefaultRetryDelay = TimeSpan.FromMilliseconds(100); @@ -60,3 +64,25 @@ public static async Task WaitUntilNodesAreHealthy(this ICompositeService service await WaitUntilNodesAreHealthy(service, serviceNamePrefix, cts.Token); } } + +public static class FluentDockerContainerServiceExtensions { + // IPAddress.Any defaults to IPAddress.Loopback + public static IPEndPoint GetPublicEndpoint(this IContainerService service, string portAndProtocol) { + var endpoint = service.ToHostExposedEndpoint(portAndProtocol); + return endpoint.Address.Equals(IPAddress.Any) ? new IPEndPoint(IPAddress.Loopback, endpoint.Port) : endpoint; + } + + public static IPEndPoint GetPublicEndpoint(this IContainerService service, int port) => + service.GetPublicEndpoint($"{port}/tcp"); + + public static ContainerBuilder WithPublicEndpointResolver(this ContainerBuilder builder) => + builder.UseCustomResolver((endpoints, portAndProtocol, dockerUri) => { + var endpoint = endpoints.ToHostPort(portAndProtocol, dockerUri); + return endpoint.Address.Equals(IPAddress.Any) ? new IPEndPoint(IPAddress.Loopback, endpoint.Port) : endpoint; + }); + + public static CommandResponse> ExecuteCommand(this IContainerService service, string command) { + var config = service.GetConfiguration(); + return service.DockerHost.Execute(config.Id, command, service.Certificates); + } +} diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/TestBypassService.cs b/test/Kurrent.Client.Tests.Common/FluentDocker/TestBypassService.cs similarity index 97% rename from test/EventStore.Client.Tests.Common/FluentDocker/TestBypassService.cs rename to test/Kurrent.Client.Tests.Common/FluentDocker/TestBypassService.cs index 3505eb9af..d09069d48 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/TestBypassService.cs +++ b/test/Kurrent.Client.Tests.Common/FluentDocker/TestBypassService.cs @@ -2,7 +2,7 @@ using Ductus.FluentDocker.Common; using Ductus.FluentDocker.Services; -namespace EventStore.Client.Tests.FluentDocker; +namespace Kurrent.Client.Tests.FluentDocker; public class TestBypassService : TestService { protected override BypassBuilder Configure() => throw new NotImplementedException(); diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/TestCompositeService.cs b/test/Kurrent.Client.Tests.Common/FluentDocker/TestCompositeService.cs similarity index 63% rename from test/EventStore.Client.Tests.Common/FluentDocker/TestCompositeService.cs rename to test/Kurrent.Client.Tests.Common/FluentDocker/TestCompositeService.cs index 104827f30..a321ad77d 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/TestCompositeService.cs +++ b/test/Kurrent.Client.Tests.Common/FluentDocker/TestCompositeService.cs @@ -1,6 +1,6 @@ using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Services; -namespace EventStore.Client.Tests.FluentDocker; +namespace Kurrent.Client.Tests.FluentDocker; -public abstract class TestCompositeService : TestService; \ No newline at end of file +public abstract class TestCompositeService : TestService; diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/TestContainerService.cs b/test/Kurrent.Client.Tests.Common/FluentDocker/TestContainerService.cs similarity index 63% rename from test/EventStore.Client.Tests.Common/FluentDocker/TestContainerService.cs rename to test/Kurrent.Client.Tests.Common/FluentDocker/TestContainerService.cs index 40ed937fc..fb135276e 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/TestContainerService.cs +++ b/test/Kurrent.Client.Tests.Common/FluentDocker/TestContainerService.cs @@ -1,6 +1,6 @@ using Ductus.FluentDocker.Builders; using Ductus.FluentDocker.Services; -namespace EventStore.Client.Tests.FluentDocker; +namespace Kurrent.Client.Tests.FluentDocker; -public abstract class TestContainerService : TestService; \ No newline at end of file +public abstract class TestContainerService : TestService; diff --git a/test/EventStore.Client.Tests.Common/FluentDocker/TestService.cs b/test/Kurrent.Client.Tests.Common/FluentDocker/TestService.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/FluentDocker/TestService.cs rename to test/Kurrent.Client.Tests.Common/FluentDocker/TestService.cs index 890f4e9b6..4bb942517 100644 --- a/test/EventStore.Client.Tests.Common/FluentDocker/TestService.cs +++ b/test/Kurrent.Client.Tests.Common/FluentDocker/TestService.cs @@ -5,7 +5,7 @@ using Serilog; using static Serilog.Core.Constants; -namespace EventStore.Client.Tests.FluentDocker; +namespace Kurrent.Client.Tests.FluentDocker; public interface ITestService : IAsyncDisposable { Task Start(); diff --git a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs b/test/Kurrent.Client.Tests.Common/GlobalEnvironment.cs similarity index 63% rename from test/EventStore.Client.Tests.Common/GlobalEnvironment.cs rename to test/Kurrent.Client.Tests.Common/GlobalEnvironment.cs index d7d26dc67..5444a6be4 100644 --- a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs +++ b/test/Kurrent.Client.Tests.Common/GlobalEnvironment.cs @@ -1,7 +1,7 @@ using System.Collections.Immutable; using Microsoft.Extensions.Configuration; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public static class GlobalEnvironment { static GlobalEnvironment() { @@ -26,6 +26,8 @@ static void EnsureDefaults(IConfiguration configuration) { configuration.EnsureValue("ES_DOCKER_TAG", "ci"); configuration.EnsureValue("ES_DOCKER_IMAGE", $"{configuration["ES_DOCKER_REGISTRY"]}:{configuration["ES_DOCKER_TAG"]}"); + configuration.EnsureValue("EVENTSTORE_TELEMETRY_OPTOUT", "true"); + configuration.EnsureValue("EVENTSTORE_ALLOW_UNKNOWN_OPTIONS", "true"); configuration.EnsureValue("EVENTSTORE_MEM_DB", "false"); configuration.EnsureValue("EVENTSTORE_RUN_PROJECTIONS", "None"); configuration.EnsureValue("EVENTSTORE_START_STANDARD_PROJECTIONS", "false"); @@ -33,7 +35,6 @@ static void EnsureDefaults(IConfiguration configuration) { configuration.EnsureValue("EVENTSTORE_DISABLE_LOG_FILE", "true"); configuration.EnsureValue("EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH", "/etc/eventstore/certs/ca"); configuration.EnsureValue("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "true"); - } } @@ -42,35 +43,4 @@ static void EnsureDefaults(IConfiguration configuration) { public static bool UseCluster { get; } public static bool UseExternalServer { get; } public static string DockerImage { get; } - - #region . Obsolete . - - //[Obsolete("Use the EventStoreFixture instead so you don't have to use this method.", false)] - public static IDictionary GetEnvironmentVariables(IDictionary? overrides = null) { - var env = new Dictionary { - ["ES_DOCKER_TAG"] = "ci", - ["EVENTSTORE_DB_LOG_FORMAT"] = "V2", - }; - - foreach (var @override in overrides ?? Enumerable.Empty>()) { - if (@override.Key.StartsWith("EVENTSTORE") && !SharedEnv.Contains(@override.Key)) - throw new Exception($"Add {@override.Key} to shared.env and _sharedEnv to pass it to the cluster containers"); - - env[@override.Key] = @override.Value; - } - - return env; - } - - // matches with the pass-through vars in shared.env... better way? - static readonly HashSet SharedEnv = new() { - "EVENTSTORE_DB_LOG_FORMAT", - "EVENTSTORE_LOG_LEVEL", - "EVENTSTORE_MAX_APPEND_SIZE", - "EVENTSTORE_MEM_DB", - "EVENTSTORE_RUN_PROJECTIONS", - "EVENTSTORE_START_STANDARD_PROJECTIONS", - }; - - #endregion } diff --git a/test/EventStore.Client.Tests.Common/InterlockedBoolean.cs b/test/Kurrent.Client.Tests.Common/InterlockedBoolean.cs similarity index 100% rename from test/EventStore.Client.Tests.Common/InterlockedBoolean.cs rename to test/Kurrent.Client.Tests.Common/InterlockedBoolean.cs diff --git a/test/Kurrent.Client.Tests.Common/Kurrent.Client.Tests.Common.csproj b/test/Kurrent.Client.Tests.Common/Kurrent.Client.Tests.Common.csproj new file mode 100644 index 000000000..447f11074 --- /dev/null +++ b/test/Kurrent.Client.Tests.Common/Kurrent.Client.Tests.Common.csproj @@ -0,0 +1,83 @@ + + + Kurrent.Client.Tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + certs\%(RecursiveDir)/%(FileName)%(Extension) + Always + + + + + + Always + + + Always + + + Always + + + PreserveNewest + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + diff --git a/test/EventStore.Client.Tests.Common/Logging.cs b/test/Kurrent.Client.Tests.Common/Logging.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/Logging.cs rename to test/Kurrent.Client.Tests.Common/Logging.cs index a9df9709a..ee991d064 100644 --- a/test/EventStore.Client.Tests.Common/Logging.cs +++ b/test/Kurrent.Client.Tests.Common/Logging.cs @@ -6,7 +6,7 @@ using Serilog.Formatting.Display; using Xunit.Sdk; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; static class Logging { static readonly Subject LogEventSubject = new(); @@ -78,4 +78,4 @@ public static void ReleaseLogs(Guid captureId) { // ignored } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/PasswordGenerator.cs b/test/Kurrent.Client.Tests.Common/PasswordGenerator.cs similarity index 98% rename from test/EventStore.Client.Tests.Common/PasswordGenerator.cs rename to test/Kurrent.Client.Tests.Common/PasswordGenerator.cs index 298600529..bbf019ce4 100644 --- a/test/EventStore.Client.Tests.Common/PasswordGenerator.cs +++ b/test/Kurrent.Client.Tests.Common/PasswordGenerator.cs @@ -1,6 +1,6 @@ using System.Text; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; static class PasswordGenerator { static PasswordGenerator() { @@ -63,4 +63,4 @@ public static string GenerateSimplePassword(int length = 8) { return new(password); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs b/test/Kurrent.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs similarity index 80% rename from test/EventStore.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs rename to test/Kurrent.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs index b75778b47..3411cd4b9 100644 --- a/test/EventStore.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs +++ b/test/Kurrent.Client.Tests.Common/Shouldly/ShouldThrowAsyncExtensions.cs @@ -7,6 +7,6 @@ namespace Shouldly; [DebuggerStepThrough] public static class ShouldThrowAsyncExtensions { - public static Task ShouldThrowAsync(this EventStoreClient.ReadStreamResult source) where TException : Exception => + public static Task ShouldThrowAsync(this KurrentClient.ReadStreamResult source) where TException : Exception => source.ToArrayAsync().AsTask().ShouldThrowAsync(); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/TestCaseGenerator.cs b/test/Kurrent.Client.Tests.Common/TestCaseGenerator.cs similarity index 95% rename from test/EventStore.Client.Tests.Common/TestCaseGenerator.cs rename to test/Kurrent.Client.Tests.Common/TestCaseGenerator.cs index 781d19063..b56c95794 100644 --- a/test/EventStore.Client.Tests.Common/TestCaseGenerator.cs +++ b/test/Kurrent.Client.Tests.Common/TestCaseGenerator.cs @@ -1,4 +1,4 @@ -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; using System.Collections; using Bogus; diff --git a/test/EventStore.Client.Tests.Common/TestCredentials.cs b/test/Kurrent.Client.Tests.Common/TestCredentials.cs similarity index 87% rename from test/EventStore.Client.Tests.Common/TestCredentials.cs rename to test/Kurrent.Client.Tests.Common/TestCredentials.cs index a489cd13d..9f68ea68b 100644 --- a/test/EventStore.Client.Tests.Common/TestCredentials.cs +++ b/test/Kurrent.Client.Tests.Common/TestCredentials.cs @@ -1,4 +1,6 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; + +namespace Kurrent.Client.Tests; public static class TestCredentials { public static readonly UserCredentials Root = new("admin", "changeit"); @@ -6,4 +8,4 @@ public static class TestCredentials { public static readonly UserCredentials TestUser2 = new("user2", "pa$$2"); public static readonly UserCredentials TestAdmin = new("adm", "admpa$$"); public static readonly UserCredentials TestBadUser = new("badlogin", "badpass"); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests.Common/appsettings.Development.json b/test/Kurrent.Client.Tests.Common/appsettings.Development.json similarity index 100% rename from test/EventStore.Client.Tests.Common/appsettings.Development.json rename to test/Kurrent.Client.Tests.Common/appsettings.Development.json diff --git a/test/EventStore.Client.Tests.Common/appsettings.json b/test/Kurrent.Client.Tests.Common/appsettings.json similarity index 100% rename from test/EventStore.Client.Tests.Common/appsettings.json rename to test/Kurrent.Client.Tests.Common/appsettings.json diff --git a/test/EventStore.Client.Tests.Common/docker-compose.certs.yml b/test/Kurrent.Client.Tests.Common/docker-compose.certs.yml similarity index 100% rename from test/EventStore.Client.Tests.Common/docker-compose.certs.yml rename to test/Kurrent.Client.Tests.Common/docker-compose.certs.yml diff --git a/test/EventStore.Client.Tests.Common/docker-compose.cluster.yml b/test/Kurrent.Client.Tests.Common/docker-compose.cluster.yml similarity index 100% rename from test/EventStore.Client.Tests.Common/docker-compose.cluster.yml rename to test/Kurrent.Client.Tests.Common/docker-compose.cluster.yml diff --git a/test/EventStore.Client.Tests.Common/docker-compose.node.yml b/test/Kurrent.Client.Tests.Common/docker-compose.node.yml similarity index 100% rename from test/EventStore.Client.Tests.Common/docker-compose.node.yml rename to test/Kurrent.Client.Tests.Common/docker-compose.node.yml diff --git a/test/EventStore.Client.Tests.Common/docker-compose.yml b/test/Kurrent.Client.Tests.Common/docker-compose.yml similarity index 100% rename from test/EventStore.Client.Tests.Common/docker-compose.yml rename to test/Kurrent.Client.Tests.Common/docker-compose.yml diff --git a/test/EventStore.Client.Tests.Common/shared.env b/test/Kurrent.Client.Tests.Common/shared.env similarity index 100% rename from test/EventStore.Client.Tests.Common/shared.env rename to test/Kurrent.Client.Tests.Common/shared.env diff --git a/test/Kurrent.Client.Tests.ExternalAssembly/ExternalEvents.cs b/test/Kurrent.Client.Tests.ExternalAssembly/ExternalEvents.cs new file mode 100644 index 000000000..eb0d95943 --- /dev/null +++ b/test/Kurrent.Client.Tests.ExternalAssembly/ExternalEvents.cs @@ -0,0 +1,10 @@ +namespace Kurrent.Client.Tests.ExternalAssembly; + +/// +/// External event class used for testing loaded assembly resolution +/// This assembly will be explicitly loaded during tests +/// +public class ExternalEvent { + public string Id { get; set; } = null!; + public string Name { get; set; } = null!; +} diff --git a/test/Kurrent.Client.Tests.ExternalAssembly/Kurrent.Client.Tests.ExternalAssembly.csproj b/test/Kurrent.Client.Tests.ExternalAssembly/Kurrent.Client.Tests.ExternalAssembly.csproj new file mode 100644 index 000000000..6078c004b --- /dev/null +++ b/test/Kurrent.Client.Tests.ExternalAssembly/Kurrent.Client.Tests.ExternalAssembly.csproj @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/Kurrent.Client.Tests.NeverLoadedAssembly/Kurrent.Client.Tests.NeverLoadedAssembly.csproj b/test/Kurrent.Client.Tests.NeverLoadedAssembly/Kurrent.Client.Tests.NeverLoadedAssembly.csproj new file mode 100644 index 000000000..6078c004b --- /dev/null +++ b/test/Kurrent.Client.Tests.NeverLoadedAssembly/Kurrent.Client.Tests.NeverLoadedAssembly.csproj @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/Kurrent.Client.Tests.NeverLoadedAssembly/NotLoadedExternalEvent.cs b/test/Kurrent.Client.Tests.NeverLoadedAssembly/NotLoadedExternalEvent.cs new file mode 100644 index 000000000..94bb5f4a2 --- /dev/null +++ b/test/Kurrent.Client.Tests.NeverLoadedAssembly/NotLoadedExternalEvent.cs @@ -0,0 +1,10 @@ +namespace Kurrent.Client.Tests.NeverLoadedAssembly; + +/// +/// External event class used for testing unloaded assembly resolution +/// This event should never be referenced directly by the test project +/// +public record NotLoadedExternalEvent { + public string Id { get; set; } = null!; + public string Name { get; set; } = null!; +} diff --git a/test/EventStore.Client.Tests/Assertions/ComparableAssertion.cs b/test/Kurrent.Client.Tests/Assertions/ComparableAssertion.cs similarity index 99% rename from test/EventStore.Client.Tests/Assertions/ComparableAssertion.cs rename to test/Kurrent.Client.Tests/Assertions/ComparableAssertion.cs index 0d626cf61..cecfe6599 100644 --- a/test/EventStore.Client.Tests/Assertions/ComparableAssertion.cs +++ b/test/Kurrent.Client.Tests/Assertions/ComparableAssertion.cs @@ -3,7 +3,7 @@ using AutoFixture.Kernel; // ReSharper disable once CheckNamespace -namespace EventStore.Client; +namespace Kurrent.Client; class ComparableAssertion : CompositeIdiomaticAssertion { public ComparableAssertion(ISpecimenBuilder builder) : base(CreateChildrenAssertions(builder)) { } @@ -142,4 +142,4 @@ public override void Verify(Type type) { } } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/Assertions/EqualityAssertion.cs b/test/Kurrent.Client.Tests/Assertions/EqualityAssertion.cs similarity index 98% rename from test/EventStore.Client.Tests/Assertions/EqualityAssertion.cs rename to test/Kurrent.Client.Tests/Assertions/EqualityAssertion.cs index 055cef40b..6c172af7f 100644 --- a/test/EventStore.Client.Tests/Assertions/EqualityAssertion.cs +++ b/test/Kurrent.Client.Tests/Assertions/EqualityAssertion.cs @@ -2,7 +2,7 @@ using AutoFixture.Kernel; // ReSharper disable once CheckNamespace -namespace EventStore.Client; +namespace Kurrent.Client; class EqualityAssertion : CompositeIdiomaticAssertion { public EqualityAssertion(ISpecimenBuilder builder) : base(CreateChildrenAssertions(builder)) { } @@ -67,4 +67,4 @@ public override void Verify(Type type) { throw new($"The type '{type}' did not implement the inequality (!=) operator correctly."); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/Assertions/NullArgumentAssertion.cs b/test/Kurrent.Client.Tests/Assertions/NullArgumentAssertion.cs similarity index 97% rename from test/EventStore.Client.Tests/Assertions/NullArgumentAssertion.cs rename to test/Kurrent.Client.Tests/Assertions/NullArgumentAssertion.cs index 770591afc..4efb616d5 100644 --- a/test/EventStore.Client.Tests/Assertions/NullArgumentAssertion.cs +++ b/test/Kurrent.Client.Tests/Assertions/NullArgumentAssertion.cs @@ -3,7 +3,7 @@ using AutoFixture.Kernel; // ReSharper disable once CheckNamespace -namespace EventStore.Client; +namespace Kurrent.Client; class NullArgumentAssertion : IdiomaticAssertion { readonly ISpecimenBuilder _builder; @@ -50,4 +50,4 @@ public override void Verify(Type type) { } ); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/Assertions/StringConversionAssertion.cs b/test/Kurrent.Client.Tests/Assertions/StringConversionAssertion.cs similarity index 97% rename from test/EventStore.Client.Tests/Assertions/StringConversionAssertion.cs rename to test/Kurrent.Client.Tests/Assertions/StringConversionAssertion.cs index 8b97e33f8..fa4da4970 100644 --- a/test/EventStore.Client.Tests/Assertions/StringConversionAssertion.cs +++ b/test/Kurrent.Client.Tests/Assertions/StringConversionAssertion.cs @@ -3,7 +3,7 @@ using AutoFixture.Kernel; // ReSharper disable once CheckNamespace -namespace EventStore.Client; +namespace Kurrent.Client; class StringConversionAssertion : IdiomaticAssertion { readonly ISpecimenBuilder _builder; @@ -43,4 +43,4 @@ public override void Verify(Type type) { if (toString is not null) Assert.Equal(value, toString.Invoke(instance, null)); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/Assertions/ValueObjectAssertion.cs b/test/Kurrent.Client.Tests/Assertions/ValueObjectAssertion.cs similarity index 94% rename from test/EventStore.Client.Tests/Assertions/ValueObjectAssertion.cs rename to test/Kurrent.Client.Tests/Assertions/ValueObjectAssertion.cs index dbdbb5ca2..ee3fce80d 100644 --- a/test/EventStore.Client.Tests/Assertions/ValueObjectAssertion.cs +++ b/test/Kurrent.Client.Tests/Assertions/ValueObjectAssertion.cs @@ -2,7 +2,7 @@ using AutoFixture.Kernel; // ReSharper disable once CheckNamespace -namespace EventStore.Client; +namespace Kurrent.Client; class ValueObjectAssertion : CompositeIdiomaticAssertion { public ValueObjectAssertion(ISpecimenBuilder builder) : base(CreateChildrenAssertions(builder)) { } @@ -13,4 +13,4 @@ static IEnumerable CreateChildrenAssertions(ISpecimenBuilde yield return new StringConversionAssertion(builder); yield return new NullArgumentAssertion(builder); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/AutoScenarioDataAttribute.cs b/test/Kurrent.Client.Tests/AutoScenarioDataAttribute.cs similarity index 95% rename from test/EventStore.Client.Tests/AutoScenarioDataAttribute.cs rename to test/Kurrent.Client.Tests/AutoScenarioDataAttribute.cs index d5840a41b..166e5b7ef 100644 --- a/test/EventStore.Client.Tests/AutoScenarioDataAttribute.cs +++ b/test/Kurrent.Client.Tests/AutoScenarioDataAttribute.cs @@ -3,7 +3,7 @@ using AutoFixture.Xunit2; using Xunit.Sdk; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; [DataDiscoverer("AutoFixture.Xunit2.NoPreDiscoveryDataDiscoverer", "AutoFixture.Xunit2")] public class AutoScenarioDataAttribute : DataAttribute { @@ -25,4 +25,4 @@ public override IEnumerable GetData(MethodInfo testMethod) { class CustomAutoData : AutoDataAttribute { public CustomAutoData(Type fixtureType) : base(() => (IFixture)Activator.CreateInstance(fixtureType)!) { } } -} \ No newline at end of file +} diff --git a/test/Kurrent.Client.Tests/ClientCertificatesTests.cs b/test/Kurrent.Client.Tests/ClientCertificatesTests.cs new file mode 100644 index 000000000..92d08fa51 --- /dev/null +++ b/test/Kurrent.Client.Tests/ClientCertificatesTests.cs @@ -0,0 +1,93 @@ +using EventStore.Client; +using Humanizer; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Misc")] +[Trait("Category", "Target:Plugins")] +[Trait("Category", "Type:UserCertificate")] +public class ClientCertificateTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), BadClientCertificatesTestCases] + async Task bad_certificates_combinations_should_return_authentication_error(string userCertFile, string userKeyFile, string tlsCaFile) { + var stream = Fixture.GetStreamName(); + var seedEvents = Fixture.CreateTestEvents(); + var port = Fixture.Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; + + var connectionString = $"esdb://localhost:{port}/?tls=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; + + var settings = KurrentClientSettings.Create(connectionString); + settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); + + await using var client = new KurrentClient(settings); + + await client.AppendToStreamAsync(stream, StreamState.NoStream, seedEvents).ShouldThrowAsync(); + } + + [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), ValidClientCertificatesTestCases] + async Task valid_certificates_combinations_should_write_to_stream(string userCertFile, string userKeyFile, string tlsCaFile) { + var stream = Fixture.GetStreamName(); + var seedEvents = Fixture.CreateTestEvents(); + var port = Fixture.Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; + + var connectionString = $"esdb://localhost:{port}/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; + + var settings = KurrentClientSettings.Create(connectionString); + settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); + + await using var client = new KurrentClient(settings); + + var result = await client.AppendToStreamAsync(stream, StreamState.NoStream, seedEvents); + result.ShouldNotBeNull(); + } + + [SupportsPlugins.Theory(EventStoreRepository.Commercial, "This server version does not support plugins"), BadClientCertificatesTestCases] + async Task basic_authentication_should_take_precedence(string userCertFile, string userKeyFile, string tlsCaFile) { + var stream = Fixture.GetStreamName(); + var seedEvents = Fixture.CreateTestEvents(); + var port = Fixture.Options.ClientSettings.ConnectivitySettings.ResolvedAddressOrDefault.Port; + + var connectionString = $"esdb://admin:changeit@localhost:{port}/?userCertFile={userCertFile}&userKeyFile={userKeyFile}&tlsCaFile={tlsCaFile}"; + + var settings = KurrentClientSettings.Create(connectionString); + settings.ConnectivitySettings.TlsVerifyCert.ShouldBeTrue(); + + await using var client = new KurrentClient(settings); + + var result = await client.AppendToStreamAsync(stream, StreamState.NoStream, seedEvents); + result.ShouldNotBeNull(); + } + + class BadClientCertificatesTestCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [Certificates.Invalid.CertAbsolute, Certificates.Invalid.KeyAbsolute, Certificates.TlsCa.Absolute]; + yield return [Certificates.Invalid.CertRelative, Certificates.Invalid.KeyRelative, Certificates.TlsCa.Absolute]; + yield return [Certificates.Invalid.CertAbsolute, Certificates.Invalid.KeyAbsolute, Certificates.TlsCa.Relative]; + yield return [Certificates.Invalid.CertRelative, Certificates.Invalid.KeyRelative, Certificates.TlsCa.Relative]; + } + } + + class ValidClientCertificatesTestCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [Certificates.Admin.CertAbsolute, Certificates.Admin.KeyAbsolute, Certificates.TlsCa.Absolute]; + yield return [Certificates.Admin.CertRelative, Certificates.Admin.KeyRelative, Certificates.TlsCa.Absolute]; + yield return [Certificates.Admin.CertAbsolute, Certificates.Admin.KeyAbsolute, Certificates.TlsCa.Relative]; + yield return [Certificates.Admin.CertRelative, Certificates.Admin.KeyRelative, Certificates.TlsCa.Relative]; + } + } +} + +public enum EventStoreRepository { + Commercial = 1 +} + +[PublicAPI] +public class SupportsPlugins { + public class TheoryAttribute(EventStoreRepository repository, string skipMessage) : Xunit.TheoryAttribute { + public override string? Skip { + get => !GlobalEnvironment.DockerImage.Contains(repository.Humanize().ToLower()) ? skipMessage : null; + set => throw new NotSupportedException(); + } + } +} diff --git a/test/EventStore.Client.Tests/ConnectionStringTests.cs b/test/Kurrent.Client.Tests/ConnectionStringTests.cs similarity index 75% rename from test/EventStore.Client.Tests/ConnectionStringTests.cs rename to test/Kurrent.Client.Tests/ConnectionStringTests.cs index e258c682d..7ef507b6c 100644 --- a/test/EventStore.Client.Tests/ConnectionStringTests.cs +++ b/test/Kurrent.Client.Tests/ConnectionStringTests.cs @@ -1,11 +1,14 @@ using System.Net; using System.Net.Http; -using AutoFixture; using System.Reflection; using System.Security.Cryptography.X509Certificates; +using AutoFixture; +using EventStore.Client; +using HashCode = EventStore.Client.HashCode; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class ConnectionStringTests { public static IEnumerable ValidCases() { var fixture = new Fixture(); @@ -24,14 +27,14 @@ public class ConnectionStringTests { return Enumerable.Range(0, 3).SelectMany(GetTestCases); IEnumerable GetTestCases(int _) { - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { ConnectionName = fixture.Create(), - ConnectivitySettings = fixture.Create(), - OperationOptions = fixture.Create() + ConnectivitySettings = fixture.Create(), + OperationOptions = fixture.Create() }; settings.ConnectivitySettings.Address = - new UriBuilder(EventStoreClientConnectivitySettings.Default.ResolvedAddressOrDefault) { + new UriBuilder(KurrentClientConnectivitySettings.Default.ResolvedAddressOrDefault) { Scheme = settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme }.Uri; @@ -45,14 +48,14 @@ public class ConnectionStringTests { settings }; - var ipGossipSettings = new EventStoreClientSettings { + var ipGossipSettings = new KurrentClientSettings { ConnectionName = fixture.Create(), - ConnectivitySettings = fixture.Create(), - OperationOptions = fixture.Create() + ConnectivitySettings = fixture.Create(), + OperationOptions = fixture.Create() }; ipGossipSettings.ConnectivitySettings.Address = - new UriBuilder(EventStoreClientConnectivitySettings.Default.ResolvedAddressOrDefault) { + new UriBuilder(KurrentClientConnectivitySettings.Default.ResolvedAddressOrDefault) { Scheme = ipGossipSettings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme }.Uri; @@ -68,10 +71,10 @@ public class ConnectionStringTests { ipGossipSettings }; - var singleNodeSettings = new EventStoreClientSettings { + var singleNodeSettings = new KurrentClientSettings { ConnectionName = fixture.Create(), - ConnectivitySettings = fixture.Create(), - OperationOptions = fixture.Create() + ConnectivitySettings = fixture.Create(), + OperationOptions = fixture.Create() }; singleNodeSettings.ConnectivitySettings.DnsGossipSeeds = null; @@ -96,18 +99,18 @@ public class ConnectionStringTests { [Theory] [MemberData(nameof(ValidCases))] - public void valid_connection_string(string connectionString, EventStoreClientSettings expected) { - var result = EventStoreClientSettings.Create(connectionString); + public void valid_connection_string(string connectionString, KurrentClientSettings expected) { + var result = KurrentClientSettings.Create(connectionString); - Assert.Equal(expected, result, EventStoreClientSettingsEqualityComparer.Instance); + Assert.Equal(expected, result, KurrentClientSettingsEqualityComparer.Instance); } [Theory] [MemberData(nameof(ValidCases))] - public void valid_connection_string_with_empty_path(string connectionString, EventStoreClientSettings expected) { - var result = EventStoreClientSettings.Create(connectionString.Replace("?", "/?")); + public void valid_connection_string_with_empty_path(string connectionString, KurrentClientSettings expected) { + var result = KurrentClientSettings.Create(connectionString.Replace("?", "/?")); - Assert.Equal(expected, result, EventStoreClientSettingsEqualityComparer.Instance); + Assert.Equal(expected, result, KurrentClientSettingsEqualityComparer.Instance); } #if !GRPC_CORE @@ -116,7 +119,7 @@ public void valid_connection_string_with_empty_path(string connectionString, Eve [InlineData(true)] public void tls_verify_cert(bool tlsVerifyCert) { var connectionString = $"esdb://localhost:2113/?tlsVerifyCert={tlsVerifyCert}"; - var result = EventStoreClientSettings.Create(connectionString); + var result = KurrentClientSettings.Create(connectionString); using var handler = result.CreateHttpMessageHandler?.Invoke(); #if NET var socketsHandler = Assert.IsType(handler); @@ -130,8 +133,7 @@ public void tls_verify_cert(bool tlsVerifyCert) { default ) ); - } - else { + } else { Assert.Null(socketsHandler.SslOptions.RemoteCertificateValidationCallback); } #else @@ -156,16 +158,14 @@ public void tls_verify_cert(bool tlsVerifyCert) { [Theory] [MemberData(nameof(InvalidTlsCertificates))] public void connection_string_with_invalid_tls_certificate_should_throw(string clientCertificatePath) { - Assert.Throws( - () => EventStoreClientSettings.Create( - $"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={clientCertificatePath}" - ) + Assert.Throws( + () => KurrentClientSettings.Create($"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={clientCertificatePath}") ); } public static IEnumerable InvalidClientCertificates() { var invalidPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "path", "not", "found"); - var validPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "certs", "ca", "ca.crt"); + var validPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "certs", "ca", "ca.crt"); yield return [invalidPath, invalidPath]; yield return [validPath, invalidPath]; } @@ -174,15 +174,15 @@ public void connection_string_with_invalid_tls_certificate_should_throw(string c [MemberData(nameof(InvalidClientCertificates))] public void connection_string_with_invalid_client_certificate_should_throw(string userCertFile, string userKeyFile) { Assert.Throws( - () => EventStoreClientSettings.Create( + () => KurrentClientSettings.Create( $"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" ) ); } - [Fact] + [RetryFact] public void infinite_grpc_timeouts() { - var result = EventStoreClientSettings.Create("esdb://localhost:2113?keepAliveInterval=-1&keepAliveTimeout=-1"); + var result = KurrentClientSettings.Create("esdb://localhost:2113?keepAliveInterval=-1&keepAliveTimeout=-1"); Assert.Equal(System.Threading.Timeout.InfiniteTimeSpan, result.ConnectivitySettings.KeepAliveInterval); Assert.Equal(System.Threading.Timeout.InfiniteTimeSpan, result.ConnectivitySettings.KeepAliveTimeout); @@ -200,22 +200,22 @@ public void infinite_grpc_timeouts() { #endif } - [Fact] - public void connection_string_with_no_schema() => Assert.Throws(() => EventStoreClientSettings.Create(":so/mething/random")); + [RetryFact] + public void connection_string_with_no_schema() => Assert.Throws(() => KurrentClientSettings.Create(":so/mething/random")); [Theory] [InlineData("esdbwrong://")] [InlineData("wrong://")] [InlineData("badesdb://")] public void connection_string_with_invalid_scheme_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [Theory] [InlineData("esdb://userpass@127.0.0.1/")] [InlineData("esdb://user:pa:ss@127.0.0.1/")] [InlineData("esdb://us:er:pa:ss@127.0.0.1/")] public void connection_string_with_invalid_userinfo_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [Theory] [InlineData("esdb://user:pass@127.0.0.1:abc")] @@ -229,14 +229,14 @@ public void connection_string_with_invalid_userinfo_should_throw(string connecti [InlineData("esdb://user:pass@localhost:1234,,127.0.0.3:4321")] [InlineData("esdb://user:pass@localhost:1234,,127.0.0.3:4321/")] public void connection_string_with_invalid_host_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [Theory] [InlineData("esdb://user:pass@127.0.0.1/test")] [InlineData("esdb://user:pass@127.0.0.1/maxDiscoverAttempts=10")] [InlineData("esdb://user:pass@127.0.0.1/hello?maxDiscoverAttempts=10")] public void connection_string_with_non_empty_path_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [Theory] [InlineData("esdb://user:pass@127.0.0.1")] @@ -244,19 +244,19 @@ public void connection_string_with_non_empty_path_should_throw(string connection [InlineData("esdb+discover://user:pass@127.0.0.1")] [InlineData("esdb+discover://user:pass@127.0.0.1/")] public void connection_string_with_no_key_value_pairs_specified_should_not_throw(string connectionString) => - EventStoreClientSettings.Create(connectionString); + KurrentClientSettings.Create(connectionString); [Theory] [InlineData("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=12=34")] [InlineData("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts1234")] public void connection_string_with_invalid_key_value_pair_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [Theory] [InlineData("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=1234&MaxDiscoverAttempts=10")] [InlineData("esdb://user:pass@127.0.0.1/?gossipTimeout=10&gossipTimeout=30")] public void connection_string_with_duplicate_key_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); [Theory] [InlineData("esdb://user:pass@127.0.0.1/?unknown=1234")] @@ -269,59 +269,59 @@ public void connection_string_with_duplicate_key_should_throw(string connectionS [InlineData("esdb://user:pass@127.0.0.1/?keepAliveInterval=-2")] [InlineData("esdb://user:pass@127.0.0.1/?keepAliveTimeout=-2")] public void connection_string_with_invalid_settings_should_throw(string connectionString) => - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + Assert.Throws(() => KurrentClientSettings.Create(connectionString)); - [Fact] + [RetryFact] public void with_default_settings() { - var settings = EventStoreClientSettings.Create("esdb://hostname:4321/"); + var settings = KurrentClientSettings.Create("esdb://hostname:4321/"); Assert.Null(settings.ConnectionName); Assert.Equal( - EventStoreClientConnectivitySettings.Default.ResolvedAddressOrDefault.Scheme, + KurrentClientConnectivitySettings.Default.ResolvedAddressOrDefault.Scheme, settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme ); Assert.Equal( - EventStoreClientConnectivitySettings.Default.DiscoveryInterval.TotalMilliseconds, + KurrentClientConnectivitySettings.Default.DiscoveryInterval.TotalMilliseconds, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds ); - Assert.Null(EventStoreClientConnectivitySettings.Default.DnsGossipSeeds); - Assert.Empty(EventStoreClientConnectivitySettings.Default.GossipSeeds); + Assert.Null(KurrentClientConnectivitySettings.Default.DnsGossipSeeds); + Assert.Empty(KurrentClientConnectivitySettings.Default.GossipSeeds); Assert.Equal( - EventStoreClientConnectivitySettings.Default.GossipTimeout.TotalMilliseconds, + KurrentClientConnectivitySettings.Default.GossipTimeout.TotalMilliseconds, settings.ConnectivitySettings.GossipTimeout.TotalMilliseconds ); - Assert.Null(EventStoreClientConnectivitySettings.Default.IpGossipSeeds); + Assert.Null(KurrentClientConnectivitySettings.Default.IpGossipSeeds); Assert.Equal( - EventStoreClientConnectivitySettings.Default.MaxDiscoverAttempts, + KurrentClientConnectivitySettings.Default.MaxDiscoverAttempts, settings.ConnectivitySettings.MaxDiscoverAttempts ); Assert.Equal( - EventStoreClientConnectivitySettings.Default.NodePreference, + KurrentClientConnectivitySettings.Default.NodePreference, settings.ConnectivitySettings.NodePreference ); Assert.Equal( - EventStoreClientConnectivitySettings.Default.Insecure, + KurrentClientConnectivitySettings.Default.Insecure, settings.ConnectivitySettings.Insecure ); Assert.Equal(TimeSpan.FromSeconds(10), settings.DefaultDeadline); Assert.Equal( - EventStoreClientOperationOptions.Default.ThrowOnAppendFailure, + KurrentClientOperationOptions.Default.ThrowOnAppendFailure, settings.OperationOptions.ThrowOnAppendFailure ); Assert.Equal( - EventStoreClientConnectivitySettings.Default.KeepAliveInterval, + KurrentClientConnectivitySettings.Default.KeepAliveInterval, settings.ConnectivitySettings.KeepAliveInterval ); Assert.Equal( - EventStoreClientConnectivitySettings.Default.KeepAliveTimeout, + KurrentClientConnectivitySettings.Default.KeepAliveTimeout, settings.ConnectivitySettings.KeepAliveTimeout ); } @@ -334,7 +334,7 @@ public void with_default_settings() { [InlineData("esdb://localhost1,localhost2,localhost3/?tls=false", false)] [InlineData("esdb://localhost1,localhost2,localhost3/?tls=true", true)] public void use_tls(string connectionString, bool expectedUseTls) { - var result = EventStoreClientSettings.Create(connectionString); + var result = KurrentClientSettings.Create(connectionString); var expectedScheme = expectedUseTls ? "https" : "http"; Assert.NotEqual(expectedUseTls, result.ConnectivitySettings.Insecure); Assert.Equal(expectedScheme, result.ConnectivitySettings.ResolvedAddressOrDefault.Scheme); @@ -360,7 +360,7 @@ public void use_tls(string connectionString, bool expectedUseTls) { [InlineData("esdb://localhost1,localhost2,localhost3/?tls=false", true, false)] [InlineData("esdb://localhost1,localhost2,localhost3/?tls=false", false, false)] public void allow_tls_override_for_single_node(string connectionString, bool? insecureOverride, bool expectedUseTls) { - var result = EventStoreClientSettings.Create(connectionString); + var result = KurrentClientSettings.Create(connectionString); var settings = result.ConnectivitySettings; if (insecureOverride.HasValue) @@ -379,7 +379,7 @@ public void allow_tls_override_for_single_node(string connectionString, bool? in [InlineData("esdb+discover://localhost:1234", null, null)] [InlineData("esdb+discover://localhost:1234,localhost:4567", null, null)] public void connection_string_with_custom_ports(string connectionString, string? expectedHost, int? expectedPort) { - var result = EventStoreClientSettings.Create(connectionString); + var result = KurrentClientSettings.Create(connectionString); var connectivitySettings = result.ConnectivitySettings; Assert.Equal(expectedHost, connectivitySettings.Address?.Host); @@ -387,17 +387,17 @@ public void connection_string_with_custom_ports(string connectionString, string? } static string GetConnectionString( - EventStoreClientSettings settings, + KurrentClientSettings settings, Func? getKey = default ) => $"{GetScheme(settings)}{GetAuthority(settings)}?{GetKeyValuePairs(settings, getKey)}"; - static string GetScheme(EventStoreClientSettings settings) => + static string GetScheme(KurrentClientSettings settings) => settings.ConnectivitySettings.IsSingleNode ? "esdb://" : "esdb+discover://"; - static string GetAuthority(EventStoreClientSettings settings) => + static string GetAuthority(KurrentClientSettings settings) => settings.ConnectivitySettings.IsSingleNode ? $"{settings.ConnectivitySettings.ResolvedAddressOrDefault.Host}:{settings.ConnectivitySettings.ResolvedAddressOrDefault.Port}" : string.Join( @@ -406,7 +406,7 @@ static string GetAuthority(EventStoreClientSettings settings) => ); static string GetKeyValuePairs( - EventStoreClientSettings settings, + KurrentClientSettings settings, Func? getKey = default ) { var pairs = new Dictionary { @@ -444,10 +444,10 @@ static string GetKeyValuePairs( return string.Join("&", pairs.Select(pair => $"{getKey?.Invoke(pair.Key) ?? pair.Key}={pair.Value}")); } - class EventStoreClientSettingsEqualityComparer : IEqualityComparer { - public static readonly EventStoreClientSettingsEqualityComparer Instance = new(); + class KurrentClientSettingsEqualityComparer : IEqualityComparer { + public static readonly KurrentClientSettingsEqualityComparer Instance = new(); - public bool Equals(EventStoreClientSettings? x, EventStoreClientSettings? y) { + public bool Equals(KurrentClientSettings? x, KurrentClientSettings? y) { if (ReferenceEquals(x, y)) return true; @@ -461,29 +461,29 @@ public bool Equals(EventStoreClientSettings? x, EventStoreClientSettings? y) { return false; return x.ConnectionName == y.ConnectionName && - EventStoreClientConnectivitySettingsEqualityComparer.Instance.Equals( + KurrentClientConnectivitySettingsEqualityComparer.Instance.Equals( x.ConnectivitySettings, y.ConnectivitySettings ) && - EventStoreClientOperationOptionsEqualityComparer.Instance.Equals( + KurrentClientOperationOptionsEqualityComparer.Instance.Equals( x.OperationOptions, y.OperationOptions ) && Equals(x.DefaultCredentials?.ToString(), y.DefaultCredentials?.ToString()); } - public int GetHashCode(EventStoreClientSettings obj) => + public int GetHashCode(KurrentClientSettings obj) => HashCode.Hash .Combine(obj.ConnectionName) - .Combine(EventStoreClientConnectivitySettingsEqualityComparer.Instance.GetHashCode(obj.ConnectivitySettings)) - .Combine(EventStoreClientOperationOptionsEqualityComparer.Instance.GetHashCode(obj.OperationOptions)); + .Combine(KurrentClientConnectivitySettingsEqualityComparer.Instance.GetHashCode(obj.ConnectivitySettings)) + .Combine(KurrentClientOperationOptionsEqualityComparer.Instance.GetHashCode(obj.OperationOptions)); } - class EventStoreClientConnectivitySettingsEqualityComparer - : IEqualityComparer { - public static readonly EventStoreClientConnectivitySettingsEqualityComparer Instance = new(); + class KurrentClientConnectivitySettingsEqualityComparer + : IEqualityComparer { + public static readonly KurrentClientConnectivitySettingsEqualityComparer Instance = new(); - public bool Equals(EventStoreClientConnectivitySettings? x, EventStoreClientConnectivitySettings? y) { + public bool Equals(KurrentClientConnectivitySettings? x, KurrentClientConnectivitySettings? y) { if (ReferenceEquals(x, y)) return true; @@ -507,7 +507,7 @@ public bool Equals(EventStoreClientConnectivitySettings? x, EventStoreClientConn x.Insecure == y.Insecure; } - public int GetHashCode(EventStoreClientConnectivitySettings obj) => + public int GetHashCode(KurrentClientConnectivitySettings obj) => obj.GossipSeeds.Aggregate( HashCode.Hash .Combine(obj.ResolvedAddressOrDefault.GetHashCode()) @@ -522,11 +522,11 @@ public int GetHashCode(EventStoreClientConnectivitySettings obj) => ); } - class EventStoreClientOperationOptionsEqualityComparer - : IEqualityComparer { - public static readonly EventStoreClientOperationOptionsEqualityComparer Instance = new(); + class KurrentClientOperationOptionsEqualityComparer + : IEqualityComparer { + public static readonly KurrentClientOperationOptionsEqualityComparer Instance = new(); - public bool Equals(EventStoreClientOperationOptions? x, EventStoreClientOperationOptions? y) { + public bool Equals(KurrentClientOperationOptions? x, KurrentClientOperationOptions? y) { if (ReferenceEquals(x, y)) return true; @@ -539,7 +539,7 @@ public bool Equals(EventStoreClientOperationOptions? x, EventStoreClientOperatio return x.GetType() == y.GetType(); } - public int GetHashCode(EventStoreClientOperationOptions obj) => + public int GetHashCode(KurrentClientOperationOptions obj) => System.HashCode.Combine(obj.ThrowOnAppendFailure); } } diff --git a/test/Kurrent.Client.Tests/Core/Serialization/ContentTypeExtensionsTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/ContentTypeExtensionsTests.cs new file mode 100644 index 000000000..d2dd02889 --- /dev/null +++ b/test/Kurrent.Client.Tests/Core/Serialization/ContentTypeExtensionsTests.cs @@ -0,0 +1,69 @@ +using EventStore.Client; +using Kurrent.Client.Core.Serialization; + +namespace Kurrent.Client.Tests.Core.Serialization; + +using static Constants.Metadata.ContentTypes; + +public class ContentTypeExtensionsTests { + [Fact] + public void FromMessageContentType_WithApplicationJson_ReturnsJsonContentType() { + // Given + // When + var result = ContentTypeExtensions.FromMessageContentType(ApplicationJson); + + // Then + Assert.Equal(ContentType.Json, result); + } + + [Fact] + public void FromMessageContentType_WithAnyOtherContentType_ReturnsBytesContentType() { + // Given + // When + var result = ContentTypeExtensions.FromMessageContentType(ApplicationOctetStream); + + // Then + Assert.Equal(ContentType.Bytes, result); + } + + [Fact] + public void FromMessageContentType_WithRandomString_ReturnsBytesContentType() { + // Given + const string contentType = "some-random-content-type"; + + // When + var result = ContentTypeExtensions.FromMessageContentType(contentType); + + // Then + Assert.Equal(ContentType.Bytes, result); + } + + [Fact] + public void ToMessageContentType_WithJsonContentType_ReturnsApplicationJson() { + // Given + // When + var result = ContentType.Json.ToMessageContentType(); + + // Then + Assert.Equal(ApplicationJson, result); + } + + [Fact] + public void ToMessageContentType_WithBytesContentType_ReturnsApplicationOctetStream() { + // Given + // When + var result = ContentType.Bytes.ToMessageContentType(); + + // Then + Assert.Equal(ApplicationOctetStream, result); + } + + [Fact] + public void ToMessageContentType_WithInvalidContentType_ThrowsArgumentOutOfRangeException() { + // Given + var contentType = (ContentType)999; // Invalid content type + + // When/Then + Assert.Throws(() => contentType.ToMessageContentType()); + } +} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializationContextTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializationContextTests.cs new file mode 100644 index 000000000..295dd1e3e --- /dev/null +++ b/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializationContextTests.cs @@ -0,0 +1,45 @@ +using Kurrent.Client.Core.Serialization; + +namespace Kurrent.Client.Tests.Core.Serialization; + +public class MessageSerializationContextTests +{ + [Fact] + public void CategoryName_ExtractsFromStreamName() + { + // Arrange + var context = new MessageSerializationContext("user-123", ContentType.Json); + + // Act + var categoryName = context.CategoryName; + + // Assert + Assert.Equal("user", categoryName); + } + + [Fact] + public void CategoryName_ExtractsFromStreamNameWithMoreThanOneDash() + { + // Arrange + var context = new MessageSerializationContext("user-some-123", ContentType.Json); + + // Act + var categoryName = context.CategoryName; + + // Assert + Assert.Equal("user", categoryName); + } + + [Fact] + public void CategoryName_ReturnsTheWholeStreamName() + { + // Arrange + var context = new MessageSerializationContext("user123", ContentType.Json); + + // Act + var categoryName = context.CategoryName; + + // Assert + Assert.Equal("user123", categoryName); + } +} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializerExtensionsTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializerExtensionsTests.cs new file mode 100644 index 000000000..6262af66a --- /dev/null +++ b/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializerExtensionsTests.cs @@ -0,0 +1,110 @@ +using System.Diagnostics.CodeAnalysis; +using EventStore.Client; +using Kurrent.Client.Core.Serialization; + +namespace Kurrent.Client.Tests.Core.Serialization; + +public class MessageSerializerExtensionsTests { + [Fact] + public void With_NullOperationSettings_ReturnsDefaultSerializer() { + // Given + var defaultSerializer = new DummyMessageSerializer(); + var defaultSettings = new KurrentClientSerializationSettings(); + + // When + var result = defaultSerializer.With(defaultSettings, null); + + // Then + Assert.Same(defaultSerializer, result); + } + + [Fact] + public void With_DisabledAutomaticDeserialization_ReturnsNullSerializer() { + // Given + var defaultSerializer = new DummyMessageSerializer(); + var defaultSettings = new KurrentClientSerializationSettings(); + var operationSettings = OperationSerializationSettings.Disabled; + + // When + var result = defaultSerializer.With(defaultSettings, operationSettings); + + // Then + Assert.Same(NullMessageSerializer.Instance, result); + } + + [Fact] + public void With_NoConfigureSettings_ReturnsDefaultSerializer() { + // Given + var defaultSerializer = new DummyMessageSerializer(); + var defaultSettings = new KurrentClientSerializationSettings(); + var operationSettings = new OperationSerializationSettings(); // Default-enabled with no config + + // When + var result = defaultSerializer.With(defaultSettings, operationSettings); + + // Then + Assert.Same(defaultSerializer, result); + } + + [Fact] + public void With_ConfigureSettings_CreatesNewMessageSerializer() { + // Given + var defaultSerializer = new DummyMessageSerializer(); + var defaultSettings = KurrentClientSerializationSettings.Default(); + + var operationSettings = OperationSerializationSettings.Configure( + s => + s.RegisterMessageType("CustomMessageName") + ); + + // When + var result = defaultSerializer.With(defaultSettings, operationSettings); + + // Then + Assert.NotSame(defaultSerializer, result); + Assert.IsType(result); + } + + [Fact] + public void Serialize_WithMultipleMessages_ReturnsArrayOfEventData() { + // Given + var serializer = new DummyMessageSerializer(); + var messages = new List { + Message.From(new object()), + Message.From(new object()), + Message.From(new object()) + }; + + var context = new MessageSerializationContext("test-stream", ContentType.Json); + + // When + var result = serializer.Serialize(messages, context); + + // Then + Assert.Equal(3, result.Length); + Assert.All(result, eventData => Assert.Equal("TestEvent", eventData.Type)); + } + + class DummyMessageSerializer : IMessageSerializer { + public EventData Serialize(Message value, MessageSerializationContext context) { + return new EventData( + Uuid.NewUuid(), + "TestEvent", + ReadOnlyMemory.Empty, + ReadOnlyMemory.Empty, + "application/json" + ); + } + +#if NET48 + public bool TryDeserialize(EventRecord record, out Message? deserialized) { +#else + public bool TryDeserialize(EventRecord record, [NotNullWhen(true)] out Message? deserialized) { +#endif + deserialized = null; + return false; + } + } + + public record UserRegistered(string UserId, string Email); +} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializerTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializerTests.cs new file mode 100644 index 000000000..fe57dab8b --- /dev/null +++ b/test/Kurrent.Client.Tests/Core/Serialization/MessageSerializerTests.cs @@ -0,0 +1,264 @@ +using EventStore.Client; +using Kurrent.Client.Core.Serialization; + +namespace Kurrent.Client.Tests.Core.Serialization; + +public class MessageSerializerTests { + [Fact] + public void Serialize_WithValidMessage_ReturnsEventData() { + // Given + var settings = CreateTestSettings(); + var schemaRegistry = SchemaRegistry.From(settings); + var serializer = new MessageSerializer(schemaRegistry); + + var data = new UserRegistered("user-123", "user@random-email.com"); + var metadata = new TestMetadata(); + var messageId = Uuid.NewUuid(); + var message = Message.From(data, metadata, messageId); + + var context = new MessageSerializationContext("user-123", ContentType.Json); + + // When + var eventData = serializer.Serialize(message, context); + + // Then + Assert.Equal(messageId, eventData.EventId); + Assert.Equal("UserRegistered", eventData.Type); + Assert.NotEmpty(eventData.Data.Span.ToArray()); + Assert.NotEmpty(eventData.Metadata.Span.ToArray()); + Assert.Equal(ContentType.Json.ToMessageContentType(), eventData.ContentType); + } + + [Fact] + public void Serialize_WithAutoGeneratedId_GeneratesNewId() { + // Given + var settings = CreateTestSettings(); + var schemaRegistry = SchemaRegistry.From(settings); + var serializer = new MessageSerializer(schemaRegistry); + + var data = new UserRegistered("user-123", "user@random-email.com"); + var message = Message.From(data); // No ID provided + + var context = new MessageSerializationContext("user-123", ContentType.Json); + + // When + var eventData = serializer.Serialize(message, context); + + // Then + Assert.NotEqual(Uuid.Empty, eventData.EventId); + } + + [Fact] + public void Serialize_WithoutMetadata_GeneratesEmptyMetadata() { + // Given + var settings = CreateTestSettings(); + var schemaRegistry = SchemaRegistry.From(settings); + var serializer = new MessageSerializer(schemaRegistry); + + var data = new UserRegistered("user-123", "user@random-email.com"); + var message = Message.From(data); + + var context = new MessageSerializationContext("user-123", ContentType.Json); + + // When + var eventData = serializer.Serialize(message, context); + + // Then + Assert.Empty(eventData.Metadata.Span.ToArray()); + } + + [Fact] + public void Serialize_WithBinaryContentType_UsesBinarySerializer() { + // Given + var settings = CreateTestSettings(); + var schemaRegistry = SchemaRegistry.From(settings); + var serializer = new MessageSerializer(schemaRegistry); + + var data = new UserRegistered("user-123", "user@random-email.com"); + var message = Message.From(data); + + var context = new MessageSerializationContext("user-123", ContentType.Bytes); + + // When + var eventData = serializer.Serialize(message, context); + + // Then + Assert.Equal(ContentType.Bytes.ToMessageContentType(), eventData.ContentType); + } + + [Fact] + public void SerializeMultiple_WithValidMessages_ReturnsEventDataArray() { + // Given + var settings = CreateTestSettings(); + var schemaRegistry = SchemaRegistry.From(settings); + var serializer = new MessageSerializer(schemaRegistry); + + var messages = new[] { + Message.From(new UserRegistered("1", "u1@random-email.com")), + Message.From(new UserRegistered("2", "u2@random-email.com")), + Message.From(new UserRegistered("3", "u3@random-email.com")) + }; + + var context = new MessageSerializationContext("user-123", ContentType.Json); + + // When + var eventDataArray = serializer.Serialize(messages, context); + + // Then + Assert.Equal(3, eventDataArray.Length); + Assert.All(eventDataArray, data => Assert.Equal("UserRegistered", data.Type)); + } + + [Fact] + public void TryDeserialize_WithValidEventRecord_DeserializesSuccessfully() { + // Given + var settings = CreateTestSettings(); + var schemaRegistry = SchemaRegistry.From(settings); + var serializer = new MessageSerializer(schemaRegistry); + + var testEvent = new UserRegistered("user-123", "user@random-email.com"); + var testMetadata = new TestMetadata { CorrelationId = "corr-123", UserId = "user-456" }; + var eventId = Uuid.NewUuid(); + + var eventRecord = CreateTestEventRecord("UserRegistered", testEvent, testMetadata, eventId); + + // When + var success = serializer.TryDeserialize(eventRecord, out var message); + + // Then + Assert.True(success); + Assert.NotNull(message); + Assert.Equal(eventId, message.MessageId); + + var deserializedEvent = Assert.IsType(message.Data); + Assert.Equal("user-123", deserializedEvent.UserId); + Assert.Equal("user@random-email.com", deserializedEvent.Email); + + var deserializedMetadata = Assert.IsType(message.Metadata); + Assert.Equal("corr-123", deserializedMetadata.CorrelationId); + Assert.Equal("user-456", deserializedMetadata.UserId); + } + + [Fact] + public void TryDeserialize_WithUnknownEventType_ReturnsFalse() { + // Given + var settings = CreateTestSettings(); + var schemaRegistry = SchemaRegistry.From(settings); + var serializer = new MessageSerializer(schemaRegistry); + + var testEvent = new UserRegistered("user-123", "user@random-email.com"); + var eventRecord = CreateTestEventRecord("UnknownEventType", testEvent); + + // When + var success = serializer.TryDeserialize(eventRecord, out var message); + + // Then + Assert.False(success); + Assert.Null(message); + } + + [Fact] + public void TryDeserialize_WithNullData_ReturnsFalse() { + // Given + var settings = CreateTestSettings(); + var schemaRegistry = SchemaRegistry.From(settings); + var serializer = new MessageSerializer(schemaRegistry); + var eventRecord = CreateTestEventRecord("UserRegistered", null); + + // When + var success = serializer.TryDeserialize(eventRecord, out var message); + + // Then + Assert.False(success); + Assert.Null(message); + } + + [Fact] + public void TryDeserialize_WithEmptyMetadata_DeserializesWithNullMetadata() { + // Given + var settings = CreateTestSettings(); + var schemaRegistry = SchemaRegistry.From(settings); + var serializer = new MessageSerializer(schemaRegistry); + + var testEvent = new UserRegistered("user-123", "user@random-email.com"); + var eventId = Uuid.NewUuid(); + + var eventRecord = CreateTestEventRecord("UserRegistered", testEvent, null, eventId); + + // When + var success = serializer.TryDeserialize(eventRecord, out var message); + + // Then + Assert.True(success); + Assert.NotNull(message); + Assert.Equal(eventId, message.MessageId); + Assert.Null(message.Metadata); + + var deserializedEvent = Assert.IsType(message.Data); + Assert.Equal("user-123", deserializedEvent.UserId); + Assert.Equal("user@random-email.com", deserializedEvent.Email); + } + + [Fact] + public void MessageSerializer_From_CreatesInstanceWithDefaultSettings() { + // When + var serializer = MessageSerializer.From(); + + // Then - if this doesn't throw, it worked + Assert.NotNull(serializer); + } + + [Fact] + public void MessageSerializer_From_CreatesInstanceWithProvidedSettings() { + // Given + var settings = CreateTestSettings(); + + // When + var serializer = MessageSerializer.From(settings); + + // Then - test it works with our settings + var data = new UserRegistered("user-123", "user@random-email.com"); + var message = Message.From(data); + var context = new MessageSerializationContext("user-123", ContentType.Json); + + var eventData = serializer.Serialize(message, context); + Assert.Equal("UserRegistered", eventData.Type); + } + + static KurrentClientSerializationSettings CreateTestSettings() { + var settings = new KurrentClientSerializationSettings(); + settings.RegisterMessageType("UserRegistered"); + settings.RegisterMessageType("UserAssignedToRole"); + settings.UseMetadataType(); + + return settings; + } + + static EventRecord CreateTestEventRecord( + string eventType, object? data = null, object? metadata = null, Uuid? eventId = null + ) => + new( + Uuid.NewUuid().ToString(), + eventId ?? Uuid.NewUuid(), + StreamPosition.FromInt64(0), + new Position(1, 1), + new Dictionary { + { Constants.Metadata.Type, eventType }, + { Constants.Metadata.Created, DateTime.UtcNow.ToTicksSinceEpoch().ToString() }, + { Constants.Metadata.ContentType, Constants.Metadata.ContentTypes.ApplicationJson } + }, + data != null ? _serializer.Serialize(data) : ReadOnlyMemory.Empty, + metadata != null ? _serializer.Serialize(metadata) : ReadOnlyMemory.Empty + ); + + static readonly SystemTextJsonSerializer _serializer = new SystemTextJsonSerializer(); + + public record UserRegistered(string UserId, string Email); + + public record UserAssignedToRole(string UserId, string Role); + + public class TestMetadata { + public string CorrelationId { get; set; } = "correlation-id"; + public string UserId { get; set; } = "user-id"; + } +} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/MessageTypeRegistryTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/MessageTypeRegistryTests.cs new file mode 100644 index 000000000..a74026ed0 --- /dev/null +++ b/test/Kurrent.Client.Tests/Core/Serialization/MessageTypeRegistryTests.cs @@ -0,0 +1,237 @@ +using Kurrent.Client.Core.Serialization; + +namespace Kurrent.Client.Tests.Core.Serialization; + +public class MessageTypeRegistryTests { + [Fact] + public void Register_StoresTypeAndTypeName() { + // Given + var registry = new MessageTypeRegistry(); + var type = typeof(TestEvent1); + const string typeName = "test-event-1"; + + // When + registry.Register(type, typeName); + + // Then + Assert.Equal(typeName, registry.GetTypeName(type)); + Assert.Equal(type, registry.GetClrType(typeName)); + } + + [Fact] + public void Register_CalledTwiceForTheSameTypeOverridesExistingRegistration() { + // Given + var registry = new MessageTypeRegistry(); + var type = typeof(TestEvent1); + const string originalTypeName = "original-name"; + const string updatedTypeName = "updated-name"; + + // When + registry.Register(type, originalTypeName); + registry.Register(type, updatedTypeName); + + // Then + Assert.Equal(updatedTypeName, registry.GetTypeName(type)); + Assert.Equal(type, registry.GetClrType(updatedTypeName)); + Assert.Equal(type, registry.GetClrType(originalTypeName)); + } + + [Fact] + public void GetTypeName_ReturnsNullForNotRegisteredType() { + // Given + var registry = new MessageTypeRegistry(); + var unregisteredType = typeof(TestEvent2); + + // When + var result = registry.GetTypeName(unregisteredType); + + // Then + Assert.Null(result); + } + + [Fact] + public void GetClrType_ReturnsNullForNotRegisteredTypeName() { + // Given + var registry = new MessageTypeRegistry(); + const string unregisteredTypeName = "unregistered-type"; + + // When + var result = registry.GetClrType(unregisteredTypeName); + + // Then + Assert.Null(result); + } + + [Fact] + public void GetOrAddTypeName_ReturnsExistingTypeName() { + // Given + var registry = new MessageTypeRegistry(); + var type = typeof(TestEvent1); + const string existingTypeName = "existing-type-name"; + + registry.Register(type, existingTypeName); + var typeResolutionCount = 0; + + // When + var result = registry.GetOrAddTypeName( + type, + _ => { + typeResolutionCount++; + return "factory-type-name"; + } + ); + + // Then + Assert.Equal(existingTypeName, result); + Assert.Equal(0, typeResolutionCount); + } + + [Fact] + public void GetOrAddTypeName_ForNotRegisteredTypeNameAddsNewTypeName() { + // Given + var registry = new MessageTypeRegistry(); + var type = typeof(TestEvent1); + const string newTypeName = "new-type-name"; + var typeResolutionCount = 0; + + // When + var result = registry.GetOrAddTypeName( + type, + _ => { + typeResolutionCount++; + return newTypeName; + } + ); + + // Then + Assert.Equal(newTypeName, result); + Assert.Equal(1, typeResolutionCount); + Assert.Equal(newTypeName, registry.GetTypeName(type)); + Assert.Equal(type, registry.GetClrType(newTypeName)); + } + + [Fact] + public void GetOrAddClrType_ReturnsExistingClrType() { + // Given + var registry = new MessageTypeRegistry(); + var type = typeof(TestEvent1); + const string typeName = "test-event-name"; + registry.Register(type, typeName); + var typeResolutionCount = 0; + + // When + var result = registry.GetOrAddClrType( + typeName, + _ => { + typeResolutionCount++; + return typeof(TestEvent2); + } + ); + + // Then + Assert.Equal(type, result); + Assert.Equal(0, typeResolutionCount); + } + + [Fact] + public void GetOrAddClrType_ForNotExistingTypeAddsNewClrType() { + // Given + var registry = new MessageTypeRegistry(); + const string typeName = "test-event-name"; + var type = typeof(TestEvent1); + var typeResolutionCount = 0; + + // When + var result = registry.GetOrAddClrType( + typeName, + _ => { + typeResolutionCount++; + return type; + } + ); + + // Then + Assert.Equal(type, result); + Assert.Equal(1, typeResolutionCount); + Assert.Equal(typeName, registry.GetTypeName(type)); + Assert.Equal(type, registry.GetClrType(typeName)); + } + + [Fact] + public void GetOrAddClrType_HandlesNullReturnFromTypeResolution() { + // Given + var registry = new MessageTypeRegistry(); + const string typeName = "unknown-type-name"; + + // When + var result = registry.GetOrAddClrType(typeName, _ => null); + + // Then + Assert.Null(result); + Assert.Null(registry.GetClrType(typeName)); + } + + + [Fact] + public void RegisterGeneric_RegistersTypeWithTypeName() { + // Given + var registry = new MessageTypeRegistry(); + const string typeName = "test-event-1"; + + // When + registry.Register(typeName); + + // Then + Assert.Equal(typeName, registry.GetTypeName(typeof(TestEvent1))); + Assert.Equal(typeof(TestEvent1), registry.GetClrType(typeName)); + } + + [Fact] + public void RegisterDictionary_RegistersMultipleTypes() { + // Given + var registry = new MessageTypeRegistry(); + var typeMap = new Dictionary { + { typeof(TestEvent1), "test-event-1" }, + { typeof(TestEvent2), "test-event-2" } + }; + + // When + registry.Register(typeMap); + + // Then + Assert.Equal("test-event-1", registry.GetTypeName(typeof(TestEvent1))); + Assert.Equal("test-event-2", registry.GetTypeName(typeof(TestEvent2))); + Assert.Equal(typeof(TestEvent1), registry.GetClrType("test-event-1")); + Assert.Equal(typeof(TestEvent2), registry.GetClrType("test-event-2")); + } + + [Fact] + public void GetTypeNameGeneric_ReturnsTypeName() { + // Given + var registry = new MessageTypeRegistry(); + const string typeName = "test-event-1"; + registry.Register(typeName); + + // When + var result = registry.GetTypeName(); + + // Then + Assert.Equal(typeName, result); + } + + [Fact] + public void GetTypeNameGeneric_ReturnsNullForUnregisteredType() { + // Given + var registry = new MessageTypeRegistry(); + + // When + var result = registry.GetTypeName(); + + // Then + Assert.Null(result); + } + + record TestEvent1; + + record TestEvent2; +} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/NullMessageSerializerTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/NullMessageSerializerTests.cs new file mode 100644 index 000000000..814440f38 --- /dev/null +++ b/test/Kurrent.Client.Tests/Core/Serialization/NullMessageSerializerTests.cs @@ -0,0 +1,47 @@ +using System.Text; +using EventStore.Client; +using Kurrent.Client.Core.Serialization; + +namespace Kurrent.Client.Tests.Core.Serialization; + +public class NullMessageSerializerTests { + [Fact] + public void Serialize_ThrowsException() { + // Given + var serializer = NullMessageSerializer.Instance; + var message = Message.From(new object()); + var context = new MessageSerializationContext("test-stream", ContentType.Json); + + // When & Assert + Assert.Throws(() => serializer.Serialize(message, context)); + } + + [Fact] + public void TryDeserialize_ReturnsFalse() { + // Given + var serializer = NullMessageSerializer.Instance; + var eventRecord = CreateTestEventRecord(); + + // When + var result = serializer.TryDeserialize(eventRecord, out var message); + + // Then + Assert.False(result); + Assert.Null(message); + } + + static EventRecord CreateTestEventRecord() => + new( + Uuid.NewUuid().ToString(), + Uuid.NewUuid(), + StreamPosition.FromInt64(0), + new Position(1, 1), + new Dictionary { + { Constants.Metadata.Type, "test-event" }, + { Constants.Metadata.Created, DateTime.UtcNow.ToTicksSinceEpoch().ToString() }, + { Constants.Metadata.ContentType, Constants.Metadata.ContentTypes.ApplicationJson } + }, + """{"x":1}"""u8.ToArray(), + """{"x":2}"""u8.ToArray() + ); +} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/SchemaRegistryTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/SchemaRegistryTests.cs new file mode 100644 index 000000000..084f6ffd7 --- /dev/null +++ b/test/Kurrent.Client.Tests/Core/Serialization/SchemaRegistryTests.cs @@ -0,0 +1,257 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using EventStore.Client; +using Kurrent.Client.Core.Serialization; + +namespace Kurrent.Client.Tests.Core.Serialization; + +public class SchemaRegistryTests { + // Test classes + record TestEvent1; + + record TestEvent2; + + record TestEvent3; + + record TestMetadata; + + [Fact] + public void Constructor_InitializesProperties() { + // Given + var serializers = new Dictionary { + { ContentType.Json, new SystemTextJsonSerializer() }, + { ContentType.Bytes, new SystemTextJsonSerializer() } + }; + + var namingStrategy = new DefaultMessageTypeNamingStrategy(typeof(TestMetadata)); + + // When + var registry = new SchemaRegistry(serializers, namingStrategy); + + // Then + Assert.Same(namingStrategy, registry.MessageTypeNamingStrategy); + } + + [Fact] + public void GetSerializer_ReturnsCorrectSerializer() { + // Given + var jsonSerializer = new SystemTextJsonSerializer(); + var bytesSerializer = new SystemTextJsonSerializer(); + + var serializers = new Dictionary { + { ContentType.Json, jsonSerializer }, + { ContentType.Bytes, bytesSerializer } + }; + + var registry = new SchemaRegistry( + serializers, + new DefaultMessageTypeNamingStrategy(typeof(TestMetadata)) + ); + + // When + var resultJsonSerializer = registry.GetSerializer(ContentType.Json); + var resultBytesSerializer = registry.GetSerializer(ContentType.Bytes); + + // Then + Assert.NotSame(resultJsonSerializer, resultBytesSerializer); + Assert.Same(jsonSerializer, resultJsonSerializer); + Assert.Same(bytesSerializer, resultBytesSerializer); + } + + [Fact] + public void From_WithDefaultSettings_CreatesRegistryWithDefaults() { + // Given + var settings = new KurrentClientSerializationSettings(); + + // When + var registry = SchemaRegistry.From(settings); + + // Then + Assert.NotNull(registry); + Assert.NotNull(registry.MessageTypeNamingStrategy); + Assert.NotNull(registry.GetSerializer(ContentType.Json)); + Assert.NotNull(registry.GetSerializer(ContentType.Bytes)); + + Assert.IsType(registry.MessageTypeNamingStrategy); + Assert.IsType(registry.GetSerializer(ContentType.Json)); + Assert.IsType(registry.GetSerializer(ContentType.Bytes)); + } + + [Fact] + public void From_WithCustomJsonSerializer_UsesProvidedSerializer() { + // Given + var customJsonSerializer = new SystemTextJsonSerializer( + new SystemTextJsonSerializationSettings { + Options = new JsonSerializerOptions { WriteIndented = true } + } + ); + + var settings = new KurrentClientSerializationSettings() + .UseJsonSerializer(customJsonSerializer); + + // When + var registry = SchemaRegistry.From(settings); + + // Then + Assert.Same(customJsonSerializer, registry.GetSerializer(ContentType.Json)); + Assert.NotSame(customJsonSerializer, registry.GetSerializer(ContentType.Bytes)); + } + + [Fact] + public void From_WithCustomBytesSerializer_UsesProvidedSerializer() { + // Given + var customBytesSerializer = new SystemTextJsonSerializer( + new SystemTextJsonSerializationSettings { + Options = new JsonSerializerOptions { WriteIndented = true } + } + ); + + var settings = new KurrentClientSerializationSettings() + .UseBytesSerializer(customBytesSerializer); + + // When + var registry = SchemaRegistry.From(settings); + + // Then + Assert.Same(customBytesSerializer, registry.GetSerializer(ContentType.Bytes)); + Assert.NotSame(customBytesSerializer, registry.GetSerializer(ContentType.Json)); + } + + [Fact] + public void From_WithMessageTypeMap_RegistersTypes() { + // Given + var settings = new KurrentClientSerializationSettings(); + settings.RegisterMessageType("test-event-1"); + settings.RegisterMessageType("test-event-2"); + + // When + var registry = SchemaRegistry.From(settings); + var namingStrategy = registry.MessageTypeNamingStrategy; + + // Then + // Verify types can be resolved + Assert.True(namingStrategy.TryResolveClrType("test-event-1", out var type1)); + Assert.Equal(typeof(TestEvent1), type1); + + Assert.True(namingStrategy.TryResolveClrType("test-event-2", out var type2)); + Assert.Equal(typeof(TestEvent2), type2); + } + + [Fact] + public void From_WithCategoryMessageTypesMap_RegistersTypesWithCategories() { + // Given + var settings = new KurrentClientSerializationSettings(); + settings.RegisterMessageTypeForCategory("category1"); + settings.RegisterMessageTypeForCategory("category1"); + settings.RegisterMessageTypeForCategory("category2"); + + // When + var registry = SchemaRegistry.From(settings); + var namingStrategy = registry.MessageTypeNamingStrategy; + + // Then + // For categories, the naming strategy should have resolved the type names + // using the ResolveTypeName method, which by default uses the type's name + string typeName1 = namingStrategy.ResolveTypeName( + typeof(TestEvent1), + new MessageTypeNamingResolutionContext("category1") + ); + + string typeName2 = namingStrategy.ResolveTypeName( + typeof(TestEvent2), + new MessageTypeNamingResolutionContext("category1") + ); + + string typeName3 = namingStrategy.ResolveTypeName( + typeof(TestEvent3), + new MessageTypeNamingResolutionContext("category2") + ); + + // Verify types can be resolved by the type names + Assert.True(namingStrategy.TryResolveClrType(typeName1, out var resolvedType1)); + Assert.Equal(typeof(TestEvent1), resolvedType1); + + Assert.True(namingStrategy.TryResolveClrType(typeName2, out var resolvedType2)); + Assert.Equal(typeof(TestEvent2), resolvedType2); + + Assert.True(namingStrategy.TryResolveClrType(typeName3, out var resolvedType3)); + Assert.Equal(typeof(TestEvent3), resolvedType3); + } + + [Fact] + public void From_WithCustomNamingStrategy_UsesProvidedStrategy() { + // Given + var customNamingStrategy = new TestNamingStrategy(); + var settings = new KurrentClientSerializationSettings() + .UseMessageTypeNamingStrategy(customNamingStrategy); + + // When + var registry = SchemaRegistry.From(settings); + + // Then + // The registry wraps the naming strategy, but should still use it + var wrappedStrategy = registry.MessageTypeNamingStrategy; + Assert.IsType(wrappedStrategy); + + // Test to make sure it behaves like our custom strategy + string typeName = wrappedStrategy.ResolveTypeName( + typeof(TestEvent1), + new MessageTypeNamingResolutionContext("test") + ); + + // Our test strategy adds "Custom-" prefix + Assert.StartsWith("Custom-", typeName); + } + + [Fact] + public void From_WithNoMessageTypeNamingStrategy_UsesDefaultStrategy() { + // Given + var settings = new KurrentClientSerializationSettings { + MessageTypeNamingStrategy = null, + DefaultMetadataType = typeof(TestMetadata) + }; + + // When + var registry = SchemaRegistry.From(settings); + + // Then + Assert.NotNull(registry.MessageTypeNamingStrategy); + + // The wrapped default strategy should use our metadata type + Assert.True( + registry.MessageTypeNamingStrategy.TryResolveClrMetadataType("some-type", out var defaultMetadataType) + ); + + Assert.Equal(typeof(TestMetadata), defaultMetadataType); + } + + // Custom naming strategy for testing + class TestNamingStrategy : IMessageTypeNamingStrategy { + public string ResolveTypeName(Type type, MessageTypeNamingResolutionContext context) { + return $"Custom-{type.Name}-{context.CategoryName}"; + } +#if NET48 + public bool TryResolveClrType(string messageTypeName, out Type? clrType) +#else + public bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? clrType) +#endif + { + // Simple implementation for testing + clrType = messageTypeName.StartsWith("Custom-TestEvent1") + ? typeof(TestEvent1) + : null; + + return clrType != null; + } + +#if NET48 + public bool TryResolveClrMetadataType(string messageTypeName, out Type? clrType) +#else + public bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? clrType) +#endif + { + clrType = typeof(TestMetadata); + return true; + } + } +} diff --git a/test/Kurrent.Client.Tests/Core/Serialization/TypeProviderTests.cs b/test/Kurrent.Client.Tests/Core/Serialization/TypeProviderTests.cs new file mode 100644 index 000000000..1c49ff102 --- /dev/null +++ b/test/Kurrent.Client.Tests/Core/Serialization/TypeProviderTests.cs @@ -0,0 +1,184 @@ +using System.Reflection; +using Kurrent.Client.Core.Serialization; + +namespace Kurrent.Client.Tests.Core.Serialization { + public record TestNestedNamespaceEvent; + + namespace Nested { + public record TestNestedNamespaceEvent; + } + + public class TypeProviderTests { + [Fact] + public void GetTypeByFullName_WorksWithLoadedAssemblyTypes() { + // Given + string enumFullName = typeof(TestEvent).FullName!; + + // When + var result = TypeProvider.GetTypeByFullName(enumFullName); + + // Then + Assert.NotNull(result); + Assert.Equal(typeof(TestEvent), result); + } + + [Fact] + public void GetTypeByFullName_ReturnsNull_WhenTypeDoesNotExist() { + // Given + const string fullName = "NonExistent.Type.That.Should.Not.Exist"; + + // When + var result = TypeProvider.GetTypeByFullName(fullName); + + // Then + Assert.Null(result); + } + + [Fact] + public void GetTypeByFullName_ForNestedNamespacesSortsAssembliesByNamespace_AndReturnsFirstMatch() { + // Given + string originalType = typeof(TestNestedNamespaceEvent).FullName!; + string nestedType = typeof(Kurrent.Client.Tests.Core.Serialization.Nested.TestNestedNamespaceEvent).FullName!; + + // When + var originalTypeResult = TypeProvider.GetTypeByFullName(originalType); + var nestedTypeResult = TypeProvider.GetTypeByFullName(nestedType); + + // Then + Assert.NotNull(originalTypeResult); + Assert.NotNull(nestedTypeResult); + Assert.NotEqual(originalTypeResult, nestedTypeResult); + } + + [Fact] + public void GetTypeByFullName_ForNestedClassesSortsAssembliesByNamespace_AndReturnsFirstMatch() { + // Given + string originalType = typeof(TestNestedInClassEvent).FullName!; + string nestedType = typeof(Nested.TestNestedInClassEvent).FullName!; + + // When + var originalTypeResult = TypeProvider.GetTypeByFullName(originalType); + var nestedTypeResult = TypeProvider.GetTypeByFullName(nestedType); + + // Then + Assert.NotNull(originalTypeResult); + Assert.NotNull(nestedTypeResult); + Assert.NotEqual(originalTypeResult, nestedTypeResult); + } + + [Fact] + public void GetTypeByFullName_HandlesGenericTypes() { + // Given + string fullName = typeof(GenericEvent).FullName!; + + // When + var result = TypeProvider.GetTypeByFullName(fullName); + + // Then + Assert.NotNull(result); + Assert.Equal(typeof(GenericEvent), result); + } + + [Fact] + public void GetTypeByFullName_ReturnsType_WhenTypeExistsInSystemLib() { + // Given + string fullName = typeof(string).FullName!; + + // When + var result = TypeProvider.GetTypeByFullName(fullName); + + // Then + Assert.NotNull(result); + Assert.Equal(typeof(string), result); + } + + public record TestEvent; + + public record TestNestedInClassEvent; + + public class Nested { + public record TestNestedInClassEvent; + } + + /// + /// Generic external event class to test generic type resolution + /// + /// The payload type + public class GenericEvent { + public string Id { get; set; } = null!; + public T Data { get; set; } = default!; + } + } + + /// + /// Tests for TypeProvider focusing on unloaded assemblies + /// Uses Kurrent.Client.Tests.NeverLoadedAssembly project which should never be loaded before this test runs + /// + public class UnloadedAssemblyTests { + const string NotLoadedTypeFullName = "Kurrent.Client.Tests.NeverLoadedAssembly.NotLoadedExternalEvent"; + + [Fact] + public void GetTypeByFullName_ReturnsNull_ForTypeInUnloadedAssembly() { + // When + var result = TypeProvider.GetTypeByFullName(NotLoadedTypeFullName); + + // Then + Assert.Null(result); + } + } + + /// + /// Tests for TypeProvider focusing on loaded assemblies + /// Uses Kurrent.Client.Tests.ExternalAssembly.dll which will be explicitly loaded during the tests + /// + public class LoadedAssemblyTests { + const string ExternalAssemblyName = "Kurrent.Client.Tests.ExternalAssembly"; + const string ExternalTypeFullName = "Kurrent.Client.Tests.ExternalAssembly.ExternalEvent"; + readonly Assembly _externalAssembly; + + public LoadedAssemblyTests() { + var path = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, + $"{ExternalAssemblyName}.dll" + ); + + _externalAssembly = Assembly.LoadFrom(path); + } + + [Fact] + public void GetTypeByFullName_FindsType_AfterAssemblyIsExplicitlyLoaded() { + // Given + + // When + var externalType = TypeProvider.GetTypeByFullName(ExternalTypeFullName); + + // Then + Assert.NotNull(externalType); + Assert.Equal(ExternalTypeFullName, externalType.FullName); + Assert.Equal(_externalAssembly, externalType.Assembly); + } + + [Fact] + public void GetTypeByFullName_PrioritizesAssembliesByNamespacePrefix() { + // This test verifies the namespace-based prioritization by: + // 1. Loading our test assembly which has a name matching its namespace prefix + // 2. Verifying that we can resolve a type from it + + // Given + // When + var result = TypeProvider.GetTypeByFullName(ExternalTypeFullName); + Assert.NotNull(result); + Assert.Equal(ExternalTypeFullName, result.FullName); + Assert.NotEqual(typeof(ExternalEvent), result); + } + + /// + /// External event class used for testing loaded assembly resolution, it should not be resolved, + /// because of prioritising exact namespaces resolutions first + /// + public class ExternalEvent { + public string Id { get; set; } = null!; + public string Name { get; set; } = null!; + } + } +} diff --git a/test/EventStore.Client.Tests/FromAllTests.cs b/test/Kurrent.Client.Tests/FromAllTests.cs similarity index 92% rename from test/EventStore.Client.Tests/FromAllTests.cs rename to test/Kurrent.Client.Tests/FromAllTests.cs index 82b8338ec..5be40e9bf 100644 --- a/test/EventStore.Client.Tests/FromAllTests.cs +++ b/test/Kurrent.Client.Tests/FromAllTests.cs @@ -1,11 +1,13 @@ using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class FromAllTests : ValueObjectTests { public FromAllTests() : base(new ScenarioFixture()) { } - [Fact] + [RetryFact] public void IsComparable() => Assert.IsAssignableFrom>(_fixture.Create()); [Theory] @@ -28,10 +30,10 @@ public FromAllTests() : base(new ScenarioFixture()) { } [MemberData(nameof(ToStringCases))] public void ToStringReturnsExpectedResult(FromAll sut, string expected) => Assert.Equal(expected, sut.ToString()); - [Fact] + [RetryFact] public void AfterLiveThrows() => Assert.Throws(() => FromAll.After(Position.End)); - [Fact] + [RetryFact] public void ToUInt64ReturnsExpectedResults() { var position = _fixture.Create(); Assert.Equal( @@ -46,4 +48,4 @@ public ScenarioFixture() { Customize(composter => composter.FromFactory(FromAll.After)); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/FromStreamTests.cs b/test/Kurrent.Client.Tests/FromStreamTests.cs similarity index 83% rename from test/EventStore.Client.Tests/FromStreamTests.cs rename to test/Kurrent.Client.Tests/FromStreamTests.cs index a293aa44d..9593c8bea 100644 --- a/test/EventStore.Client.Tests/FromStreamTests.cs +++ b/test/Kurrent.Client.Tests/FromStreamTests.cs @@ -1,11 +1,13 @@ using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class FromStreamTests : ValueObjectTests { public FromStreamTests() : base(new ScenarioFixture()) { } - [Fact] + [RetryFact] public void IsComparable() => Assert.IsAssignableFrom>(_fixture.Create()); [Theory] @@ -19,19 +21,19 @@ public FromStreamTests() : base(new ScenarioFixture()) { } public static IEnumerable ToStringCases() { var fixture = new ScenarioFixture(); var position = fixture.Create(); - yield return new object?[] { FromStream.After(position), position.ToString() }; - yield return new object?[] { FromStream.Start, "Start" }; - yield return new object?[] { FromStream.End, "Live" }; + yield return [FromStream.After(position), position.ToString()]; + yield return [FromStream.Start, "Start"]; + yield return [FromStream.End, "Live"]; } [Theory] [MemberData(nameof(ToStringCases))] public void ToStringReturnsExpectedResult(FromStream sut, string expected) => Assert.Equal(expected, sut.ToString()); - [Fact] + [RetryFact] public void AfterLiveThrows() => Assert.Throws(() => FromStream.After(StreamPosition.End)); - [Fact] + [RetryFact] public void ToUInt64ReturnsExpectedResults() { var position = _fixture.Create(); Assert.Equal(position.ToUInt64(), FromStream.After(position).ToUInt64()); @@ -43,4 +45,4 @@ public ScenarioFixture() { Customize(composter => composter.FromFactory(FromStream.After)); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/GossipChannelSelectorTests.cs b/test/Kurrent.Client.Tests/GossipChannelSelectorTests.cs similarity index 91% rename from test/EventStore.Client.Tests/GossipChannelSelectorTests.cs rename to test/Kurrent.Client.Tests/GossipChannelSelectorTests.cs index 5967f7384..1e5afe076 100644 --- a/test/EventStore.Client.Tests/GossipChannelSelectorTests.cs +++ b/test/Kurrent.Client.Tests/GossipChannelSelectorTests.cs @@ -1,10 +1,12 @@ using System.Net; +using EventStore.Client; using Grpc.Core; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class GossipChannelSelectorTests { - [Fact] + [RetryFact] public async Task ExplicitlySettingEndPointChangesChannels() { var firstId = Uuid.NewUuid(); var secondId = Uuid.NewUuid(); @@ -12,7 +14,7 @@ public async Task ExplicitlySettingEndPointChangesChannels() { var firstSelection = new DnsEndPoint(firstId.ToString(), 2113); var secondSelection = new DnsEndPoint(secondId.ToString(), 2113); - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { ConnectivitySettings = { DnsGossipSeeds = new[] { firstSelection, @@ -53,9 +55,9 @@ public async Task ExplicitlySettingEndPointChangesChannels() { Assert.Equal($"{secondSelection.Host}:{secondSelection.Port}", channel.Target); } - [Fact] + [RetryFact] public async Task ThrowsWhenDiscoveryFails() { - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { ConnectivitySettings = { IpGossipSeeds = new[] { new IPEndPoint(IPAddress.Loopback, 2113) @@ -93,4 +95,4 @@ CancellationToken cancellationToken ) => throw new NotSupportedException(); } -} \ No newline at end of file +} diff --git a/test/Kurrent.Client.Tests/GrpcServerCapabilitiesClientTests.cs b/test/Kurrent.Client.Tests/GrpcServerCapabilitiesClientTests.cs new file mode 100644 index 000000000..1bb44dc60 --- /dev/null +++ b/test/Kurrent.Client.Tests/GrpcServerCapabilitiesClientTests.cs @@ -0,0 +1,102 @@ +// #if NET +// using System.Net; +// using Kurrent.Client.ServerFeatures; +// using Grpc.Core; +// using Microsoft.AspNetCore.Builder; +// using Microsoft.AspNetCore.Hosting; +// using Microsoft.AspNetCore.TestHost; +// using Microsoft.Extensions.DependencyInjection; +// +// namespace Kurrent.Client.Tests; +// +// public class GrpcServerCapabilitiesClientTests { +// public static IEnumerable ExpectedResultsCases() { +// yield return new object?[] { new SupportedMethods(), new ServerCapabilities() }; +// yield return new object?[] { +// new SupportedMethods { +// Methods = { +// new SupportedMethod { +// ServiceName = "event_store.client.streams.streams", +// MethodName = "batchappend" +// } +// } +// }, +// new ServerCapabilities(true) +// }; +// +// yield return new object?[] { +// new SupportedMethods { +// Methods = { +// new SupportedMethod { +// ServiceName = "event_store.client.persistent_subscriptions.persistentsubscriptions", +// MethodName = "read", +// Features = { +// "all" +// } +// } +// } +// }, +// new ServerCapabilities(SupportsPersistentSubscriptionsToAll: true) +// }; +// +// yield return new object?[] { +// new SupportedMethods { +// Methods = { +// new SupportedMethod { +// ServiceName = "event_store.client.persistent_subscriptions.persistentsubscriptions", +// MethodName = "read" +// } +// } +// }, +// new ServerCapabilities() +// }; +// } +// +// [Theory] +// [MemberData(nameof(ExpectedResultsCases))] +// internal async Task GetAsyncReturnsExpectedResults( +// SupportedMethods supportedMethods, +// ServerCapabilities expected +// ) { +// using var kestrel = new TestServer( +// new WebHostBuilder() +// .ConfigureServices( +// services => services +// .AddRouting() +// .AddGrpc().Services +// .AddSingleton(new FakeServerFeatures(supportedMethods)) +// ) +// .Configure( +// app => app +// .UseRouting() +// .UseEndpoints(ep => ep.MapGrpcService()) +// ) +// ); +// +// var sut = new GrpcServerCapabilitiesClient(new()); +// +// var actual = +// await sut.GetAsync( +// ChannelFactory +// .CreateChannel( +// new() { +// CreateHttpMessageHandler = kestrel.CreateHandler +// }, +// new DnsEndPoint("localhost", 80) +// ) +// .CreateCallInvoker(), +// default +// ); +// +// Assert.Equal(expected, actual); +// } +// +// class FakeServerFeatures : ServerFeatures.ServerFeatures.ServerFeaturesBase { +// readonly SupportedMethods _supportedMethods; +// +// public FakeServerFeatures(SupportedMethods supportedMethods) => _supportedMethods = supportedMethods; +// +// public override Task GetSupportedMethods(Empty request, ServerCallContext context) => Task.FromResult(_supportedMethods); +// } +// } +// #endif diff --git a/test/EventStore.Client.UserManagement.Tests/InvalidCredentialsTestCases.cs b/test/Kurrent.Client.Tests/InvalidCredentialsTestCases.cs similarity index 57% rename from test/EventStore.Client.UserManagement.Tests/InvalidCredentialsTestCases.cs rename to test/Kurrent.Client.Tests/InvalidCredentialsTestCases.cs index e32a340bd..b60499693 100644 --- a/test/EventStore.Client.UserManagement.Tests/InvalidCredentialsTestCases.cs +++ b/test/Kurrent.Client.Tests/InvalidCredentialsTestCases.cs @@ -1,6 +1,7 @@ using System.Collections; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; public abstract record InvalidCredentialsTestCase(TestUser User, Type ExpectedException); @@ -13,18 +14,15 @@ public class InvalidCredentialsTestCases : IEnumerable { IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public record MissingCredentials() - : InvalidCredentialsTestCase(Fakers.Users.WithNoCredentials(), typeof(AccessDeniedException)) { + public record MissingCredentials() : InvalidCredentialsTestCase(Fakers.Users.WithNoCredentials(), typeof(AccessDeniedException)) { public override string ToString() => nameof(MissingCredentials); } - public record WrongUsername() - : InvalidCredentialsTestCase(Fakers.Users.WithInvalidCredentials(false), typeof(NotAuthenticatedException)) { + public record WrongUsername() : InvalidCredentialsTestCase(Fakers.Users.WithInvalidCredentials(false), typeof(NotAuthenticatedException)) { public override string ToString() => nameof(WrongUsername); } - public record WrongPassword() - : InvalidCredentialsTestCase(Fakers.Users.WithInvalidCredentials(wrongPassword: false), typeof(NotAuthenticatedException)) { + public record WrongPassword() : InvalidCredentialsTestCase(Fakers.Users.WithInvalidCredentials(wrongPassword: false), typeof(NotAuthenticatedException)) { public override string ToString() => nameof(WrongPassword); } -} \ No newline at end of file +} diff --git a/test/Kurrent.Client.Tests/Kurrent.Client.Tests.csproj b/test/Kurrent.Client.Tests/Kurrent.Client.Tests.csproj new file mode 100644 index 000000000..9bb11a881 --- /dev/null +++ b/test/Kurrent.Client.Tests/Kurrent.Client.Tests.csproj @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + diff --git a/test/Kurrent.Client.Tests/KurrentClientOperationsTests.cs b/test/Kurrent.Client.Tests/KurrentClientOperationsTests.cs new file mode 100644 index 000000000..c8b70bdce --- /dev/null +++ b/test/Kurrent.Client.Tests/KurrentClientOperationsTests.cs @@ -0,0 +1,17 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Misc")] +public class KurrentClientOperationOptionsTests { + [RetryFact] + public void setting_options_on_clone_should_not_modify_original() { + var options = KurrentClientOperationOptions.Default; + + var clonedOptions = options.Clone(); + clonedOptions.BatchAppendSize = int.MaxValue; + + Assert.Equal(options.BatchAppendSize, KurrentClientOperationOptions.Default.BatchAppendSize); + Assert.Equal(int.MaxValue, clonedOptions.BatchAppendSize); + } +} diff --git a/test/EventStore.Client.Tests/NodePreferenceComparerTests.cs b/test/Kurrent.Client.Tests/NodePreferenceComparerTests.cs similarity index 96% rename from test/EventStore.Client.Tests/NodePreferenceComparerTests.cs rename to test/Kurrent.Client.Tests/NodePreferenceComparerTests.cs index 1866e3444..cf42ded5e 100644 --- a/test/EventStore.Client.Tests/NodePreferenceComparerTests.cs +++ b/test/Kurrent.Client.Tests/NodePreferenceComparerTests.cs @@ -1,7 +1,9 @@ +using EventStore.Client; using static EventStore.Client.ClusterMessages.VNodeState; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class NodePreferenceComparerTests { static ClusterMessages.VNodeState RunTest(IComparer sut, params ClusterMessages.VNodeState[] states) => states @@ -56,4 +58,4 @@ internal void ReadOnlyReplicaTests(ClusterMessages.VNodeState expected, params C Assert.Equal(expected, actual); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/NodeSelectorTests.cs b/test/Kurrent.Client.Tests/NodeSelectorTests.cs similarity index 92% rename from test/EventStore.Client.Tests/NodeSelectorTests.cs rename to test/Kurrent.Client.Tests/NodeSelectorTests.cs index 9815305cd..b2b43ea2b 100644 --- a/test/EventStore.Client.Tests/NodeSelectorTests.cs +++ b/test/Kurrent.Client.Tests/NodeSelectorTests.cs @@ -1,7 +1,9 @@ using System.Net; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class NodeSelectorTests { static readonly ClusterMessages.VNodeState[] NotAllowedStates = { ClusterMessages.VNodeState.Manager, @@ -26,7 +28,7 @@ public class NodeSelectorTests { var notAllowedNodeId = Uuid.NewUuid(); var notAllowedNode = new DnsEndPoint(notAllowedNodeId.ToString(), 2114); - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { ConnectivitySettings = { DnsGossipSeeds = new[] { allowedNode, notAllowedNode }, Insecure = true @@ -50,7 +52,7 @@ public class NodeSelectorTests { [MemberData(nameof(InvalidStatesCases))] internal void InvalidStatesAreNotConsidered( ClusterMessages.ClusterInfo clusterInfo, - EventStoreClientSettings settings, + KurrentClientSettings settings, DnsEndPoint allowedNode ) { var sut = new NodeSelector(settings); @@ -60,7 +62,7 @@ DnsEndPoint allowedNode Assert.Equal(allowedNode.Port, selectedNode.Port); } - [Fact] + [RetryFact] public void DeadNodesAreNotConsidered() { var allowedNodeId = Uuid.NewUuid(); var allowedNode = new DnsEndPoint(allowedNodeId.ToString(), 2113); @@ -68,7 +70,7 @@ public void DeadNodesAreNotConsidered() { var notAllowedNodeId = Uuid.NewUuid(); var notAllowedNode = new DnsEndPoint(notAllowedNodeId.ToString(), 2114); - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { ConnectivitySettings = { DnsGossipSeeds = new[] { allowedNode, notAllowedNode }, Insecure = true @@ -96,7 +98,7 @@ public void DeadNodesAreNotConsidered() { [InlineData(NodePreference.ReadOnlyReplica, "readOnlyReplica")] [InlineData(NodePreference.Random, "any")] public void CanPrefer(NodePreference nodePreference, string expectedHost) { - var settings = new EventStoreClientSettings { + var settings = new KurrentClientSettings { ConnectivitySettings = { NodePreference = nodePreference } @@ -119,4 +121,4 @@ public void CanPrefer(NodePreference nodePreference, string expectedHost) { Assert.Equal(expectedHost, selectedNode.Host); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Operations.Tests/AuthenticationTests.cs b/test/Kurrent.Client.Tests/Operations/AuthenticationTests.cs similarity index 62% rename from test/EventStore.Client.Operations.Tests/AuthenticationTests.cs rename to test/Kurrent.Client.Tests/Operations/AuthenticationTests.cs index 084a92989..9a1d4dee2 100644 --- a/test/EventStore.Client.Operations.Tests/AuthenticationTests.cs +++ b/test/Kurrent.Client.Tests/Operations/AuthenticationTests.cs @@ -1,18 +1,18 @@ -namespace EventStore.Client.Operations.Tests; +using EventStore.Client; -public class AuthenticationTests : IClassFixture { - public AuthenticationTests(ITestOutputHelper output, InsecureClientTestFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); +namespace Kurrent.Client.Tests; - InsecureClientTestFixture Fixture { get; } - +[Trait("Category", "Target:Operations")] +public class AuthenticationTests(ITestOutputHelper output, AuthenticationTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { public enum CredentialsCase { None, TestUser, RootUser } public static IEnumerable InvalidAuthenticationCases() { - yield return new object?[] { 2, CredentialsCase.TestUser, CredentialsCase.None }; - yield return new object?[] { 3, CredentialsCase.None, CredentialsCase.None }; - yield return new object?[] { 4, CredentialsCase.RootUser, CredentialsCase.TestUser }; - yield return new object?[] { 5, CredentialsCase.TestUser, CredentialsCase.TestUser }; - yield return new object?[] { 6, CredentialsCase.None, CredentialsCase.TestUser }; + yield return [2, CredentialsCase.TestUser, CredentialsCase.None]; + yield return [3, CredentialsCase.None, CredentialsCase.None]; + yield return [4, CredentialsCase.RootUser, CredentialsCase.TestUser]; + yield return [5, CredentialsCase.TestUser, CredentialsCase.TestUser]; + yield return [6, CredentialsCase.None, CredentialsCase.TestUser]; } [Theory] @@ -21,10 +21,10 @@ public async Task system_call_with_invalid_credentials(int caseNr, CredentialsCa await ExecuteTest(caseNr, defaultCredentials, actualCredentials, true); public static IEnumerable ValidAuthenticationCases() { - yield return new object?[] { 1, CredentialsCase.RootUser, CredentialsCase.None }; - yield return new object?[] { 7, CredentialsCase.RootUser, CredentialsCase.RootUser }; - yield return new object?[] { 8, CredentialsCase.TestUser, CredentialsCase.RootUser }; - yield return new object?[] { 9, CredentialsCase.None, CredentialsCase.RootUser }; + yield return [1, CredentialsCase.RootUser, CredentialsCase.None]; + yield return [7, CredentialsCase.RootUser, CredentialsCase.RootUser]; + yield return [8, CredentialsCase.TestUser, CredentialsCase.RootUser]; + yield return [9, CredentialsCase.None, CredentialsCase.RootUser]; } [Theory] @@ -34,7 +34,7 @@ public async Task system_call_with_valid_credentials(int caseNr, CredentialsCase async Task ExecuteTest(int caseNr, CredentialsCase defaultCredentials, CredentialsCase actualCredentials, bool shouldThrow) { var testUser = await Fixture.CreateTestUser(); - + var defaultUserCredentials = GetCredentials(defaultCredentials); var actualUserCredentials = GetCredentials(actualCredentials); @@ -43,7 +43,7 @@ async Task ExecuteTest(int caseNr, CredentialsCase defaultCredentials, Credentia settings.DefaultCredentials = defaultUserCredentials; settings.ConnectionName = $"Authentication case #{caseNr} {defaultCredentials}"; - await using var operations = new EventStoreOperationsClient(settings); + await using var operations = new KurrentOperationsClient(settings); if (shouldThrow) await operations @@ -64,4 +64,6 @@ await operations _ => throw new ArgumentOutOfRangeException(nameof(credentialsCase), credentialsCase, null) }; } -} \ No newline at end of file + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/Operations/MergeIndexTests.cs b/test/Kurrent.Client.Tests/Operations/MergeIndexTests.cs new file mode 100644 index 000000000..35112ddee --- /dev/null +++ b/test/Kurrent.Client.Tests/Operations/MergeIndexTests.cs @@ -0,0 +1,21 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Operations")] +public class MergeIndexTests(ITestOutputHelper output, MergeIndexTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task merge_indexes_does_not_throw() => + await Fixture.Operations + .MergeIndexesAsync(userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + + [RetryFact] + public async Task merge_indexes_without_credentials_throws() => + await Fixture.Operations + .MergeIndexesAsync() + .ShouldThrowAsync(); + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/Operations/ResignNodeTests.cs b/test/Kurrent.Client.Tests/Operations/ResignNodeTests.cs new file mode 100644 index 000000000..82bd88252 --- /dev/null +++ b/test/Kurrent.Client.Tests/Operations/ResignNodeTests.cs @@ -0,0 +1,23 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.Operations; + +[Trait("Category", "Target:Operations")] +public class ResignNodeTests(ITestOutputHelper output, ResignNodeTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task resign_node_does_not_throw() => + await Fixture.Operations + .ResignNodeAsync(userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + + [RetryFact] + public async Task resign_node_without_credentials_throws() => + await Fixture.Operations + .ResignNodeAsync() + .ShouldThrowAsync(); + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs b/test/Kurrent.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs new file mode 100644 index 000000000..f414a3529 --- /dev/null +++ b/test/Kurrent.Client.Tests/Operations/RestartPersistentSubscriptionsTests.cs @@ -0,0 +1,22 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Operations; + +[Trait("Category", "Target:Operations")] +public class RestartPersistentSubscriptionsTests(ITestOutputHelper output, RestartPersistentSubscriptionsTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task restart_persistent_subscriptions_does_not_throw() => + await Fixture.Operations + .RestartPersistentSubscriptions(userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + + [RetryFact] + public async Task restart_persistent_subscriptions_without_credentials_throws() => + await Fixture.Operations + .RestartPersistentSubscriptions() + .ShouldThrowAsync(); + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.Operations.Tests/ScavengeTests.cs b/test/Kurrent.Client.Tests/Operations/ScavengeTests.cs similarity index 78% rename from test/EventStore.Client.Operations.Tests/ScavengeTests.cs rename to test/Kurrent.Client.Tests/Operations/ScavengeTests.cs index 35c1b3a1d..039a3b22b 100644 --- a/test/EventStore.Client.Operations.Tests/ScavengeTests.cs +++ b/test/Kurrent.Client.Tests/Operations/ScavengeTests.cs @@ -1,21 +1,20 @@ -namespace EventStore.Client.Operations.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -public class ScavengeTests : IClassFixture { - public class TestFixture() : EventStoreFixture(x => x.WithoutDefaultCredentials().RunInMemory(false)); - - public ScavengeTests(ITestOutputHelper output, TestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); +namespace Kurrent.Client.Tests; - TestFixture Fixture { get; } - - [Fact] +[Trait("Category", "Target:Operations")] +public class ScavengeTests(ITestOutputHelper output, ScavengeTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] public async Task start() { var result = await Fixture.Operations.StartScavengeAsync(userCredentials: TestCredentials.Root); - result.ShouldBe(DatabaseScavengeResult.Started(result.ScavengeId)); + result.ScavengeId.ShouldNotBeNullOrEmpty(); } - [Fact] + [RetryFact] public async Task start_without_credentials_throws() => await Fixture.Operations .StartScavengeAsync() @@ -56,10 +55,10 @@ public async Task stop() { stopResult.ShouldBe(DatabaseScavengeResult.Stopped(startResult.ScavengeId)); } - [Fact] + [RetryFact] public async Task stop_when_no_scavenge_is_running() { var scavengeId = Guid.NewGuid().ToString(); - + var ex = await Fixture.Operations .StopScavengeAsync(scavengeId, userCredentials: TestCredentials.Root) .ShouldThrowAsync(); @@ -67,9 +66,11 @@ public async Task stop_when_no_scavenge_is_running() { // ex.ScavengeId.ShouldBeNull(); // it is blowing up because of this } - [Fact] + [RetryFact] public async Task stop_without_credentials_throws() => await Fixture.Operations .StopScavengeAsync(Guid.NewGuid().ToString()) .ShouldThrowAsync(); -} \ No newline at end of file + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs b/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs new file mode 100644 index 000000000..f88fa32c8 --- /dev/null +++ b/test/Kurrent.Client.Tests/Operations/ShutdownNodeAuthenticationTests.cs @@ -0,0 +1,14 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Operations")] +public class ShutdownNodeAuthenticationTests(ITestOutputHelper output, ShutdownNodeAuthenticationTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task shutdown_without_credentials_throws() => + await Fixture.Operations.ShutdownAsync().ShouldThrowAsync(); + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/Operations/ShutdownNodeTests.cs b/test/Kurrent.Client.Tests/Operations/ShutdownNodeTests.cs new file mode 100644 index 000000000..4e685c37d --- /dev/null +++ b/test/Kurrent.Client.Tests/Operations/ShutdownNodeTests.cs @@ -0,0 +1,14 @@ +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.Operations; + +[Trait("Category", "Target:Operations")] +public class ShutdownNodeTests(ITestOutputHelper output, ShutdownNodeTests.NoDefaultCredentialsFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task shutdown_does_not_throw() => + await Fixture.Operations.ShutdownAsync(userCredentials: TestCredentials.Root).ShouldNotThrowAsync(); + + public class NoDefaultCredentialsFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/FilterTestCase.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/FilterTestCases.cs similarity index 94% rename from test/EventStore.Client.PersistentSubscriptions.Tests/FilterTestCase.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/FilterTestCases.cs index 4787105a4..5dc4c098b 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/FilterTestCase.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/FilterTestCases.cs @@ -1,6 +1,7 @@ using System.Reflection; +using EventStore.Client; -namespace EventStore.Client.PersistentSubscriptions.Tests; +namespace Kurrent.Client.Tests.PersistentSubscriptions; public static class Filters { const string StreamNamePrefix = nameof(StreamNamePrefix); @@ -37,4 +38,4 @@ public static class Filters { public static (Func getFilter, Func prepareEvent) GetFilter(string name) => s_filters[name]; -} \ No newline at end of file +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromNotSetObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromNotSetObsoleteTests.cs new file mode 100644 index 000000000..1eb96f88a --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromNotSetObsoleteTests.cs @@ -0,0 +1,51 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllConnectToExistingWithStartFromNotSetObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + TaskCompletionSource firstNonSystemEventSource = new(); + + foreach (var @event in Fixture.CreateTestEvents(10)) + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + new[] { + @event + } + ); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { + await subscription.Ack(e); + return; + } + + firstNonSystemEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstNonSystemEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Assert.ThrowsAsync(() => firstNonSystemEventSource.Task.WithTimeout()); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionObsoleteTests.cs new file mode 100644 index 000000000..2706e0669 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionObsoleteTests.cs @@ -0,0 +1,46 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllConnectToExistingWithStartFromSetToEndPositionObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + TaskCompletionSource firstNonSystemEventSource = new(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { + await subscription.Ack(e); + return; + } + + firstNonSystemEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstNonSystemEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Assert.ThrowsAsync(() => firstNonSystemEventSource.Task.WithTimeout()); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectWithoutReadPermissionsObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectWithoutReadPermissionsObsoleteTests.cs new file mode 100644 index 000000000..f7004b890 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllConnectWithoutReadPermissionsObsoleteTests.cs @@ -0,0 +1,34 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllConnectWithoutReadPermissionsObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_without_read_all_permissions() { + var group = Fixture.GetGroupName(); + var user = Fixture.GetUserCredentials(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Users.CreateUserWithRetry( + user.Username!, + user.Username!, + [], + user.Password!, + TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; }, + userCredentials: user + ); + } + ); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllFilterObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllFilterObsoleteTests.cs new file mode 100644 index 000000000..95c3aba2b --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllFilterObsoleteTests.cs @@ -0,0 +1,131 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllFilterObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryTheory] + [MemberData(nameof(FilterCases))] + public async Task happy_case_filtered_reads_all_existing_filtered_events(string filterName) { + var streamPrefix = $"{filterName}-{Fixture.GetStreamName()}"; + var group = Fixture.GetGroupName(); + var (getFilter, prepareEvent) = Filters.GetFilter(filterName); + var filter = getFilter(streamPrefix); + + await Fixture.Streams.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + Fixture.CreateTestEvents(256) + ); + + await Fixture.Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.Any, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + var appearedEvents = new List(); + var events = Fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + [e] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + filter, + new(startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await subscription.Messages + .OfType() + .Take(events.Length) + .ForEachAwaitAsync( + async e => { + var (resolvedEvent, _) = e; + appearedEvents.Add(resolvedEvent.Event); + await subscription.Ack(resolvedEvent); + } + ) + .WithTimeout(); + + Assert.Equal(events.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); + } + + [RetryTheory] + [MemberData(nameof(FilterCases))] + public async Task happy_case_filtered_with_start_from_set(string filterName) { + var group = Fixture.GetGroupName(); + var streamPrefix = $"{filterName}-{Fixture.GetStreamName()}"; + var (getFilter, prepareEvent) = Filters.GetFilter(filterName); + var filter = getFilter(streamPrefix); + + var events = Fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); + var eventsToSkip = events.Take(10).ToArray(); + var eventsToCapture = events.Skip(10).ToArray(); + + IWriteResult? eventToCaptureResult = null; + + await Fixture.Streams.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + Fixture.CreateTestEvents(256) + ); + + await Fixture.Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.Any, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + foreach (var e in eventsToSkip) { + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + new[] { e } + ); + } + + foreach (var e in eventsToCapture) { + var result = await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + new[] { e } + ); + + eventToCaptureResult ??= result; + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + filter, + new(startFrom: eventToCaptureResult!.LogPosition), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + var appearedEvents = await subscription.Messages.OfType() + .Take(10) + .Select(e => e.ResolvedEvent.Event) + .ToArrayAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(eventsToCapture.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); + } + + public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllGetInfoObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllGetInfoObsoleteTests.cs new file mode 100644 index 000000000..2b8532ad4 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllGetInfoObsoleteTests.cs @@ -0,0 +1,97 @@ +// ReSharper disable InconsistentNaming + +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllGetInfoObsoleteTests(SubscribeToAllGetInfoObsoleteTests.CustomFixture fixture) + : IClassFixture { + static readonly PersistentSubscriptionSettings Settings = new( + resolveLinkTos: true, + startFrom: Position.Start, + extraStatistics: true, + messageTimeout: TimeSpan.FromSeconds(9), + maxRetryCount: 11, + liveBufferSize: 303, + readBatchSize: 30, + historyBufferSize: 909, + checkPointAfter: TimeSpan.FromSeconds(1), + checkPointLowerBound: 1, + checkPointUpperBound: 1, + maxSubscriberCount: 500, + consumerStrategyName: SystemConsumerStrategies.Pinned + ); + + [RetryFact] + public async Task throws_with_non_existing_subscription() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + + await Assert.ThrowsAsync( + async () => await fixture.Subscriptions.GetInfoToAllAsync(group, userCredentials: TestCredentials.Root) + ); + } + + [RetryFact] + public async Task throws_with_no_credentials() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + + await Assert.ThrowsAsync( + async () => + await fixture.Subscriptions.GetInfoToAllAsync(group) + ); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + + await Assert.ThrowsAsync( + async () => + await fixture.Subscriptions.GetInfoToAllAsync(group, userCredentials: TestCredentials.TestBadUser) + ); + } + + [RetryFact] + public async Task returns_result_with_normal_user_credentials() { + var result = await fixture.Subscriptions.GetInfoToAllAsync(fixture.Group, userCredentials: TestCredentials.Root); + + Assert.Equal("$all", result.EventSource); + } + + public class CustomFixture : KurrentTemporaryFixture { + public string Group { get; } + + public CustomFixture() : base(x => x.WithoutDefaultCredentials()) { + Group = GetGroupName(); + + OnSetup += async () => { + await Subscriptions.CreateToAllAsync(Group, Settings, userCredentials: TestCredentials.Root); + + var counter = 0; + var tcs = new TaskCompletionSource(); + + await Subscriptions.SubscribeToAllAsync( + Group, + (s, e, r, ct) => { + counter++; + + switch (counter) { + case 1: + s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); + break; + + case > 10: + tcs.TrySetResult(); + break; + } + + return Task.CompletedTask; + }, + userCredentials: TestCredentials.Root + ); + }; + } + }; +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllListWithIncorrectCredentialsObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllListWithIncorrectCredentialsObsoleteTests.cs new file mode 100644 index 000000000..7c1426073 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllListWithIncorrectCredentialsObsoleteTests.cs @@ -0,0 +1,64 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllListWithIncorrectCredentialsObsoleteTests(ITestOutputHelper output, SubscribeToAllListWithIncorrectCredentialsObsoleteTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task throws_with_no_credentials() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync(async () => await Fixture.Subscriptions.ListToAllAsync()); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.TestBadUser) + ); + } + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllNoDefaultCredentialsObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllNoDefaultCredentialsObsoleteTests.cs new file mode 100644 index 000000000..36685373f --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllNoDefaultCredentialsObsoleteTests.cs @@ -0,0 +1,103 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllNoDefaultCredentialsObsoleteTests(ITestOutputHelper output, SubscribeToAllNoDefaultCredentialsObsoleteTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_without_permissions() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; } + ); + } + ); + } + + [RetryFact] + public async Task throws_persistent_subscription_not_found() { + var group = Fixture.GetGroupName(); + + var ex = await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ); + } + ).WithTimeout(); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task deleting_without_permissions() { + await Assert.ThrowsAsync(() => Fixture.Subscriptions.DeleteToAllAsync(Guid.NewGuid().ToString())); + } + + [RetryFact] + public async Task create_without_permissions() { + var group = Fixture.GetGroupName(); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.CreateToAllAsync( + group, + new() + ) + ); + } + + [RetryFact] + public async Task update_existing_without_permissions() { + var group = Fixture.GetGroupName(); + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.UpdateToAllAsync( + group, + new() + ) + ); + } + + [RetryFact] + public async Task update_non_existent() { + var group = Fixture.GetGroupName(); + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.UpdateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ) + ); + } + + [RetryFact] + public Task update_with_prepare_position_larger_than_commit_position() { + var group = Fixture.GetGroupName(); + return Assert.ThrowsAsync( + () => + Fixture.Subscriptions.UpdateToAllAsync( + group, + new(startFrom: new Position(0, 1)), + userCredentials: TestCredentials.Root + ) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllObsoleteTests.cs new file mode 100644 index 000000000..05dc4baff --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllObsoleteTests.cs @@ -0,0 +1,786 @@ +using System.Text; +using EventStore.Client; +using Grpc.Core; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllObsoleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_max_one_client() { + // Arrange + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(maxSubscriberCount: 1), userCredentials: TestCredentials.Root); + + using var first = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ).WithTimeout(); + + var ex = await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ); + } + ).WithTimeout(); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task connect_to_existing_with_permissions() { + // Arrange + var group = Fixture.GetGroupName(); + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>(); + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; }, + (s, reason, ex) => dropped.TrySetResult((reason, ex)), + TestCredentials.Root + ).WithTimeout(); + + Assert.NotNull(subscription); + + await Assert.ThrowsAsync(() => dropped.Task.WithTimeout()); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning() { + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + // append 10 events to random streams to make sure we have at least 10 events in the transaction file + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + [@event] + ); + } + + var events = await Fixture.Streams + .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + var resolvedEvent = await firstEventSource.Task.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(events![0].Event.EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set_then_event_written() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + var expectedStreamId = Guid.NewGuid().ToString(); + var expectedEvent = Fixture.CreateTestEvents(1).First(); + + TaskCompletionSource firstNonSystemEventSource = new(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { + await subscription.Ack(e); + return; + } + + firstNonSystemEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstNonSystemEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(expectedStreamId, StreamState.NoStream, [expectedEvent]); + + var resolvedEvent = await firstNonSystemEventSource.Task.WithTimeout(); + Assert.Equal(expectedEvent!.EventId, resolvedEvent.Event.EventId); + Assert.Equal(expectedStreamId, resolvedEvent.Event.EventStreamId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var expectedStreamId = Fixture.GetStreamName(); + var expectedEvent = Fixture.CreateTestEvents().First(); + + TaskCompletionSource firstNonSystemEventSource = new(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + if (SystemStreams.IsSystemStream(e.OriginalStreamId)) { + await subscription.Ack(e); + return; + } + + firstNonSystemEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstNonSystemEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(expectedStreamId, StreamState.NoStream, [expectedEvent]); + + var resolvedEvent = await firstNonSystemEventSource.Task.WithTimeout(); + Assert.Equal(expectedEvent.EventId, resolvedEvent.Event.EventId); + Assert.Equal(expectedStreamId, resolvedEvent.Event.EventStreamId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_invalid_middle_position() { + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> dropped = new(); + + var invalidPosition = new Position(1L, 1L); + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: invalidPosition), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => await subscription.Ack(e), + (subscription, reason, ex) => { dropped.TrySetResult((reason, ex)); }, + TestCredentials.Root + ); + + var (reason, exception) = await dropped.Task.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + Assert.IsType(exception); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_valid_middle_position() { + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = await Fixture.Streams + .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + var expectedEvent = events[events.Length / 2]; //just a random event in the middle of the results + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: expectedEvent.OriginalPosition), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + var resolvedEvent = await firstEventSource.Task.WithTimeout(); + Assert.Equal(expectedEvent.OriginalPosition, resolvedEvent.Event.Position); + Assert.Equal(expectedEvent.Event.EventId, resolvedEvent.Event.EventId); + Assert.Equal(expectedEvent.Event.EventStreamId, resolvedEvent.Event.EventStreamId); + } + + [RetryFact] + public async Task connect_with_retries() { + // Arrange + var group = Fixture.GetGroupName(); + + TaskCompletionSource retryCountSource = new(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + // Act + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, r, ct) => { + if (r > 4) { + retryCountSource.TrySetResult(r.Value); + await subscription.Ack(e.Event.EventId); + } else { + await subscription.Nack( + PersistentSubscriptionNakEventAction.Retry, + "Not yet tried enough times", + e + ); + } + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + retryCountSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + // Assert + Assert.Equal(5, await retryCountSource.Task.WithTimeout()); + } + + [RetryFact] + public async Task deleting_existing_with_subscriber() { + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> dropped = new(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (s, e, i, ct) => await s.Ack(e), + (s, r, e) => dropped.TrySetResult((r, e)), + TestCredentials.Root + ); + + // todo: investigate why this test is flaky without this delay + await Task.Delay(500); + + await Fixture.Subscriptions.DeleteToAllAsync(group, userCredentials: TestCredentials.Root); + + var (reason, exception) = await dropped.Task.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + // [RetryFact] + // public async Task happy_case_catching_up_to_link_to_events_manual_ack() { + // var group = Fixture.GetGroupName(); + // var bufferCount = 10; + // var eventWriteCount = bufferCount * 2; + // TaskCompletionSource eventsReceived = new(); + // int eventReceivedCount = 0; + // + // var events = Fixture.CreateTestEvents(eventWriteCount) + // .Select( + // (e, i) => new EventData( + // e.EventId, + // SystemEventTypes.LinkTo, + // Encoding.UTF8.GetBytes($"{i}@test"), + // contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + // ) + // ) + // .ToArray(); + // + // foreach (var e in events) { + // await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + // } + // + // await Fixture.Subscriptions.CreateToAllAsync( + // group, + // new(startFrom: Position.Start, resolveLinkTos: true), + // userCredentials: TestCredentials.Root + // ); + // + // using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + // group, + // async (subscription, e, retryCount, ct) => { + // await subscription.Ack(e); + // + // if (e.OriginalStreamId.StartsWith("test-") + // && Interlocked.Increment(ref eventReceivedCount) == events.Length) + // eventsReceived.TrySetResult(true); + // }, + // (s, r, e) => { + // if (e != null) + // eventsReceived.TrySetException(e); + // }, + // bufferSize: bufferCount, + // userCredentials: TestCredentials.Root + // ); + // + // await eventsReceived.Task.WithTimeout(); + // } + // + // [RetryFact] + // public async Task happy_case_catching_up_to_normal_events_manual_ack() { + // var group = Fixture.GetGroupName(); + // var stream = Fixture.GetStreamName(); + // var bufferCount = 10; + // var eventWriteCount = bufferCount * 2; + // int eventReceivedCount = 0; + // + // TaskCompletionSource eventsReceived = new(); + // + // var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + // + // foreach (var e in events) + // await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + // + // await Fixture.Subscriptions.CreateToAllAsync( + // group, + // new(startFrom: Position.Start, resolveLinkTos: true), + // userCredentials: TestCredentials.Root + // ); + // + // using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + // group, + // async (subscription, e, retryCount, ct) => { + // await subscription.Ack(e); + // + // if (e.OriginalStreamId.StartsWith("test-") + // && Interlocked.Increment(ref eventReceivedCount) == events.Length) + // eventsReceived.TrySetResult(true); + // }, + // (s, r, e) => { + // if (e != null) + // eventsReceived.TrySetException(e); + // }, + // bufferSize: bufferCount, + // userCredentials: TestCredentials.Root + // ); + // + // await eventsReceived.Task.WithTimeout(); + // } + + [RetryFact] + public async Task happy_case_writing_and_subscribing_to_normal_events_manual_ack() { + var group = Fixture.GetGroupName(); + var bufferCount = 10; + var eventWriteCount = bufferCount * 2; + int eventReceivedCount = 0; + + TaskCompletionSource eventsReceived = new(); + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: Position.End, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + if (e.OriginalStreamId.StartsWith("test-") + && Interlocked.Increment(ref eventReceivedCount) == events.Length) + eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + eventsReceived.TrySetException(e); + }, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + } + + await eventsReceived.Task.WithTimeout(); + } + + [RetryFact] + public async Task update_existing_with_check_point() { + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(5).ToArray(); + var appearedEvents = new List(); + + TaskCompletionSource appeared = new(); + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> droppedSource = new(); + TaskCompletionSource resumedSource = new(); + Position checkPoint = default; + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + using var firstSubscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (s, e, r, ct) => { + appearedEvents.Add(e); + + if (appearedEvents.Count == events.Length) + appeared.TrySetResult(true); + + await s.Ack(e); + }, + (subscription, reason, ex) => droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + await Task.WhenAll(appeared.Task, WaitForCheckpoint().WithTimeout()); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await droppedSource.Task.WithTimeout(); + + using var secondSubscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (s, e, r, ct) => { + resumedSource.TrySetResult(e); + await s.Ack(e); + s.Dispose(); + }, + (_, reason, ex) => { + if (ex is not null) + resumedSource.TrySetException(ex); + else + resumedSource.TrySetResult(default); + }, + TestCredentials.Root + ); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); + + var resumedEvent = await resumedSource.Task.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.True(resumedEvent.Event.Position > checkPoint); + + return; + + async Task WaitForCheckpoint() { + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-$all::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + + [RetryFact] + public async Task update_existing_with_check_point_filtered() { + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> droppedSource = new(); + TaskCompletionSource resumedSource = new(); + TaskCompletionSource appeared = new(); + + List appearedEvents = []; + + EventData[] events = Fixture.CreateTestEvents(5).ToArray(); + + Position checkPoint = default; + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + StreamFilter.Prefix("test"), + new( + checkPointLowerBound: 5, + checkPointAfter: TimeSpan.FromSeconds(1), + startFrom: Position.Start + ), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (s, e, r, ct) => { + appearedEvents.Add(e); + + if (appearedEvents.Count == events.Length) + appeared.TrySetResult(true); + + await s.Ack(e); + }, + (subscription, reason, ex) => droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + await Task.WhenAll(appeared.Task, Checkpointed()).WithTimeout(); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await droppedSource.Task.WithTimeout(); + + await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (s, e, r, ct) => { + resumedSource.TrySetResult(e); + await s.Ack(e); + s.Dispose(); + }, + (_, reason, ex) => { + if (ex is not null) + resumedSource.TrySetException(ex); + else + resumedSource.TrySetResult(default); + }, + TestCredentials.Root + ); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); + + Task resumed = resumedSource.Task; + + var resumedEvent = await resumed.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.True(resumedEvent.Event.Position > checkPoint); + + return; + + async Task Checkpointed() { + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-$all::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + + [RetryFact] + public async Task update_existing_with_subscribers() { + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> droppedSource = new(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + using var subscription = Fixture.Subscriptions.SubscribeToAllAsync( + group, + delegate { return Task.CompletedTask; }, + (subscription, reason, ex) => droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + // todo: investigate why this test is flaky without this delay + await Task.Delay(500); + + await Fixture.Subscriptions.UpdateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + var (reason, exception) = await droppedSource.Task.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task when_writing_and_filtering_out_events() { + var events = Fixture.CreateTestEvents(10).ToArray(); + var group = Fixture.GetGroupName(); + var prefix = Guid.NewGuid().ToString("N"); + + TaskCompletionSource appeared = new(); + + Position firstCheckPoint = default; + Position secondCheckPoint = default; + List appearedEvents = []; + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(prefix + Guid.NewGuid(), StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + StreamFilter.Prefix(prefix), + new( + checkPointLowerBound: 1, + checkPointUpperBound: 5, + checkPointAfter: TimeSpan.FromSeconds(1), + startFrom: Position.Start + ), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToAllAsync( + group, + async (s, e, r, ct) => { + appearedEvents.Add(e); + + if (appearedEvents.Count == events.Length) + appeared.TrySetResult(true); + + await s.Ack(e); + }, + userCredentials: TestCredentials.Root + ); + + await Task.WhenAll(appeared.Task, WaitForCheckpoints().WithTimeout()); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync( + "filtered-out-stream-" + Guid.NewGuid(), + StreamState.Any, + [e] + ); + } + + Assert.True(secondCheckPoint > firstCheckPoint); + Assert.Equal(events.Select(e => e.EventId), appearedEvents.Select(e => e.Event.EventId)); + + return; + + async Task WaitForCheckpoints() { + bool firstCheckpointSet = false; + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-$all::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + if (!firstCheckpointSet) { + firstCheckPoint = resolvedEvent.Event.Data.ParsePosition(); + firstCheckpointSet = true; + } else { + secondCheckPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + } + + // [RetryFact] + // public async Task when_writing_and_subscribing_to_normal_events_manual_nack() { + // var group = Fixture.GetGroupName(); + // var bufferCount = 10; + // var eventWriteCount = bufferCount * 2; + // var prefix = $"{Guid.NewGuid():N}-"; + // + // var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + // + // await Fixture.Subscriptions.CreateToAllAsync( + // group, + // new(startFrom: Position.Start, resolveLinkTos: true), + // userCredentials: TestCredentials.Root + // ); + // + // var subscription = Fixture.Subscriptions.SubscribeToAll(group, bufferSize: bufferCount, userCredentials: TestCredentials.Root); + // + // foreach (var e in events) + // await Fixture.Streams.AppendToStreamAsync(prefix + Guid.NewGuid(), StreamState.Any, [e]); + // + // await subscription.Messages.OfType() + // .SelectAwait( + // async e => { + // await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e.ResolvedEvent); + // return e; + // } + // ) + // .Where(e => e.ResolvedEvent.OriginalStreamId.StartsWith(prefix)) + // .Take(events.Length) + // .ToArrayAsync() + // .AsTask() + // .WithTimeout(); + // } + + // [RetryFact] + // public async Task update_existing_with_commit_position_larger_than_last_indexed_position() { + // var group = Fixture.GetGroupName(); + // + // await Fixture.Subscriptions.CreateToAllAsync( + // group, + // new(), + // userCredentials: TestCredentials.Root + // ); + // + // var lastEvent = await Fixture.Streams.ReadAllAsync( + // Direction.Backwards, + // Position.End, + // 1, + // userCredentials: TestCredentials.Root + // ).FirstAsync(); + // + // var lastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); + // var ex = await Assert.ThrowsAsync( + // () => + // Fixture.Subscriptions.UpdateToAllAsync( + // group, + // new(startFrom: new Position(lastCommitPosition + 1, lastCommitPosition)), + // userCredentials: TestCredentials.Root + // ) + // ); + // + // Assert.Equal(StatusCode.Internal, ex.StatusCode); + // } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReplayParkedObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReplayParkedObsoleteTests.cs new file mode 100644 index 000000000..7e1f2366d --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReplayParkedObsoleteTests.cs @@ -0,0 +1,87 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllReplayParkedObsoleteTests(ITestOutputHelper output, SubscribeToAllReplayParkedTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task does_not_throw() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + group, + 100, + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task throws_when_given_non_existing_subscription() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + var nonExistingGroup = Fixture.GetGroupName(); + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + nonExistingGroup, + userCredentials: TestCredentials.Root + ) + ); + } + + [RetryFact] + public async Task throws_with_no_credentials() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync(group) + ); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + group, + userCredentials: TestCredentials.TestBadUser + ) + ); + } + + [RetryFact] + public async Task throws_with_normal_user_credentials() { + var user = Fixture.GetUserCredentials(); + + await Fixture.Users + .CreateUserWithRetry(user.Username!, user.Username!, [], user.Password!, TestCredentials.Root) + .WithTimeout(); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + Fixture.GetGroupName(), + userCredentials: user + ) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllResultWithNormalUserCredentialsObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllResultWithNormalUserCredentialsObsoleteTests.cs new file mode 100644 index 000000000..e1815bf9b --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllResultWithNormalUserCredentialsObsoleteTests.cs @@ -0,0 +1,35 @@ +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllResultWithNormalUserCredentialsObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task returns_result_with_normal_user_credentials() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + var result = await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root); + Assert.Equal(allStreamSubscriptionCount, result.Count()); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsAllSubscriptionsObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsAllSubscriptionsObsoleteTests.cs new file mode 100644 index 000000000..07035b60b --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsAllSubscriptionsObsoleteTests.cs @@ -0,0 +1,42 @@ +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllReturnsAllSubscriptionsObsoleteTests(ITestOutputHelper output, SubscribeToAllReturnsAllSubscriptions.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task returns_all_subscriptions() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + const int totalSubscriptionCount = streamSubscriptionCount + allStreamSubscriptionCount; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + var result = (await Fixture.Subscriptions.ListAllAsync(userCredentials: TestCredentials.Root)).ToList(); + Assert.Equal(totalSubscriptionCount, result.Count); + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() { + SkipPsWarmUp = true; + } + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsSubscriptionsToAllStreamObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsSubscriptionsToAllStreamObsoleteTests.cs new file mode 100644 index 000000000..e0c1f647b --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllReturnsSubscriptionsToAllStreamObsoleteTests.cs @@ -0,0 +1,36 @@ +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllReturnsSubscriptionsToAllStreamObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task returns_subscriptions_to_all_stream() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + var result = (await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root)).ToList(); + Assert.Equal(allStreamSubscriptionCount, result.Count); + Assert.All(result, s => Assert.Equal("$all", s.EventSource)); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllUpdateExistingWithCheckpointObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllUpdateExistingWithCheckpointObsoleteTests.cs new file mode 100644 index 000000000..844b24644 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllUpdateExistingWithCheckpointObsoleteTests.cs @@ -0,0 +1,88 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllUpdateExistingWithCheckpointObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task update_existing_with_check_point_should_resumes_from_check_point() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + StreamPosition checkPoint = default; + + var events = Fixture.CreateTestEvents(5).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents(1)); + + await using var sub = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + var resolvedEvent = await sub.Messages + .OfType() + .Select(e => e.ResolvedEvent) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(checkPoint.Next(), resolvedEvent.Event.EventNumber); + + return; + + async Task Subscribe() { + var count = 0; + + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + count++; + + await subscription.Ack(resolvedEvent); + if (count >= events.Length) { + break; + } + } + } + + async Task WaitForCheckpoint() { + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-{stream}::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event (var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParseStreamPosition(); + return; + } + } + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllWithoutPSObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllWithoutPSObsoleteTests.cs new file mode 100644 index 000000000..f676c9cb0 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/Obsolete/SubscribeToAllWithoutPSObsoleteTests.cs @@ -0,0 +1,17 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllWithoutPSObsoleteTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task list_without_persistent_subscriptions() { + await Assert.ThrowsAsync( + async () => + await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root) + ); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs new file mode 100644 index 000000000..d69349f3d --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromNotSetTests.cs @@ -0,0 +1,33 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllConnectToExistingWithStartFromNotSetTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + foreach (var @event in Fixture.CreateTestEvents(10)) + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => subscription.Messages + .OfType() + .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) + .AnyAsync() + .AsTask() + .WithTimeout() + ); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs new file mode 100644 index 000000000..effbd67c2 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests.cs @@ -0,0 +1,35 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllConnectToExistingWithStartFromSetToEndPositionTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => subscription.Messages + .OfType() + .Where(e => !SystemStreams.IsSystemStream(e.ResolvedEvent.OriginalStreamId)) + .AnyAsync() + .AsTask() + .WithTimeout() + ); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectWithoutReadPermissionsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectWithoutReadPermissionsTests.cs new file mode 100644 index 000000000..99fa039e5 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllConnectWithoutReadPermissionsTests.cs @@ -0,0 +1,31 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllConnectWithoutReadPermissionsTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_without_read_all_permissions() { + var group = Fixture.GetGroupName(); + var user = Fixture.GetUserCredentials(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Users.CreateUserWithRetry( + user.Username!, + user.Username!, + [], + user.Password!, + TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => { + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: user); + await subscription.Messages.AnyAsync().AsTask().WithTimeout(); + } + ); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllFilterTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllFilterTests.cs new file mode 100644 index 000000000..3021fb696 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllFilterTests.cs @@ -0,0 +1,131 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllFilterTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryTheory] + [MemberData(nameof(FilterCases))] + public async Task happy_case_filtered_reads_all_existing_filtered_events(string filterName) { + var streamPrefix = $"{filterName}-{Fixture.GetStreamName()}"; + var group = Fixture.GetGroupName(); + var (getFilter, prepareEvent) = Filters.GetFilter(filterName); + var filter = getFilter(streamPrefix); + + await Fixture.Streams.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + Fixture.CreateTestEvents(256) + ); + + await Fixture.Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.Any, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + var appearedEvents = new List(); + var events = Fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + [e] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + filter, + new(startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await subscription.Messages + .OfType() + .Take(events.Length) + .ForEachAwaitAsync( + async e => { + var (resolvedEvent, _) = e; + appearedEvents.Add(resolvedEvent.Event); + await subscription.Ack(resolvedEvent); + } + ) + .WithTimeout(); + + Assert.Equal(events.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); + } + + [RetryTheory] + [MemberData(nameof(FilterCases))] + public async Task happy_case_filtered_with_start_from_set(string filterName) { + var group = Fixture.GetGroupName(); + var streamPrefix = $"{filterName}-{Fixture.GetStreamName()}"; + var (getFilter, prepareEvent) = Filters.GetFilter(filterName); + var filter = getFilter(streamPrefix); + + var events = Fixture.CreateTestEvents(20).Select(e => prepareEvent(streamPrefix, e)).ToArray(); + var eventsToSkip = events.Take(10).ToArray(); + var eventsToCapture = events.Skip(10).ToArray(); + + IWriteResult? eventToCaptureResult = null; + + await Fixture.Streams.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + Fixture.CreateTestEvents(256) + ); + + await Fixture.Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.Any, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + foreach (var e in eventsToSkip) { + await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + new[] { e } + ); + } + + foreach (var e in eventsToCapture) { + var result = await Fixture.Streams.AppendToStreamAsync( + $"{streamPrefix}_{Guid.NewGuid():n}", + StreamState.NoStream, + new[] { e } + ); + + eventToCaptureResult ??= result; + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + filter, + new(startFrom: eventToCaptureResult!.LogPosition), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + var appearedEvents = await subscription.Messages.OfType() + .Take(10) + .Select(e => e.ResolvedEvent.Event) + .ToArrayAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(eventsToCapture.Select(x => x.EventId), appearedEvents.Select(x => x.EventId)); + } + + public static IEnumerable FilterCases() => Filters.All.Select(filter => new object[] { filter }); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllGetInfoTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllGetInfoTests.cs new file mode 100644 index 000000000..3b4beb81e --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllGetInfoTests.cs @@ -0,0 +1,102 @@ +// ReSharper disable InconsistentNaming + +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllGetInfoTests(SubscribeToAllGetInfoTests.CustomFixture fixture) + : IClassFixture { + static readonly PersistentSubscriptionSettings Settings = new( + resolveLinkTos: true, + startFrom: Position.Start, + extraStatistics: true, + messageTimeout: TimeSpan.FromSeconds(9), + maxRetryCount: 11, + liveBufferSize: 303, + readBatchSize: 30, + historyBufferSize: 909, + checkPointAfter: TimeSpan.FromSeconds(1), + checkPointLowerBound: 1, + checkPointUpperBound: 1, + maxSubscriberCount: 500, + consumerStrategyName: SystemConsumerStrategies.Pinned + ); + + [RetryFact] + public async Task throws_with_non_existing_subscription() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + + await Assert.ThrowsAsync( + async () => await fixture.Subscriptions.GetInfoToAllAsync(group, userCredentials: TestCredentials.Root) + ); + } + + [RetryFact] + public async Task throws_with_no_credentials() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + + await Assert.ThrowsAsync( + async () => + await fixture.Subscriptions.GetInfoToAllAsync(group) + ); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + + await Assert.ThrowsAsync( + async () => + await fixture.Subscriptions.GetInfoToAllAsync(group, userCredentials: TestCredentials.TestBadUser) + ); + } + + [RetryFact] + public async Task returns_result_with_normal_user_credentials() { + var result = await fixture.Subscriptions.GetInfoToAllAsync(fixture.Group, userCredentials: TestCredentials.Root); + + Assert.Equal("$all", result.EventSource); + } + + public class CustomFixture : KurrentTemporaryFixture { + public string Group { get; } + + public CustomFixture() : base(x => x.WithoutDefaultCredentials()) { + Group = GetGroupName(); + + OnSetup += async () => { + await Subscriptions.CreateToAllAsync(Group, Settings, userCredentials: TestCredentials.Root); + + foreach (var eventData in CreateTestEvents(20)) { + await Streams.AppendToStreamAsync( + $"test-{Guid.NewGuid():n}", + StreamState.NoStream, + [eventData], + userCredentials: TestCredentials.Root + ); + } + + var counter = 0; + + await using var subscription = Subscriptions.SubscribeToAll(Group, userCredentials: TestCredentials.Root); + + var enumerator = subscription.Messages.GetAsyncEnumerator(); + + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event (var resolvedEvent, _)) + continue; + + counter++; + + if (counter == 1) + await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "Test", resolvedEvent); + + if (counter > 10) + return; + } + }; + } + }; +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllListWithIncorrectCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllListWithIncorrectCredentialsTests.cs new file mode 100644 index 000000000..aa3957ed0 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllListWithIncorrectCredentialsTests.cs @@ -0,0 +1,64 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllListWithIncorrectCredentialsTests(ITestOutputHelper output, SubscribeToAllListWithIncorrectCredentialsTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task throws_with_no_credentials() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync(async () => await Fixture.Subscriptions.ListToAllAsync()); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.TestBadUser) + ); + } + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllNoDefaultCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllNoDefaultCredentialsTests.cs new file mode 100644 index 000000000..49e8cfa30 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllNoDefaultCredentialsTests.cs @@ -0,0 +1,96 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllNoDefaultCredentialsTests(ITestOutputHelper output, SubscribeToAllNoDefaultCredentialsTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_without_permissions() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + async () => { + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group); + await subscription.AnyAsync().AsTask().WithTimeout(); + } + ); + } + + [RetryFact] + public async Task throws_persistent_subscription_not_found() { + var group = Fixture.GetGroupName(); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + Assert.True( + await subscription.Messages.OfType().AnyAsync() + .AsTask() + .WithTimeout() + ); + } + + [RetryFact] + public async Task deleting_without_permissions() { + await Assert.ThrowsAsync(() => Fixture.Subscriptions.DeleteToAllAsync(Guid.NewGuid().ToString())); + } + + [RetryFact] + public async Task create_without_permissions() { + var group = Fixture.GetGroupName(); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.CreateToAllAsync( + group, + new() + ) + ); + } + + [RetryFact] + public async Task update_existing_without_permissions() { + var group = Fixture.GetGroupName(); + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.UpdateToAllAsync( + group, + new() + ) + ); + } + + [RetryFact] + public async Task update_non_existent() { + var group = Fixture.GetGroupName(); + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.UpdateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ) + ); + } + + [RetryFact] + public Task update_with_prepare_position_larger_than_commit_position() { + var group = Fixture.GetGroupName(); + return Assert.ThrowsAsync( + () => + Fixture.Subscriptions.UpdateToAllAsync( + group, + new(startFrom: new Position(0, 1)), + userCredentials: TestCredentials.Root + ) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReplayParkedTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReplayParkedTests.cs new file mode 100644 index 000000000..f2eacb3e7 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReplayParkedTests.cs @@ -0,0 +1,87 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllReplayParkedTests(ITestOutputHelper output, SubscribeToAllReplayParkedTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task does_not_throw() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + group, + 100, + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task throws_when_given_non_existing_subscription() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + var nonExistingGroup = Fixture.GetGroupName(); + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + nonExistingGroup, + userCredentials: TestCredentials.Root + ) + ); + } + + [RetryFact] + public async Task throws_with_no_credentials() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync(group) + ); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + group, + userCredentials: TestCredentials.TestBadUser + ) + ); + } + + [RetryFact] + public async Task throws_with_normal_user_credentials() { + var user = Fixture.GetUserCredentials(); + + await Fixture.Users + .CreateUserWithRetry(user.Username!, user.Username!, [], user.Password!, TestCredentials.Root) + .WithTimeout(); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToAllAsync( + Fixture.GetGroupName(), + userCredentials: user + ) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllResultWithNormalUserCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllResultWithNormalUserCredentialsTests.cs new file mode 100644 index 000000000..627a3bc19 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllResultWithNormalUserCredentialsTests.cs @@ -0,0 +1,35 @@ +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllResultWithNormalUserCredentialsTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task returns_result_with_normal_user_credentials() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + var result = await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root); + Assert.Equal(allStreamSubscriptionCount, result.Count()); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReturnsAllSubscriptions.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReturnsAllSubscriptions.cs new file mode 100644 index 000000000..148607b83 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReturnsAllSubscriptions.cs @@ -0,0 +1,42 @@ +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllReturnsAllSubscriptions(ITestOutputHelper output, SubscribeToAllReturnsAllSubscriptions.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task returns_all_subscriptions() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + const int totalSubscriptionCount = streamSubscriptionCount + allStreamSubscriptionCount; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + var result = (await Fixture.Subscriptions.ListAllAsync(userCredentials: TestCredentials.Root)).ToList(); + Assert.Equal(totalSubscriptionCount, result.Count); + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() { + SkipPsWarmUp = true; + } + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs new file mode 100644 index 000000000..07498fc28 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllReturnsSubscriptionsToAllStreamTests.cs @@ -0,0 +1,36 @@ +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllReturnsSubscriptionsToAllStreamTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task returns_subscriptions_to_all_stream() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + const int streamSubscriptionCount = 4; + const int allStreamSubscriptionCount = 3; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < allStreamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToAllAsync( + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + var result = (await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root)).ToList(); + Assert.Equal(allStreamSubscriptionCount, result.Count); + Assert.All(result, s => Assert.Equal("$all", s.EventSource)); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllTests.cs new file mode 100644 index 000000000..e7e67f51f --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllTests.cs @@ -0,0 +1,853 @@ +using System.Text; +using EventStore.Client; +using Grpc.Core; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task can_create_duplicate_name_on_different_streams() { + // Arrange + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + // Act + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + } + + [RetryFact] + public async Task connect_to_existing_with_max_one_client() { + // Arrange + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(maxSubscriberCount: 1), userCredentials: TestCredentials.Root); + + var ex = await Assert.ThrowsAsync(() => Task.WhenAll(Subscribe().WithTimeout(), Subscribe().WithTimeout())); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + return; + + async Task Subscribe() { + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + await subscription.Messages.AnyAsync(); + } + } + + [RetryFact] + public async Task connect_to_existing_with_permissions() { + // Arrange + var group = Fixture.GetGroupName(); + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + // Act + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + // Assert + subscription.Messages + .FirstAsync().AsTask().WithTimeout() + .GetAwaiter().GetResult() + .ShouldBeOfType(); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning() { + var group = Fixture.GetGroupName(); + + // append 10 events to random streams to make sure we have at least 10 events in the transaction file + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + Guid.NewGuid().ToString(), + StreamState.NoStream, + [@event] + ); + } + + var events = await Fixture.Streams + .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(events[0].Event.EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set_then_event_written() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + var expectedStreamId = Guid.NewGuid().ToString(); + var expectedEvent = Fixture.CreateTestEvents(1).First(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await Fixture.Streams.AppendToStreamAsync(expectedStreamId, StreamState.NoStream, [expectedEvent]); + + var resolvedEvent = await subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .Where(resolvedEvent => !SystemStreams.IsSystemStream(resolvedEvent.OriginalStreamId)) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(expectedEvent.EventId, resolvedEvent.Event.EventId); + Assert.Equal(expectedStreamId, resolvedEvent.Event.EventStreamId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var expectedStreamId = Fixture.GetStreamName(); + var expectedEvent = Fixture.CreateTestEvents().First(); + + foreach (var @event in Fixture.CreateTestEvents(10)) { + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.Any, + [@event] + ); + } + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.End), userCredentials: TestCredentials.Root); + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + await Fixture.Streams.AppendToStreamAsync(expectedStreamId, StreamState.NoStream, [expectedEvent]); + + var resolvedEvent = await subscription!.Messages + .OfType() + .Select(e => e.ResolvedEvent) + .Where(resolvedEvent => !SystemStreams.IsSystemStream(resolvedEvent.OriginalStreamId)) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(expectedEvent.EventId, resolvedEvent.Event.EventId); + Assert.Equal(expectedStreamId, resolvedEvent.Event.EventStreamId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_invalid_middle_position() { + var group = Fixture.GetGroupName(); + + var invalidPosition = new Position(1L, 1L); + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: invalidPosition), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + var ex = await Assert.ThrowsAsync( + async () => + await enumerator!.MoveNextAsync() + ); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_valid_middle_position() { + var group = Fixture.GetGroupName(); + + var events = await Fixture.Streams + .ReadAllAsync(Direction.Forwards, Position.Start, 10, userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + var expectedEvent = events[events.Length / 2]; //just a random event in the middle of the results + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: expectedEvent.OriginalPosition), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + var resolvedEvent = await subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(expectedEvent.OriginalPosition, resolvedEvent.Event.Position); + Assert.Equal(expectedEvent.Event.EventId, resolvedEvent.Event.EventId); + Assert.Equal(expectedEvent.Event.EventStreamId, resolvedEvent.Event.EventStreamId); + } + + [RetryFact] + public async Task connect_with_retries() { + // Arrange + var group = Fixture.GetGroupName(); + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + // Act + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + var retryCount = await subscription.Messages.OfType() + .SelectAwait( + async e => { + if (e.RetryCount > 4) { + await subscription.Ack(e.ResolvedEvent); + } else { + await subscription.Nack( + PersistentSubscriptionNakEventAction.Retry, + "Not yet tried enough times", + e.ResolvedEvent + ); + } + + return e.RetryCount; + } + ) + .Where(retryCount => retryCount > 4) + .FirstOrDefaultAsync() + .AsTask() + .WithTimeout(); + + // Assert + retryCount.ShouldBe(5); + } + + [RetryFact] + public async Task create_after_deleting_the_same() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + await Fixture.Subscriptions.DeleteToAllAsync(group, userCredentials: TestCredentials.Root); + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + } + + [RetryFact] + public async Task create_duplicate() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + var ex = await Assert.ThrowsAsync( + () => Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ) + ); + + ex.StatusCode.ShouldBe(StatusCode.AlreadyExists); + } + + [RetryFact] + public async Task create_with_commit_position_equal_to_last_indexed_position() { + // Arrange + var group = Fixture.GetGroupName(); + + // Act + var lastEvent = await Fixture.Streams.ReadAllAsync( + Direction.Backwards, + Position.End, + 1, + userCredentials: TestCredentials.Root + ).FirstAsync(); + + var lastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: new Position(lastCommitPosition, lastCommitPosition)), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public Task create_with_prepare_position_larger_than_commit_position() { + var group = Fixture.GetGroupName(); + + return Assert.ThrowsAsync( + () => + Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: new Position(0, 1)), + userCredentials: TestCredentials.Root + ) + ); + } + + [RetryFact] + public async Task deleting_existing_with_subscriber() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + await Fixture.Subscriptions.DeleteToAllAsync(group, userCredentials: TestCredentials.Root); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + Assert.True( + await subscription.Messages.OfType().AnyAsync() + .AsTask() + .WithTimeout() + ); + } + + [RetryFact] + public async Task deleting_existing_with_permissions() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.DeleteToAllAsync( + group, + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task deleting_filtered() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + EventTypeFilter.Prefix("prefix-filter-"), + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.DeleteToAllAsync(group, userCredentials: TestCredentials.Root); + } + + [RetryFact] + public async Task deleting_nonexistent() { + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.DeleteToAllAsync(Guid.NewGuid().ToString(), userCredentials: TestCredentials.Root) + ); + } + + [RetryFact] + public async Task happy_case_catching_up_to_link_to_events_manual_ack() { + var group = Fixture.GetGroupName(); + var bufferCount = 10; + var eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount) + .Select( + (e, i) => new EventData( + e.EventId, + SystemEventTypes.LinkTo, + Encoding.UTF8.GetBytes($"{i}@test"), + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + ) + ) + .ToArray(); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: Position.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, bufferSize: bufferCount, userCredentials: TestCredentials.Root); + await subscription.Messages.OfType() + .Take(events.Length) + .ForEachAwaitAsync(e => subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } + + [RetryFact] + public async Task happy_case_catching_up_to_normal_events_manual_ack() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + var bufferCount = 10; + var eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: Position.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, bufferSize: bufferCount, userCredentials: TestCredentials.Root); + await subscription!.Messages.OfType() + .Take(events.Length) + .ForEachAwaitAsync(e => subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } + + [RetryFact] + public async Task happy_case_writing_and_subscribing_to_normal_events_manual_ack() { + var group = Fixture.GetGroupName(); + var bufferCount = 10; + var eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: Position.End, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, bufferSize: bufferCount, userCredentials: TestCredentials.Root); + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, new[] { e }); + } + + await subscription!.Messages.OfType() + .SelectAwait( + async e => { + await subscription.Ack(e.ResolvedEvent); + return e; + } + ) + .Where(e => e.ResolvedEvent.OriginalStreamId.StartsWith("test-")) + .Take(events.Length) + .ToArrayAsync() + .AsTask() + .WithTimeout(); + } + + [RetryFact] + public async Task update_existing() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.UpdateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task update_existing_filtered() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + EventTypeFilter.Prefix("prefix-filter-"), + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.UpdateToAllAsync( + group, + new(true), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task update_existing_with_check_point() { + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(5).ToArray(); + var appearedEvents = new List(); + Position checkPoint = default; + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + var enumerator = subscription.Messages.GetAsyncEnumerator(); + await enumerator.MoveNextAsync(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + try { + while (await enumerator!.MoveNextAsync()) { } + } catch (PersistentSubscriptionDroppedByServerException) { } + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, new[] { e }); + } + + await using var sub = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + var resumed = await sub.Messages.OfType() + .Select(e => e.ResolvedEvent) + .Take(1) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.True(resumed.Event.Position > checkPoint); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + appearedEvents.Add(resolvedEvent); + await subscription.Ack(resolvedEvent); + if (appearedEvents.Count == events.Length) { + break; + } + } + } + + async Task WaitForCheckpoint() { + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-$all::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + + [RetryFact] + public async Task update_existing_with_check_point_filtered() { + List appearedEvents = []; + var events = Fixture.CreateTestEvents(5).ToArray(); + var group = Fixture.GetGroupName(); + Position checkPoint = default; + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, [e]); + } + + await Fixture.Subscriptions.CreateToAllAsync( + group, + StreamFilter.Prefix("test"), + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + var enumerator = subscription.Messages.GetAsyncEnumerator(); + await enumerator.MoveNextAsync(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + + try { + while (await enumerator.MoveNextAsync()) { } + } catch (PersistentSubscriptionDroppedByServerException) { } + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync("test-" + Guid.NewGuid(), StreamState.NoStream, [e]); + } + + await using var sub = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + var resumed = await sub.Messages.OfType() + .Select(e => e.ResolvedEvent) + .Take(1) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.True(resumed.Event.Position > checkPoint); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + appearedEvents.Add(resolvedEvent); + await subscription.Ack(resolvedEvent); + if (appearedEvents.Count == events.Length) { + break; + } + } + } + + async Task WaitForCheckpoint() { + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-$all::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + + [RetryFact] + public async Task update_existing_with_commit_position_equal_to_last_indexed_position() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + new(), + userCredentials: TestCredentials.Root + ); + + var lastEvent = await Fixture.Streams.ReadAllAsync( + Direction.Backwards, + Position.End, + 1, + userCredentials: TestCredentials.Root + ).FirstAsync(); + + var lastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); + await Fixture.Subscriptions.UpdateToAllAsync( + group, + new(startFrom: new Position(lastCommitPosition, lastCommitPosition)), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task update_existing_with_subscribers() { + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToAllAsync(group, new(startFrom: Position.Start), userCredentials: TestCredentials.Root); + + var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + + var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + await Fixture.Subscriptions.UpdateToAllAsync(group, new(), userCredentials: TestCredentials.Root); + var ex = await Assert.ThrowsAsync( + async () => { + while (await enumerator.MoveNextAsync()) { } + } + ).WithTimeout(); + + Assert.Equal(SystemStreams.AllStream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task when_writing_and_filtering_out_events() { + var events = Fixture.CreateTestEvents(10).ToArray(); + var group = Fixture.GetGroupName(); + var prefix = Guid.NewGuid().ToString("N"); + + Position firstCheckPoint = default; + Position secondCheckPoint = default; + List appearedEvents = []; + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(prefix + Guid.NewGuid(), StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToAllAsync( + group, + StreamFilter.Prefix(prefix), + new( + checkPointLowerBound: 1, + checkPointUpperBound: 5, + checkPointAfter: TimeSpan.FromSeconds(1), + startFrom: Position.Start + ), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToAll(group, userCredentials: TestCredentials.Root); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoints().WithTimeout()); + + foreach (var e in events) { + await Fixture.Streams.AppendToStreamAsync( + "filtered-out-stream-" + Guid.NewGuid(), + StreamState.Any, + [e] + ); + } + + Assert.True(secondCheckPoint > firstCheckPoint); + Assert.Equal(events.Select(e => e.EventId), appearedEvents.Select(e => e.Event.EventId)); + + return; + + async Task Subscribe() { + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + appearedEvents.Add(resolvedEvent); + await subscription.Ack(resolvedEvent); + if (appearedEvents.Count == events.Length) { + break; + } + } + } + + async Task WaitForCheckpoints() { + bool firstCheckpointSet = false; + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-$all::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + if (!firstCheckpointSet) { + firstCheckPoint = resolvedEvent.Event.Data.ParsePosition(); + firstCheckpointSet = true; + } else { + secondCheckPoint = resolvedEvent.Event.Data.ParsePosition(); + return; + } + } + } + } + + // [RetryFact] + // public async Task when_writing_and_subscribing_to_normal_events_manual_nack() { + // var group = Fixture.GetGroupName(); + // var bufferCount = 10; + // var eventWriteCount = bufferCount * 2; + // var prefix = $"{Guid.NewGuid():N}-"; + // + // var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + // + // await Fixture.Subscriptions.CreateToAllAsync( + // group, + // new(startFrom: Position.Start, resolveLinkTos: true), + // userCredentials: TestCredentials.Root + // ); + // + // var subscription = Fixture.Subscriptions.SubscribeToAll(group, bufferSize: bufferCount, userCredentials: TestCredentials.Root); + // + // foreach (var e in events) + // await Fixture.Streams.AppendToStreamAsync(prefix + Guid.NewGuid(), StreamState.Any, [e]); + // + // await subscription.Messages.OfType() + // .SelectAwait( + // async e => { + // await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e.ResolvedEvent); + // return e; + // } + // ) + // .Where(e => e.ResolvedEvent.OriginalStreamId.StartsWith(prefix)) + // .Take(events.Length) + // .ToArrayAsync() + // .AsTask() + // .WithTimeout(); + // } + + // [RetryFact] + // public async Task update_existing_with_commit_position_larger_than_last_indexed_position() { + // var group = Fixture.GetGroupName(); + // + // await Fixture.Subscriptions.CreateToAllAsync( + // group, + // new(), + // userCredentials: TestCredentials.Root + // ); + // + // var lastEvent = await Fixture.Streams.ReadAllAsync( + // Direction.Backwards, + // Position.End, + // 1, + // userCredentials: TestCredentials.Root + // ).FirstAsync(); + // + // var lastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); + // var ex = await Assert.ThrowsAsync( + // () => + // Fixture.Subscriptions.UpdateToAllAsync( + // group, + // new(startFrom: new Position(lastCommitPosition + 1, lastCommitPosition)), + // userCredentials: TestCredentials.Root + // ) + // ); + // + // Assert.Equal(StatusCode.Internal, ex.StatusCode); + // } + + [RetryFact] + public async Task create_with_commit_position_larger_than_last_indexed_position() { + var group = Fixture.GetGroupName(); + + var lastEvent = await Fixture.Streams.ReadAllAsync( + Direction.Backwards, + Position.End, + 1, + userCredentials: TestCredentials.Root + ).FirstAsync(); + + var lastCommitPosition = lastEvent.OriginalPosition?.CommitPosition ?? throw new(); + var ex = await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.CreateToAllAsync( + group, + new(startFrom: new Position(lastCommitPosition + 1, lastCommitPosition)), + userCredentials: TestCredentials.Root + ) + ); + + Assert.Equal(StatusCode.Internal, ex.StatusCode); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllUpdateExistingWithCheckpointTest.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllUpdateExistingWithCheckpointTest.cs new file mode 100644 index 000000000..0a0ae75f4 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllUpdateExistingWithCheckpointTest.cs @@ -0,0 +1,88 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllUpdateExistingWithCheckpointTest(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task update_existing_with_check_point_should_resumes_from_check_point() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + StreamPosition checkPoint = default; + + var events = Fixture.CreateTestEvents(5).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(checkPointLowerBound: 5, checkPointAfter: TimeSpan.FromSeconds(1), startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + + await Task.WhenAll(Subscribe().WithTimeout(), WaitForCheckpoint().WithTimeout()); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents(1)); + + await using var sub = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + var resolvedEvent = await sub.Messages + .OfType() + .Select(e => e.ResolvedEvent) + .FirstAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(checkPoint.Next(), resolvedEvent.Event.EventNumber); + + return; + + async Task Subscribe() { + var count = 0; + + while (await enumerator.MoveNextAsync()) { + if (enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + count++; + + await subscription.Ack(resolvedEvent); + if (count >= events.Length) { + break; + } + } + } + + async Task WaitForCheckpoint() { + await using var subscription = Fixture.Streams.SubscribeToStream( + $"$persistentsubscription-{stream}::{group}-checkpoint", + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event (var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParseStreamPosition(); + return; + } + } + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllWithoutPSTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllWithoutPSTests.cs new file mode 100644 index 000000000..237ca1f2b --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToAll/SubscribeToAllWithoutPSTests.cs @@ -0,0 +1,17 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToAllWithoutPsTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task list_without_persistent_subscriptions() { + await Assert.ThrowsAsync( + async () => + await Fixture.Subscriptions.ListToAllAsync(userCredentials: TestCredentials.Root) + ); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests.cs new file mode 100644 index 000000000..fa2ca1a32 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests.cs @@ -0,0 +1,36 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests( + ITestOutputHelper output, + SubscribeToStreamConnectToExistingWithoutPermissionObsoleteTests.CustomFixture fixture +) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task connect_to_existing_without_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + delegate { return Task.CompletedTask; } + ); + } + ).WithTimeout(); + } + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/get_info_obsolete.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamGetInfoObsoleteTests.cs similarity index 55% rename from test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/get_info_obsolete.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamGetInfoObsoleteTests.cs index 6f42531fa..5381dc013 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToAll/Obsolete/get_info_obsolete.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamGetInfoObsoleteTests.cs @@ -1,51 +1,46 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToAll.Obsolete; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class get_info_obsolete : IClassFixture { - const string GroupName = nameof(get_info_obsolete); +namespace Kurrent.Client.Tests.PersistentSubscriptions; +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamGetInfoObsoleteTests(SubscribeToStreamGetInfoObsoleteTests.CustomFixture fixture) + : IClassFixture { static readonly PersistentSubscriptionSettings Settings = new( - resolveLinkTos: true, - startFrom: Position.Start, - extraStatistics: true, - messageTimeout: TimeSpan.FromSeconds(9), - maxRetryCount: 11, - liveBufferSize: 303, - readBatchSize: 30, - historyBufferSize: 909, - checkPointAfter: TimeSpan.FromSeconds(1), - checkPointLowerBound: 1, - checkPointUpperBound: 1, - maxSubscriberCount: 500, - consumerStrategyName: SystemConsumerStrategies.Pinned + true, + StreamPosition.Start, + true, + TimeSpan.FromSeconds(9), + 11, + 303, + 30, + 909, + TimeSpan.FromSeconds(1), + 1, + 1, + 500, + SystemConsumerStrategies.RoundRobin ); - readonly Fixture _fixture; - - public get_info_obsolete(Fixture fixture) => _fixture = fixture; - - [Fact] - public async Task throws_when_not_supported() { - if (SupportsPSToAll.No) - await Assert.ThrowsAsync( - async () => { await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root); } - ); + public static IEnumerable AllowedUsers() { + yield return new object[] { TestCredentials.Root }; } - [SupportsPSToAll.Fact] - public async Task returns_expected_result() { - var result = await _fixture.Client.GetInfoToAllAsync(GroupName, userCredentials: TestCredentials.Root); - - Assert.Equal("$all", result.EventSource); - Assert.Equal(GroupName, result.GroupName); - Assert.Equal("Live", result.Status); + [Theory] + [MemberData(nameof(AllowedUsers))] + public async Task returns_expected_result(UserCredentials credentials) { + var result = await fixture.Subscriptions.GetInfoToStreamAsync(fixture.Stream, fixture.Group, userCredentials: credentials); + Assert.Equal(fixture.Stream, result.EventSource); + Assert.Equal(fixture.Group, result.GroupName); Assert.NotNull(Settings.StartFrom); Assert.True(result.Stats.TotalItems > 0); Assert.True(result.Stats.OutstandingMessagesCount > 0); Assert.True(result.Stats.AveragePerSecond >= 0); - Assert.True(result.Stats.ParkedMessageCount > 0); + Assert.True(result.Stats.ParkedMessageCount >= 0); Assert.True(result.Stats.AveragePerSecond >= 0); + Assert.True(result.Stats.ReadBufferCount >= 0); + Assert.True(result.Stats.RetryBufferCount >= 0); Assert.True(result.Stats.CountSinceLastMeasurement >= 0); Assert.True(result.Stats.TotalInFlightMessages >= 0); Assert.NotNull(result.Stats.LastKnownEventPosition); @@ -54,13 +49,12 @@ public async Task returns_expected_result() { Assert.NotNull(result.Connections); Assert.NotEmpty(result.Connections); - var connection = result.Connections.First(); Assert.NotNull(connection.From); Assert.Equal(TestCredentials.Root.Username, connection.Username); Assert.NotEmpty(connection.ConnectionName); Assert.True(connection.AverageItemsPerSecond >= 0); - Assert.True(connection.TotalItems > 0); + Assert.True(connection.TotalItems >= 0); Assert.True(connection.CountSinceLastMeasurement >= 0); Assert.True(connection.AvailableSlots >= 0); Assert.True(connection.InFlightMessages >= 0); @@ -98,86 +92,110 @@ public async Task returns_expected_result() { Assert.Equal(Settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName); } - [SupportsPSToAll.Fact] - public async Task throws_with_non_existing_subscription() => + [RetryFact] + public async Task throws_when_given_non_existing_subscription() => await Assert.ThrowsAsync( async () => { - await _fixture.Client.GetInfoToAllAsync( + await fixture.Subscriptions.GetInfoToStreamAsync( + "NonExisting", "NonExisting", userCredentials: TestCredentials.Root ); } ); - [SupportsPSToAll.Fact] - public async Task throws_with_no_credentials() => - await Assert.ThrowsAsync(async () => { await _fixture.Client.GetInfoToAllAsync("NonExisting"); }); + [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] + public async Task throws_with_non_existing_user() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + var stream = $"NonExisting-{fixture.GetStreamName()}"; - [SupportsPSToAll.Fact] - public async Task throws_with_non_existing_user() => await Assert.ThrowsAsync( async () => { - await _fixture.Client.GetInfoToAllAsync( - "NonExisting", + await fixture.Subscriptions.GetInfoToStreamAsync( + stream, + group, userCredentials: TestCredentials.TestBadUser ); } ); + } - [SupportsPSToAll.Fact] - public async Task returns_result_with_normal_user_credentials() { - var result = await _fixture.Client.GetInfoToAllAsync( - GroupName, - userCredentials: TestCredentials.TestUser1 - ); + [RetryFact] + public async Task throws_with_no_credentials() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + var stream = $"NonExisting-{fixture.GetStreamName()}"; - Assert.Equal("$all", result.EventSource); + await Assert.ThrowsAsync( + async () => { + await fixture.Subscriptions.GetInfoToStreamAsync( + stream, + group + ); + } + ); } - void AssertKeyAndValue(IDictionary items, string key) { - Assert.True(items.ContainsKey(key)); - Assert.True(items[key] > 0); + [RetryFact] + public async Task returns_result_for_normal_user() { + var result = await fixture.Subscriptions.GetInfoToStreamAsync( + fixture.Stream, + fixture.Group, + userCredentials: TestCredentials.Root + ); + + Assert.NotNull(result); } - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } + public class CustomFixture : KurrentTemporaryFixture { + public string Group { get; set; } + public string Stream { get; set; } - protected override async Task Given() { - if (SupportsPSToAll.No) - return; + public CustomFixture() : base(x => x.WithoutDefaultCredentials()) { + Group = GetGroupName(); + Stream = GetStreamName(); - await Client.CreateToAllAsync( - GroupName, - get_info_obsolete.Settings, - userCredentials: TestCredentials.Root - ); - } + OnSetup += async () => { + await Subscriptions.CreateToStreamAsync( + groupName: Group, + streamName: Stream, + settings: Settings, + userCredentials: TestCredentials.Root + ); - protected override async Task When() { - if (SupportsPSToAll.No) - return; + var counter = 0; + var tcs = new TaskCompletionSource(); - var counter = 0; - var tcs = new TaskCompletionSource(); + await Subscriptions.SubscribeToStreamAsync( + Stream, + Group, + (s, e, r, ct) => { + counter++; - await Client.SubscribeToAllAsync( - GroupName, - (s, e, r, ct) => { - counter++; + if (counter == 1) + s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); - switch (counter) { - case 1: s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); - break; - case > 10: + if (counter > 10) tcs.TrySetResult(); - break; - } - return Task.CompletedTask; - }, - userCredentials: TestCredentials.Root - ); - - await tcs.Task; + + return Task.CompletedTask; + }, + userCredentials: TestCredentials.Root + ); + + for (var i = 0; i < 15; i++) { + await Streams.AppendToStreamAsync( + Stream, + StreamState.Any, + [new EventData(Uuid.NewUuid(), "test-event", ReadOnlyMemory.Empty)], + userCredentials: TestCredentials.Root + ); + } + }; } + }; + + void AssertKeyAndValue(IDictionary items, string key) { + Assert.True(items.ContainsKey(key)); + Assert.True(items[key] > 0); } } diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamObsoleteTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamObsoleteTests.cs new file mode 100644 index 000000000..2f192c426 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/Obsolete/SubscribeToStreamObsoleteTests.cs @@ -0,0 +1,948 @@ +using System.Text; +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamObsoleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_max_one_client() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(maxSubscriberCount: 1), + userCredentials: TestCredentials.Root + ); + + using var first = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ).WithTimeout(); + + var ex = await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ); + } + ).WithTimeout(); + + Assert.Equal(stream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task connect_to_existing_with_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + var dropped = new TaskCompletionSource<(SubscriptionDroppedReason, Exception?)>(); + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + delegate { return Task.CompletedTask; }, + (s, reason, ex) => dropped.TrySetResult((reason, ex)), + TestCredentials.Root + ).WithTimeout(); + + Assert.NotNull(subscription); + + await Assert.ThrowsAsync(() => dropped.Task.WithTimeout()); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + var firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); + Assert.Equal(events[0].EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning_and_no_stream() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents().ToArray(); + + var eventId = events.Single().EventId; + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + var firstEvent = await firstEventSource.Task.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(StreamPosition.Start, firstEvent.Event.EventNumber); + Assert.Equal(eventId, firstEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + var firstEvent = firstEventSource.Task; + + await Assert.ThrowsAsync(() => firstEvent.WithTimeout()); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(11).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(); + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.End), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + var firstEvent = firstEventSource.Task; + await Assert.ThrowsAsync(() => firstEvent.WithTimeout()); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(11).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.End), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(); + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_two_and_no_stream() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(3).ToArray(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(2)), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + var firstEvent = firstEventSource.Task; + var resolvedEvent = await firstEvent.WithTimeout(TimeSpan.FromSeconds(10)); + var eventId = events.Last().EventId; + + Assert.Equal(new(2), resolvedEvent.Event.EventNumber); + Assert.Equal(eventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_x_set_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(4)), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(); + Assert.Equal(new(4), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Skip(4).First().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(11).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(10)), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(); + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(12).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(11)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(11)), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(10), events.Skip(11)); + + var firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(); + Assert.Equal(new(11), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_non_existing_with_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + var ex = await Assert.ThrowsAsync( + async () => { + using var _ = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + delegate { return Task.CompletedTask; }, + userCredentials: TestCredentials.Root + ); + } + ).WithTimeout(); + + Assert.Equal(stream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task connect_with_retries() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource retryCountSource = new(); + + var events = Fixture.CreateTestEvents().ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + if (r > 4) { + retryCountSource.TrySetResult(r.Value); + await subscription.Ack(e.Event.EventId); + } else { + await subscription.Nack( + PersistentSubscriptionNakEventAction.Retry, + "Not yet tried enough times", + e + ); + } + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + retryCountSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + Task retryCount = retryCountSource.Task; + Assert.Equal(5, await retryCount.WithTimeout()); + } + + [RetryFact] + public async Task connecting_to_a_persistent_subscription() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource firstEventSource = new(); + + var events = Fixture.CreateTestEvents(12).ToArray(); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(11)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(11)), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, r, ct) => { + firstEventSource.TrySetResult(e); + await subscription.Ack(e); + }, + (subscription, reason, ex) => { + if (reason != SubscriptionDroppedReason.Disposed) + firstEventSource.TrySetException(ex!); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(10), events.Skip(11)); + + Task firstEvent = firstEventSource.Task; + + var resolvedEvent = await firstEvent.WithTimeout(); + Assert.Equal(new(11), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task deleting_existing_with_subscriber_the_subscription_is_dropped() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> dropped = new(); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + (_, _, _, _) => Task.CompletedTask, + (_, r, e) => dropped.TrySetResult((r, e)), + TestCredentials.Root + ); + + await Fixture.Subscriptions.DeleteToStreamAsync( + stream, + group, + userCredentials: TestCredentials.Root + ); + + var (reason, exception) = await dropped.Task.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + + Assert.Equal(stream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [Fact(Skip = "Isn't this how it should work?")] + public async Task deleting_existing_with_subscriber_the_subscription_is_dropped_with_not_found() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> _dropped = new(); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + (_, _, _, _) => Task.CompletedTask, + (_, r, e) => _dropped.TrySetResult((r, e)), + TestCredentials.Root + ); + + await Fixture.Subscriptions.DeleteToStreamAsync( + stream, + group, + userCredentials: TestCredentials.Root + ); + + Task<(SubscriptionDroppedReason, Exception?)> dropped = _dropped.Task; + + var (reason, exception) = await dropped.WithTimeout(); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + Assert.Equal(stream, ex.StreamName); + Assert.Equal("groupname123", ex.GroupName); + } + + [RetryFact] + public async Task happy_case_catching_up_to_link_to_events_manual_ack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + EventData[] events; + TaskCompletionSource eventsReceived = new(); + int eventReceivedCount = 0; + + events = Fixture.CreateTestEvents(eventWriteCount) + .Select( + (e, i) => new EventData( + e.EventId, + SystemEventTypes.LinkTo, + Encoding.UTF8.GetBytes($"{i}@{stream}"), + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + ) + ) + .ToArray(); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, new[] { e }); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + + if (Interlocked.Increment(ref eventReceivedCount) == events.Length) + eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + eventsReceived.TrySetException(e); + }, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + await eventsReceived.Task.WithTimeout(); + } + + [RetryFact] + public async Task happy_case_catching_up_to_normal_events_manual_ack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + int eventReceivedCount = 0; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + TaskCompletionSource eventsReceived = new(); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, new[] { e }); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + + if (Interlocked.Increment(ref eventReceivedCount) == events.Length) + eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + eventsReceived.TrySetException(e); + }, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + await eventsReceived.Task.WithTimeout(); + } + + [RetryFact] + public async Task happy_case_writing_and_subscribing_to_normal_events_manual_ack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + int eventReceivedCount = 0; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + TaskCompletionSource eventsReceived = new(); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.End, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, retryCount, ct) => { + await subscription.Ack(e); + if (Interlocked.Increment(ref eventReceivedCount) == events.Length) + eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + eventsReceived.TrySetException(e); + }, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await eventsReceived.Task.WithTimeout(); + } + + [RetryFact] + public async Task update_existing_with_check_point() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + StreamPosition checkPoint = default; + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> droppedSource = new(); + TaskCompletionSource resumedSource = new(); + TaskCompletionSource appeared = new(); + List appearedEvents = []; + EventData[] events = Fixture.CreateTestEvents(5).ToArray(); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new( + checkPointLowerBound: 5, + checkPointAfter: TimeSpan.FromSeconds(1), + startFrom: StreamPosition.Start + ), + userCredentials: TestCredentials.Root + ); + + var checkPointStream = $"$persistentsubscription-{stream}::{group}-checkpoint"; + + await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (s, e, _, _) => { + appearedEvents.Add(e); + await s.Ack(e); + + if (appearedEvents.Count == events.Length) + appeared.TrySetResult(true); + }, + (_, reason, ex) => droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + await Task.WhenAll(appeared.Task.WithTimeout(), Checkpointed()); + + // Force restart of the subscription + await Fixture.Subscriptions.UpdateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await droppedSource.Task.WithTimeout(); + + await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (s, e, _, _) => { + resumedSource.TrySetResult(e); + await s.Ack(e); + }, + (_, reason, ex) => { + if (ex is not null) + resumedSource.TrySetException(ex); + else + resumedSource.TrySetResult(default); + }, + TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents(1)); + + var resumedEvent = await resumedSource.Task.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(checkPoint.Next(), resumedEvent.Event.EventNumber); + + return; + + async Task Checkpointed() { + await using var subscription = Fixture.Streams.SubscribeToStream( + checkPointStream, + FromStream.Start, + userCredentials: TestCredentials.Root + ); + + await foreach (var message in subscription.Messages) { + if (message is not StreamMessage.Event(var resolvedEvent)) { + continue; + } + + checkPoint = resolvedEvent.Event.Data.ParseStreamPosition(); + return; + } + + throw new InvalidOperationException(); + } + } + + [RetryFact] + public async Task update_existing_with_subscribers() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + TaskCompletionSource<(SubscriptionDroppedReason, Exception?)> droppedSource = new(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, Fixture.CreateTestEvents()); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + delegate { return Task.CompletedTask; }, + (_, reason, ex) => droppedSource.TrySetResult((reason, ex)), + TestCredentials.Root + ); + + await Fixture.Subscriptions.UpdateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + var dropped = droppedSource.Task; + + var (reason, exception) = await dropped.WithTimeout(TimeSpan.FromSeconds(10)); + Assert.Equal(SubscriptionDroppedReason.ServerError, reason); + var ex = Assert.IsType(exception); + + Assert.Equal(stream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [RetryFact] + public async Task when_writing_and_subscribing_to_normal_events_manual_nack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + int eventReceivedCount = 0; + + EventData[] events = Fixture.CreateTestEvents(eventWriteCount) + .ToArray(); + + TaskCompletionSource eventsReceived = new(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + using var subscription = await Fixture.Subscriptions.SubscribeToStreamAsync( + stream, + group, + async (subscription, e, retryCount, ct) => { + await subscription.Nack(PersistentSubscriptionNakEventAction.Park, "fail", e); + + if (Interlocked.Increment(ref eventReceivedCount) == events.Length) + eventsReceived.TrySetResult(true); + }, + (s, r, e) => { + if (e != null) + eventsReceived.TrySetException(e); + }, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await eventsReceived.Task.WithTimeout(); + } +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs new file mode 100644 index 000000000..61eb46eb0 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamConnectToExistingWithStartFromBeginningTests.cs @@ -0,0 +1,33 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamConnectToExistingWithStartFromBeginningTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning_and_no_streamconnect_to_existing_with_start_from_not_set_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + () => subscription.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event).AsTask().WithTimeout() + ); + } +} diff --git a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/get_info_obsolete.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamGetInfoTests.cs similarity index 50% rename from test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/get_info_obsolete.cs rename to test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamGetInfoTests.cs index 34068a937..d0c593078 100644 --- a/test/EventStore.Client.PersistentSubscriptions.Tests/SubscriptionToStream/Obsolete/get_info_obsolete.cs +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamGetInfoTests.cs @@ -1,11 +1,14 @@ -namespace EventStore.Client.PersistentSubscriptions.Tests.SubscriptionToStream.Obsolete; +// ReSharper disable InconsistentNaming -[Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class get_info_obsolete : IClassFixture { - const string GroupName = nameof(get_info_obsolete); - const string StreamName = nameof(get_info_obsolete); +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; - static readonly PersistentSubscriptionSettings _settings = new( +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamGetInfoTests(SubscribeToStreamGetInfoTests.CustomFixture fixture) + : IClassFixture { + static readonly PersistentSubscriptionSettings Settings = new( true, StreamPosition.Start, true, @@ -21,27 +24,18 @@ public class get_info_obsolete : IClassFixture { SystemConsumerStrategies.RoundRobin ); - readonly Fixture _fixture; - - public get_info_obsolete(Fixture fixture) => _fixture = fixture; - public static IEnumerable AllowedUsers() { yield return new object[] { TestCredentials.Root }; - yield return new object[] { TestCredentials.TestUser1 }; } [Theory] [MemberData(nameof(AllowedUsers))] public async Task returns_expected_result(UserCredentials credentials) { - var result = await _fixture.Client.GetInfoToStreamAsync( - StreamName, - GroupName, - userCredentials: credentials - ); + var result = await fixture.Subscriptions.GetInfoToStreamAsync(fixture.Stream, fixture.Group, userCredentials: credentials); - Assert.Equal(StreamName, result.EventSource); - Assert.Equal(GroupName, result.GroupName); - Assert.NotNull(_settings.StartFrom); + Assert.Equal(fixture.Stream, result.EventSource); + Assert.Equal(fixture.Group, result.GroupName); + Assert.NotNull(Settings.StartFrom); Assert.True(result.Stats.TotalItems > 0); Assert.True(result.Stats.OutstandingMessagesCount > 0); Assert.True(result.Stats.AveragePerSecond >= 0); @@ -85,26 +79,26 @@ public async Task returns_expected_result(UserCredentials credentials) { AssertKeyAndValue(connection.ExtraStatistics, PersistentSubscriptionExtraStatistic.NinetyNinePointNinePercent); Assert.NotNull(result.Settings); - Assert.Equal(_settings.StartFrom, result.Settings!.StartFrom); - Assert.Equal(_settings.ResolveLinkTos, result.Settings!.ResolveLinkTos); - Assert.Equal(_settings.ExtraStatistics, result.Settings!.ExtraStatistics); - Assert.Equal(_settings.MessageTimeout, result.Settings!.MessageTimeout); - Assert.Equal(_settings.MaxRetryCount, result.Settings!.MaxRetryCount); - Assert.Equal(_settings.LiveBufferSize, result.Settings!.LiveBufferSize); - Assert.Equal(_settings.ReadBatchSize, result.Settings!.ReadBatchSize); - Assert.Equal(_settings.HistoryBufferSize, result.Settings!.HistoryBufferSize); - Assert.Equal(_settings.CheckPointAfter, result.Settings!.CheckPointAfter); - Assert.Equal(_settings.CheckPointLowerBound, result.Settings!.CheckPointLowerBound); - Assert.Equal(_settings.CheckPointUpperBound, result.Settings!.CheckPointUpperBound); - Assert.Equal(_settings.MaxSubscriberCount, result.Settings!.MaxSubscriberCount); - Assert.Equal(_settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName); + Assert.Equal(Settings.StartFrom, result.Settings!.StartFrom); + Assert.Equal(Settings.ResolveLinkTos, result.Settings!.ResolveLinkTos); + Assert.Equal(Settings.ExtraStatistics, result.Settings!.ExtraStatistics); + Assert.Equal(Settings.MessageTimeout, result.Settings!.MessageTimeout); + Assert.Equal(Settings.MaxRetryCount, result.Settings!.MaxRetryCount); + Assert.Equal(Settings.LiveBufferSize, result.Settings!.LiveBufferSize); + Assert.Equal(Settings.ReadBatchSize, result.Settings!.ReadBatchSize); + Assert.Equal(Settings.HistoryBufferSize, result.Settings!.HistoryBufferSize); + Assert.Equal(Settings.CheckPointAfter, result.Settings!.CheckPointAfter); + Assert.Equal(Settings.CheckPointLowerBound, result.Settings!.CheckPointLowerBound); + Assert.Equal(Settings.CheckPointUpperBound, result.Settings!.CheckPointUpperBound); + Assert.Equal(Settings.MaxSubscriberCount, result.Settings!.MaxSubscriberCount); + Assert.Equal(Settings.ConsumerStrategyName, result.Settings!.ConsumerStrategyName); } - [Fact] + [RetryFact] public async Task throws_when_given_non_existing_subscription() => await Assert.ThrowsAsync( async () => { - await _fixture.Client.GetInfoToStreamAsync( + await fixture.Subscriptions.GetInfoToStreamAsync( "NonExisting", "NonExisting", userCredentials: TestCredentials.Root @@ -113,87 +107,105 @@ await _fixture.Client.GetInfoToStreamAsync( ); [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] - public async Task throws_with_non_existing_user() => + public async Task throws_with_non_existing_user() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + var stream = $"NonExisting-{fixture.GetStreamName()}"; + await Assert.ThrowsAsync( async () => { - await _fixture.Client.GetInfoToStreamAsync( - "NonExisting", - "NonExisting", + await fixture.Subscriptions.GetInfoToStreamAsync( + stream, + group, userCredentials: TestCredentials.TestBadUser ); } ); + } + + [RetryFact] + public async Task throws_with_no_credentials() { + var group = $"NonExisting-{fixture.GetGroupName()}"; + var stream = $"NonExisting-{fixture.GetStreamName()}"; - [Fact] - public async Task throws_with_no_credentials() => await Assert.ThrowsAsync( async () => { - await _fixture.Client.GetInfoToStreamAsync( - "NonExisting", - "NonExisting" + await fixture.Subscriptions.GetInfoToStreamAsync( + stream, + group ); } ); + } - [Fact] + [RetryFact] public async Task returns_result_for_normal_user() { - var result = await _fixture.Client.GetInfoToStreamAsync( - StreamName, - GroupName, - userCredentials: TestCredentials.TestUser1 + var result = await fixture.Subscriptions.GetInfoToStreamAsync( + fixture.Stream, + fixture.Group, + userCredentials: TestCredentials.Root ); Assert.NotNull(result); } - void AssertKeyAndValue(IDictionary items, string key) { - Assert.True(items.ContainsKey(key)); - Assert.True(items[key] > 0); - } + public class CustomFixture : KurrentTemporaryFixture { + public string Group { get; set; } + public string Stream { get; set; } - public class Fixture : EventStoreClientFixture { - public Fixture() : base(noDefaultCredentials: true) { } - - protected override Task Given() => - Client.CreateToStreamAsync( - groupName: GroupName, - streamName: StreamName, - settings: _settings, - userCredentials: TestCredentials.Root - ); - - protected override async Task When() { - var counter = 0; - var tcs = new TaskCompletionSource(); - - await Client.SubscribeToStreamAsync( - StreamName, - GroupName, - (s, e, r, ct) => { - counter++; - - if (counter == 1) - s.Nack(PersistentSubscriptionNakEventAction.Park, "Test", e); + KurrentPersistentSubscriptionsClient.PersistentSubscriptionResult? Subscription; + IAsyncEnumerator? Enumerator; - if (counter > 10) - tcs.TrySetResult(); + public CustomFixture() : base(x => x.WithoutDefaultCredentials()) { + Group = GetGroupName(); + Stream = GetStreamName(); - return Task.CompletedTask; - }, - userCredentials: TestCredentials.Root - ); - - for (var i = 0; i < 15; i++) - await StreamsClient.AppendToStreamAsync( - StreamName, - StreamState.Any, - new[] { - new EventData(Uuid.NewUuid(), "test-event", ReadOnlyMemory.Empty) - }, + OnSetup += async () => { + await Subscriptions.CreateToStreamAsync( + groupName: Group, + streamName: Stream, + settings: Settings, userCredentials: TestCredentials.Root ); - await tcs.Task; + var counter = 0; + Subscription = Subscriptions.SubscribeToStream(Stream, Group, userCredentials: TestCredentials.Root); + Enumerator = Subscription.Messages.GetAsyncEnumerator(); + + for (var i = 0; i < 15; i++) { + await Streams.AppendToStreamAsync( + Stream, + StreamState.Any, + [new EventData(Uuid.NewUuid(), "test-event", ReadOnlyMemory.Empty)], + userCredentials: TestCredentials.Root + ); + } + + while (await Enumerator.MoveNextAsync()) { + if (Enumerator.Current is not PersistentSubscriptionMessage.Event(var resolvedEvent, _)) { + continue; + } + + counter++; + + if (counter == 1) { + await Subscription.Nack(PersistentSubscriptionNakEventAction.Park, "Test", resolvedEvent); + } + + if (counter > 10) { + return; + } + } + }; + + OnTearDown += async () => { + if (Enumerator is not null) await Enumerator.DisposeAsync(); + if (Subscription is not null) await Subscription.DisposeAsync(); + }; } + }; + + void AssertKeyAndValue(IDictionary items, string key) { + Assert.True(items.ContainsKey(key)); + Assert.True(items[key] > 0); } } diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamListTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamListTests.cs new file mode 100644 index 000000000..e27f4eee2 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamListTests.cs @@ -0,0 +1,48 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamListTests(ITestOutputHelper output, SubscribeToStreamListTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] + public async Task throws_with_no_credentials() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int streamSubscriptionCount = 4; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync(async () => await Fixture.Subscriptions.ListToStreamAsync(stream)); + } + + [RetryFact] + public async Task throws_with_non_existing_user() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int streamSubscriptionCount = 4; + + for (var i = 0; i < streamSubscriptionCount; i++) + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group + i, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => await Fixture.Subscriptions.ListToStreamAsync(stream, userCredentials: TestCredentials.TestBadUser) + ); + } + + public class CustomFixture() : KurrentTemporaryFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamNoDefaultCredentialsTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamNoDefaultCredentialsTests.cs new file mode 100644 index 000000000..b1889475c --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamNoDefaultCredentialsTests.cs @@ -0,0 +1,80 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamNoDefaultCredentialsTests(ITestOutputHelper output, SubscribeToStreamNoDefaultCredentialsTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task connect_to_existing_without_permissions() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + async () => { + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group); + await subscription.Messages.AnyAsync(); + } + ).WithTimeout(); + } + + [RetryFact] + public async Task create_without_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new() + ) + ); + } + + [RetryFact] + public async Task deleting_without_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Assert.ThrowsAsync(() => Fixture.Subscriptions.DeleteToStreamAsync(stream, group)); + } + + [RetryFact] + public async Task update_existing_without_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + Fixture.CreateTestEvents(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.UpdateToStreamAsync( + stream, + group, + new() + ) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamReplayParkedTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamReplayParkedTests.cs new file mode 100644 index 000000000..fffe7b154 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamReplayParkedTests.cs @@ -0,0 +1,71 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamReplayParkedTests(ITestOutputHelper output, SubscribeToStreamReplayParkedTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task does_not_throw() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Subscriptions.ReplayParkedMessagesToStreamAsync( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.ReplayParkedMessagesToStreamAsync( + stream, + group, + 100, + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task throws_with_no_credentials() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToStreamAsync(stream, group) + ); + } + + [Fact(Skip = "Unable to produce same behavior with HTTP fallback!")] + public async Task throws_with_non_existing_user() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.ReplayParkedMessagesToStreamAsync(stream, group, userCredentials: TestCredentials.TestBadUser) + ); + } + + [RetryFact] + public async Task throws_with_normal_user_credentials() { + var group = Fixture.GetGroupName(); + var stream = Fixture.GetStreamName(); + var user = Fixture.GetUserCredentials(); + + await Fixture.Users + .CreateUserWithRetry(user.Username!, user.Username!, [], user.Password!, TestCredentials.Root) + .WithTimeout(); + + await Assert.ThrowsAsync( + () => + Fixture.Subscriptions.ReplayParkedMessagesToStreamAsync(stream, group, userCredentials: user) + ); + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamTests.cs b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamTests.cs new file mode 100644 index 000000000..7fc9779f5 --- /dev/null +++ b/test/Kurrent.Client.Tests/PersistentSubscriptions/SubscribeToStream/SubscribeToStreamTests.cs @@ -0,0 +1,720 @@ +using System.Text; +using EventStore.Client; +using Grpc.Core; + +namespace Kurrent.Client.Tests.PersistentSubscriptions; + +[Trait("Category", "Target:PersistentSubscriptions")] +public class SubscribeToStreamTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task can_create_duplicate_name_on_different_streams() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.CreateToStreamAsync( + "someother" + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task connect_to_existing_with_max_one_client() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(maxSubscriberCount: 1), + userCredentials: TestCredentials.Root + ); + + var ex = await Assert.ThrowsAsync(() => Task.WhenAll(Subscribe().WithTimeout(), Subscribe().WithTimeout())); + + Assert.Equal(stream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + return; + + async Task Subscribe() { + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + await subscription.Messages.AnyAsync(); + } + } + + [RetryFact] + public async Task connect_to_existing_with_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + Assert.True(await subscription.Messages.FirstAsync().AsTask().WithTimeout() is PersistentSubscriptionMessage.SubscriptionConfirmation); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning_and_events_in_it() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(10).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); + Assert.Equal(events[0].EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_beginning_and_no_stream() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents().ToArray(); + var eventId = events.Single().EventId; + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + var resolvedEvent = await subscription!.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(StreamPosition.Start, resolvedEvent.Event.EventNumber); + Assert.Equal(eventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_not_set_and_events_in_it_then_event_written() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(11).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position_and_events_in_it() { + var events = Fixture.CreateTestEvents(10).ToArray(); + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.End), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Assert.ThrowsAsync( + () => subscription.Messages.AnyAsync(message => message is PersistentSubscriptionMessage.Event).AsTask().WithTimeout(TimeSpan.FromMilliseconds(250)) + ); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_set_to_end_position_and_events_in_it_then_event_written() { + var events = Fixture.CreateTestEvents(11).ToArray(); + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.End), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.TestUser1); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_two_and_no_stream() { + var events = Fixture.CreateTestEvents(3).ToArray(); + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + var eventId = events.Last().EventId; + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(2)), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(new(2), resolvedEvent.Event.EventNumber); + Assert.Equal(eventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_x_set_and_events_in_it() { + var events = Fixture.CreateTestEvents(10).ToArray(); + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(4)), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var resolvedEvent = await subscription.Messages + .OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(new(4), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Skip(4).First().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_x_set_and_events_in_it_then_event_written() { + var events = Fixture.CreateTestEvents(11).ToArray(); + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(10)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(10)), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(9), events.Skip(10)); + + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(new(10), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_existing_with_start_from_x_set_higher_than_x_and_events_in_it_then_event_written() { + var events = Fixture.CreateTestEvents(12).ToArray(); + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(11)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(11)), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(10), events.Skip(11)); + + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(new(11), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task connect_to_non_existing_with_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + Assert.True( + await subscription.Messages.OfType() + .AnyAsync() + .AsTask() + .WithTimeout() + ); + } + + [RetryFact] + public async Task connect_with_retries() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents().ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + var retryCount = await subscription.Messages.OfType() + .SelectAwait( + async e => { + if (e.RetryCount > 4) { + await subscription.Ack(e.ResolvedEvent); + } else { + await subscription.Nack( + PersistentSubscriptionNakEventAction.Retry, + "Not yet tried enough times", + e.ResolvedEvent + ); + } + + return e.RetryCount; + } + ) + .Where(retryCount => retryCount > 4) + .FirstOrDefaultAsync() + .AsTask() + .WithTimeout(); + + Assert.Equal(5, retryCount); + } + + [RetryFact] + public async Task connecting_to_a_persistent_subscription() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + var events = Fixture.CreateTestEvents(12).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(11)); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: new StreamPosition(11)), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(10), events.Skip(11)); + + var resolvedEvent = await subscription.Messages.OfType() + .Select(e => e.ResolvedEvent) + .FirstOrDefaultAsync().AsTask().WithTimeout(); + + Assert.Equal(new(11), resolvedEvent.Event.EventNumber); + Assert.Equal(events.Last().EventId, resolvedEvent.Event.EventId); + } + + [RetryFact] + public async Task create_after_deleting_the_same() { + var stream = Fixture.GetStreamName(); + var group = $"existing-{Fixture.GetGroupName()}"; + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents()); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.DeleteToStreamAsync( + stream, + group, + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task create_duplicate() { + var stream = Fixture.GetStreamName(); + var group = $"duplicate-{Fixture.GetGroupName()}"; + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + var ex = await Assert.ThrowsAsync( + () => Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ) + ); + + Assert.Equal(StatusCode.AlreadyExists, ex.StatusCode); + } + + [RetryFact] + public async Task create_on_existing_stream() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents()); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task create_on_non_existing_stream() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task create_with_dont_timeout() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(messageTimeout: TimeSpan.Zero), + userCredentials: TestCredentials.Root + ); + } + + [RetryFact] + public async Task deleting_existing_with_permissions() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + await Fixture.Subscriptions.DeleteToStreamAsync(stream, group, userCredentials: TestCredentials.Root); + } + + [RetryFact] + public async Task deleting_existing_with_subscriber() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await Fixture.Subscriptions.DeleteToStreamAsync(stream, group, userCredentials: TestCredentials.Root); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + + Assert.True( + await subscription.Messages.OfType().AnyAsync() + .AsTask() + .WithTimeout() + ); + } + + [RetryFact] + public async Task deleting_nonexistent() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.DeleteToStreamAsync(stream, group, userCredentials: TestCredentials.Root) + ); + } + + [RetryFact] + public async Task happy_case_catching_up_to_link_to_events_manual_ack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount) + .Select( + (e, i) => new EventData( + e.EventId, + SystemEventTypes.LinkTo, + Encoding.UTF8.GetBytes($"{i}@{stream}"), + contentType: Constants.Metadata.ContentTypes.ApplicationOctetStream + ) + ).ToArray(); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + await subscription!.Messages.OfType() + .Take(events.Length) + .ForEachAwaitAsync(e => subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } + + [RetryFact] + public async Task happy_case_catching_up_to_normal_events_manual_ack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, new[] { e }); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + await subscription!.Messages.OfType() + .Take(events.Length) + .ForEachAwaitAsync(e => subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } + + [RetryFact] + public async Task happy_case_writing_and_subscribing_to_normal_events_manual_ack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.End, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream( + stream, + group, + bufferSize: bufferCount, + userCredentials: TestCredentials.Root + ); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await subscription.Messages.OfType() + .Take(events.Length) + .ForEachAwaitAsync(e => subscription.Ack(e.ResolvedEvent)) + .WithTimeout(); + } + + [RetryFact] + public async Task list_without_persistent_subscriptions_should_throw() { + var stream = Fixture.GetStreamName(); + + await Assert.ThrowsAsync( + async () => + await Fixture.Subscriptions.ListToStreamAsync(stream, userCredentials: TestCredentials.Root) + ); + } + + [RetryFact] + public async Task update_existing() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, Fixture.CreateTestEvents()); + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ); + + await Fixture.Subscriptions.UpdateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + } + + [RetryFact] + public async Task update_existing_with_subscribers() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, Fixture.CreateTestEvents()); + await Fixture.Subscriptions.CreateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, userCredentials: TestCredentials.Root); + await using var enumerator = subscription.Messages.GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + + await Fixture.Subscriptions.UpdateToStreamAsync(stream, group, new(), userCredentials: TestCredentials.Root); + + var ex = await Assert.ThrowsAsync( + async () => { + while (await enumerator.MoveNextAsync()) { } + } + ).WithTimeout(); + + Assert.Equal(stream, ex.StreamName); + Assert.Equal(group, ex.GroupName); + } + + [Regression.Fact(21, "20.x returns the wrong exception")] + public async Task update_non_existent() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + await Assert.ThrowsAsync( + () => Fixture.Subscriptions.UpdateToStreamAsync( + stream, + group, + new(), + userCredentials: TestCredentials.Root + ) + ); + } + + [RetryFact] + public async Task when_writing_and_subscribing_to_normal_events_manual_nack() { + var stream = Fixture.GetStreamName(); + var group = Fixture.GetGroupName(); + + const int bufferCount = 10; + const int eventWriteCount = bufferCount * 2; + + var events = Fixture.CreateTestEvents(eventWriteCount).ToArray(); + + await Fixture.Subscriptions.CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start, resolveLinkTos: true), + userCredentials: TestCredentials.Root + ); + + await using var subscription = Fixture.Subscriptions.SubscribeToStream(stream, group, bufferSize: bufferCount, userCredentials: TestCredentials.Root); + + foreach (var e in events) + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, [e]); + + await subscription.Messages.OfType() + .Take(1) + .ForEachAwaitAsync( + async message => + await subscription.Nack( + PersistentSubscriptionNakEventAction.Park, + "fail", + message.ResolvedEvent + ) + ) + .WithTimeout(); + } +} diff --git a/test/EventStore.Client.Tests/PositionTests.cs b/test/Kurrent.Client.Tests/PositionTests.cs similarity index 95% rename from test/EventStore.Client.Tests/PositionTests.cs rename to test/Kurrent.Client.Tests/PositionTests.cs index b68748af8..fc4401267 100644 --- a/test/EventStore.Client.Tests/PositionTests.cs +++ b/test/Kurrent.Client.Tests/PositionTests.cs @@ -1,11 +1,13 @@ using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class PositionTests : ValueObjectTests { public PositionTests() : base(new ScenarioFixture()) { } - [Fact] + [RetryFact] public void IsComparable() => Assert.IsAssignableFrom>(_fixture.Create()); [Theory] @@ -16,7 +18,7 @@ public PositionTests() : base(new ScenarioFixture()) { } [AutoScenarioData(typeof(ScenarioFixture))] public void LiveIsGreaterThanAll(Position other) => Assert.True(Position.End > other); - [Fact] + [RetryFact] public void ToStringReturnsExpectedResult() { var sut = _fixture.Create(); Assert.Equal($"C:{sut.CommitPosition}/P:{sut.PreparePosition}", sut.ToString()); @@ -60,4 +62,4 @@ public void TryParse(string s, bool success, Position? expected) { class ScenarioFixture : Fixture { public ScenarioFixture() => Customize(composer => composer.FromFactory(value => new(value, value))); } -} \ No newline at end of file +} diff --git a/test/Kurrent.Client.Tests/ProjectionManagement/DisableProjectionTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/DisableProjectionTests.cs new file mode 100644 index 000000000..15405cdcc --- /dev/null +++ b/test/Kurrent.Client.Tests/ProjectionManagement/DisableProjectionTests.cs @@ -0,0 +1,22 @@ +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +[Trait("Category", "Target:ProjectionManagement")] +public class DisableProjectionTests(ITestOutputHelper output, DisableProjectionTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task disable_projection() { + var name = Names.First(); + await Fixture.Projections.DisableAsync(name, userCredentials: TestCredentials.Root); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + Assert.NotNull(result); + Assert.Contains(["Aborted/Stopped", "Stopped"], x => x == result!.Status); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/ProjectionManagement/EnableProjectionTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/EnableProjectionTests.cs new file mode 100644 index 000000000..7261bfb9f --- /dev/null +++ b/test/Kurrent.Client.Tests/ProjectionManagement/EnableProjectionTests.cs @@ -0,0 +1,22 @@ +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +[Trait("Category", "Target:ProjectionManagement")] +public class EnableProjectionTests(ITestOutputHelper output, EnableProjectionTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task enable_projection() { + var name = Names.First(); + await Fixture.Projections.EnableAsync(name, userCredentials: TestCredentials.Root); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + Assert.NotNull(result); + Assert.Equal("Running", result.Status); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionResultTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionResultTests.cs new file mode 100644 index 000000000..2b2c8ef08 --- /dev/null +++ b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionResultTests.cs @@ -0,0 +1,51 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +[Trait("Category", "Target:ProjectionManagement")] +public class GetProjectionResultTests(ITestOutputHelper output, GetProjectionResultTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task get_result() { + var name = Fixture.GetProjectionName(); + Result? result = null; + + var projection = $$""" + fromStream('{{name}}').when({ + "$init": function() { return { Count: 0 }; }, + "$any": function(s, e) { s.Count++; return s; } + }); + """; + + await Fixture.Projections.CreateContinuousAsync( + name, + projection, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync( + name, + StreamState.NoStream, + Fixture.CreateTestEvents() + ); + + await AssertEx.IsOrBecomesTrue( + async () => { + result = await Fixture.Projections.GetResultAsync(name, userCredentials: TestCredentials.Root); + return result.Count > 0; + } + ); + + Assert.NotNull(result); + Assert.Equal(1, result!.Count); + } + + record Result { + public int Count { get; set; } + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStateTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStateTests.cs new file mode 100644 index 000000000..f180813ff --- /dev/null +++ b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStateTests.cs @@ -0,0 +1,52 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +[Trait("Category", "Target:ProjectionManagement")] +public class GetProjectionStateTests(ITestOutputHelper output, GetProjectionStateTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task get_state() { + var name = Fixture.GetProjectionName(); + + var projection = $$""" + fromStream('{{name}}').when({ + "$init": function() { return { Count: 0 }; }, + "$any": function(s, e) { s.Count++; return s; } + }); + """; + + Result? result = null; + + await Fixture.Projections.CreateContinuousAsync( + name, + projection, + userCredentials: TestCredentials.Root + ); + + await Fixture.Streams.AppendToStreamAsync( + name, + StreamState.NoStream, + Fixture.CreateTestEvents() + ); + + await AssertEx.IsOrBecomesTrue( + async () => { + result = await Fixture.Projections.GetStateAsync(name, userCredentials: TestCredentials.Root); + return result.Count > 0; + } + ); + + Assert.NotNull(result); + Assert.Equal(1, result!.Count); + } + + record Result { + public int Count { get; set; } + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStatusTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStatusTests.cs new file mode 100644 index 000000000..41ed02932 --- /dev/null +++ b/test/Kurrent.Client.Tests/ProjectionManagement/GetProjectionStatusTests.cs @@ -0,0 +1,22 @@ +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +[Trait("Category", "Target:ProjectionManagement")] +public class GetProjectionStatusTests(ITestOutputHelper output, GetProjectionStatusTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task get_status() { + var name = Names.First(); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + + Assert.NotNull(result); + Assert.Equal(name, result.Name); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/ProjectionManagement/ListAllProjectionsTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ListAllProjectionsTests.cs new file mode 100644 index 000000000..18f717efc --- /dev/null +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ListAllProjectionsTests.cs @@ -0,0 +1,36 @@ +// ReSharper disable InconsistentNaming + +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:ProjectionManagement")] +public class ListAllProjectionsTests(ITestOutputHelper output, ListAllProjectionsTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task list_continuous_projections() { + var name = Fixture.GetProjectionName(); + + await Fixture.Projections.CreateContinuousAsync( + name, + "fromAll().when({$init: function (state, ev) {return {};}});", + userCredentials: TestCredentials.Root + ); + + var result = await Fixture.Projections.ListContinuousAsync(userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + Assert.Equal( + result.Select(x => x.Name).OrderBy(x => x), + Names.Concat([name]).OrderBy(x => x) + ); + + Assert.True(result.All(x => x.Mode == "Continuous")); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/ProjectionManagement/ListContinuousProjectionsTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ListContinuousProjectionsTests.cs new file mode 100644 index 000000000..774ad5a0f --- /dev/null +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ListContinuousProjectionsTests.cs @@ -0,0 +1,36 @@ +// ReSharper disable InconsistentNaming + +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:ProjectionManagement")] +public class ListContinuousProjectionsTests(ITestOutputHelper output, ListContinuousProjectionsTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task list_continuous_projections() { + var name = Fixture.GetProjectionName(); + + await Fixture.Projections.CreateContinuousAsync( + name, + "fromAll().when({$init: function (state, ev) {return {};}});", + userCredentials: TestCredentials.Root + ); + + var result = await Fixture.Projections.ListContinuousAsync(userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + Assert.Equal( + result.Select(x => x.Name).OrderBy(x => x), + Names.Concat([name]).OrderBy(x => x) + ); + + Assert.True(result.All(x => x.Mode == "Continuous")); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/ProjectionManagement/ListOneTimeProjectionsTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ListOneTimeProjectionsTests.cs new file mode 100644 index 000000000..d664d24fa --- /dev/null +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ListOneTimeProjectionsTests.cs @@ -0,0 +1,22 @@ +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +[Trait("Category", "Target:ProjectionManagement")] +public class ListOneTimeProjectionsTests(ITestOutputHelper output, ListOneTimeProjectionsTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task list_one_time_projections() { + await Fixture.Projections.CreateOneTimeAsync("fromAll().when({$init: function (state, ev) {return {};}});", userCredentials: TestCredentials.Root); + + var result = await Fixture.Projections.ListOneTimeAsync(userCredentials: TestCredentials.Root) + .ToArrayAsync(); + + var details = Assert.Single(result); + Assert.Equal("OneTime", details.Mode); + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/ProjectionManagement/ProjectionManagementTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ProjectionManagementTests.cs new file mode 100644 index 000000000..fd8d9b42a --- /dev/null +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ProjectionManagementTests.cs @@ -0,0 +1,54 @@ +// ReSharper disable InconsistentNaming +// ReSharper disable ClassNeverInstantiated.Local + +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:ProjectionManagement")] +public class ProjectionManagementTests(ITestOutputHelper output, ProjectionManagementTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task status_is_aborted() { + var name = Names.First(); + await Fixture.Projections.AbortAsync(name, userCredentials: TestCredentials.Root); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + Assert.NotNull(result); + Assert.Contains(["Aborted/Stopped", "Stopped"], x => x == result.Status); + } + + [Fact] + public async Task one_time() => + await Fixture.Projections.CreateOneTimeAsync("fromAll().when({$init: function (state, ev) {return {};}});", userCredentials: TestCredentials.Root); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task continuous(bool trackEmittedStreams) { + var name = Fixture.GetProjectionName(); + + await Fixture.Projections.CreateContinuousAsync( + name, + "fromAll().when({$init: function (state, ev) {return {};}});", + trackEmittedStreams, + userCredentials: TestCredentials.Root + ); + } + + [Fact] + public async Task transient() { + var name = Fixture.GetProjectionName(); + + await Fixture.Projections.CreateTransientAsync( + name, + "fromAll().when({$init: function (state, ev) {return {};}});", + userCredentials: TestCredentials.Root + ); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/ProjectionManagement/ResetProjectionTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/ResetProjectionTests.cs new file mode 100644 index 000000000..71d5f8fcc --- /dev/null +++ b/test/Kurrent.Client.Tests/ProjectionManagement/ResetProjectionTests.cs @@ -0,0 +1,23 @@ +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +[Trait("Category", "Target:ProjectionManagement")] +public class ResetProjectionTests(ITestOutputHelper output, ResetProjectionTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task reset_projection() { + var name = Names.First(); + await Fixture.Projections.ResetAsync(name, userCredentials: TestCredentials.Root); + var result = await Fixture.Projections.GetStatusAsync(name, userCredentials: TestCredentials.Root); + + Assert.NotNull(result); + Assert.Equal("Running", result.Status); + } + + static readonly string[] Names = ["$streams", "$stream_by_category", "$by_category", "$by_event_type", "$by_correlation_id"]; + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/ProjectionManagement/RestartSubsystemTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/RestartSubsystemTests.cs new file mode 100644 index 000000000..6431fbee5 --- /dev/null +++ b/test/Kurrent.Client.Tests/ProjectionManagement/RestartSubsystemTests.cs @@ -0,0 +1,20 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +[Trait("Category", "Target:ProjectionManagement")] +public class RestartSubsystemTests(ITestOutputHelper output, RestartSubsystemTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Fact] + public async Task restart_subsystem_does_not_throw() => + await Fixture.Projections.RestartSubsystemAsync(userCredentials: TestCredentials.Root); + + [Fact] + public async Task restart_subsystem_throws_when_given_no_credentials() => + await Assert.ThrowsAsync(() => Fixture.Projections.RestartSubsystemAsync(userCredentials: TestCredentials.TestUser1)); + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/Kurrent.Client.Tests/ProjectionManagement/UpdateProjectionTests.cs b/test/Kurrent.Client.Tests/ProjectionManagement/UpdateProjectionTests.cs new file mode 100644 index 000000000..1af8e880b --- /dev/null +++ b/test/Kurrent.Client.Tests/ProjectionManagement/UpdateProjectionTests.cs @@ -0,0 +1,31 @@ +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests.Projections; + +[Trait("Category", "Target:ProjectionManagement")] +public class UpdateProjectionTests(ITestOutputHelper output, UpdateProjectionTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [Theory] + [InlineData(true)] + [InlineData(false)] + [InlineData(null)] + public async Task update_projection(bool? emitEnabled) { + var name = Fixture.GetProjectionName(); + await Fixture.Projections.CreateContinuousAsync( + name, + "fromAll().when({$init: function (state, ev) {return {};}});", + userCredentials: TestCredentials.Root + ); + + await Fixture.Projections.UpdateAsync( + name, + "fromAll().when({$init: function (s, e) {return {};}});", + emitEnabled, + userCredentials: TestCredentials.Root + ); + } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { } + } +} diff --git a/test/EventStore.Client.Tests/RegularFilterExpressionTests.cs b/test/Kurrent.Client.Tests/RegularFilterExpressionTests.cs similarity index 80% rename from test/EventStore.Client.Tests/RegularFilterExpressionTests.cs rename to test/Kurrent.Client.Tests/RegularFilterExpressionTests.cs index 481e5cb46..4fe4f6650 100644 --- a/test/EventStore.Client.Tests/RegularFilterExpressionTests.cs +++ b/test/Kurrent.Client.Tests/RegularFilterExpressionTests.cs @@ -1,12 +1,14 @@ using System.Text.RegularExpressions; using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class RegularFilterExpressionTests : ValueObjectTests { public RegularFilterExpressionTests() : base(new ScenarioFixture()) { } class ScenarioFixture : Fixture { public ScenarioFixture() => Customize(composer => composer.FromFactory(value => new(value))); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/all_stream_with_no_acl_security.cs b/test/Kurrent.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs similarity index 88% rename from test/EventStore.Client.Streams.Tests/Security/all_stream_with_no_acl_security.cs rename to test/Kurrent.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs index 215e0f05d..e33f707ac 100644 --- a/test/EventStore.Client.Streams.Tests/Security/all_stream_with_no_acl_security.cs +++ b/test/Kurrent.Client.Tests/Security/AllStreamWithNoAclSecurityTests.cs @@ -1,7 +1,12 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Security")] -public class all_stream_with_no_acl_security(ITestOutputHelper output, all_stream_with_no_acl_security.CustomFixture fixture) : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class AllStreamWithNoAclSecurityTests(ITestOutputHelper output, AllStreamWithNoAclSecurityTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task write_to_all_is_never_allowed() { await Assert.ThrowsAsync(() => Fixture.AppendStream(SecurityFixture.AllStream)); @@ -59,4 +64,4 @@ protected override async Task Given() { await Streams.SetStreamMetadataAsync(AllStream, StreamState.Any, new(), userCredentials: TestCredentials.Root); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/delete_stream_security.cs b/test/Kurrent.Client.Tests/Security/DeleteStreamSecurityTests.cs similarity index 86% rename from test/EventStore.Client.Streams.Tests/Security/delete_stream_security.cs rename to test/Kurrent.Client.Tests/Security/DeleteStreamSecurityTests.cs index 61e6253ce..69b31609c 100644 --- a/test/EventStore.Client.Streams.Tests/Security/delete_stream_security.cs +++ b/test/Kurrent.Client.Tests/Security/DeleteStreamSecurityTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Security")] -public class delete_stream_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class DeleteStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task delete_of_all_is_never_allowed() { await Assert.ThrowsAsync(() => Fixture.DeleteStream(SecurityFixture.AllStream)); @@ -113,7 +117,7 @@ public async Task deleting_normal_all_stream_with_admin_user_is_allowed() { public async Task deleting_system_no_acl_stream_with_no_user_is_not_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new() + metadataPermanent: new() ); await Assert.ThrowsAsync(() => Fixture.DeleteStream(streamId)); @@ -123,7 +127,7 @@ public async Task deleting_system_no_acl_stream_with_no_user_is_not_allowed() { public async Task deleting_system_no_acl_stream_with_existing_user_is_not_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new() + metadataPermanent: new() ); await Assert.ThrowsAsync(() => Fixture.DeleteStream(streamId, TestCredentials.TestUser1)); @@ -133,7 +137,7 @@ public async Task deleting_system_no_acl_stream_with_existing_user_is_not_allowe public async Task deleting_system_no_acl_stream_with_admin_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new() + metadataPermanent: new() ); await Fixture.DeleteStream(streamId, TestCredentials.TestAdmin); @@ -143,7 +147,7 @@ public async Task deleting_system_no_acl_stream_with_admin_user_is_allowed() { public async Task deleting_system_user_stream_with_no_user_is_not_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) + metadataPermanent: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) ); await Assert.ThrowsAsync(() => Fixture.DeleteStream(streamId)); @@ -153,7 +157,7 @@ public async Task deleting_system_user_stream_with_no_user_is_not_allowed() { public async Task deleting_system_user_stream_with_not_authorized_user_is_not_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) + metadataPermanent: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) ); await Assert.ThrowsAsync(() => Fixture.DeleteStream(streamId, TestCredentials.TestUser2)); @@ -163,7 +167,7 @@ public async Task deleting_system_user_stream_with_not_authorized_user_is_not_al public async Task deleting_system_user_stream_with_authorized_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) + metadataPermanent: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) ); await Fixture.DeleteStream(streamId, TestCredentials.TestUser1); @@ -173,7 +177,7 @@ public async Task deleting_system_user_stream_with_authorized_user_is_allowed() public async Task deleting_system_user_stream_with_admin_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) + metadataPermanent: new(acl: new(deleteRole: TestCredentials.TestUser1.Username)) ); await Fixture.DeleteStream(streamId, TestCredentials.TestAdmin); @@ -183,7 +187,7 @@ public async Task deleting_system_user_stream_with_admin_user_is_allowed() { public async Task deleting_system_admin_stream_with_no_user_is_not_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: SystemRoles.Admins)) + metadataPermanent: new(acl: new(deleteRole: SystemRoles.Admins)) ); await Assert.ThrowsAsync(() => Fixture.DeleteStream(streamId)); @@ -193,7 +197,7 @@ public async Task deleting_system_admin_stream_with_no_user_is_not_allowed() { public async Task deleting_system_admin_stream_with_existing_user_is_not_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: SystemRoles.Admins)) + metadataPermanent: new(acl: new(deleteRole: SystemRoles.Admins)) ); await Assert.ThrowsAsync(() => Fixture.DeleteStream(streamId, TestCredentials.TestUser1)); @@ -203,7 +207,7 @@ public async Task deleting_system_admin_stream_with_existing_user_is_not_allowed public async Task deleting_system_admin_stream_with_admin_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: SystemRoles.Admins)) + metadataPermanent: new(acl: new(deleteRole: SystemRoles.Admins)) ); await Fixture.DeleteStream(streamId, TestCredentials.TestAdmin); @@ -213,7 +217,7 @@ public async Task deleting_system_admin_stream_with_admin_user_is_allowed() { public async Task deleting_system_all_stream_with_no_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: SystemRoles.All)) + metadataPermanent: new(acl: new(deleteRole: SystemRoles.All)) ); await Fixture.DeleteStream(streamId); @@ -223,7 +227,7 @@ public async Task deleting_system_all_stream_with_no_user_is_allowed() { public async Task deleting_system_all_stream_with_existing_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: SystemRoles.All)) + metadataPermanent: new(acl: new(deleteRole: SystemRoles.All)) ); await Fixture.DeleteStream(streamId, TestCredentials.TestUser1); @@ -233,9 +237,9 @@ public async Task deleting_system_all_stream_with_existing_user_is_allowed() { public async Task deleting_system_all_stream_with_admin_user_is_allowed() { var streamId = await Fixture.CreateStreamWithMeta( streamId: $"${Fixture.GetStreamName()}", - metadata: new(acl: new(deleteRole: SystemRoles.All)) + metadataPermanent: new(acl: new(deleteRole: SystemRoles.All)) ); await Fixture.DeleteStream(streamId, TestCredentials.TestAdmin); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/multiple_role_security.cs b/test/Kurrent.Client.Tests/Security/MultipleRoleSecurityTests.cs similarity index 80% rename from test/EventStore.Client.Streams.Tests/Security/multiple_role_security.cs rename to test/Kurrent.Client.Tests/Security/MultipleRoleSecurityTests.cs index 3195c22df..f38d3fd27 100644 --- a/test/EventStore.Client.Streams.Tests/Security/multiple_role_security.cs +++ b/test/Kurrent.Client.Tests/Security/MultipleRoleSecurityTests.cs @@ -1,7 +1,12 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Security")] -public class multiple_role_security(ITestOutputHelper output, multiple_role_security.CustomFixture fixture) : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class MultipleRoleSecurityTests(ITestOutputHelper output, MultipleRoleSecurityTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task multiple_roles_are_handled_correctly() { await Assert.ThrowsAsync(() => Fixture.ReadEvent("usr-stream")); @@ -20,7 +25,7 @@ public async Task multiple_roles_are_handled_correctly() { } [AnonymousAccess.Fact] - public async Task multiple_roles_are_handled_correctly_without_authentication() => + public async Task multiple_roles_are_handled_correctly_without_authentication() => await Fixture.DeleteStream("usr-stream1"); public class CustomFixture : SecurityFixture { @@ -36,4 +41,4 @@ protected override async Task When() { await Streams.SetSystemSettingsAsync(settings, userCredentials: TestCredentials.TestAdmin); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security_for_all.cs b/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs similarity index 84% rename from test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security_for_all.cs rename to test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs index 2364717bb..ebc66f3c9 100644 --- a/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security_for_all.cs +++ b/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityForAllTests.cs @@ -1,7 +1,12 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Security")] -public class overriden_system_stream_security_for_all(ITestOutputHelper output, overriden_system_stream_security_for_all.CustomFixture fixture) : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class OverridenSystemStreamSecurityForAllTests(ITestOutputHelper output, OverridenSystemStreamSecurityForAllTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task operations_on_system_stream_succeeds_for_user() { var stream = $"${Fixture.GetStreamName()}"; diff --git a/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security.cs b/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs similarity index 90% rename from test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security.cs rename to test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs index fcdaec541..eded40e7e 100644 --- a/test/EventStore.Client.Streams.Tests/Security/overriden_system_stream_security.cs +++ b/test/Kurrent.Client.Tests/Security/OverridenSystemStreamSecurityTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -[Trait("Category", "Security")] -public class overriden_system_stream_security(ITestOutputHelper output, overriden_system_stream_security.CustomFixture fixture) : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class OverridenSystemStreamSecurityTests(ITestOutputHelper output, OverridenSystemStreamSecurityTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task operations_on_system_stream_succeed_for_authorized_user() { var stream = $"${Fixture.GetStreamName()}"; diff --git a/test/EventStore.Client.Streams.Tests/Security/overriden_user_stream_security.cs b/test/Kurrent.Client.Tests/Security/OverridenUserStreamSecurityTests.cs similarity index 90% rename from test/EventStore.Client.Streams.Tests/Security/overriden_user_stream_security.cs rename to test/Kurrent.Client.Tests/Security/OverridenUserStreamSecurityTests.cs index 287d5a64f..445bf564b 100644 --- a/test/EventStore.Client.Streams.Tests/Security/overriden_user_stream_security.cs +++ b/test/Kurrent.Client.Tests/Security/OverridenUserStreamSecurityTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -[Trait("Category", "Security")] -public class overriden_user_stream_security(ITestOutputHelper output, overriden_user_stream_security.CustomFixture fixture) : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class OverridenUserStreamSecurityTests(ITestOutputHelper output, OverridenUserStreamSecurityTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task operations_on_user_stream_succeeds_for_authorized_user() { var stream = Fixture.GetStreamName(); diff --git a/test/EventStore.Client.Streams.Tests/Security/read_all_security.cs b/test/Kurrent.Client.Tests/Security/ReadAllSecurityTests.cs similarity index 80% rename from test/EventStore.Client.Streams.Tests/Security/read_all_security.cs rename to test/Kurrent.Client.Tests/Security/ReadAllSecurityTests.cs index 01089db2e..fcbf0980f 100644 --- a/test/EventStore.Client.Streams.Tests/Security/read_all_security.cs +++ b/test/Kurrent.Client.Tests/Security/ReadAllSecurityTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Security")] -public class read_all_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class ReadAllSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task reading_all_with_not_existing_credentials_is_not_authenticated() { await Assert.ThrowsAsync(() => Fixture.ReadAllForward(TestCredentials.TestBadUser)); @@ -31,4 +35,4 @@ public async Task reading_all_with_admin_credentials_succeeds() { await Fixture.ReadAllForward(TestCredentials.TestAdmin); await Fixture.ReadAllBackward(TestCredentials.TestAdmin); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/read_stream_meta_security.cs b/test/Kurrent.Client.Tests/Security/ReadStreamMetaSecurityTests.cs similarity index 72% rename from test/EventStore.Client.Streams.Tests/Security/read_stream_meta_security.cs rename to test/Kurrent.Client.Tests/Security/ReadStreamMetaSecurityTests.cs index b3a7e7afa..4bdb20689 100644 --- a/test/EventStore.Client.Streams.Tests/Security/read_stream_meta_security.cs +++ b/test/Kurrent.Client.Tests/Security/ReadStreamMetaSecurityTests.cs @@ -1,12 +1,14 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Security")] -public class read_stream_meta_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class ReadStreamMetaSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task reading_stream_meta_with_not_existing_credentials_is_not_authenticated() => - await Assert.ThrowsAsync( - () => Fixture.ReadMeta(SecurityFixture.MetaReadStream, TestCredentials.TestBadUser) - ); + await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture.MetaReadStream, TestCredentials.TestBadUser)); [Fact] public async Task reading_stream_meta_with_no_credentials_is_denied() => @@ -14,9 +16,7 @@ public async Task reading_stream_meta_with_no_credentials_is_denied() => [Fact] public async Task reading_stream_meta_with_not_authorized_user_credentials_is_denied() => - await Assert.ThrowsAsync( - () => Fixture.ReadMeta(SecurityFixture.MetaReadStream, TestCredentials.TestUser2) - ); + await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture.MetaReadStream, TestCredentials.TestUser2)); [Fact] public async Task reading_stream_meta_with_authorized_user_credentials_succeeds() => @@ -31,9 +31,7 @@ public async Task reading_stream_meta_with_admin_user_credentials_succeeds() => [Fact] public async Task reading_no_acl_stream_meta_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync( - () => Fixture.ReadMeta(SecurityFixture.NoAclStream, TestCredentials.TestBadUser) - ); + await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture.NoAclStream, TestCredentials.TestBadUser)); [Fact] public async Task reading_no_acl_stream_meta_succeeds_when_any_existing_user_credentials_are_passed() { @@ -52,9 +50,7 @@ public async Task reading_all_access_normal_stream_meta_succeeds_when_no_credent [Fact] public async Task reading_all_access_normal_stream_meta_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync( - () => Fixture.ReadMeta(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser) - ); + await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser)); [Fact] public async Task @@ -66,4 +62,4 @@ public async Task [Fact] public async Task reading_all_access_normal_stream_meta_succeeds_when_admin_user_credentials_are_passed() => await Fixture.ReadMeta(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/read_stream_security.cs b/test/Kurrent.Client.Tests/Security/ReadStreamSecurityTests.cs similarity index 95% rename from test/EventStore.Client.Streams.Tests/Security/read_stream_security.cs rename to test/Kurrent.Client.Tests/Security/ReadStreamSecurityTests.cs index 206627128..7c3795a44 100644 --- a/test/EventStore.Client.Streams.Tests/Security/read_stream_security.cs +++ b/test/Kurrent.Client.Tests/Security/ReadStreamSecurityTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Security")] -public class read_stream_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class ReadStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task reading_stream_with_not_existing_credentials_is_not_authenticated() { await Assert.ThrowsAsync(() => Fixture.ReadEvent(SecurityFixture.ReadStream, TestCredentials.TestBadUser)); @@ -26,7 +30,7 @@ public async Task reading_stream_with_not_authorized_user_credentials_is_denied( [Fact] public async Task reading_stream_with_authorized_user_credentials_succeeds() { await Fixture.AppendStream(SecurityFixture.ReadStream, TestCredentials.TestUser1); - + await Fixture.ReadEvent(SecurityFixture.ReadStream, TestCredentials.TestUser1); await Fixture.ReadStreamForward(SecurityFixture.ReadStream, TestCredentials.TestUser1); await Fixture.ReadStreamBackward(SecurityFixture.ReadStream, TestCredentials.TestUser1); @@ -110,4 +114,4 @@ public async Task reading_all_access_normal_stream_succeeds_when_admin_user_cred await Fixture.ReadStreamForward(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin); await Fixture.ReadStreamBackward(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/SecurityFixture.cs b/test/Kurrent.Client.Tests/Security/SecurityFixture.cs similarity index 97% rename from test/EventStore.Client.Streams.Tests/Security/SecurityFixture.cs rename to test/Kurrent.Client.Tests/Security/SecurityFixture.cs index 2ffc0b73c..366284a11 100644 --- a/test/EventStore.Client.Streams.Tests/Security/SecurityFixture.cs +++ b/test/Kurrent.Client.Tests/Security/SecurityFixture.cs @@ -1,8 +1,9 @@ using System.Runtime.CompilerServices; +using EventStore.Client; +using Kurrent.Client; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Streams.Tests; - -public class SecurityFixture : EventStoreFixture { +public class SecurityFixture : KurrentTemporaryFixture { public const string NoAclStream = nameof(NoAclStream); public const string ReadStream = nameof(ReadStream); public const string WriteStream = nameof(WriteStream); @@ -41,7 +42,7 @@ await Users.CreateUserWithRetry( TestCredentials.TestAdmin.Password!, TestCredentials.Root ).WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); - + await Given(); await When(); }; @@ -245,6 +246,7 @@ public Task WriteMeta(string streamId, UserCredentials? userCreden public async Task SubscribeToStream(string streamId, UserCredentials? userCredentials = default) { await using var subscription = Streams.SubscribeToStream(streamId, FromStream.Start, userCredentials: userCredentials); + await subscription .Messages.OfType().AnyAsync().AsTask() .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); @@ -253,16 +255,17 @@ await subscription public async Task SubscribeToAll(UserCredentials? userCredentials = default) { await using var subscription = Streams.SubscribeToAll(FromAll.Start, userCredentials: userCredentials); + await subscription .Messages.OfType().AnyAsync().AsTask() .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); } - - public async Task CreateStreamWithMeta(StreamMetadata metadata, [CallerMemberName] string streamId = "") { + + public async Task CreateStreamWithMeta(StreamMetadata metadataPermanent, [CallerMemberName] string streamId = "") { await Streams.SetStreamMetadataAsync( streamId, StreamState.NoStream, - metadata, + metadataPermanent, userCredentials: TestCredentials.TestAdmin ) .WithTimeout(TimeSpan.FromMilliseconds(TimeoutMs)); diff --git a/test/EventStore.Client.Streams.Tests/Security/stream_security_inheritance.cs b/test/Kurrent.Client.Tests/Security/StreamSecurityInheritanceTests.cs similarity index 94% rename from test/EventStore.Client.Streams.Tests/Security/stream_security_inheritance.cs rename to test/Kurrent.Client.Tests/Security/StreamSecurityInheritanceTests.cs index 1e1221e16..cb6d0b177 100644 --- a/test/EventStore.Client.Streams.Tests/Security/stream_security_inheritance.cs +++ b/test/Kurrent.Client.Tests/Security/StreamSecurityInheritanceTests.cs @@ -1,8 +1,13 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Security")] -public class stream_security_inheritance(ITestOutputHelper output, stream_security_inheritance.CustomFixture fixture) : EventStoreTests(output, fixture) { - [Fact] +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class StreamSecurityInheritanceTests(ITestOutputHelper output, StreamSecurityInheritanceTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { + [RetryFact] public async Task acl_inheritance_is_working_properly_on_user_streams() { await Assert.ThrowsAsync(() => Fixture.AppendStream("user-no-acl")); await Fixture.AppendStream("user-no-acl", TestCredentials.TestUser1); @@ -54,7 +59,7 @@ public async Task acl_inheritance_is_working_properly_on_user_streams_when_not_a await Fixture.ReadEvent("user-no-acl"); } - [Fact] + [RetryFact] public async Task acl_inheritance_is_working_properly_on_system_streams() { await Assert.ThrowsAsync(() => Fixture.AppendStream("$sys-no-acl")); await Fixture.AppendStream("$sys-no-acl", TestCredentials.TestUser1); @@ -181,4 +186,4 @@ await Streams.SetStreamMetadataAsync( ); } } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_all_security.cs b/test/Kurrent.Client.Tests/Security/SubscribeToAllSecurityTests.cs similarity index 73% rename from test/EventStore.Client.Streams.Tests/Security/subscribe_to_all_security.cs rename to test/Kurrent.Client.Tests/Security/SubscribeToAllSecurityTests.cs index 48b62a52d..ba3cea9bc 100644 --- a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_all_security.cs +++ b/test/Kurrent.Client.Tests/Security/SubscribeToAllSecurityTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Security")] -public class subscribe_to_all_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class SubscribeToAllSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task subscribing_to_all_with_not_existing_credentials_is_not_authenticated() => await Assert.ThrowsAsync(() => Fixture.SubscribeToAll(TestCredentials.TestBadUser)); @@ -18,4 +22,4 @@ public async Task subscribing_to_all_with_not_authorized_user_credentials_is_den [Fact] public async Task subscribing_to_all_with_admin_user_credentials_succeeds() => await Fixture.SubscribeToAll(TestCredentials.TestAdmin); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security.cs b/test/Kurrent.Client.Tests/Security/SubscribeToStreamSecurityTests.cs similarity index 77% rename from test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security.cs rename to test/Kurrent.Client.Tests/Security/SubscribeToStreamSecurityTests.cs index 0af3f8da9..e1226122b 100644 --- a/test/EventStore.Client.Streams.Tests/Security/subscribe_to_stream_security.cs +++ b/test/Kurrent.Client.Tests/Security/SubscribeToStreamSecurityTests.cs @@ -1,13 +1,15 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Security")] -public class subscribe_to_stream_security(ITestOutputHelper output, SecurityFixture fixture) - : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class SubscribeToStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task subscribing_to_stream_with_not_existing_credentials_is_not_authenticated() => - await Assert.ThrowsAsync( - () => Fixture.SubscribeToStream(SecurityFixture.ReadStream, TestCredentials.TestBadUser) - ); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.ReadStream, TestCredentials.TestBadUser)); [Fact] public async Task subscribing_to_stream_with_no_credentials_is_denied() => @@ -15,9 +17,7 @@ public async Task subscribing_to_stream_with_no_credentials_is_denied() => [Fact] public async Task subscribing_to_stream_with_not_authorized_user_credentials_is_denied() => - await Assert.ThrowsAsync( - () => Fixture.SubscribeToStream(SecurityFixture.ReadStream, TestCredentials.TestUser2) - ); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.ReadStream, TestCredentials.TestUser2)); [Fact] public async Task reading_stream_with_authorized_user_credentials_succeeds() { @@ -39,9 +39,7 @@ public async Task subscribing_to_no_acl_stream_succeeds_when_no_credentials_are_ [Fact] public async Task subscribing_to_no_acl_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync( - () => Fixture.SubscribeToStream(SecurityFixture.NoAclStream, TestCredentials.TestBadUser) - ); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.NoAclStream, TestCredentials.TestBadUser)); [Fact] public async Task subscribing_to_no_acl_stream_succeeds_when_any_existing_user_credentials_are_passed() { @@ -65,9 +63,7 @@ public async Task subscribing_to_all_access_normal_stream_succeeds_when_no_crede [Fact] public async Task subscribing_to_all_access_normal_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync( - () => Fixture.SubscribeToStream(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser) - ); + await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser)); [Fact] public async Task subscribing_to_all_access_normal_stream_succeeds_when_any_existing_user_credentials_are_passed() { diff --git a/test/EventStore.Client.Streams.Tests/Security/system_stream_security.cs b/test/Kurrent.Client.Tests/Security/SystemStreamSecurityTests.cs similarity index 94% rename from test/EventStore.Client.Streams.Tests/Security/system_stream_security.cs rename to test/Kurrent.Client.Tests/Security/SystemStreamSecurityTests.cs index c4e001b4a..720ae8ec1 100644 --- a/test/EventStore.Client.Streams.Tests/Security/system_stream_security.cs +++ b/test/Kurrent.Client.Tests/Security/SystemStreamSecurityTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Security")] -public class system_stream_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class SystemStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task operations_on_system_stream_with_no_acl_set_fail_for_non_admin() { await Assert.ThrowsAsync(() => Fixture.ReadEvent("$system-no-acl", TestCredentials.TestUser1)); @@ -40,7 +44,9 @@ public async Task operations_on_system_stream_with_acl_set_to_usual_user_fail_fo await Assert.ThrowsAsync(() => Fixture.AppendStream(SecurityFixture.SystemAclStream, TestCredentials.TestUser2)); await Assert.ThrowsAsync(() => Fixture.ReadMeta(SecurityFixture.SystemAclStream, TestCredentials.TestUser2)); - await Assert.ThrowsAsync(() => Fixture.WriteMeta(SecurityFixture.SystemAclStream, TestCredentials.TestUser2, TestCredentials.TestUser1.Username)); + await Assert.ThrowsAsync( + () => Fixture.WriteMeta(SecurityFixture.SystemAclStream, TestCredentials.TestUser2, TestCredentials.TestUser1.Username) + ); await Assert.ThrowsAsync(() => Fixture.SubscribeToStream(SecurityFixture.SystemAclStream, TestCredentials.TestUser2)); } diff --git a/test/EventStore.Client.Streams.Tests/Security/write_stream_meta_security.cs b/test/Kurrent.Client.Tests/Security/WriteStreamMetaSecurityTests.cs similarity index 85% rename from test/EventStore.Client.Streams.Tests/Security/write_stream_meta_security.cs rename to test/Kurrent.Client.Tests/Security/WriteStreamMetaSecurityTests.cs index 9b003795c..942a155cc 100644 --- a/test/EventStore.Client.Streams.Tests/Security/write_stream_meta_security.cs +++ b/test/Kurrent.Client.Tests/Security/WriteStreamMetaSecurityTests.cs @@ -1,7 +1,11 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Security")] -public class write_stream_meta_security(ITestOutputHelper output, SecurityFixture fixture) : EventStoreTests(output, fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class WriteStreamMetaSecurityTests(ITestOutputHelper output, SecurityFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task writing_meta_with_not_existing_credentials_is_not_authenticated() => await Assert.ThrowsAsync( @@ -19,7 +23,8 @@ public async Task writing_meta_to_stream_with_no_credentials_is_denied() => [Fact] public async Task writing_meta_to_stream_with_not_authorized_user_credentials_is_denied() => - await Assert.ThrowsAsync(() => + await Assert.ThrowsAsync( + () => Fixture.WriteMeta( SecurityFixture.MetaWriteStream, TestCredentials.TestUser2, @@ -66,7 +71,9 @@ public async Task writing_meta_to_all_access_normal_stream_succeeds_when_no_cred [Fact] public async Task writing_meta_to_all_access_normal_stream_is_not_authenticated_when_not_existing_credentials_are_passed() => - await Assert.ThrowsAsync(() => Fixture.WriteMeta(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser, SystemRoles.All)); + await Assert.ThrowsAsync( + () => Fixture.WriteMeta(SecurityFixture.NormalAllStream, TestCredentials.TestBadUser, SystemRoles.All) + ); [Fact] public async Task @@ -78,4 +85,4 @@ public async Task [Fact] public async Task writing_meta_to_all_access_normal_stream_succeeds_when_admin_user_credentials_are_passed() => await Fixture.WriteMeta(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin, SystemRoles.All); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Security/write_stream_security.cs b/test/Kurrent.Client.Tests/Security/WriteStreamSecurityTests.cs similarity index 91% rename from test/EventStore.Client.Streams.Tests/Security/write_stream_security.cs rename to test/Kurrent.Client.Tests/Security/WriteStreamSecurityTests.cs index d002d37c9..67ef35bc5 100644 --- a/test/EventStore.Client.Streams.Tests/Security/write_stream_security.cs +++ b/test/Kurrent.Client.Tests/Security/WriteStreamSecurityTests.cs @@ -1,8 +1,10 @@ -namespace EventStore.Client.Streams.Tests; +using EventStore.Client; -[Trait("Category", "Security")] -public class write_stream_security : IClassFixture { - public write_stream_security(ITestOutputHelper output, SecurityFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Security")] +public class WriteStreamSecurityTests : IClassFixture { + public WriteStreamSecurityTests(ITestOutputHelper output, SecurityFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); SecurityFixture Fixture { get; } @@ -67,4 +69,4 @@ public async Task writing_to_all_access_normal_stream_succeeds_when_any_existing [Fact] public async Task writing_to_all_access_normal_stream_succeeds_when_any_admin_user_credentials_are_passed() => await Fixture.AppendStream(SecurityFixture.NormalAllStream, TestCredentials.TestAdmin); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/SharingProviderTests.cs b/test/Kurrent.Client.Tests/SharingProviderTests.cs similarity index 95% rename from test/EventStore.Client.Tests/SharingProviderTests.cs rename to test/Kurrent.Client.Tests/SharingProviderTests.cs index ddb4c9d5f..3c55cfdda 100644 --- a/test/EventStore.Client.Tests/SharingProviderTests.cs +++ b/test/Kurrent.Client.Tests/SharingProviderTests.cs @@ -1,9 +1,12 @@ -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously +using EventStore.Client; -namespace EventStore.Client.Tests; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Misc")] public class SharingProviderTests { - [Fact] + [RetryFact] public async Task CanGetCurrent() { using var sut = new SharingProvider( async (x, _) => x + 1, @@ -14,7 +17,7 @@ public async Task CanGetCurrent() { Assert.Equal(6, await sut.CurrentAsync); } - [Fact] + [RetryFact] public async Task CanReset() { var count = 0; using var sut = new SharingProvider( @@ -28,7 +31,7 @@ public async Task CanReset() { Assert.Equal(1, await sut.CurrentAsync); } - [Fact] + [RetryFact] public async Task CanReturnBroken() { Action? onBroken = null; var count = 0; @@ -50,7 +53,7 @@ public async Task CanReturnBroken() { Assert.Equal(2, await sut.CurrentAsync); } - [Fact] + [RetryFact] public async Task CanReturnSameBoxTwice() { Action? onBroken = null; var count = 0; @@ -74,7 +77,7 @@ public async Task CanReturnSameBoxTwice() { Assert.Equal(1, await sut.CurrentAsync); } - [Fact] + [RetryFact] public async Task CanReturnPendingBox() { var trigger = new SemaphoreSlim(0); Action? onBroken = null; @@ -113,7 +116,7 @@ public async Task CanReturnPendingBox() { Assert.Equal(1, count); } - [Fact] + [RetryFact] public async Task FactoryCanThrow() { using var sut = new SharingProvider( (x, _) => throw new($"input {x}"), @@ -130,7 +133,7 @@ public async Task FactoryCanThrow() { // safe to call onBroken before the factory has returned, but it doesn't // do anything because the box is not populated yet. // the factory has to indicate failure by throwing. - [Fact] + [RetryFact] public async Task FactoryCanCallOnBrokenSynchronously() { using var sut = new SharingProvider( async (x, onBroken) => { @@ -147,7 +150,7 @@ public async Task FactoryCanCallOnBrokenSynchronously() { Assert.Equal(0, await sut.CurrentAsync); } - [Fact] + [RetryFact] public async Task FactoryCanCallOnBrokenSynchronouslyAndThrow() { using var sut = new SharingProvider( async (x, onBroken) => { @@ -167,7 +170,7 @@ public async Task FactoryCanCallOnBrokenSynchronouslyAndThrow() { Assert.Equal("input 0", ex.Message); } - [Fact] + [RetryFact] public async Task StopsAfterBeingDisposed() { Action? onBroken = null; var count = 0; @@ -193,7 +196,7 @@ public async Task StopsAfterBeingDisposed() { Assert.Equal(1, count); } - [Fact] + [RetryFact] public async Task ExampleUsage() { // factory waits to be signalled by completeConstruction being released // sometimes the factory succeeds, sometimes it throws. @@ -263,4 +266,4 @@ async Task Factory(int input, Action onBroken) { await constructionCompleted.WaitAsync(); Assert.Equal(0, await sut.CurrentAsync); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/StreamPositionTests.cs b/test/Kurrent.Client.Tests/StreamPositionTests.cs similarity index 93% rename from test/EventStore.Client.Tests/StreamPositionTests.cs rename to test/Kurrent.Client.Tests/StreamPositionTests.cs index 22578b548..06a4ad0ce 100644 --- a/test/EventStore.Client.Tests/StreamPositionTests.cs +++ b/test/Kurrent.Client.Tests/StreamPositionTests.cs @@ -1,21 +1,23 @@ using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class StreamPositionTests : ValueObjectTests { public StreamPositionTests() : base(new ScenarioFixture()) { } - [Fact] + [RetryFact] public void IsComparable() => Assert.IsAssignableFrom>(_fixture.Create()); - [Fact] + [RetryFact] public void AdditionOperator() { var sut = StreamPosition.Start; Assert.Equal(new(1), sut + 1); Assert.Equal(new(1), 1 + sut); } - [Fact] + [RetryFact] public void NextReturnsExpectedResult() { var sut = StreamPosition.Start; Assert.Equal(sut + 1, sut.Next()); @@ -33,7 +35,7 @@ public void AdditionOutOfBoundsThrows(StreamPosition StreamPosition, ulong opera Assert.Throws(() => operand + StreamPosition); } - [Fact] + [RetryFact] public void SubtractionOperator() { var sut = new StreamPosition(1); Assert.Equal(new(0), sut - 1); @@ -64,17 +66,17 @@ public void ArgumentOutOfRange(ulong value) { Assert.Equal(nameof(value), ex.ParamName); } - [Fact] + [RetryFact] public void FromStreamPositionEndThrows() => Assert.Throws(() => StreamRevision.FromStreamPosition(StreamPosition.End)); - [Fact] + [RetryFact] public void FromStreamPositionReturnsExpectedResult() { var result = StreamPosition.FromStreamRevision(new(0)); Assert.Equal(new(0), result); } - [Fact] + [RetryFact] public void ExplicitConversionToUInt64ReturnsExpectedResult() { const ulong value = 0UL; @@ -82,7 +84,7 @@ public void ExplicitConversionToUInt64ReturnsExpectedResult() { Assert.Equal(value, actual); } - [Fact] + [RetryFact] public void ImplicitConversionToUInt64ReturnsExpectedResult() { const ulong value = 0UL; @@ -90,7 +92,7 @@ public void ImplicitConversionToUInt64ReturnsExpectedResult() { Assert.Equal(value, actual); } - [Fact] + [RetryFact] public void ExplicitConversionToStreamPositionReturnsExpectedResult() { const ulong value = 0UL; @@ -99,7 +101,7 @@ public void ExplicitConversionToStreamPositionReturnsExpectedResult() { Assert.Equal(expected, actual); } - [Fact] + [RetryFact] public void ImplicitConversionToStreamPositionReturnsExpectedResult() { const ulong value = 0UL; @@ -109,14 +111,14 @@ public void ImplicitConversionToStreamPositionReturnsExpectedResult() { Assert.Equal(expected, actual); } - [Fact] + [RetryFact] public void ToStringExpectedResult() { var expected = 0UL.ToString(); Assert.Equal(expected, new StreamPosition(0UL).ToString()); } - [Fact] + [RetryFact] public void ToUInt64ExpectedResult() { var expected = 0UL; @@ -126,4 +128,4 @@ public void ToUInt64ExpectedResult() { class ScenarioFixture : Fixture { public ScenarioFixture() => Customize(composer => composer.FromFactory(value => new(value))); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/StreamRevisionTests.cs b/test/Kurrent.Client.Tests/StreamRevisionTests.cs similarity index 93% rename from test/EventStore.Client.Tests/StreamRevisionTests.cs rename to test/Kurrent.Client.Tests/StreamRevisionTests.cs index e42039946..eb1d5c534 100644 --- a/test/EventStore.Client.Tests/StreamRevisionTests.cs +++ b/test/Kurrent.Client.Tests/StreamRevisionTests.cs @@ -1,21 +1,23 @@ using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class StreamRevisionTests : ValueObjectTests { public StreamRevisionTests() : base(new ScenarioFixture()) { } - [Fact] + [RetryFact] public void IsComparable() => Assert.IsAssignableFrom>(_fixture.Create()); - [Fact] + [RetryFact] public void AdditionOperator() { var sut = new StreamRevision(0); Assert.Equal(new(1), sut + 1); Assert.Equal(new(1), 1 + sut); } - [Fact] + [RetryFact] public void NextReturnsExpectedResult() { var sut = new StreamRevision(0); Assert.Equal(sut + 1, sut.Next()); @@ -32,7 +34,7 @@ public void AdditionOutOfBoundsThrows(StreamRevision streamRevision, ulong opera Assert.Throws(() => operand + streamRevision); } - [Fact] + [RetryFact] public void SubtractionOperator() { var sut = new StreamRevision(1); Assert.Equal(new(0), sut - 1); @@ -63,17 +65,17 @@ public void ArgumentOutOfRange(ulong value) { Assert.Equal(nameof(value), ex.ParamName); } - [Fact] + [RetryFact] public void FromStreamPositionEndThrows() => Assert.Throws(() => StreamRevision.FromStreamPosition(StreamPosition.End)); - [Fact] + [RetryFact] public void FromStreamPositionReturnsExpectedResult() { var result = StreamRevision.FromStreamPosition(StreamPosition.Start); Assert.Equal(new(0), result); } - [Fact] + [RetryFact] public void ExplicitConversionToUInt64ReturnsExpectedResult() { const ulong value = 0UL; @@ -82,7 +84,7 @@ public void ExplicitConversionToUInt64ReturnsExpectedResult() { Assert.Equal(value, actual); } - [Fact] + [RetryFact] public void ImplicitConversionToUInt64ReturnsExpectedResult() { const ulong value = 0UL; @@ -91,7 +93,7 @@ public void ImplicitConversionToUInt64ReturnsExpectedResult() { Assert.Equal(value, actual); } - [Fact] + [RetryFact] public void ExplicitConversionToStreamRevisionReturnsExpectedResult() { const ulong value = 0UL; @@ -101,7 +103,7 @@ public void ExplicitConversionToStreamRevisionReturnsExpectedResult() { Assert.Equal(expected, actual); } - [Fact] + [RetryFact] public void ImplicitConversionToStreamRevisionReturnsExpectedResult() { const ulong value = 0UL; @@ -111,14 +113,14 @@ public void ImplicitConversionToStreamRevisionReturnsExpectedResult() { Assert.Equal(expected, actual); } - [Fact] + [RetryFact] public void ToStringExpectedResult() { var expected = 0UL.ToString(); Assert.Equal(expected, new StreamRevision(0UL).ToString()); } - [Fact] + [RetryFact] public void ToUInt64ExpectedResult() { var expected = 0UL; @@ -128,4 +130,4 @@ public void ToUInt64ExpectedResult() { class ScenarioFixture : Fixture { public ScenarioFixture() => Customize(composer => composer.FromFactory(value => new(value))); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/StreamStateTests.cs b/test/Kurrent.Client.Tests/StreamStateTests.cs similarity index 77% rename from test/EventStore.Client.Tests/StreamStateTests.cs rename to test/Kurrent.Client.Tests/StreamStateTests.cs index b4d0424c6..1faf56214 100644 --- a/test/EventStore.Client.Tests/StreamStateTests.cs +++ b/test/Kurrent.Client.Tests/StreamStateTests.cs @@ -1,15 +1,17 @@ using System.Reflection; using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class StreamStateTests : ValueObjectTests { public StreamStateTests() : base(new ScenarioFixture()) { } public static IEnumerable ArgumentOutOfRangeTestCases() { - yield return new object?[] { 0 }; - yield return new object?[] { int.MaxValue }; - yield return new object?[] { -3 }; + yield return [0]; + yield return [int.MaxValue]; + yield return [-3]; } [Theory] @@ -19,23 +21,23 @@ public void ArgumentOutOfRange(int value) { Assert.Equal(nameof(value), ex.ParamName); } - [Fact] + [RetryFact] public void ExplicitConversionExpectedResult() { const int expected = 1; var actual = (int)new StreamState(expected); Assert.Equal(expected, actual); } - [Fact] + [RetryFact] public void ImplicitConversionExpectedResult() { const int expected = 1; Assert.Equal(expected, new StreamState(expected)); } public static IEnumerable ToStringTestCases() { - yield return new object?[] { StreamState.Any, nameof(StreamState.Any) }; - yield return new object?[] { StreamState.NoStream, nameof(StreamState.NoStream) }; - yield return new object?[] { StreamState.StreamExists, nameof(StreamState.StreamExists) }; + yield return [StreamState.Any, nameof(StreamState.Any)]; + yield return [StreamState.NoStream, nameof(StreamState.NoStream)]; + yield return [StreamState.StreamExists, nameof(StreamState.StreamExists)]; } [Theory] @@ -54,4 +56,4 @@ class ScenarioFixture : Fixture { public ScenarioFixture() => Customize(composer => composer.FromFactory(() => Instances[Interlocked.Increment(ref RefCount) % Instances.Length])); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Append/append_to_stream.cs b/test/Kurrent.Client.Tests/Streams/AppendTests.cs similarity index 55% rename from test/EventStore.Client.Streams.Tests/Append/append_to_stream.cs rename to test/Kurrent.Client.Tests/Streams/AppendTests.cs index 6cd5b813d..f6442219e 100644 --- a/test/EventStore.Client.Streams.Tests/Append/append_to_stream.cs +++ b/test/Kurrent.Client.Tests/Streams/AppendTests.cs @@ -1,19 +1,13 @@ -using System.Text; +using EventStore.Client; using Grpc.Core; +using Humanizer; -namespace EventStore.Client.Streams.Tests.Append; +namespace Kurrent.Client.Tests.Streams; -[Trait("Category", "Target:Stream")] +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Append")] -public class append_to_stream(ITestOutputHelper output, EventStoreFixture fixture) - : EventStoreTests(output, fixture) { - public static IEnumerable ExpectedVersionCreateStreamTestCases() { - yield return new object?[] { StreamState.Any }; - yield return new object?[] { StreamState.NoStream }; - } - - [Theory] - [MemberData(nameof(ExpectedVersionCreateStreamTestCases))] +public class AppendTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { + [Theory, ExpectedVersionCreateStreamTestCases] public async Task appending_zero_events(StreamState expectedStreamState) { var stream = $"{Fixture.GetStreamName()}_{expectedStreamState}"; @@ -33,8 +27,7 @@ await Fixture.Streams .ShouldThrowAsync(ex => ex.Stream.ShouldBe(stream)); } - [Theory] - [MemberData(nameof(ExpectedVersionCreateStreamTestCases))] + [Theory, ExpectedVersionCreateStreamTestCases] public async Task appending_zero_events_again(StreamState expectedStreamState) { var stream = $"{Fixture.GetStreamName()}_{expectedStreamState}"; @@ -54,8 +47,7 @@ await Fixture.Streams .ShouldThrowAsync(ex => ex.Stream.ShouldBe(stream)); } - [Theory] - [MemberData(nameof(ExpectedVersionCreateStreamTestCases))] + [Theory, ExpectedVersionCreateStreamTestCases] public async Task create_stream_expected_version_on_first_write_if_does_not_exist(StreamState expectedStreamState) { var stream = $"{Fixture.GetStreamName()}_{expectedStreamState}"; @@ -73,7 +65,7 @@ public async Task create_stream_expected_version_on_first_write_if_does_not_exis Assert.Equal(1, count); } - [Fact] + [RetryFact] public async Task multiple_idempotent_writes() { var stream = Fixture.GetStreamName(); var events = Fixture.CreateTestEvents(4).ToArray(); @@ -85,7 +77,7 @@ public async Task multiple_idempotent_writes() { Assert.Equal(new(3), writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task multiple_idempotent_writes_with_same_id_bug_case() { var stream = Fixture.GetStreamName(); @@ -97,7 +89,7 @@ public async Task multiple_idempotent_writes_with_same_id_bug_case() { Assert.Equal(new(5), writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task in_case_where_multiple_writes_of_multiple_events_with_the_same_ids_using_expected_version_any_then_next_expected_version_is_unreliable() { var stream = Fixture.GetStreamName(); @@ -114,7 +106,7 @@ public async Task Assert.Equal(new(0), writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task in_case_where_multiple_writes_of_multiple_events_with_the_same_ids_using_expected_version_nostream_then_next_expected_version_is_correct() { var stream = Fixture.GetStreamName(); @@ -132,7 +124,7 @@ public async Task Assert.Equal(streamRevision, writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task writing_with_correct_expected_version_to_deleted_stream_throws_stream_deleted() { var stream = Fixture.GetStreamName(); @@ -143,7 +135,7 @@ await Fixture.Streams .ShouldThrowAsync(); } - [Fact] + [RetryFact] public async Task returns_log_position_when_writing() { var stream = Fixture.GetStreamName(); @@ -157,7 +149,7 @@ public async Task returns_log_position_when_writing() { Assert.True(0 < result.LogPosition.CommitPosition); } - [Fact] + [RetryFact] public async Task writing_with_any_expected_version_to_deleted_stream_throws_stream_deleted() { var stream = Fixture.GetStreamName(); await Fixture.Streams.TombstoneAsync(stream, StreamState.NoStream); @@ -167,7 +159,7 @@ await Fixture.Streams .ShouldThrowAsync(); } - [Fact] + [RetryFact] public async Task writing_with_invalid_expected_version_to_deleted_stream_throws_stream_deleted() { var stream = Fixture.GetStreamName(); @@ -178,7 +170,7 @@ await Fixture.Streams .ShouldThrowAsync(); } - [Fact] + [RetryFact] public async Task append_with_correct_expected_version_to_existing_stream() { var stream = Fixture.GetStreamName(); @@ -197,7 +189,7 @@ public async Task append_with_correct_expected_version_to_existing_stream() { Assert.Equal(new(1), writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task append_with_any_expected_version_to_existing_stream() { var stream = Fixture.GetStreamName(); @@ -218,7 +210,7 @@ public async Task append_with_any_expected_version_to_existing_stream() { Assert.Equal(new(1), writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task appending_with_wrong_expected_version_to_existing_stream_throws_wrong_expected_version() { var stream = Fixture.GetStreamName(); @@ -232,7 +224,7 @@ public async Task appending_with_wrong_expected_version_to_existing_stream_throw ex.ExpectedStreamRevision.ShouldBe(new(999)); } - [Fact] + [RetryFact] public async Task appending_with_wrong_expected_version_to_existing_stream_returns_wrong_expected_version() { var stream = Fixture.GetStreamName(); @@ -248,7 +240,7 @@ public async Task appending_with_wrong_expected_version_to_existing_stream_retur Assert.Equal(new(1), wrongExpectedVersionResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task append_with_stream_exists_expected_version_to_existing_stream() { var stream = Fixture.GetStreamName(); @@ -261,7 +253,7 @@ await Fixture.Streams.AppendToStreamAsync( ); } - [Fact] + [RetryFact] public async Task append_with_stream_exists_expected_version_to_stream_with_multiple_events() { var stream = Fixture.GetStreamName(); @@ -275,7 +267,7 @@ await Fixture.Streams.AppendToStreamAsync( ); } - [Fact] + [RetryFact] public async Task append_with_stream_exists_expected_version_if_metadata_stream_exists() { var stream = Fixture.GetStreamName(); @@ -292,7 +284,7 @@ await Fixture.Streams.AppendToStreamAsync( ); } - [Fact] + [RetryFact] public async Task appending_with_stream_exists_expected_version_and_stream_does_not_exist_throws_wrong_expected_version() { var stream = Fixture.GetStreamName(); @@ -304,7 +296,7 @@ public async Task ex.ActualStreamRevision.ShouldBe(StreamRevision.None); } - [Fact] + [RetryFact] public async Task appending_with_stream_exists_expected_version_and_stream_does_not_exist_returns_wrong_expected_version() { var stream = Fixture.GetStreamName(); @@ -321,7 +313,7 @@ public async Task Assert.Equal(StreamRevision.None, wrongExpectedVersionResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task appending_with_stream_exists_expected_version_to_hard_deleted_stream_throws_stream_deleted() { var stream = Fixture.GetStreamName(); @@ -332,7 +324,7 @@ await Fixture.Streams .ShouldThrowAsync(); } - [Fact] + [RetryFact] public async Task appending_with_stream_exists_expected_version_to_deleted_stream_throws_stream_deleted() { var stream = Fixture.GetStreamName(); @@ -345,7 +337,7 @@ await Fixture.Streams .ShouldThrowAsync(); } - [Fact] + [RetryFact] public async Task can_append_multiple_events_at_once() { var stream = Fixture.GetStreamName(); @@ -358,7 +350,7 @@ public async Task can_append_multiple_events_at_once() { Assert.Equal(new(99), writeResult.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task returns_failure_status_when_conditionally_appending_with_version_mismatch() { var stream = Fixture.GetStreamName(); @@ -374,7 +366,7 @@ public async Task returns_failure_status_when_conditionally_appending_with_versi ); } - [Fact] + [RetryFact] public async Task returns_success_status_when_conditionally_appending_with_matching_version() { var stream = Fixture.GetStreamName(); @@ -390,7 +382,7 @@ public async Task returns_success_status_when_conditionally_appending_with_match ); } - [Fact] + [RetryFact] public async Task returns_failure_status_when_conditionally_appending_to_a_deleted_stream() { var stream = Fixture.GetStreamName(); @@ -407,7 +399,7 @@ public async Task returns_failure_status_when_conditionally_appending_to_a_delet Assert.Equal(ConditionalWriteResult.StreamDeleted, result); } - [Fact] + [RetryFact] public async Task expected_version_no_stream() { var result = await Fixture.Streams.AppendToStreamAsync( Fixture.GetStreamName(), @@ -418,7 +410,7 @@ public async Task expected_version_no_stream() { Assert.Equal(new(0), result!.NextExpectedStreamRevision); } - [Fact] + [RetryFact] public async Task expected_version_no_stream_returns_position() { var result = await Fixture.Streams.AppendToStreamAsync( Fixture.GetStreamName(), @@ -429,7 +421,7 @@ public async Task expected_version_no_stream_returns_position() { Assert.True(result.LogPosition > Position.Start); } - [Fact] + [RetryFact] public async Task with_timeout_any_stream_revision_fails_when_operation_expired() { var stream = Fixture.GetStreamName(); @@ -443,7 +435,7 @@ public async Task with_timeout_any_stream_revision_fails_when_operation_expired( ex.StatusCode.ShouldBe(StatusCode.DeadlineExceeded); } - [Fact] + [RetryFact] public async Task with_timeout_stream_revision_fails_when_operation_expired() { var stream = Fixture.GetStreamName(); @@ -459,7 +451,7 @@ public async Task with_timeout_stream_revision_fails_when_operation_expired() { ex.StatusCode.ShouldBe(StatusCode.DeadlineExceeded); } - [Fact] + [RetryFact] public async Task when_events_enumerator_throws_the_write_does_not_succeed() { var streamName = Fixture.GetStreamName(); @@ -477,4 +469,317 @@ await Fixture.Streams state.ShouldBe(ReadState.StreamNotFound); } + + [RetryFact] + public async Task succeeds_when_size_is_less_than_max_append_size() { + // Arrange + var maxAppendSize = (uint)100.Kilobytes().Bytes; + var stream = Fixture.GetStreamName(); + + // Act + var (events, size) = Fixture.CreateTestEventsUpToMaxSize(maxAppendSize - 1); + + // Assert + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + } + + [RetryFact] + public async Task fails_when_size_exceeds_max_append_size() { + // Arrange + var maxAppendSize = (uint)100.Kilobytes().Bytes; + var stream = Fixture.GetStreamName(); + var eventsAppendSize = maxAppendSize * 2; + + // Act + var (events, size) = Fixture.CreateTestEventsUpToMaxSize(eventsAppendSize); + + // Assert + size.ShouldBeGreaterThan(maxAppendSize); + + var ex = await Fixture.Streams + .AppendToStreamAsync(stream, StreamState.NoStream, events) + .ShouldThrowAsync(); + + ex.MaxAppendSize.ShouldBe(maxAppendSize); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0em1_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_4e4_0any_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e5_non_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(5), events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 2).CountAsync(); + + Assert.Equal(events.Length + 1, count); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e6_throws_wev() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + await Assert.ThrowsAsync(() => Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(6), events.Take(1))); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e6_returns_wev() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + var writeResult = await Fixture.Streams.AppendToStreamAsync( + stream, + new StreamRevision(6), + events.Take(1), + options => options.ThrowOnAppendFailure = false + ); + + Assert.IsType(writeResult); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e4_throws_wev() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + await Assert.ThrowsAsync(() => Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(4), events.Take(1))); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_3e2_4e3_5e4_0e4_returns_wev() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(6).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + var writeResult = await Fixture.Streams.AppendToStreamAsync( + stream, + new StreamRevision(4), + events.Take(1), + options => options.ThrowOnAppendFailure = false + ); + + Assert.IsType(writeResult); + } + + [RetryFact] + public async Task sequence_0em1_0e0_non_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents().ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(0), events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 2).CountAsync(); + + Assert.Equal(events.Length + 1, count); + } + + [RetryFact] + public async Task sequence_0em1_0any_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents().ToArray(); + + await Task.Delay(TimeSpan.FromSeconds(30)); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_0em1_0em1_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents().ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_0em1_1e0_2e1_1any_1any_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(3).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Skip(1).Take(1)); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Skip(1).Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_S_0em1_1em1_E_S_0em1_E_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(2).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_S_0em1_1em1_E_S_0any_E_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(2).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_S_0em1_1em1_E_S_1e0_E_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(2).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + + await Fixture.Streams.AppendToStreamAsync(stream, new StreamRevision(0), events.Skip(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_S_0em1_1em1_E_S_1any_E_idempotent() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(2).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events); + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, events.Skip(1).Take(1)); + + var count = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start, events.Length + 1).CountAsync(); + + Assert.Equal(events.Length, count); + } + + [RetryFact] + public async Task sequence_S_0em1_1em1_E_S_0em1_1em1_2em1_E_idempotancy_fail_throws() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(3).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(2)); + + await Assert.ThrowsAsync( + () => Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + events + ) + ); + } + + [RetryFact] + public async Task sequence_S_0em1_1em1_E_S_0em1_1em1_2em1_E_idempotancy_fail_returns() { + var stream = Fixture.GetStreamName(); + + var events = Fixture.CreateTestEvents(3).ToArray(); + + await Fixture.Streams.AppendToStreamAsync(stream, StreamState.NoStream, events.Take(2)); + + var writeResult = await Fixture.Streams.AppendToStreamAsync( + stream, + StreamState.NoStream, + events, + options => options.ThrowOnAppendFailure = false + ); + + Assert.IsType(writeResult); + } + + // [Fact] + // public async Task sending_and_receiving_large_messages_over_the_hard_limit() { + // uint maxAppendSize = 16 * 1024 * 1024 - 10000; + // var streamName = Fixture.GetStreamName(); + // var largeEvent = Fixture.CreateTestEvents() + // .Select(e => new EventData(e.EventId, "-", new byte[maxAppendSize + 1])); + // + // var ex = await Assert.ThrowsAsync(() => Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, largeEvent)); + // + // Assert.Equal(StatusCode.ResourceExhausted, ex.StatusCode); + // } + + class ExpectedVersionCreateStreamTestCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [StreamState.Any]; + yield return [StreamState.NoStream]; + } + } } diff --git a/test/EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_104.cs b/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs similarity index 86% rename from test/EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_104.cs rename to test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs index 922414820..4fe12ecbe 100644 --- a/test/EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_104.cs +++ b/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue104.cs @@ -1,8 +1,11 @@ -namespace EventStore.Client.Streams.Tests.Bugs.Obsolete; +using EventStore.Client; +namespace Kurrent.Client.Tests.Bugs.Obsolete; + +[Trait("Category", "Target:Streams")] [Trait("Category", "Bug")] [Obsolete("Tests will be removed in future release when older subscriptions APIs are removed from the client")] -public class Issue_104(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { +public class Issue104(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [Fact] public async Task subscription_does_not_send_checkpoint_reached_after_disposal() { var streamName = Fixture.GetStreamName(); diff --git a/test/EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_2544.cs b/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs similarity index 81% rename from test/EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_2544.cs rename to test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs index 3d65b01ec..a1add87e2 100644 --- a/test/EventStore.Client.Streams.Tests/Bugs/Obsolete/Issue_2544.cs +++ b/test/Kurrent.Client.Tests/Streams/Bugs/Obsolete/Issue2544.cs @@ -1,30 +1,32 @@ -#pragma warning disable 1998 +// ReSharper disable InconsistentNaming -namespace EventStore.Client.Streams.Tests.Bugs.Obsolete; +using EventStore.Client; +namespace Kurrent.Client.Tests.Bugs.Obsolete; + +[Trait("Category", "Target:Streams")] [Trait("Category", "Bug")] [Obsolete("Tests will be removed in future release when older subscriptions APIs are removed from the client")] -public class Issue_2544 : IClassFixture { - public Issue_2544(ITestOutputHelper output, EventStoreFixture fixture) { +public class Issue2544 : IClassFixture { + public Issue2544(ITestOutputHelper output, KurrentPermanentFixture fixture) { Fixture = fixture.With(x => x.CaptureTestRun(output)); - - _seen = Enumerable.Range(0, 1 + Batches * BatchSize) + + Seen = Enumerable.Range(0, 1 + Batches * BatchSize) .Select(i => new StreamPosition((ulong)i)) .ToDictionary(r => r, _ => false); - _completed = new(); + Completed = new(); } - EventStoreFixture Fixture { get; } + KurrentPermanentFixture Fixture { get; } const int BatchSize = 18; const int Batches = 4; - - readonly TaskCompletionSource _completed; - readonly Dictionary _seen; - public static IEnumerable TestCases() => - Enumerable.Range(0, 5).Select(i => new object[] { i }); + readonly TaskCompletionSource Completed; + readonly Dictionary Seen; + + public static IEnumerable TestCases() => Enumerable.Range(0, 5).Select(i => new object[] { i }); [Theory] [MemberData(nameof(TestCases))] @@ -46,7 +48,7 @@ await Fixture.Streams await AppendEvents(streamName); - await _completed.Task.WithTimeout(); + await Completed.Task.WithTimeout(); } [Theory] @@ -68,7 +70,7 @@ await Fixture.Streams await AppendEvents(streamName); - await _completed.Task.WithTimeout(); + await Completed.Task.WithTimeout(); } [Theory] @@ -91,7 +93,7 @@ await Fixture.Streams await AppendEvents(streamName); - await _completed.Task.WithTimeout(); + await Completed.Task.WithTimeout(); } async Task AppendEvents(string streamName) { @@ -108,8 +110,7 @@ async Task AppendEvents(string streamName) { ); expectedRevision = result.NextExpectedStreamRevision; - } - else { + } else { var result = await Fixture.Streams.AppendToStreamAsync( streamName, expectedRevision, @@ -143,7 +144,7 @@ Func> resubscribe return; } - _completed.TrySetException(ex); + Completed.TrySetException(ex); } Task EventAppeared(ResolvedEvent e, string streamName, out FromStream startFrom) { @@ -160,12 +161,12 @@ Task EventAppeared(ResolvedEvent e, string streamName) { if (e.OriginalStreamId != streamName) return Task.CompletedTask; - if (_seen[e.Event.EventNumber]) + if (Seen[e.Event.EventNumber]) throw new($"Event {e.Event.EventNumber} was already seen"); - _seen[e.Event.EventNumber] = true; + Seen[e.Event.EventNumber] = true; if (e.Event.EventType == "completed") - _completed.TrySetResult(true); + Completed.TrySetResult(true); return Task.CompletedTask; } diff --git a/test/Kurrent.Client.Tests/Streams/DecisionMaking/Functional/GettingStateTests.cs b/test/Kurrent.Client.Tests/Streams/DecisionMaking/Functional/GettingStateTests.cs new file mode 100644 index 000000000..3bab7c76d --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/DecisionMaking/Functional/GettingStateTests.cs @@ -0,0 +1,150 @@ +using EventStore.Client; +using Kurrent.Client.Streams.GettingState; + +namespace Kurrent.Client.Tests.Streams.DecisionMaking.Functional; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:GetState")] +public class GettingStateTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task gets_state_for_state_builder_with_evolve_function() { + // Given + var shoppingCartId = Guid.NewGuid(); + var clientId = Guid.NewGuid(); + var shoesId = Guid.NewGuid(); + var tShirtId = Guid.NewGuid(); + var twoPairsOfShoes = new PricedProductItem(shoesId, 2, 100); + var pairOfShoes = new PricedProductItem(shoesId, 1, 100); + var tShirt = new PricedProductItem(tShirtId, 1, 50); + + var events = new object[] { + new ShoppingCartOpened(shoppingCartId, clientId), + new ProductItemAddedToShoppingCart(shoppingCartId, twoPairsOfShoes), + new ProductItemAddedToShoppingCart(shoppingCartId, tShirt), + new ProductItemRemovedFromShoppingCart(shoppingCartId, pairOfShoes), + new ShoppingCartConfirmed(shoppingCartId, DateTime.UtcNow), + new ShoppingCartCanceled(shoppingCartId, DateTime.UtcNow) + }; + + var streamName = $"shopping_cart-{shoppingCartId}"; + + await Fixture.Streams.AppendToStreamAsync(streamName, events); + + var stateBuilder = StateBuilder.For(ShoppingCart.Evolve, ShoppingCart.Default); + + // When + var result = await Fixture.Streams.GetStateAsync(streamName, stateBuilder); + + var shoppingCart = result.State; + + // Then + Assert.Equal(shoppingCartId, shoppingCart.Id); + Assert.Equal(2, shoppingCart.ProductItems.Length); + + Assert.Equal(shoesId, shoppingCart.ProductItems[0].ProductId); + Assert.Equal(pairOfShoes.Quantity, shoppingCart.ProductItems[0].Quantity); + Assert.Equal(pairOfShoes.UnitPrice, shoppingCart.ProductItems[0].UnitPrice); + + Assert.Equal(tShirtId, shoppingCart.ProductItems[1].ProductId); + Assert.Equal(tShirt.Quantity, shoppingCart.ProductItems[1].Quantity); + Assert.Equal(tShirt.UnitPrice, shoppingCart.ProductItems[1].UnitPrice); + } +} + +public record ShoppingCartOpened( + Guid ShoppingCartId, + Guid ClientId +); + +public record ProductItemAddedToShoppingCart( + Guid ShoppingCartId, + PricedProductItem ProductItem +); + +public record ProductItemRemovedFromShoppingCart( + Guid ShoppingCartId, + PricedProductItem ProductItem +); + +public record ShoppingCartConfirmed( + Guid ShoppingCartId, + DateTime ConfirmedAt +); + +public record ShoppingCartCanceled( + Guid ShoppingCartId, + DateTime CanceledAt +); + +// VALUE OBJECTS +public record PricedProductItem( + Guid ProductId, + int Quantity, + decimal UnitPrice +) { + public decimal TotalPrice => Quantity * UnitPrice; +} + +// ENTITY +public record ShoppingCart( + Guid Id, + ShoppingCartStatus Status, + PricedProductItem[] ProductItems +) { + public static ShoppingCart Default() => + new(Guid.Empty, default, []); + + public static ShoppingCart Evolve(ShoppingCart shoppingCart, object @event) => + @event switch { + ShoppingCartOpened(var shoppingCartId, var clientId) => + shoppingCart with { + Id = shoppingCartId, + Status = ShoppingCartStatus.Pending + }, + ProductItemAddedToShoppingCart(_, var pricedProductItem) => + shoppingCart with { + ProductItems = shoppingCart.ProductItems + .Concat([pricedProductItem]) + .GroupBy(pi => pi.ProductId) + .Select( + group => group.Count() == 1 + ? group.First() + : new PricedProductItem( + group.Key, + group.Sum(pi => pi.Quantity), + group.First().UnitPrice + ) + ) + .ToArray() + }, + ProductItemRemovedFromShoppingCart(_, var pricedProductItem) => + shoppingCart with { + ProductItems = shoppingCart.ProductItems + .Select( + pi => pi.ProductId == pricedProductItem.ProductId + ? pi with { Quantity = pi.Quantity - pricedProductItem.Quantity } + : pi + ) + .Where(pi => pi.Quantity > 0) + .ToArray() + }, + ShoppingCartConfirmed => + shoppingCart with { + Status = ShoppingCartStatus.Confirmed + }, + ShoppingCartCanceled => + shoppingCart with { + Status = ShoppingCartStatus.Canceled + }, + _ => shoppingCart + }; +} + +public enum ShoppingCartStatus { + Pending = 1, + Confirmed = 2, + Canceled = 4, + + Closed = Confirmed | Canceled +} diff --git a/test/Kurrent.Client.Tests/Streams/DecisionMaking/StateBased/DecisionMakingWithAggregateStore.cs b/test/Kurrent.Client.Tests/Streams/DecisionMaking/StateBased/DecisionMakingWithAggregateStore.cs new file mode 100644 index 000000000..0d71fe052 --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/DecisionMaking/StateBased/DecisionMakingWithAggregateStore.cs @@ -0,0 +1,266 @@ +using EventStore.Client; +using Kurrent.Client.Streams.DecisionMaking; +using Kurrent.Client.Streams.GettingState; + +namespace Kurrent.Client.Tests.Streams.DecisionMaking.StateBased; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:GetState")] +public class DecisionMakingWithAggregateStore(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task handles_business_logic_with_aggregate_and_aggregate_store() { + // Given + var shoppingCartId = Guid.NewGuid(); + var clientId = Guid.NewGuid(); + var shoesId = Guid.NewGuid(); + var tShirtId = Guid.NewGuid(); + var twoPairsOfShoes = new PricedProductItem { + ProductId = shoesId, + Quantity = 2, + UnitPrice = 100 + }; + + var pairOfShoes = new PricedProductItem { + ProductId = shoesId, + Quantity = 1, + UnitPrice = 100 + }; + + var tShirt = new PricedProductItem { + ProductId = tShirtId, + Quantity = 1, + UnitPrice = 50 + }; + + var streamName = $"shopping_cart-{shoppingCartId}"; + + var stateBuilder = StateBuilder.For(ShoppingCart.Initial); + + var aggregateStore = new AggregateStore( + Fixture.Streams, + new AggregateStoreOptions { StateBuilder = stateBuilder } + ); + + var result = await aggregateStore.AddAsync( + streamName, + ShoppingCart.Open(shoppingCartId, clientId, DateTime.UtcNow) + ); + + Assert.IsType(result); + + result = await aggregateStore.HandleAsync( + streamName, + state => state.AddProductItem(twoPairsOfShoes, DateTime.UtcNow) + ); + + Assert.IsType(result); + + result = await aggregateStore.HandleAsync( + streamName, + state => state.AddProductItem(tShirt, DateTime.UtcNow) + ); + + Assert.IsType(result); + + result = await aggregateStore.HandleAsync( + streamName, + state => state.RemoveProductItem(pairOfShoes, DateTime.UtcNow) + ); + + Assert.IsType(result); + + result = await aggregateStore.HandleAsync( + streamName, + state => state.Confirm(DateTime.UtcNow) + ); + + Assert.IsType(result); + + await Assert.ThrowsAsync( + () => + aggregateStore.HandleAsync( + streamName, + state => state.Cancel(DateTime.UtcNow) + ) + ); + } +} + +public record ShoppingCartOpened( + Guid ShoppingCartId, + Guid ClientId, + DateTimeOffset OpenedAt +); + +public record ProductItemAddedToShoppingCart( + Guid ShoppingCartId, + PricedProductItem ProductItem, + DateTimeOffset AddedAt +); + +public record ProductItemRemovedFromShoppingCart( + Guid ShoppingCartId, + PricedProductItem ProductItem, + DateTimeOffset RemovedAt +); + +public record ShoppingCartConfirmed( + Guid ShoppingCartId, + DateTimeOffset ConfirmedAt +); + +public record ShoppingCartCanceled( + Guid ShoppingCartId, + DateTimeOffset CanceledAt +); + +public class PricedProductItem { + public Guid ProductId { get; set; } + public decimal UnitPrice { get; set; } + public int Quantity { get; set; } + public decimal TotalPrice => Quantity * UnitPrice; +} + +public class ShoppingCart : Aggregate { + public Guid Id { get; private set; } + public ShoppingCartStatus Status { get; private set; } + public IList ProductItems { get; } = new List(); + + public bool IsClosed => ShoppingCartStatus.Closed.HasFlag(Status); + + public static ShoppingCart Open(Guid cartId, Guid clientId, DateTimeOffset now) => + new(cartId, clientId, now); + + public static ShoppingCart Initial() => new(); + + ShoppingCart(Guid id, Guid clientId, DateTimeOffset now) { + var @event = new ShoppingCartOpened(id, clientId, now); + + Enqueue(@event); + } + + //just for default creation of empty object + ShoppingCart() { } + + void Apply(ShoppingCartOpened opened) { + Id = opened.ShoppingCartId; + Status = ShoppingCartStatus.Pending; + } + + public void AddProductItem(PricedProductItem productItem, DateTimeOffset now) { + if (IsClosed) + throw new InvalidOperationException($"Adding product item for cart in '{Status}' status is not allowed."); + + var @event = new ProductItemAddedToShoppingCart(Id, productItem, now); + + Enqueue(@event); + } + + void Apply(ProductItemAddedToShoppingCart productItemAdded) { + var (_, pricedProductItem, _) = productItemAdded; + var productId = pricedProductItem.ProductId; + var quantityToAdd = pricedProductItem.Quantity; + + var current = ProductItems.SingleOrDefault(pi => pi.ProductId == productId); + + if (current == null) + ProductItems.Add(pricedProductItem); + else + current.Quantity += quantityToAdd; + } + + public void RemoveProductItem(PricedProductItem productItemToBeRemoved, DateTimeOffset now) { + if (IsClosed) + throw new InvalidOperationException($"Removing product item for cart in '{Status}' status is not allowed."); + + if (!HasEnough(productItemToBeRemoved)) + throw new InvalidOperationException("Not enough product items to remove"); + + var @event = new ProductItemRemovedFromShoppingCart(Id, productItemToBeRemoved, now); + + Enqueue(@event); + } + + bool HasEnough(PricedProductItem productItem) { + var currentQuantity = ProductItems.Where(pi => pi.ProductId == productItem.ProductId) + .Select(pi => pi.Quantity) + .FirstOrDefault(); + + return currentQuantity >= productItem.Quantity; + } + + void Apply(ProductItemRemovedFromShoppingCart productItemRemoved) { + var (_, pricedProductItem, _) = productItemRemoved; + var productId = pricedProductItem.ProductId; + var quantityToRemove = pricedProductItem.Quantity; + + var current = ProductItems.Single(pi => pi.ProductId == productId); + + if (current.Quantity == quantityToRemove) + ProductItems.Remove(current); + else + current.Quantity -= quantityToRemove; + } + + public void Confirm(DateTimeOffset now) { + if (IsClosed) + throw new InvalidOperationException($"Confirming cart in '{Status}' status is not allowed."); + + if (ProductItems.Count == 0) + throw new InvalidOperationException("Cannot confirm empty shopping cart"); + + var @event = new ShoppingCartConfirmed(Id, now); + + Enqueue(@event); + } + + void Apply(ShoppingCartConfirmed confirmed) { + Status = ShoppingCartStatus.Confirmed; + } + + public void Cancel(DateTimeOffset now) { + if (IsClosed) + throw new InvalidOperationException($"Canceling cart in '{Status}' status is not allowed."); + + var @event = new ShoppingCartCanceled(Id, now); + + Enqueue(@event); + } + + void Apply(ShoppingCartCanceled canceled) { + Status = ShoppingCartStatus.Canceled; + } + + public override void Apply(object @event) { + switch (@event) { + case ShoppingCartOpened opened: + Apply(opened); + return; + + case ProductItemAddedToShoppingCart productItemAdded: + Apply(productItemAdded); + return; + + case ProductItemRemovedFromShoppingCart productItemRemoved: + Apply(productItemRemoved); + return; + + case ShoppingCartConfirmed confirmed: + Apply(confirmed); + return; + + case ShoppingCartCanceled canceled: + Apply(canceled); + return; + } + } +} + +public enum ShoppingCartStatus { + Pending = 1, + Confirmed = 2, + Canceled = 4, + + Closed = Confirmed | Canceled +} diff --git a/test/Kurrent.Client.Tests/Streams/DecisionMaking/UnionTypes/DecisionMakingWithDeciderTests.cs b/test/Kurrent.Client.Tests/Streams/DecisionMaking/UnionTypes/DecisionMakingWithDeciderTests.cs new file mode 100644 index 000000000..607485adc --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/DecisionMaking/UnionTypes/DecisionMakingWithDeciderTests.cs @@ -0,0 +1,217 @@ +using System.Collections.Immutable; +using EventStore.Client; +using Kurrent.Client.Streams.DecisionMaking; +using Kurrent.Client.Streams.GettingState; + +namespace Kurrent.Client.Tests.Streams.DecisionMaking.UnionTypes; + +using static ShoppingCart; +using static ShoppingCart.Event; +using static ShoppingCart.Command; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:Decide")] +public class DecisionMakingWithDeciderTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task handles_business_logic_with_decider_and_typed_events() { + // Given + var shoppingCartId = Guid.NewGuid(); + var clientId = Guid.NewGuid(); + var shoesId = Guid.NewGuid(); + var tShirtId = Guid.NewGuid(); + var twoPairsOfShoes = new PricedProductItem(shoesId, 2, 100); + var pairOfShoes = new PricedProductItem(shoesId, 1, 100); + var tShirt = new PricedProductItem(tShirtId, 1, 50); + + var streamName = $"shopping_cart-{shoppingCartId}"; + + var result = await Fixture.Streams.DecideAsync( + streamName, + new Open(clientId, DateTime.UtcNow), + Decider + ); + + Assert.IsType(result); + + result = await Fixture.Streams.DecideAsync( + streamName, + new AddProductItem(twoPairsOfShoes, DateTime.UtcNow), + Decider + ); + + Assert.IsType(result); + + result = await Fixture.Streams.DecideAsync( + streamName, + new AddProductItem(tShirt, DateTime.UtcNow), + Decider + ); + + Assert.IsType(result); + + result = await Fixture.Streams.DecideAsync( + streamName, + new RemoveProductItem(pairOfShoes, DateTime.UtcNow), + Decider + ); + + Assert.IsType(result); + + result = await Fixture.Streams.DecideAsync( + streamName, + new Confirm(DateTime.UtcNow), + Decider + ); + + Assert.IsType(result); + + await Assert.ThrowsAsync( + () => + Fixture.Streams.DecideAsync( + streamName, + new Cancel(DateTime.UtcNow), + Decider + ) + ); + } +} + +public record PricedProductItem( + Guid ProductId, + int Quantity, + decimal UnitPrice +) { + public decimal TotalPrice => Quantity * UnitPrice; +} + +public abstract record ShoppingCart { + public abstract record Event { + public record Opened( + Guid ClientId, + DateTimeOffset OpenedAt + ) : Event; + + public record ProductItemAdded( + PricedProductItem ProductItem, + DateTimeOffset AddedAt + ) : Event; + + public record ProductItemRemoved( + PricedProductItem ProductItem, + DateTimeOffset RemovedAt + ) : Event; + + public record Confirmed( + DateTimeOffset ConfirmedAt + ) : Event; + + public record Canceled( + DateTimeOffset CanceledAt + ) : Event; + + // This won't allow external inheritance and mimic union type in C# + Event() { } + } + + public record Initial : ShoppingCart; + + public record Pending(ProductItems ProductItems) : ShoppingCart; + + public record Closed : ShoppingCart; + + public static ShoppingCart Evolve(ShoppingCart state, Event @event) => + (state, @event) switch { + (Initial, Opened) => + new Pending(ProductItems.Empty), + + (Pending(var productItems), ProductItemAdded(var productItem, _)) => + new Pending(productItems.Add(productItem)), + + (Pending(var productItems), ProductItemRemoved(var productItem, _)) => + new Pending(productItems.Remove(productItem)), + + (Pending, Confirmed) => + new Closed(), + + (Pending, Canceled) => + new Closed(), + + _ => state + }; + + public abstract record Command { + public record Open( + Guid ClientId, + DateTimeOffset Now + ) : Command; + + public record AddProductItem( + PricedProductItem ProductItem, + DateTimeOffset Now + ) : Command; + + public record RemoveProductItem( + PricedProductItem ProductItem, + DateTimeOffset Now + ) : Command; + + public record Confirm( + DateTimeOffset Now + ) : Command; + + public record Cancel( + DateTimeOffset Now + ) : Command; + + Command() { } + } + + public static Event[] Decide(Command command, ShoppingCart state) => + (state, command) switch { + (Pending, Open) => [], + + (Initial, Open(var clientId, var now)) => [new Opened(clientId, now)], + + (Pending, AddProductItem(var productItem, var now)) => [new ProductItemAdded(productItem, now)], + + (Pending(var productItems), RemoveProductItem(var productItem, var now)) => + productItems.HasEnough(productItem) + ? [new ProductItemRemoved(productItem, now)] + : throw new InvalidOperationException("Not enough product items to remove"), + + (Pending, Confirm(var now)) => [new Confirmed(now)], + + (Pending, Cancel(var now)) => [new Canceled(now)], + + _ => throw new InvalidOperationException( + $"Cannot {command.GetType().Name} for {state.GetType().Name} shopping cart" + ) + }; + + public static readonly Decider Decider = + new Decider( + Decide, + Evolve, + () => new Initial() + ); +} + +public record ProductItems(ImmutableDictionary Items) { + public static ProductItems Empty => new(ImmutableDictionary.Empty); + + public ProductItems Add(PricedProductItem productItem) => + IncrementQuantity(Key(productItem), productItem.Quantity); + + public ProductItems Remove(PricedProductItem productItem) => + IncrementQuantity(Key(productItem), -productItem.Quantity); + + public bool HasEnough(PricedProductItem productItem) => + Items.TryGetValue(Key(productItem), out var currentQuantity) && currentQuantity >= productItem.Quantity; + + static string Key(PricedProductItem pricedProductItem) => + $"{pricedProductItem.ProductId}_{pricedProductItem.UnitPrice}"; + + ProductItems IncrementQuantity(string key, int quantity) => + new(Items.SetItem(key, Items.TryGetValue(key, out var current) ? current + quantity : quantity)); +} diff --git a/test/EventStore.Client.Streams.Tests/Delete/deleting_stream.cs b/test/Kurrent.Client.Tests/Streams/DeleteTests.cs similarity index 72% rename from test/EventStore.Client.Streams.Tests/Delete/deleting_stream.cs rename to test/Kurrent.Client.Tests/Streams/DeleteTests.cs index 552a8a4b9..c09e3fdbe 100644 --- a/test/EventStore.Client.Streams.Tests/Delete/deleting_stream.cs +++ b/test/Kurrent.Client.Tests/Streams/DeleteTests.cs @@ -1,16 +1,12 @@ +using EventStore.Client; using Grpc.Core; -namespace EventStore.Client.Streams.Tests.Delete; +namespace Kurrent.Client.Tests.Streams; +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Delete")] -public class deleting_stream(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { - public static IEnumerable ExpectedStreamStateCases() { - yield return new object?[] { StreamState.Any, nameof(StreamState.Any) }; - yield return new object?[] { StreamState.NoStream, nameof(StreamState.NoStream) }; - } - - [Theory] - [MemberData(nameof(ExpectedStreamStateCases))] +public class DeleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { + [Theory, ExpectedStreamStateCases] public async Task hard_deleting_a_stream_that_does_not_exist_with_expected_version_does_not_throw(StreamState expectedVersion, string name) { var stream = $"{Fixture.GetStreamName()}_{name}"; @@ -78,14 +74,11 @@ public async Task hard_deleting_a_deleted_stream_should_throw() { await Assert.ThrowsAsync(() => Fixture.Streams.TombstoneAsync(stream, StreamState.NoStream)); } - - + [Fact] public async Task with_timeout_any_stream_revision_delete_fails_when_operation_expired() { - var stream = Fixture.GetStreamName(); - var rpcException = await Assert.ThrowsAsync( - () => Fixture.Streams.DeleteAsync(stream, StreamState.Any, TimeSpan.Zero) - ); + var stream = Fixture.GetStreamName(); + var rpcException = await Assert.ThrowsAsync(() => Fixture.Streams.DeleteAsync(stream, StreamState.Any, TimeSpan.Zero)); Assert.Equal(StatusCode.DeadlineExceeded, rpcException.StatusCode); } @@ -94,19 +87,15 @@ public async Task with_timeout_any_stream_revision_delete_fails_when_operation_e public async Task with_timeout_stream_revision_delete_fails_when_operation_expired() { var stream = Fixture.GetStreamName(); - var rpcException = await Assert.ThrowsAsync( - () => Fixture.Streams.DeleteAsync(stream, new StreamRevision(0), TimeSpan.Zero) - ); + var rpcException = await Assert.ThrowsAsync(() => Fixture.Streams.DeleteAsync(stream, new StreamRevision(0), TimeSpan.Zero)); Assert.Equal(StatusCode.DeadlineExceeded, rpcException.StatusCode); } [Fact] public async Task with_timeout_any_stream_revision_tombstoning_fails_when_operation_expired() { - var stream = Fixture.GetStreamName(); - var rpcException = await Assert.ThrowsAsync( - () => Fixture.Streams.TombstoneAsync(stream, StreamState.Any, TimeSpan.Zero) - ); + var stream = Fixture.GetStreamName(); + var rpcException = await Assert.ThrowsAsync(() => Fixture.Streams.TombstoneAsync(stream, StreamState.Any, TimeSpan.Zero)); Assert.Equal(StatusCode.DeadlineExceeded, rpcException.StatusCode); } @@ -115,10 +104,15 @@ public async Task with_timeout_any_stream_revision_tombstoning_fails_when_operat public async Task with_timeout_stream_revision_tombstoning_fails_when_operation_expired() { var stream = Fixture.GetStreamName(); - var rpcException = await Assert.ThrowsAsync( - () => Fixture.Streams.TombstoneAsync(stream, new StreamRevision(0), TimeSpan.Zero) - ); + var rpcException = await Assert.ThrowsAsync(() => Fixture.Streams.TombstoneAsync(stream, new StreamRevision(0), TimeSpan.Zero)); Assert.Equal(StatusCode.DeadlineExceeded, rpcException.StatusCode); } -} \ No newline at end of file + + class ExpectedStreamStateCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [StreamState.Any, nameof(StreamState.Any)]; + yield return [StreamState.NoStream, nameof(StreamState.NoStream)]; + } + } +} diff --git a/test/Kurrent.Client.Tests/Streams/GettingState/Functional/GettingStateTests.cs b/test/Kurrent.Client.Tests/Streams/GettingState/Functional/GettingStateTests.cs new file mode 100644 index 000000000..54d39bcd5 --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/GettingState/Functional/GettingStateTests.cs @@ -0,0 +1,150 @@ +using EventStore.Client; +using Kurrent.Client.Streams.GettingState; + +namespace Kurrent.Client.Tests.Streams.GettingState.Functional; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:GetState")] +public class GettingStateTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task gets_state_for_state_builder_with_evolve_function() { + // Given + var shoppingCartId = Guid.NewGuid(); + var clientId = Guid.NewGuid(); + var shoesId = Guid.NewGuid(); + var tShirtId = Guid.NewGuid(); + var twoPairsOfShoes = new PricedProductItem(shoesId, 2, 100); + var pairOfShoes = new PricedProductItem(shoesId, 1, 100); + var tShirt = new PricedProductItem(tShirtId, 1, 50); + + var events = new object[] { + new ShoppingCartOpened(shoppingCartId, clientId), + new ProductItemAddedToShoppingCart(shoppingCartId, twoPairsOfShoes), + new ProductItemAddedToShoppingCart(shoppingCartId, tShirt), + new ProductItemRemovedFromShoppingCart(shoppingCartId, pairOfShoes), + new ShoppingCartConfirmed(shoppingCartId, DateTime.UtcNow), + new ShoppingCartCanceled(shoppingCartId, DateTime.UtcNow) + }; + + var streamName = $"shopping_cart-{shoppingCartId}"; + + await Fixture.Streams.AppendToStreamAsync(streamName, events); + + var stateBuilder = StateBuilder.For(ShoppingCart.Evolve, ShoppingCart.Default); + + // When + var result = await Fixture.Streams.GetStateAsync(streamName, stateBuilder); + + var shoppingCart = result.State; + + // Then + Assert.Equal(shoppingCartId, shoppingCart.Id); + Assert.Equal(2, shoppingCart.ProductItems.Length); + + Assert.Equal(shoesId, shoppingCart.ProductItems[0].ProductId); + Assert.Equal(pairOfShoes.Quantity, shoppingCart.ProductItems[0].Quantity); + Assert.Equal(pairOfShoes.UnitPrice, shoppingCart.ProductItems[0].UnitPrice); + + Assert.Equal(tShirtId, shoppingCart.ProductItems[1].ProductId); + Assert.Equal(tShirt.Quantity, shoppingCart.ProductItems[1].Quantity); + Assert.Equal(tShirt.UnitPrice, shoppingCart.ProductItems[1].UnitPrice); + } +} + +public record ShoppingCartOpened( + Guid ShoppingCartId, + Guid ClientId +); + +public record ProductItemAddedToShoppingCart( + Guid ShoppingCartId, + PricedProductItem ProductItem +); + +public record ProductItemRemovedFromShoppingCart( + Guid ShoppingCartId, + PricedProductItem ProductItem +); + +public record ShoppingCartConfirmed( + Guid ShoppingCartId, + DateTime ConfirmedAt +); + +public record ShoppingCartCanceled( + Guid ShoppingCartId, + DateTime CanceledAt +); + +// VALUE OBJECTS +public record PricedProductItem( + Guid ProductId, + int Quantity, + decimal UnitPrice +) { + public decimal TotalPrice => Quantity * UnitPrice; +} + +// ENTITY +public record ShoppingCart( + Guid Id, + ShoppingCartStatus Status, + PricedProductItem[] ProductItems +) { + public static ShoppingCart Default() => + new(Guid.Empty, default, []); + + public static ShoppingCart Evolve(ShoppingCart shoppingCart, object @event) => + @event switch { + ShoppingCartOpened(var shoppingCartId, var clientId) => + shoppingCart with { + Id = shoppingCartId, + Status = ShoppingCartStatus.Pending + }, + ProductItemAddedToShoppingCart(_, var pricedProductItem) => + shoppingCart with { + ProductItems = shoppingCart.ProductItems + .Concat([pricedProductItem]) + .GroupBy(pi => pi.ProductId) + .Select( + group => group.Count() == 1 + ? group.First() + : new PricedProductItem( + group.Key, + group.Sum(pi => pi.Quantity), + group.First().UnitPrice + ) + ) + .ToArray() + }, + ProductItemRemovedFromShoppingCart(_, var pricedProductItem) => + shoppingCart with { + ProductItems = shoppingCart.ProductItems + .Select( + pi => pi.ProductId == pricedProductItem.ProductId + ? pi with { Quantity = pi.Quantity - pricedProductItem.Quantity } + : pi + ) + .Where(pi => pi.Quantity > 0) + .ToArray() + }, + ShoppingCartConfirmed => + shoppingCart with { + Status = ShoppingCartStatus.Confirmed + }, + ShoppingCartCanceled => + shoppingCart with { + Status = ShoppingCartStatus.Canceled + }, + _ => shoppingCart + }; +} + +public enum ShoppingCartStatus { + Pending = 1, + Confirmed = 2, + Canceled = 4, + + Closed = Confirmed | Canceled +} diff --git a/test/Kurrent.Client.Tests/Streams/GettingState/Projections/GettingStateTests.cs b/test/Kurrent.Client.Tests/Streams/GettingState/Projections/GettingStateTests.cs new file mode 100644 index 000000000..5cc9f92dd --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/GettingState/Projections/GettingStateTests.cs @@ -0,0 +1,196 @@ +using EventStore.Client; +using Kurrent.Client.Streams.GettingState; + +namespace Kurrent.Client.Tests.Streams.GettingState.Projections; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:GetState")] +public class GettingStateTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task gets_state_for_state_builder_with_evolve_function() { + // Given + var streamPrefix = "shopping_cart" + Guid.NewGuid().ToString("N"); + var clientId = Guid.NewGuid(); + // First Shopping Cart + + var shoppingCartId = Guid.NewGuid(); + var shoesId = Guid.NewGuid(); + var tShirtId = Guid.NewGuid(); + var twoPairsOfShoes = new PricedProductItem(shoesId, 2, 100); + var pairOfShoes = new PricedProductItem(shoesId, 1, 100); + var tShirt = new PricedProductItem(tShirtId, 1, 50); + + var events = new object[] { + new ShoppingCartOpened(shoppingCartId, clientId), + new ProductItemAddedToShoppingCart(shoppingCartId, twoPairsOfShoes), + new ProductItemAddedToShoppingCart(shoppingCartId, tShirt), + new ProductItemRemovedFromShoppingCart(shoppingCartId, pairOfShoes), + new ShoppingCartConfirmed(shoppingCartId, DateTime.UtcNow) + }; + + var streamName = $"{streamPrefix}-{shoppingCartId}"; + await Fixture.Streams.AppendToStreamAsync(streamName, events); + + // Second Shopping Cart + var otherShoppingCartId = Guid.NewGuid(); + var jacketId = Guid.NewGuid(); + var jacket = new PricedProductItem(jacketId, 1, 250); + + var otherEvents = new object[] { + new ShoppingCartOpened(otherShoppingCartId, clientId), + new ProductItemAddedToShoppingCart(otherShoppingCartId, jacket) + }; + + var otherStreamName = $"{streamPrefix}-{otherShoppingCartId}"; + await Fixture.Streams.AppendToStreamAsync(otherStreamName, otherEvents); + + var stateBuilder = StateBuilder.For(ShoppingCart.Evolve, ShoppingCart.Default); + + // When + var result = await Fixture.Streams.ReadStreamAsync(streamName) + .ProjectState(stateBuilder) + .ToListAsync(); + + var otherResult = await Fixture.Streams.ReadStreamAsync(otherStreamName) + .ProjectState(stateBuilder) + .ToListAsync(); + + var cts = new CancellationTokenSource(); + var token = cts.Token; + cts.CancelAfter(3000); + + var allResult = await Fixture.Streams + .SubscribeToAll(new SubscribeToAllOptions { Filter = StreamFilter.Prefix(streamPrefix) }) + .ProjectState(stateBuilder, ct: token) + .Take(7) + .ToListAsync(cancellationToken: token); + + var reulst = Fixture.Streams + .SubscribeToAll(new SubscribeToAllOptions { Filter = StreamFilter.Prefix(streamPrefix) }) + .ProjectState(stateBuilder, ct: token); + + // Then + Assert.Equal(5, result.Count); + Assert.Equal(2, otherResult.Count); + Assert.Equal(7, allResult.Count); + + var shoppingCart = result.Last().State; + Assert.Equal(shoppingCartId, shoppingCart.Id); + Assert.Equal(2, shoppingCart.ProductItems.Length); + + Assert.Equal(shoesId, shoppingCart.ProductItems[0].ProductId); + Assert.Equal(pairOfShoes.Quantity, shoppingCart.ProductItems[0].Quantity); + Assert.Equal(pairOfShoes.UnitPrice, shoppingCart.ProductItems[0].UnitPrice); + + Assert.Equal(tShirtId, shoppingCart.ProductItems[1].ProductId); + Assert.Equal(tShirt.Quantity, shoppingCart.ProductItems[1].Quantity); + Assert.Equal(tShirt.UnitPrice, shoppingCart.ProductItems[1].UnitPrice); + + var otherShoppingCart = otherResult.Last().State; + Assert.Contains(otherShoppingCart, allResult.Select(a => a.State)); + Assert.Equal(otherShoppingCartId, otherShoppingCart.Id); + Assert.Single(otherShoppingCart.ProductItems); + + Assert.Equal(jacketId, otherShoppingCart.ProductItems[0].ProductId); + Assert.Equal(jacket.Quantity, otherShoppingCart.ProductItems[0].Quantity); + Assert.Equal(jacket.UnitPrice, otherShoppingCart.ProductItems[0].UnitPrice); + } +} + +public record ShoppingCartOpened( + Guid ShoppingCartId, + Guid ClientId +); + +public record ProductItemAddedToShoppingCart( + Guid ShoppingCartId, + PricedProductItem ProductItem +); + +public record ProductItemRemovedFromShoppingCart( + Guid ShoppingCartId, + PricedProductItem ProductItem +); + +public record ShoppingCartConfirmed( + Guid ShoppingCartId, + DateTime ConfirmedAt +); + +public record ShoppingCartCanceled( + Guid ShoppingCartId, + DateTime CanceledAt +); + +// VALUE OBJECTS +public record PricedProductItem( + Guid ProductId, + int Quantity, + decimal UnitPrice +) { + public decimal TotalPrice => Quantity * UnitPrice; +} + +// ENTITY +public record ShoppingCart( + Guid Id, + ShoppingCartStatus Status, + PricedProductItem[] ProductItems +) { + public static ShoppingCart Default() => + new(Guid.Empty, default, []); + + public static ShoppingCart Evolve(ShoppingCart shoppingCart, object @event) => + @event switch { + ShoppingCartOpened(var shoppingCartId, var clientId) => + shoppingCart with { + Id = shoppingCartId, + Status = ShoppingCartStatus.Pending + }, + ProductItemAddedToShoppingCart(_, var pricedProductItem) => + shoppingCart with { + ProductItems = shoppingCart.ProductItems + .Concat([pricedProductItem]) + .GroupBy(pi => pi.ProductId) + .Select( + group => group.Count() == 1 + ? group.First() + : new PricedProductItem( + group.Key, + group.Sum(pi => pi.Quantity), + group.First().UnitPrice + ) + ) + .ToArray() + }, + ProductItemRemovedFromShoppingCart(_, var pricedProductItem) => + shoppingCart with { + ProductItems = shoppingCart.ProductItems + .Select( + pi => pi.ProductId == pricedProductItem.ProductId + ? pi with { Quantity = pi.Quantity - pricedProductItem.Quantity } + : pi + ) + .Where(pi => pi.Quantity > 0) + .ToArray() + }, + ShoppingCartConfirmed => + shoppingCart with { + Status = ShoppingCartStatus.Confirmed + }, + ShoppingCartCanceled => + shoppingCart with { + Status = ShoppingCartStatus.Canceled + }, + _ => shoppingCart + }; +} + +public enum ShoppingCartStatus { + Pending = 1, + Confirmed = 2, + Canceled = 4, + + Closed = Confirmed | Canceled +} diff --git a/test/Kurrent.Client.Tests/Streams/GettingState/StateBased/GettingStateTests.cs b/test/Kurrent.Client.Tests/Streams/GettingState/StateBased/GettingStateTests.cs new file mode 100644 index 000000000..aef91feea --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/GettingState/StateBased/GettingStateTests.cs @@ -0,0 +1,178 @@ +using EventStore.Client; +using Kurrent.Client.Streams.GettingState; + +namespace Kurrent.Client.Tests.Streams.GettingState.StateBased; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:GetState")] +public class GettingStateTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task gets_state_for_istate_with_default_constructor() { + // Given + var shoppingCartId = Guid.NewGuid(); + var clientId = Guid.NewGuid(); + var shoesId = Guid.NewGuid(); + var tShirtId = Guid.NewGuid(); + var twoPairsOfShoes = new PricedProductItem { + ProductId = shoesId, + Quantity = 2, + UnitPrice = 100 + }; + + var pairOfShoes = new PricedProductItem { + ProductId = shoesId, + Quantity = 1, + UnitPrice = 100 + }; + + var tShirt = new PricedProductItem { + ProductId = tShirtId, + Quantity = 1, + UnitPrice = 50 + }; + + var events = new object[] { + new ShoppingCartOpened(shoppingCartId, clientId), + new ProductItemAddedToShoppingCart(shoppingCartId, twoPairsOfShoes), + new ProductItemAddedToShoppingCart(shoppingCartId, tShirt), + new ProductItemRemovedFromShoppingCart(shoppingCartId, pairOfShoes), + new ShoppingCartConfirmed(shoppingCartId, DateTime.UtcNow), + new ShoppingCartCanceled(shoppingCartId, DateTime.UtcNow) + }; + + var streamName = $"shopping_cart-{shoppingCartId}"; + + await Fixture.Streams.AppendToStreamAsync(streamName, events); + + // When + var result = await Fixture.Streams.GetStateAsync(streamName); + + var shoppingCart = result.State; + + // Then + Assert.Equal(shoppingCartId, shoppingCart.Id); + Assert.Equal(2, shoppingCart.ProductItems.Count); + + Assert.Equal(shoesId, shoppingCart.ProductItems[0].ProductId); + Assert.Equal(pairOfShoes.Quantity, shoppingCart.ProductItems[0].Quantity); + Assert.Equal(pairOfShoes.UnitPrice, shoppingCart.ProductItems[0].UnitPrice); + + Assert.Equal(tShirtId, shoppingCart.ProductItems[1].ProductId); + Assert.Equal(tShirt.Quantity, shoppingCart.ProductItems[1].Quantity); + Assert.Equal(tShirt.UnitPrice, shoppingCart.ProductItems[1].UnitPrice); + } +} + +public record ShoppingCartOpened( + Guid ShoppingCartId, + Guid ClientId +); + +public record ProductItemAddedToShoppingCart( + Guid ShoppingCartId, + PricedProductItem ProductItem +); + +public record ProductItemRemovedFromShoppingCart( + Guid ShoppingCartId, + PricedProductItem ProductItem +); + +public record ShoppingCartConfirmed( + Guid ShoppingCartId, + DateTime ConfirmedAt +); + +public record ShoppingCartCanceled( + Guid ShoppingCartId, + DateTime CanceledAt +); + +public class PricedProductItem { + public Guid ProductId { get; set; } + public decimal UnitPrice { get; set; } + public int Quantity { get; set; } + public decimal TotalPrice => Quantity * UnitPrice; +} + +public class ShoppingCart : IState { + public Guid Id { get; private set; } + public ShoppingCartStatus Status { get; private set; } + public IList ProductItems { get; } = new List(); + + public bool IsClosed => ShoppingCartStatus.Closed.HasFlag(Status); + + public void Apply(object @event) { + switch (@event) { + case ShoppingCartOpened opened: + Apply(opened); + return; + + case ProductItemAddedToShoppingCart productItemAdded: + Apply(productItemAdded); + return; + + case ProductItemRemovedFromShoppingCart productItemRemoved: + Apply(productItemRemoved); + return; + + case ShoppingCartConfirmed confirmed: + Apply(confirmed); + return; + + case ShoppingCartCanceled canceled: + Apply(canceled); + return; + } + } + + public static ShoppingCart Initial() => new(); + + void Apply(ShoppingCartOpened opened) { + Id = opened.ShoppingCartId; + Status = ShoppingCartStatus.Pending; + } + + void Apply(ProductItemAddedToShoppingCart productItemAdded) { + var (_, pricedProductItem) = productItemAdded; + var productId = pricedProductItem.ProductId; + var quantityToAdd = pricedProductItem.Quantity; + + var current = ProductItems.SingleOrDefault(pi => pi.ProductId == productId); + + if (current == null) + ProductItems.Add(pricedProductItem); + else + current.Quantity += quantityToAdd; + } + + void Apply(ProductItemRemovedFromShoppingCart productItemRemoved) { + var (_, pricedProductItem) = productItemRemoved; + var productId = pricedProductItem.ProductId; + var quantityToRemove = pricedProductItem.Quantity; + + var current = ProductItems.Single(pi => pi.ProductId == productId); + + if (current.Quantity == quantityToRemove) + ProductItems.Remove(current); + else + current.Quantity -= quantityToRemove; + } + + void Apply(ShoppingCartConfirmed confirmed) { + Status = ShoppingCartStatus.Confirmed; + } + + void Apply(ShoppingCartCanceled canceled) { + Status = ShoppingCartStatus.Canceled; + } +} + +public enum ShoppingCartStatus { + Pending = 1, + Confirmed = 2, + Canceled = 4, + + Closed = Confirmed | Canceled +} diff --git a/test/Kurrent.Client.Tests/Streams/GettingState/UnionTypes/GettingStateTests.cs b/test/Kurrent.Client.Tests/Streams/GettingState/UnionTypes/GettingStateTests.cs new file mode 100644 index 000000000..af0cfd108 --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/GettingState/UnionTypes/GettingStateTests.cs @@ -0,0 +1,162 @@ +using System.Collections.Immutable; +using EventStore.Client; +using Kurrent.Client.Streams.GettingState; + +namespace Kurrent.Client.Tests.Streams.GettingState.UnionTypes; + +using static ShoppingCart; +using static ShoppingCart.Event; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:GetState")] +public class GettingStateTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task gets_state_for_state_builder_with_evolve_function_and_typed_events() { + // Given + var shoppingCartId = Guid.NewGuid(); + var clientId = Guid.NewGuid(); + var shoesId = Guid.NewGuid(); + var tShirtId = Guid.NewGuid(); + var twoPairsOfShoes = new PricedProductItem(shoesId, 2, 100); + var pairOfShoes = new PricedProductItem(shoesId, 1, 100); + var tShirt = new PricedProductItem(tShirtId, 1, 50); + + var events = new Event[] { + new Opened(shoppingCartId, clientId, DateTime.UtcNow), + new ProductItemAdded(shoppingCartId, twoPairsOfShoes, DateTime.UtcNow), + new ProductItemAdded(shoppingCartId, tShirt, DateTime.UtcNow), + new ProductItemRemoved(shoppingCartId, pairOfShoes, DateTime.UtcNow), + new Confirmed(shoppingCartId, DateTime.UtcNow), + new Canceled(shoppingCartId, DateTime.UtcNow) + }; + + var streamName = $"shopping_cart-{shoppingCartId}"; + + await Fixture.Streams.AppendToStreamAsync(streamName, events); + + var stateBuilder = StateBuilder.For(Evolve, () => new Initial()); + + // When + var result = await Fixture.Streams.GetStateAsync(streamName, stateBuilder); + + var shoppingCart = result.State; + + // Then + Assert.IsType(shoppingCart); + // TODO: Add some time travelling + // Assert.Equal(2, shoppingCart.); + // + // Assert.Equal(shoesId, shoppingCart.ProductItems[0].ProductId); + // Assert.Equal(pairOfShoes.Quantity, shoppingCart.ProductItems[0].Quantity); + // Assert.Equal(pairOfShoes.UnitPrice, shoppingCart.ProductItems[0].UnitPrice); + // + // Assert.Equal(tShirtId, shoppingCart.ProductItems[1].ProductId); + // Assert.Equal(tShirt.Quantity, shoppingCart.ProductItems[1].Quantity); + // Assert.Equal(tShirt.UnitPrice, shoppingCart.ProductItems[1].UnitPrice); + } +} + +public record PricedProductItem( + Guid ProductId, + int Quantity, + decimal UnitPrice +) { + public decimal TotalPrice => Quantity * UnitPrice; +} + +public abstract record ShoppingCart { + public abstract record Event { + public record Opened( + Guid ShoppingCartId, + Guid ClientId, + DateTimeOffset OpenedAt + ) : Event; + + public record ProductItemAdded( + Guid ShoppingCartId, + PricedProductItem ProductItem, + DateTimeOffset AddedAt + ) : Event; + + public record ProductItemRemoved( + Guid ShoppingCartId, + PricedProductItem ProductItem, + DateTimeOffset RemovedAt + ) : Event; + + public record Confirmed( + Guid ShoppingCartId, + DateTimeOffset ConfirmedAt + ) : Event; + + public record Canceled( + Guid ShoppingCartId, + DateTimeOffset CanceledAt + ) : Event; + + // This won't allow external inheritance and mimic union type in C# + Event() { } + } + + public record Initial : ShoppingCart; + + public record Pending(ProductItems ProductItems) : ShoppingCart; + + public record Closed : ShoppingCart; + + public static ShoppingCart Evolve(ShoppingCart state, Event @event) => + (state, @event) switch { + (Initial, Opened) => + new Pending(ProductItems.Empty), + + (Pending(var productItems), ProductItemAdded(_, var productItem, _)) => + new Pending(productItems.Add(productItem)), + + (Pending(var productItems), ProductItemRemoved(_, var productItem, _)) => + new Pending(productItems.Remove(productItem)), + + (Pending, Confirmed) => + new Closed(), + + (Pending, Canceled) => + new Closed(), + + _ => state + }; +} + +public record ProductItems(ImmutableDictionary Items) { + public static ProductItems Empty => new(ImmutableDictionary.Empty); + + public ProductItems Add(PricedProductItem productItem) => + IncrementQuantity(Key(productItem), productItem.Quantity); + + public ProductItems Remove(PricedProductItem productItem) => + IncrementQuantity(Key(productItem), -productItem.Quantity); + + public bool HasEnough(PricedProductItem productItem) => + Items.TryGetValue(Key(productItem), out var currentQuantity) && currentQuantity >= productItem.Quantity; + + static string Key(PricedProductItem pricedProductItem) => + $"{pricedProductItem.ProductId}_{pricedProductItem.UnitPrice}"; + + ProductItems IncrementQuantity(string key, int quantity) => + new(Items.SetItem(key, Items.TryGetValue(key, out var current) ? current + quantity : quantity)); +} + +public static class DictionaryExtensions { + public static ImmutableDictionary Set( + this ImmutableDictionary dictionary, + TKey key, + Func set + ) where TKey : notnull => + dictionary.SetItem(key, set(dictionary.TryGetValue(key, out var current) ? current : default)); + + public static void Set( + this Dictionary dictionary, + TKey key, + Func set + ) where TKey : notnull => + dictionary[key] = set(dictionary.TryGetValue(key, out var current) ? current : default); +} diff --git a/test/EventStore.Client.Streams.Tests/Read/EventBinaryData.cs b/test/Kurrent.Client.Tests/Streams/Read/EventBinaryData.cs similarity index 81% rename from test/EventStore.Client.Streams.Tests/Read/EventBinaryData.cs rename to test/Kurrent.Client.Tests/Streams/Read/EventBinaryData.cs index 923a40cd1..09f8ce6ae 100644 --- a/test/EventStore.Client.Streams.Tests/Read/EventBinaryData.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/EventBinaryData.cs @@ -1,33 +1,35 @@ -namespace EventStore.Client.Streams.Tests.Read; +using EventStore.Client; + +namespace Kurrent.Client.Tests; public readonly record struct EventBinaryData(Uuid Id, byte[] Data, byte[] Metadata) { - public bool Equals(EventBinaryData other) => - Id.Equals(other.Id) - && Data.SequenceEqual(other.Data) + public bool Equals(EventBinaryData other) => + Id.Equals(other.Id) + && Data.SequenceEqual(other.Data) && Metadata.SequenceEqual(other.Metadata); public override int GetHashCode() => System.HashCode.Combine(Id, Data, Metadata); } public static class EventBinaryDataConverters { - public static EventBinaryData ToBinaryData(this EventData source) => + public static EventBinaryData ToBinaryData(this EventData source) => new(source.EventId, source.Data.ToArray(), source.Metadata.ToArray()); - public static EventBinaryData ToBinaryData(this EventRecord source) => + public static EventBinaryData ToBinaryData(this EventRecord source) => new(source.EventId, source.Data.ToArray(), source.Metadata.ToArray()); - public static EventBinaryData ToBinaryData(this ResolvedEvent source) => + public static EventBinaryData ToBinaryData(this ResolvedEvent source) => source.Event.ToBinaryData(); - - public static EventBinaryData[] ToBinaryData(this IEnumerable source) => + + public static EventBinaryData[] ToBinaryData(this IEnumerable source) => source.Select(x => x.ToBinaryData()).ToArray(); - - public static EventBinaryData[] ToBinaryData(this IEnumerable source) => + + public static EventBinaryData[] ToBinaryData(this IEnumerable source) => source.Select(x => x.ToBinaryData()).ToArray(); - - public static EventBinaryData[] ToBinaryData(this IEnumerable source) => + + public static EventBinaryData[] ToBinaryData(this IEnumerable source) => source.Select(x => x.ToBinaryData()).ToArray(); public static ValueTask ToBinaryData(this IAsyncEnumerable source) => source.DefaultIfEmpty().Select(x => x.ToBinaryData()).ToArrayAsync(); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Read/EventDataComparer.cs b/test/Kurrent.Client.Tests/Streams/Read/EventDataComparer.cs similarity index 90% rename from test/EventStore.Client.Streams.Tests/Read/EventDataComparer.cs rename to test/Kurrent.Client.Tests/Streams/Read/EventDataComparer.cs index cd9c6f41a..86113e285 100644 --- a/test/EventStore.Client.Streams.Tests/Read/EventDataComparer.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/EventDataComparer.cs @@ -1,4 +1,6 @@ -namespace EventStore.Client.Streams.Tests.Read; +using EventStore.Client; + +namespace Kurrent.Client.Tests; static class EventDataComparer { public static bool Equal(EventData expected, EventRecord actual) { @@ -18,4 +20,4 @@ public static bool Equal(EventData[] expected, EventRecord[] actual) { return !expected.Where((t, i) => !Equal(t, actual[i])).Any(); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Read/read_all_events_backward.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs similarity index 85% rename from test/EventStore.Client.Streams.Tests/Read/read_all_events_backward.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs index b07f199ef..a5850e906 100644 --- a/test/EventStore.Client.Streams.Tests/Read/read_all_events_backward.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsBackwardTests.cs @@ -1,12 +1,16 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; using Grpc.Core; -namespace EventStore.Client.Streams.Tests.Read; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Streams")] [Trait("Category", "Target:All")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Backwards")] [Trait("Category", "Database:Dedicated")] -public class read_all_events_backward(ITestOutputHelper output, ReadAllEventsFixture fixture) : EventStoreTests(output, fixture) { +public class ReadAllEventsBackwardTests(ITestOutputHelper output, ReadAllEventsFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task return_empty_if_reading_from_start() { var result = await Fixture.Streams.ReadAllAsync(Direction.Backwards, Position.Start, 1).CountAsync(); @@ -28,7 +32,7 @@ public async Task return_events_in_reversed_order_compared_to_written() { .ReadAllAsync(Direction.Backwards, Position.End) .Where(x => x.OriginalStreamId == Fixture.ExpectedStreamName) .ToBinaryData(); - + result.ShouldBe(Fixture.ExpectedEventsReversed); } @@ -37,7 +41,7 @@ public async Task return_single_event() { var result = await Fixture.Streams .ReadAllAsync(Direction.Backwards, Position.End, 1) .ToArrayAsync(); - + result.ShouldHaveSingleItem(); } @@ -51,36 +55,36 @@ public async Task max_count_is_respected() { result.Length.ShouldBe(maxCount); } - + [Fact] public async Task stream_found() { var count = await Fixture.Streams .ReadAllAsync(Direction.Backwards, Position.End) .Where(x => x.OriginalStreamId == Fixture.ExpectedStreamName) .CountAsync(); - + count.ShouldBe(Fixture.ExpectedEvents.Length); } - + [Fact] public async Task with_timeout_fails_when_operation_expired() { var ex = await Fixture.Streams .ReadAllAsync(Direction.Backwards, Position.Start, 1, false, TimeSpan.Zero) .ToArrayAsync() .AsTask().ShouldThrowAsync(); - + ex.StatusCode.ShouldBe(StatusCode.DeadlineExceeded); } - + [Fact] public async Task filter_events_by_type() { var result = await Fixture.Streams - .ReadAllAsync(Direction.Backwards, Position.End, EventTypeFilter.Prefix(EventStoreFixture.AnotherTestEventTypePrefix)) + .ReadAllAsync(Direction.Backwards, Position.End, EventTypeFilter.Prefix(KurrentTemporaryFixture.AnotherTestEventTypePrefix)) .ToListAsync(); - - result.ForEach(x => x.Event.EventType.ShouldStartWith(EventStoreFixture.AnotherTestEventTypePrefix)); + + result.ForEach(x => x.Event.EventType.ShouldStartWith(KurrentTemporaryFixture.AnotherTestEventTypePrefix)); } - + [Fact(Skip = "Not Implemented")] public Task be_able_to_read_all_one_by_one_until_end_of_stream() => throw new NotImplementedException(); diff --git a/test/EventStore.Client.Streams.Tests/Read/ReadAllEventsFixture.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs similarity index 75% rename from test/EventStore.Client.Streams.Tests/Read/ReadAllEventsFixture.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs index 197c8a481..e98a6ae28 100644 --- a/test/EventStore.Client.Streams.Tests/Read/ReadAllEventsFixture.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsFixture.cs @@ -1,6 +1,13 @@ -namespace EventStore.Client.Streams.Tests.Read; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -public class ReadAllEventsFixture : EventStoreFixture { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Target:All")] +[Trait("Category", "Operation:Read")] +[Trait("Category", "Database:Dedicated")] +public class ReadAllEventsFixture : KurrentTemporaryFixture { public ReadAllEventsFixture() { OnSetup = async () => { _ = await Streams.SetStreamMetadataAsync( @@ -20,7 +27,7 @@ public ReadAllEventsFixture() { await Streams.AppendToStreamAsync(ExpectedStreamName, StreamState.NoStream, Events); ExpectedEvents = Events.ToBinaryData(); - ExpectedEventsReversed = ExpectedEvents.Reverse().ToArray(); + ExpectedEventsReversed = Enumerable.Reverse(ExpectedEvents).ToArray(); ExpectedFirstEvent = ExpectedEvents.First(); ExpectedLastEvent = ExpectedEvents.Last(); @@ -36,4 +43,4 @@ public ReadAllEventsFixture() { public EventBinaryData ExpectedFirstEvent { get; private set; } public EventBinaryData ExpectedLastEvent { get; private set; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Read/read_all_events_forward.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs similarity index 90% rename from test/EventStore.Client.Streams.Tests/Read/read_all_events_forward.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs index fed9629ad..f1c4ec2bd 100644 --- a/test/EventStore.Client.Streams.Tests/Read/read_all_events_forward.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadAllEventsForwardTests.cs @@ -1,12 +1,15 @@ using System.Text; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -namespace EventStore.Client.Streams.Tests.Read; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Streams")] [Trait("Category", "Target:All")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Forwards")] [Trait("Category", "Database:Dedicated")] -public class read_all_events_forward(ITestOutputHelper output, ReadAllEventsFixture fixture) : EventStoreTests(output, fixture) { +public class ReadAllEventsForwardTests(ITestOutputHelper output, ReadAllEventsFixture fixture) : KurrentTemporaryTests(output, fixture) { [Fact] public async Task return_empty_if_reading_from_end() { var result = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.End, 1).CountAsync(); @@ -27,7 +30,7 @@ public async Task return_events_in_correct_order_compared_to_written() { .ReadAllAsync(Direction.Forwards, Position.Start) .Where(x => x.OriginalStreamId == Fixture.ExpectedStreamName) .ToBinaryData(); - + result.ShouldBe(Fixture.ExpectedEvents); } @@ -36,7 +39,7 @@ public async Task return_single_event() { var result = await Fixture.Streams .ReadAllAsync(Direction.Forwards, Position.Start, 1) .ToArrayAsync(); - + result.ShouldHaveSingleItem(); } @@ -59,22 +62,22 @@ public async Task reads_all_events_by_default() { Assert.True(count >= Fixture.ExpectedEvents.Length); } - + [Fact] public async Task stream_found() { var count = await Fixture.Streams .ReadAllAsync(Direction.Forwards, Position.Start) .Where(x => x.OriginalStreamId == Fixture.ExpectedStreamName) .CountAsync(); - + count.ShouldBe(Fixture.ExpectedEvents.Length); } - + [Fact] public async Task with_linkto_passed_max_count_one_event_is_read() { const string deletedStream = nameof(deletedStream); const string linkedStream = nameof(linkedStream); - + await Fixture.Streams.AppendToStreamAsync(deletedStream, StreamState.Any, Fixture.CreateTestEvents()); await Fixture.Streams.SetStreamMetadataAsync( deletedStream, @@ -105,10 +108,10 @@ await Fixture.Streams.AppendToStreamAsync( resolveLinkTos: true ) .ToArrayAsync(); - + Assert.Single(events); } - + [Fact] public async Task enumeration_all_referencing_messages_twice_does_not_throw() { var result = Fixture.Streams.ReadAllAsync( @@ -138,16 +141,16 @@ await Assert.ThrowsAsync( await result.Messages.ToArrayAsync() ); } - + [Fact] public async Task filter_events_by_type() { var result = await Fixture.Streams - .ReadAllAsync(Direction.Forwards, Position.Start, EventTypeFilter.Prefix(EventStoreFixture.AnotherTestEventTypePrefix)) + .ReadAllAsync(Direction.Forwards, Position.Start, EventTypeFilter.Prefix(KurrentTemporaryFixture.AnotherTestEventTypePrefix)) .ToListAsync(); - - result.ForEach(x => x.Event.EventType.ShouldStartWith(EventStoreFixture.AnotherTestEventTypePrefix)); + + result.ForEach(x => x.Event.EventType.ShouldStartWith(KurrentTemporaryFixture.AnotherTestEventTypePrefix)); } - + [Fact(Skip = "Not Implemented")] public Task be_able_to_read_all_one_by_one_until_end_of_stream() => throw new NotImplementedException(); diff --git a/test/EventStore.Client.Streams.Tests/Read/read_stream_backward.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs similarity index 94% rename from test/EventStore.Client.Streams.Tests/Read/read_stream_backward.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs index ec32888e8..5ca640feb 100644 --- a/test/EventStore.Client.Streams.Tests/Read/read_stream_backward.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamBackwardTests.cs @@ -1,11 +1,15 @@ +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; using Grpc.Core; -namespace EventStore.Client.Streams.Tests.Read; +namespace Kurrent.Client.Tests; -[Trait("Category", "Target:Stream")] +[Trait("Category", "Target:Streams")] +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Backwards")] -public class read_stream_backward(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { +public class ReadStreamBackwardTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Theory] [InlineData(0)] public async Task count_le_equal_zero_throws(long maxCount) { @@ -74,7 +78,7 @@ public async Task returns_events_in_reversed_order(string suffix, int count, int Assert.True( EventDataComparer.Equal( - expected.Reverse().ToArray(), + Enumerable.Reverse(expected).ToArray(), actual ) ); @@ -165,7 +169,7 @@ await Fixture.Streams.AppendToStreamAsync( [Fact] public async Task populates_log_position_of_event() { - if (EventStoreTestServer.Version.Major < 22) + if (KurrentTemporaryTestNode.Version.Major < 22) return; var stream = Fixture.GetStreamName(); @@ -181,7 +185,7 @@ public async Task populates_log_position_of_event() { Assert.Equal(writeResult.LogPosition.PreparePosition, writeResult.LogPosition.CommitPosition); Assert.Equal(writeResult.LogPosition, actual.First().Position); } - + [Fact] public async Task with_timeout_fails_when_operation_expired() { var stream = Fixture.GetStreamName(); @@ -203,7 +207,7 @@ public async Task with_timeout_fails_when_operation_expired() { Assert.Equal(StatusCode.DeadlineExceeded, rpcException.StatusCode); } - + [Fact] public async Task enumeration_referencing_messages_twice_does_not_throw() { var result = Fixture.Streams.ReadStreamAsync( @@ -235,7 +239,7 @@ await Assert.ThrowsAsync( await result.Messages.ToArrayAsync() ); } - + [Fact] public async Task stream_not_found() { var result = await Fixture.Streams.ReadStreamAsync( @@ -250,7 +254,7 @@ public async Task stream_not_found() { [Fact] public async Task stream_found() { const int eventCount = 32; - + var events = Fixture.CreateTestEvents(eventCount).ToArray(); var streamName = Fixture.GetStreamName(); @@ -274,4 +278,4 @@ public async Task stream_found() { if (Fixture.EventStoreHasLastStreamPosition) Assert.Equal(new StreamMessage.LastStreamPosition(new(31)), result[^1]); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Read/read_stream_events_linked_to_deleted_stream.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs similarity index 67% rename from test/EventStore.Client.Streams.Tests/Read/read_stream_events_linked_to_deleted_stream.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs index ba11b75f0..b1a803c71 100644 --- a/test/EventStore.Client.Streams.Tests/Read/read_stream_events_linked_to_deleted_stream.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamEventsLinkedToDeletedStreamTests.cs @@ -1,13 +1,19 @@ using System.Text; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -namespace EventStore.Client.Streams.Tests.Read; +// ReSharper disable ClassNeverInstantiated.Global -[Trait("Category", "Target:Stream")] -public abstract class read_stream_events_linked_to_deleted_stream(ReadEventsLinkedToDeletedStreamFixture fixture) { +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Target:Streams")] +public abstract class ReadStreamEventsLinkedToDeletedStreamTests(ReadEventsLinkedToDeletedStreamFixture fixture) { ReadEventsLinkedToDeletedStreamFixture Fixture { get; } = fixture; [Fact] - public void one_event_is_read() => Assert.Single(Fixture.Events ?? Array.Empty()); + public void one_event_is_read() => Assert.Single(Fixture.Events ?? []); [Fact] public void the_linked_event_is_not_resolved() => Assert.Null(Fixture.Events![0].Event); @@ -17,32 +23,30 @@ public abstract class read_stream_events_linked_to_deleted_stream(ReadEventsLink [Fact] public void the_event_is_not_resolved() => Assert.False(Fixture.Events![0].IsResolved); - + + [UsedImplicitly] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Forwards")] - public class @forwards(forwards.CustomFixture fixture) - : read_stream_events_linked_to_deleted_stream(fixture), IClassFixture { - + public class Forwards(Forwards.CustomFixture fixture) : ReadStreamEventsLinkedToDeletedStreamTests(fixture), IClassFixture { public class CustomFixture() : ReadEventsLinkedToDeletedStreamFixture(Direction.Forwards); } - + + [UsedImplicitly] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Backwards")] - public class @backwards(backwards.CustomFixture fixture) - : read_stream_events_linked_to_deleted_stream(fixture), IClassFixture { - + public class Backwards(Backwards.CustomFixture fixture) : ReadStreamEventsLinkedToDeletedStreamTests(fixture), IClassFixture { public class CustomFixture() : ReadEventsLinkedToDeletedStreamFixture(Direction.Backwards); } } -public abstract class ReadEventsLinkedToDeletedStreamFixture : EventStoreFixture { +public abstract class ReadEventsLinkedToDeletedStreamFixture : KurrentTemporaryFixture { const string DeletedStream = nameof(DeletedStream); const string LinkedStream = nameof(LinkedStream); protected ReadEventsLinkedToDeletedStreamFixture(Direction direction) { OnSetup = async () => { await Streams.AppendToStreamAsync(DeletedStream, StreamState.Any, CreateTestEvents()); - + await Streams.AppendToStreamAsync( LinkedStream, StreamState.Any, @@ -58,7 +62,7 @@ await Streams.AppendToStreamAsync( ); await Streams.DeleteAsync(DeletedStream, StreamState.Any); - + Events = await Streams.ReadStreamAsync( direction, LinkedStream, @@ -70,4 +74,4 @@ await Streams.AppendToStreamAsync( } public ResolvedEvent[]? Events { get; private set; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Read/read_stream_forward.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamForwardTests.cs similarity index 95% rename from test/EventStore.Client.Streams.Tests/Read/read_stream_forward.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadStreamForwardTests.cs index 14c08a79c..1e8d5db5b 100644 --- a/test/EventStore.Client.Streams.Tests/Read/read_stream_forward.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamForwardTests.cs @@ -1,9 +1,12 @@ -namespace EventStore.Client.Streams.Tests.Read; +using EventStore.Client; -[Trait("Category", "Target:Stream")] +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Read")] [Trait("Category", "Operation:Read:Forwards")] -public class read_stream_forward(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { +public class ReadStreamForwardTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [Theory] [InlineData(0)] public async Task count_le_equal_zero_throws(long maxCount) { @@ -159,7 +162,7 @@ await Fixture.Streams.AppendToStreamAsync( [Fact] public async Task populates_log_position_of_event() { - if (EventStoreTestServer.Version.Major < 22) + if (KurrentPermanentTestNode.Version.Major < 22) return; var stream = Fixture.GetStreamName(); @@ -191,7 +194,7 @@ public async Task stream_not_found() { [Fact] public async Task stream_found() { const int eventCount = 64; - + var events = Fixture.CreateTestEvents(eventCount).ToArray(); var streamName = Fixture.GetStreamName(); @@ -226,7 +229,7 @@ public async Task stream_found() { [Fact] public async Task stream_found_truncated() { const int eventCount = 64; - + var events = Fixture.CreateTestEvents(eventCount).ToArray(); var streamName = Fixture.GetStreamName(); @@ -263,4 +266,4 @@ await Fixture.Streams.SetStreamMetadataAsync( result[^1] ); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Read/read_stream_when_having_max_count_set_for_stream.cs b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs similarity index 91% rename from test/EventStore.Client.Streams.Tests/Read/read_stream_when_having_max_count_set_for_stream.cs rename to test/Kurrent.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs index 0142ebd9a..1e22b5eb1 100644 --- a/test/EventStore.Client.Streams.Tests/Read/read_stream_when_having_max_count_set_for_stream.cs +++ b/test/Kurrent.Client.Tests/Streams/Read/ReadStreamWhenHavingMaxCountSetForStreamTests.cs @@ -1,8 +1,14 @@ -namespace EventStore.Client.Streams.Tests.Read; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; +using Kurrent.Client.Tests; -[Trait("Category", "Target:Stream")] +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Read")] -public class read_stream_when_having_max_count_set_for_stream (ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { +public class ReadStreamWhenHavingMaxCountSetForStreamTests(ITestOutputHelper output, KurrentTemporaryFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task read_stream_forwards_respects_max_count() { var stream = Fixture.GetStreamName(); @@ -118,4 +124,4 @@ public async Task after_setting_more_strict_max_count_read_stream_backwards_read Assert.Equal(2, actual.Length); Assert.True(EventDataComparer.Equal(expected.Skip(3).Reverse().ToArray(), actual)); } -} \ No newline at end of file +} diff --git a/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.PersistentSubscriptions.cs b/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.PersistentSubscriptions.cs new file mode 100644 index 000000000..61dc3c18f --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.PersistentSubscriptions.cs @@ -0,0 +1,471 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.Json; +using EventStore.Client; +using Kurrent.Client.Core.Serialization; +using Kurrent.Diagnostics.Tracing; + +namespace Kurrent.Client.Tests.Streams.Serialization; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:Append")] +public class PersistentSubscriptionsSerializationTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task plain_clr_objects_are_serialized_and_deserialized_using_auto_serialization() { + // Given + var stream = Fixture.GetStreamName(); + List expected = GenerateMessages(); + + //When + await Fixture.Streams.AppendToStreamAsync(stream, expected); + + var group = await CreateToStreamSubscription(stream); + + var resolvedEvents = await Fixture.Subscriptions.SubscribeToStream(stream, group).Take(2).ToListAsync(); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task + message_data_and_metadata_are_serialized_and_deserialized_using_auto_serialization_with_registered_metadata() { + // Given + await using var client = NewClientWith(serialization => serialization.UseMetadataType()); + await using var subscriptionsClient = + NewSubscriptionsClientWith(serialization => serialization.UseMetadataType()); + + var stream = Fixture.GetStreamName(); + var metadata = new CustomMetadata(Guid.NewGuid()); + var expected = GenerateMessages(); + List messagesWithMetadata = + expected.Select(message => Message.From(message, metadata, Uuid.NewUuid())).ToList(); + + // When + await client.AppendToStreamAsync(stream, messagesWithMetadata); + + // Then + var group = await CreateToStreamSubscription(stream, subscriptionsClient); + var resolvedEvents = await subscriptionsClient.SubscribeToStream(stream, group).Take(2).ToListAsync(); + var messages = AssertThatMessages(AreDeserialized, expected, resolvedEvents); + + Assert.Equal(messagesWithMetadata, messages); + } + + [RetryFact] + public async Task + message_metadata_is_serialized_fully_byt_deserialized_to_tracing_metadata_using_auto_serialization_WITHOUT_registered_custom_metadata() { + var stream = Fixture.GetStreamName(); + var metadata = new CustomMetadata(Guid.NewGuid()); + var expected = GenerateMessages(); + List messagesWithMetadata = + expected.Select(message => Message.From(message, metadata, Uuid.NewUuid())).ToList(); + + // When + await Fixture.Streams.AppendToStreamAsync(stream, messagesWithMetadata); + + // Then + var group = await CreateToStreamSubscription(stream); + var resolvedEvents = await Fixture.Subscriptions.SubscribeToStream(stream, group).Take(2) + .ToListAsync(); + + var messages = AssertThatMessages(AreDeserialized, expected, resolvedEvents); + + Assert.Equal(messagesWithMetadata.Select(m => m with { Metadata = new TracingMetadata() }), messages); + } + + [RetryFact] + public async Task subscribe_to_stream_without_options_does_NOT_deserialize_resolved_message() { + // Given + var (stream, expected) = await AppendEventsUsingAutoSerialization(); + + // When + var group = await CreateToStreamSubscription(stream); + var resolvedEvents = await Fixture.Subscriptions + .SubscribeToStream(stream, group, int.MaxValue).Take(2) + .ToListAsync(); + + // Then + AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task subscribe_to_all_without_options_does_NOT_deserialize_resolved_message() { + // Given + var (stream, expected) = await AppendEventsUsingAutoSerialization(); + + // When + var group = await CreateToAllSubscription(stream); + var resolvedEvents = await Fixture.Subscriptions + .SubscribeToAll(group, int.MaxValue) + .Take(2) + .ToListAsync(); + + // Then + AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); + } + + public static TheoryData> CustomTypeMappings() { + return [ + (settings, typeName) => + settings.RegisterMessageType(typeName), + (settings, typeName) => + settings.RegisterMessageType(typeof(UserRegistered), typeName), + (settings, typeName) => + settings.RegisterMessageTypes(new Dictionary { { typeof(UserRegistered), typeName } }) + ]; + } + + [RetryTheory] + [MemberData(nameof(CustomTypeMappings))] + public async Task append_and_subscribe_to_stream_uses_custom_type_mappings( + Action customTypeMapping + ) { + // Given + await using var client = NewClientWith(serialization => customTypeMapping(serialization, "user_registered")); + await using var subscriptionsClient = + NewSubscriptionsClientWith(serialization => customTypeMapping(serialization, "user_registered")); + + // When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + // Then + var group = await CreateToStreamSubscription(stream, subscriptionsClient); + var resolvedEvents = await subscriptionsClient.SubscribeToStream(stream, group).Take(2) + .ToListAsync(); + + Assert.All(resolvedEvents, resolvedEvent => Assert.Equal("user_registered", resolvedEvent.Event.EventType)); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryTheory] + [MemberData(nameof(CustomTypeMappings))] + public async Task append_and_subscribe_to_all_uses_custom_type_mappings( + Action customTypeMapping + ) { + // Given + await using var client = NewClientWith(serialization => customTypeMapping(serialization, "user_registered")); + await using var subscriptionsClient = + NewSubscriptionsClientWith(serialization => customTypeMapping(serialization, "user_registered")); + + // When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + // Then + var group = await CreateToAllSubscription(stream, subscriptionsClient); + var resolvedEvents = await subscriptionsClient + .SubscribeToAll(group) + .Where(r => r.Event.EventStreamId == stream) + .Take(2) + .ToListAsync(); + + Assert.All(resolvedEvents, resolvedEvent => Assert.Equal("user_registered", resolvedEvent.Event.EventType)); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task automatic_serialization_custom_json_settings_are_applied() { + // Given + var systemTextJsonOptions = new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, + }; + + await using var client = NewClientWith(serialization => serialization.UseJsonSettings(systemTextJsonOptions)); + await using var subscriptionsClient = + NewSubscriptionsClientWith(serialization => serialization.UseJsonSettings(systemTextJsonOptions)); + + // When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + // Then + var group = await CreateToStreamSubscription(stream, subscriptionsClient); + var resolvedEvents = await subscriptionsClient.SubscribeToStream(stream, group).Take(2) + .ToListAsync(); + + var jsons = resolvedEvents.Select(e => JsonDocument.Parse(e.Event.Data).RootElement).ToList(); + + Assert.Equal(expected.Select(m => m.UserId), jsons.Select(j => j.GetProperty("user-id").GetGuid())); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + public class CustomMessageTypeNamingStrategy : IMessageTypeNamingStrategy { + public string ResolveTypeName(Type messageType, MessageTypeNamingResolutionContext resolutionContext) { + return $"custom-{messageType}"; + } + +#if NET48 + public bool TryResolveClrType(string messageTypeName, out Type? type) { +#else + public bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? type) { +#endif + var typeName = messageTypeName[(messageTypeName.IndexOf('-') + 1)..]; + type = Type.GetType(typeName); + + return type != null; + } + +#if NET48 + public bool TryResolveClrMetadataType(string messageTypeName, out Type? type) { +#else + public bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? type) { +#endif + type = null; + return false; + } + } + + [RetryFact] + public async Task append_and_subscribe_to_stream_uses_custom_message_type_naming_strategy() { + // Given + await using var client = NewClientWith( + serialization => serialization.UseMessageTypeNamingStrategy() + ); + + await using var subscriptionsClient = + NewSubscriptionsClientWith( + serialization => serialization.UseMessageTypeNamingStrategy() + ); + + //When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + //Then + var group = await CreateToStreamSubscription(stream, subscriptionsClient); + var resolvedEvents = await subscriptionsClient.SubscribeToStream(stream,group).Take(2) + .ToListAsync(); + + Assert.All( + resolvedEvents, + resolvedEvent => Assert.Equal($"custom-{typeof(UserRegistered).FullName}", resolvedEvent.Event.EventType) + ); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task append_and_subscribe_to_all_uses_custom_message_type_naming_strategy() { + // Given + await using var client = NewClientWith( + serialization => serialization.UseMessageTypeNamingStrategy() + ); + + await using var subscriptionsClient = + NewSubscriptionsClientWith( + serialization => serialization.UseMessageTypeNamingStrategy() + ); + + //When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + //Then + var group = await CreateToAllSubscription(stream, subscriptionsClient); + var resolvedEvents = await subscriptionsClient + .SubscribeToAll(group) + .Where(r => r.Event.EventStreamId == stream) + .Take(2) + .ToListAsync(); + + Assert.All( + resolvedEvents, + resolvedEvent => Assert.Equal($"custom-{typeof(UserRegistered).FullName}", resolvedEvent.Event.EventType) + ); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task + subscribe_to_stream_deserializes_resolved_message_appended_with_manual_compatible_serialization() { + // Given + var (stream, expected) = await AppendEventsUsingManualSerialization( + message => $"stream-{message.GetType().FullName!}" + ); + + // When + var group = await CreateToStreamSubscription(stream); + var resolvedEvents = await Fixture.Subscriptions.SubscribeToStream(stream, group).Take(2) + .ToListAsync(); + + // Then + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task subscribe_to_all_deserializes_resolved_message_appended_with_manual_compatible_serialization() { + // Given + var (stream, expected) = await AppendEventsUsingManualSerialization( + message => $"stream-{message.GetType().FullName!}" + ); + + // When + var group = await CreateToAllSubscription(stream); + var resolvedEvents = await Fixture.Subscriptions + .SubscribeToAll(group) + .Where(r => r.Event.EventStreamId == stream) + .Take(2) + .ToListAsync(); + + // Then + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task + subscribe_to_stream_does_NOT_deserialize_resolved_message_appended_with_manual_incompatible_serialization() { + // Given + var (stream, expected) = await AppendEventsUsingManualSerialization(_ => "user_registered"); + + // When + var group = await CreateToStreamSubscription(stream); + var resolvedEvents = await Fixture.Subscriptions.SubscribeToStream(stream, group).Take(2) + .ToListAsync(); + + // Then + AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task + subscribe_to_all_does_NOT_deserialize_resolved_message_appended_with_manual_incompatible_serialization() { + // Given + var (stream, expected) = await AppendEventsUsingManualSerialization(_ => "user_registered"); + + // When + var group = await CreateToAllSubscription(stream); + var resolvedEvents = await Fixture.Subscriptions + .SubscribeToAll(group) + .Where(r => r.Event.EventStreamId == stream) + .Take(2) + .ToListAsync(); + + // Then + AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); + } + + static List AssertThatMessages( + Action assertMatches, + List expected, + List resolvedEvents + ) { + Assert.Equal(expected.Count, resolvedEvents.Count); + Assert.NotEmpty(resolvedEvents); + + Assert.All(resolvedEvents, (resolvedEvent, idx) => assertMatches(expected[idx], resolvedEvent)); + + return resolvedEvents.Select(resolvedEvent => resolvedEvent.Message!).ToList(); + } + + static void AreDeserialized(UserRegistered expected, ResolvedEvent resolvedEvent) { + Assert.NotNull(resolvedEvent.Message); + Assert.Equal(expected, resolvedEvent.Message.Data); + Assert.Equal(expected, resolvedEvent.DeserializedData); + } + + static void AreNotDeserialized(UserRegistered expected, ResolvedEvent resolvedEvent) { + Assert.Null(resolvedEvent.Message); + Assert.Equal( + expected, + JsonSerializer.Deserialize( + resolvedEvent.Event.Data.Span, + SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions + ) + ); + } + + async Task<(string, List)> AppendEventsUsingAutoSerialization(KurrentClient? kurrentClient = null) { + var stream = Fixture.GetStreamName(); + var messages = GenerateMessages(); + + var writeResult = await (kurrentClient ?? Fixture.Streams).AppendToStreamAsync(stream, messages); + Assert.Equal(new((ulong)messages.Count - 1), writeResult.NextExpectedStreamRevision); + + return (stream, messages); + } + + async Task<(string, List)> AppendEventsUsingManualSerialization( + Func getTypeName + ) { + var stream = Fixture.GetStreamName(); + var messages = GenerateMessages(); + var eventData = messages.Select( + message => + new EventData( + Uuid.NewUuid(), + getTypeName(message), + Encoding.UTF8.GetBytes( + JsonSerializer.Serialize( + message, + SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions + ) + ) + ) + ); + + var writeResult = await Fixture.Streams.AppendToStreamAsync(stream, StreamRevision.None, eventData); + Assert.Equal(new((ulong)messages.Count - 1), writeResult.NextExpectedStreamRevision); + + return (stream, messages); + } + + static List GenerateMessages(int count = 2) => + Enumerable.Range(0, count) + .Select( + _ => new UserRegistered( + Guid.NewGuid(), + new Address(Guid.NewGuid().ToString(), Guid.NewGuid().GetHashCode()) + ) + ).ToList(); + + KurrentClient NewClientWith(Action customizeSerialization) { + var settings = Fixture.ClientSettings; + settings.Serialization = settings.Serialization.Clone(); + customizeSerialization(settings.Serialization); + + return new KurrentClient(settings); + } + + KurrentPersistentSubscriptionsClient NewSubscriptionsClientWith( + Action customizeSerialization + ) { + var settings = Fixture.ClientSettings; + settings.Serialization = settings.Serialization.Clone(); + customizeSerialization(settings.Serialization); + + return new KurrentPersistentSubscriptionsClient(settings); + } + + async Task CreateToStreamSubscription(string stream, KurrentPersistentSubscriptionsClient? client = null) { + string group = Fixture.GetGroupName(); + + await (client ?? Fixture.Subscriptions).CreateToStreamAsync( + stream, + group, + new(startFrom: StreamPosition.Start), + userCredentials: TestCredentials.Root + ); + + return group; + } + + async Task CreateToAllSubscription(string stream, KurrentPersistentSubscriptionsClient? client = null) { + string group = Fixture.GetGroupName(); + + await (client ?? Fixture.Subscriptions).CreateToAllAsync( + group, + StreamFilter.Prefix(stream), + new(startFrom: Position.Start), + userCredentials: TestCredentials.Root + ); + + return group; + } + + public record Address(string Street, int Number); + + public record UserRegistered(Guid UserId, Address Address); + + public record CustomMetadata(Guid UserId); +} diff --git a/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.Subscriptions.cs b/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.Subscriptions.cs new file mode 100644 index 000000000..12529d6c3 --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.Subscriptions.cs @@ -0,0 +1,383 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.Json; +using EventStore.Client; +using Kurrent.Client.Core.Serialization; +using Kurrent.Diagnostics.Tracing; + +namespace Kurrent.Client.Tests.Streams.Serialization; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:Append")] +public class SubscriptionsSerializationTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task plain_clr_objects_are_serialized_and_deserialized_using_auto_serialization() { + // Given + var stream = Fixture.GetStreamName(); + List expected = GenerateMessages(); + + //When + await Fixture.Streams.AppendToStreamAsync(stream, expected); + + //Then + var resolvedEvents = await Fixture.Streams.SubscribeToStream(stream).Take(2).ToListAsync(); + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task + message_data_and_metadata_are_serialized_and_deserialized_using_auto_serialization_with_registered_metadata() { + // Given + await using var client = NewClientWith(serialization => serialization.UseMetadataType()); + + var stream = Fixture.GetStreamName(); + var metadata = new CustomMetadata(Guid.NewGuid()); + var expected = GenerateMessages(); + List messagesWithMetadata = + expected.Select(message => Message.From(message, metadata, Uuid.NewUuid())).ToList(); + + // When + await client.AppendToStreamAsync(stream, messagesWithMetadata); + + // Then + var resolvedEvents = await client.SubscribeToStream(stream).Take(2).ToListAsync(); + var messages = AssertThatMessages(AreDeserialized, expected, resolvedEvents); + + Assert.Equal(messagesWithMetadata, messages); + } + + [RetryFact] + public async Task + message_metadata_is_serialized_fully_byt_deserialized_to_tracing_metadata_using_auto_serialization_WITHOUT_registered_custom_metadata() { + var stream = Fixture.GetStreamName(); + var metadata = new CustomMetadata(Guid.NewGuid()); + var expected = GenerateMessages(); + List messagesWithMetadata = + expected.Select(message => Message.From(message, metadata, Uuid.NewUuid())).ToList(); + + // When + await Fixture.Streams.AppendToStreamAsync(stream, messagesWithMetadata); + + // Then + var resolvedEvents = await Fixture.Streams.SubscribeToStream(stream).Take(2).ToListAsync(); + var messages = AssertThatMessages(AreDeserialized, expected, resolvedEvents); + + Assert.Equal(messagesWithMetadata.Select(m => m with { Metadata = new TracingMetadata() }), messages); + } + + [RetryFact] + public async Task subscribe_to_stream_without_options_does_NOT_deserialize_resolved_message() { + // Given + var (stream, expected) = await AppendEventsUsingAutoSerialization(); + + // When + var resolvedEvents = await Fixture.Streams + .SubscribeToStream(stream, FromStream.Start).Take(2) + .ToListAsync(); + + // Then + AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task subscribe_to_all_without_options_does_NOT_deserialize_resolved_message() { + // Given + var (stream, expected) = await AppendEventsUsingAutoSerialization(); + + // When + var resolvedEvents = await Fixture.Streams + .SubscribeToAll(FromAll.Start, filterOptions: new SubscriptionFilterOptions(StreamFilter.Prefix(stream))) + .Take(2) + .ToListAsync(); + + // Then + AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); + } + + public static TheoryData> CustomTypeMappings() { + return [ + (settings, typeName) => + settings.RegisterMessageType(typeName), + (settings, typeName) => + settings.RegisterMessageType(typeof(UserRegistered), typeName), + (settings, typeName) => + settings.RegisterMessageTypes(new Dictionary { { typeof(UserRegistered), typeName } }) + ]; + } + + [RetryTheory] + [MemberData(nameof(CustomTypeMappings))] + public async Task append_and_subscribe_to_stream_uses_custom_type_mappings( + Action customTypeMapping + ) { + // Given + await using var client = NewClientWith(serialization => customTypeMapping(serialization, "user_registered")); + + // When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + // Then + var resolvedEvents = await client.SubscribeToStream(stream).Take(2).ToListAsync(); + Assert.All(resolvedEvents, resolvedEvent => Assert.Equal("user_registered", resolvedEvent.Event.EventType)); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryTheory] + [MemberData(nameof(CustomTypeMappings))] + public async Task append_and_subscribe_to_all_uses_custom_type_mappings( + Action customTypeMapping + ) { + // Given + await using var client = NewClientWith(serialization => customTypeMapping(serialization, "user_registered")); + + // When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + // Then + var resolvedEvents = await client + .SubscribeToAll(new SubscribeToAllOptions { Filter = StreamFilter.Prefix(stream) }).Take(2) + .ToListAsync(); + + Assert.All(resolvedEvents, resolvedEvent => Assert.Equal("user_registered", resolvedEvent.Event.EventType)); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task automatic_serialization_custom_json_settings_are_applied() { + // Given + var systemTextJsonOptions = new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, + }; + + await using var client = NewClientWith(serialization => serialization.UseJsonSettings(systemTextJsonOptions)); + + // When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + // Then + var resolvedEvents = await client.SubscribeToStream(stream).Take(2).ToListAsync(); + var jsons = resolvedEvents.Select(e => JsonDocument.Parse(e.Event.Data).RootElement).ToList(); + + Assert.Equal(expected.Select(m => m.UserId), jsons.Select(j => j.GetProperty("user-id").GetGuid())); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + public class CustomMessageTypeNamingStrategy : IMessageTypeNamingStrategy { + public string ResolveTypeName(Type messageType, MessageTypeNamingResolutionContext resolutionContext) { + return $"custom-{messageType}"; + } + +#if NET48 + public bool TryResolveClrType(string messageTypeName, out Type? type) { +#else + public bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? type) { +#endif + var typeName = messageTypeName[(messageTypeName.IndexOf('-') + 1)..]; + type = Type.GetType(typeName); + + return type != null; + } + +#if NET48 + public bool TryResolveClrMetadataType(string messageTypeName, out Type? type) { +#else + public bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? type) { +#endif + type = null; + return false; + } + } + + [RetryFact] + public async Task append_and_subscribe_to_stream_uses_custom_message_type_naming_strategy() { + // Given + await using var client = NewClientWith( + serialization => serialization.UseMessageTypeNamingStrategy() + ); + + //When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + //Then + var resolvedEvents = await Fixture.Streams.SubscribeToStream(stream).Take(2).ToListAsync(); + Assert.All( + resolvedEvents, + resolvedEvent => Assert.Equal($"custom-{typeof(UserRegistered).FullName}", resolvedEvent.Event.EventType) + ); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task append_and_subscribe_to_all_uses_custom_message_type_naming_strategy() { + // Given + await using var client = NewClientWith( + serialization => serialization.UseMessageTypeNamingStrategy() + ); + + //When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + //Then + var resolvedEvents = await client + .SubscribeToAll(new SubscribeToAllOptions { Filter = StreamFilter.Prefix(stream) }).Take(2) + .ToListAsync(); + + Assert.All( + resolvedEvents, + resolvedEvent => Assert.Equal($"custom-{typeof(UserRegistered).FullName}", resolvedEvent.Event.EventType) + ); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task + subscribe_to_stream_deserializes_resolved_message_appended_with_manual_compatible_serialization() { + // Given + var (stream, expected) = await AppendEventsUsingManualSerialization( + message => $"stream-{message.GetType().FullName!}" + ); + + // When + var resolvedEvents = await Fixture.Streams.SubscribeToStream(stream).Take(2).ToListAsync(); + + // Then + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task subscribe_to_all_deserializes_resolved_message_appended_with_manual_compatible_serialization() { + // Given + var (stream, expected) = await AppendEventsUsingManualSerialization( + message => $"stream-{message.GetType().FullName!}" + ); + + // When + var resolvedEvents = await Fixture.Streams + .SubscribeToAll(new SubscribeToAllOptions { Filter = StreamFilter.Prefix(stream) }).Take(2) + .ToListAsync(); + + // Then + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task subscribe_to_stream_does_NOT_deserialize_resolved_message_appended_with_manual_incompatible_serialization() { + // Given + var (stream, expected) = await AppendEventsUsingManualSerialization(_ => "user_registered"); + + // When + var resolvedEvents = await Fixture.Streams.SubscribeToStream(stream).Take(2).ToListAsync(); + + // Then + AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task + subscribe_to_all_does_NOT_deserialize_resolved_message_appended_with_manual_incompatible_serialization() { + // Given + var (stream, expected) = await AppendEventsUsingManualSerialization(_ => "user_registered"); + + // When + var resolvedEvents = await Fixture.Streams + .SubscribeToAll(new SubscribeToAllOptions { Filter = StreamFilter.Prefix(stream) }).Take(2) + .ToListAsync(); + + // Then + AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); + } + + static List AssertThatMessages( + Action assertMatches, + List expected, + List resolvedEvents + ) { + Assert.Equal(expected.Count, resolvedEvents.Count); + Assert.NotEmpty(resolvedEvents); + + Assert.All(resolvedEvents, (resolvedEvent, idx) => assertMatches(expected[idx], resolvedEvent)); + + return resolvedEvents.Select(resolvedEvent => resolvedEvent.Message!).ToList(); + } + + static void AreDeserialized(UserRegistered expected, ResolvedEvent resolvedEvent) { + Assert.NotNull(resolvedEvent.Message); + Assert.Equal(expected, resolvedEvent.Message.Data); + Assert.Equal(expected, resolvedEvent.DeserializedData); + } + + static void AreNotDeserialized(UserRegistered expected, ResolvedEvent resolvedEvent) { + Assert.Null(resolvedEvent.Message); + Assert.Equal( + expected, + JsonSerializer.Deserialize( + resolvedEvent.Event.Data.Span, + SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions + ) + ); + } + + async Task<(string, List)> AppendEventsUsingAutoSerialization(KurrentClient? kurrentClient = null) { + var stream = Fixture.GetStreamName(); + var messages = GenerateMessages(); + + var writeResult = await (kurrentClient ?? Fixture.Streams).AppendToStreamAsync(stream, messages); + Assert.Equal(new((ulong)messages.Count - 1), writeResult.NextExpectedStreamRevision); + + return (stream, messages); + } + + async Task<(string, List)> AppendEventsUsingManualSerialization( + Func getTypeName + ) { + var stream = Fixture.GetStreamName(); + var messages = GenerateMessages(); + var eventData = messages.Select( + message => + new EventData( + Uuid.NewUuid(), + getTypeName(message), + Encoding.UTF8.GetBytes( + JsonSerializer.Serialize( + message, + SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions + ) + ) + ) + ); + + var writeResult = await Fixture.Streams.AppendToStreamAsync(stream, StreamRevision.None, eventData); + Assert.Equal(new((ulong)messages.Count - 1), writeResult.NextExpectedStreamRevision); + + return (stream, messages); + } + + static List GenerateMessages(int count = 2) => + Enumerable.Range(0, count) + .Select( + _ => new UserRegistered( + Guid.NewGuid(), + new Address(Guid.NewGuid().ToString(), Guid.NewGuid().GetHashCode()) + ) + ).ToList(); + + KurrentClient NewClientWith(Action customizeSerialization) { + var settings = Fixture.ClientSettings; + settings.Serialization = settings.Serialization.Clone(); + customizeSerialization(settings.Serialization); + + return new KurrentClient(settings); + } + + public record Address(string Street, int Number); + + public record UserRegistered(Guid UserId, Address Address); + + public record CustomMetadata(Guid UserId); +} diff --git a/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.cs b/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.cs new file mode 100644 index 000000000..06224c227 --- /dev/null +++ b/test/Kurrent.Client.Tests/Streams/Serialization/SerializationTests.cs @@ -0,0 +1,376 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.Json; +using EventStore.Client; +using Kurrent.Client.Core.Serialization; +using Kurrent.Diagnostics.Tracing; + +namespace Kurrent.Client.Tests.Streams.Serialization; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:Append")] +public class SerializationTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] + public async Task plain_clr_objects_are_serialized_and_deserialized_using_auto_serialization() { + // Given + var stream = Fixture.GetStreamName(); + List expected = GenerateMessages(); + + //When + await Fixture.Streams.AppendToStreamAsync(stream, expected); + + //Then + var resolvedEvents = await Fixture.Streams.ReadStreamAsync(stream).ToListAsync(); + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task + message_data_and_metadata_are_serialized_and_deserialized_using_auto_serialization_with_registered_metadata() { + // Given + await using var client = NewClientWith(serialization => serialization.UseMetadataType()); + + var stream = Fixture.GetStreamName(); + var metadata = new CustomMetadata(Guid.NewGuid()); + var expected = GenerateMessages(); + List messagesWithMetadata = + expected.Select(message => Message.From(message, metadata, Uuid.NewUuid())).ToList(); + + // When + await client.AppendToStreamAsync(stream, messagesWithMetadata); + + // Then + var resolvedEvents = await client.ReadStreamAsync(stream).ToListAsync(); + var messages = AssertThatMessages(AreDeserialized, expected, resolvedEvents); + + Assert.Equal(messagesWithMetadata, messages); + } + + [RetryFact] + public async Task + message_metadata_is_serialized_fully_byt_deserialized_to_tracing_metadata_using_auto_serialization_WITHOUT_registered_custom_metadata() { + var stream = Fixture.GetStreamName(); + var metadata = new CustomMetadata(Guid.NewGuid()); + var expected = GenerateMessages(); + List messagesWithMetadata = + expected.Select(message => Message.From(message, metadata, Uuid.NewUuid())).ToList(); + + // When + await Fixture.Streams.AppendToStreamAsync(stream, messagesWithMetadata); + + // Then + var resolvedEvents = await Fixture.Streams.ReadStreamAsync(stream).ToListAsync(); + var messages = AssertThatMessages(AreDeserialized, expected, resolvedEvents); + + Assert.Equal(messagesWithMetadata.Select(m => m with { Metadata = new TracingMetadata() }), messages); + } + + [RetryFact] + public async Task read_stream_without_options_does_NOT_deserialize_resolved_message() { + // Given + var (stream, expected) = await AppendEventsUsingAutoSerialization(); + + // When + var resolvedEvents = await Fixture.Streams + .ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start) + .ToListAsync(); + + // Then + AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task read_all_without_options_does_NOT_deserialize_resolved_message() { + // Given + var (stream, expected) = await AppendEventsUsingAutoSerialization(); + + // When + var resolvedEvents = await Fixture.Streams + .ReadAllAsync(Direction.Forwards, Position.Start, StreamFilter.Prefix(stream)) + .ToListAsync(); + + // Then + AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); + } + + public static TheoryData> CustomTypeMappings() { + return [ + (settings, typeName) => + settings.RegisterMessageType(typeName), + (settings, typeName) => + settings.RegisterMessageType(typeof(UserRegistered), typeName), + (settings, typeName) => + settings.RegisterMessageTypes(new Dictionary { { typeof(UserRegistered), typeName } }) + ]; + } + + [RetryTheory] + [MemberData(nameof(CustomTypeMappings))] + public async Task append_and_read_stream_uses_custom_type_mappings( + Action customTypeMapping + ) { + // Given + await using var client = NewClientWith(serialization => customTypeMapping(serialization, "user_registered")); + + // When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + // Then + var resolvedEvents = await client.ReadStreamAsync(stream).ToListAsync(); + Assert.All(resolvedEvents, resolvedEvent => Assert.Equal("user_registered", resolvedEvent.Event.EventType)); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryTheory] + [MemberData(nameof(CustomTypeMappings))] + public async Task append_and_read_all_uses_custom_type_mappings( + Action customTypeMapping + ) { + // Given + await using var client = NewClientWith(serialization => customTypeMapping(serialization, "user_registered")); + + // When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + // Then + var resolvedEvents = await client + .ReadAllAsync(new ReadAllOptions { Filter = StreamFilter.Prefix(stream) }) + .ToListAsync(); + + Assert.All(resolvedEvents, resolvedEvent => Assert.Equal("user_registered", resolvedEvent.Event.EventType)); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task automatic_serialization_custom_json_settings_are_applied() { + // Given + var systemTextJsonOptions = new JsonSerializerOptions { + PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, + }; + + await using var client = NewClientWith(serialization => serialization.UseJsonSettings(systemTextJsonOptions)); + + // When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + // Then + var resolvedEvents = await client.ReadStreamAsync(stream).ToListAsync(); + var jsons = resolvedEvents.Select(e => JsonDocument.Parse(e.Event.Data).RootElement).ToList(); + + Assert.Equal(expected.Select(m => m.UserId), jsons.Select(j => j.GetProperty("user-id").GetGuid())); + + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + public class CustomMessageTypeNamingStrategy : IMessageTypeNamingStrategy { + public string ResolveTypeName(Type messageType, MessageTypeNamingResolutionContext resolutionContext) { + return $"custom-{messageType}"; + } + +#if NET48 + public bool TryResolveClrType(string messageTypeName, out Type? type) { +#else + public bool TryResolveClrType(string messageTypeName, [NotNullWhen(true)] out Type? type) { +#endif + var typeName = messageTypeName[(messageTypeName.IndexOf('-') + 1)..]; + type = Type.GetType(typeName); + + return type != null; + } + +#if NET48 + public bool TryResolveClrMetadataType(string messageTypeName, out Type? type) { +#else + public bool TryResolveClrMetadataType(string messageTypeName, [NotNullWhen(true)] out Type? type) { +#endif + type = null; + return false; + } + } + + [RetryFact] + public async Task append_and_read_stream_uses_custom_message_type_naming_strategy() { + // Given + await using var client = NewClientWith( + serialization => serialization.UseMessageTypeNamingStrategy() + ); + + //When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + //Then + var resolvedEvents = await Fixture.Streams.ReadStreamAsync(stream).ToListAsync(); + Assert.All(resolvedEvents, resolvedEvent => Assert.Equal($"custom-{typeof(UserRegistered).FullName}", resolvedEvent.Event.EventType)); + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task append_and_read_all_uses_custom_message_type_naming_strategy() { + // Given + await using var client = NewClientWith( + serialization => serialization.UseMessageTypeNamingStrategy() + ); + + //When + var (stream, expected) = await AppendEventsUsingAutoSerialization(client); + + //Then + var resolvedEvents = await client + .ReadAllAsync(new ReadAllOptions { Filter = StreamFilter.Prefix(stream) }) + .ToListAsync(); + + Assert.All(resolvedEvents, resolvedEvent => Assert.Equal($"custom-{typeof(UserRegistered).FullName}", resolvedEvent.Event.EventType)); + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task read_stream_deserializes_resolved_message_appended_with_manual_compatible_serialization() { + // Given + var (stream, expected) = await AppendEventsUsingManualSerialization( + message => $"stream-{message.GetType().FullName!}" + ); + + // When + var resolvedEvents = await Fixture.Streams + .ReadStreamAsync(stream) + .ToListAsync(); + + // Then + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task read_all_deserializes_resolved_message_appended_with_manual_compatible_serialization() { + // Given + var (stream, expected) = await AppendEventsUsingManualSerialization( + message => $"stream-{message.GetType().FullName!}" + ); + + // When + var resolvedEvents = await Fixture.Streams + .ReadAllAsync(new ReadAllOptions { Filter = StreamFilter.Prefix(stream) }) + .ToListAsync(); + + // Then + AssertThatMessages(AreDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task read_stream_does_NOT_deserialize_resolved_message_appended_with_manual_incompatible_serialization() { + // Given + var (stream, expected) = await AppendEventsUsingManualSerialization(_ => "user_registered"); + + // When + var resolvedEvents = await Fixture.Streams + .ReadStreamAsync(stream) + .ToListAsync(); + + // Then + AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); + } + + [RetryFact] + public async Task read_all_does_NOT_deserialize_resolved_message_appended_with_manual_incompatible_serialization() { + // Given + var (stream, expected) = await AppendEventsUsingManualSerialization(_ => "user_registered"); + + // When + var resolvedEvents = await Fixture.Streams + .ReadAllAsync(new ReadAllOptions { Filter = StreamFilter.Prefix(stream) }) + .ToListAsync(); + + // Then + AssertThatMessages(AreNotDeserialized, expected, resolvedEvents); + } + + static List AssertThatMessages( + Action assertMatches, + List expected, + List resolvedEvents + ) { + Assert.Equal(expected.Count, resolvedEvents.Count); + Assert.NotEmpty(resolvedEvents); + + Assert.All(resolvedEvents, (resolvedEvent, idx) => assertMatches(expected[idx], resolvedEvent)); + + return resolvedEvents.Select(resolvedEvent => resolvedEvent.Message!).ToList(); + } + + static void AreDeserialized(UserRegistered expected, ResolvedEvent resolvedEvent) { + Assert.NotNull(resolvedEvent.Message); + Assert.Equal(expected, resolvedEvent.Message.Data); + Assert.Equal(expected, resolvedEvent.DeserializedData); + } + + static void AreNotDeserialized(UserRegistered expected, ResolvedEvent resolvedEvent) { + Assert.Null(resolvedEvent.Message); + Assert.Equal( + expected, + JsonSerializer.Deserialize( + resolvedEvent.Event.Data.Span, + SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions + ) + ); + } + + async Task<(string, List)> AppendEventsUsingAutoSerialization(KurrentClient? kurrentClient = null) { + var stream = Fixture.GetStreamName(); + var messages = GenerateMessages(); + + var writeResult = await (kurrentClient ?? Fixture.Streams).AppendToStreamAsync(stream, messages); + Assert.Equal(new((ulong)messages.Count - 1), writeResult.NextExpectedStreamRevision); + + return (stream, messages); + } + + async Task<(string, List)> AppendEventsUsingManualSerialization( + Func getTypeName + ) { + var stream = Fixture.GetStreamName(); + var messages = GenerateMessages(); + var eventData = messages.Select( + message => + new EventData( + Uuid.NewUuid(), + getTypeName(message), + Encoding.UTF8.GetBytes( + JsonSerializer.Serialize( + message, + SystemTextJsonSerializationSettings.DefaultJsonSerializerOptions + ) + ) + ) + ); + + var writeResult = await Fixture.Streams.AppendToStreamAsync(stream, StreamRevision.None, eventData); + Assert.Equal(new((ulong)messages.Count - 1), writeResult.NextExpectedStreamRevision); + + return (stream, messages); + } + + static List GenerateMessages(int count = 2) => + Enumerable.Range(0, count) + .Select( + _ => new UserRegistered( + Guid.NewGuid(), + new Address(Guid.NewGuid().ToString(), Guid.NewGuid().GetHashCode()) + ) + ).ToList(); + + KurrentClient NewClientWith(Action customizeSerialization) { + var settings = Fixture.ClientSettings; + settings.Serialization = settings.Serialization.Clone(); + customizeSerialization(settings.Serialization); + + return new KurrentClient(settings); + } + + public record Address(string Street, int Number); + + public record UserRegistered(Guid UserId, Address Address); + + public record CustomMetadata(Guid UserId); +} diff --git a/test/EventStore.Client.Streams.Tests/Delete/soft_deleted_stream.cs b/test/Kurrent.Client.Tests/Streams/SoftDeleteTests.cs similarity index 94% rename from test/EventStore.Client.Streams.Tests/Delete/soft_deleted_stream.cs rename to test/Kurrent.Client.Tests/Streams/SoftDeleteTests.cs index 25877d2ee..3e8cf2617 100644 --- a/test/EventStore.Client.Streams.Tests/Delete/soft_deleted_stream.cs +++ b/test/Kurrent.Client.Tests/Streams/SoftDeleteTests.cs @@ -1,12 +1,14 @@ using System.Text.Json; +using EventStore.Client; -namespace EventStore.Client.Streams.Tests.Delete; +namespace Kurrent.Client.Tests.Streams; +[Trait("Category", "Target:Streams")] [Trait("Category", "Operation:Delete")] -public class deleted_stream(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { +public class SoftDeleteTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { static JsonDocument CustomMetadata { get; } - static deleted_stream() { + static SoftDeleteTests() { var customMetadata = new Dictionary { ["key1"] = true, ["key2"] = 17, @@ -15,7 +17,7 @@ static deleted_stream() { CustomMetadata = JsonDocument.Parse(JsonSerializer.Serialize(customMetadata)); } - + [Fact] public async Task reading_throws() { var stream = Fixture.GetStreamName(); @@ -36,13 +38,7 @@ await Assert.ThrowsAsync( ); } - public static IEnumerable RecreatingTestCases() { - yield return new object?[] { StreamState.Any, nameof(StreamState.Any) }; - yield return new object?[] { StreamState.NoStream, nameof(StreamState.NoStream) }; - } - - [Theory] - [MemberData(nameof(RecreatingTestCases))] + [Theory, RecreatingTestCases] public async Task recreated_with_any_expected_version(StreamState expectedState, string name) { var stream = $"{Fixture.GetStreamName()}_{name}"; @@ -124,7 +120,7 @@ public async Task recreated_with_expected_version() { [Fact] public async Task recreated_preserves_metadata_except_truncate_before() { - const int count = 2; + const int count = 2; var stream = Fixture.GetStreamName(); @@ -213,9 +209,7 @@ await Fixture.Streams.AppendToStreamAsync( Assert.Equal(SystemStreams.MetastreamOf(stream), ex.Stream); - await Assert.ThrowsAsync( - () => Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents()) - ); + await Assert.ThrowsAsync(() => Fixture.Streams.AppendToStreamAsync(stream, StreamState.Any, Fixture.CreateTestEvents())); } [Fact] @@ -366,7 +360,7 @@ await Assert.ThrowsAsync( [Fact] public async Task recreated_on_non_empty_when_metadata_set() { - const int count = 2; + const int count = 2; var stream = Fixture.GetStreamName(); @@ -420,4 +414,11 @@ public async Task recreated_on_non_empty_when_metadata_set() { Assert.Equal(expected, metadataResult.Metadata); } -} \ No newline at end of file + + class RecreatingTestCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [StreamState.Any, nameof(StreamState.Any)]; + yield return [StreamState.NoStream, nameof(StreamState.NoStream)]; + } + } +} diff --git a/test/EventStore.Client.Streams.Tests/Metadata/stream_metadata.cs b/test/Kurrent.Client.Tests/Streams/StreamMetadataTests.cs similarity index 92% rename from test/EventStore.Client.Streams.Tests/Metadata/stream_metadata.cs rename to test/Kurrent.Client.Tests/Streams/StreamMetadataTests.cs index 706d5e2b6..a02083f6c 100644 --- a/test/EventStore.Client.Streams.Tests/Metadata/stream_metadata.cs +++ b/test/Kurrent.Client.Tests/Streams/StreamMetadataTests.cs @@ -1,10 +1,12 @@ using System.Text.Json; +using EventStore.Client; using Grpc.Core; -namespace EventStore.Client.Streams.Tests.Metadata; +namespace Kurrent.Client.Tests.Streams; -[Trait("Category", "Metadata")] -public class stream_metadata(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:Metadata")] +public class StreamMetadataTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [Fact] public async Task getting_for_an_existing_stream_and_no_metadata_exists() { var stream = Fixture.GetStreamName(); @@ -150,7 +152,7 @@ public async Task latest_metadata_returned_stream_revision_any() { Assert.Equal(expected.CacheControl, actual.Metadata.CacheControl); Assert.Equal(expected.Acl, actual.Metadata.Acl); } - + [Fact] public async Task with_timeout_set_with_any_stream_revision_fails_when_operation_expired() { var stream = Fixture.GetStreamName(); @@ -186,9 +188,7 @@ public async Task with_timeout_set_with_stream_revision_fails_when_operation_exp [Fact] public async Task with_timeout_get_fails_when_operation_expired() { - var stream = Fixture.GetStreamName(); - var rpcException = await Assert.ThrowsAsync( - () => Fixture.Streams.GetStreamMetadataAsync(stream, TimeSpan.Zero) - ); + var stream = Fixture.GetStreamName(); + var rpcException = await Assert.ThrowsAsync(() => Fixture.Streams.GetStreamMetadataAsync(stream, TimeSpan.Zero)); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionFilter.cs b/test/Kurrent.Client.Tests/Streams/SubscriptionFilter.cs similarity index 91% rename from test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionFilter.cs rename to test/Kurrent.Client.Tests/Streams/SubscriptionFilter.cs index e670b69c9..91621aba9 100644 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionFilter.cs +++ b/test/Kurrent.Client.Tests/Streams/SubscriptionFilter.cs @@ -1,8 +1,12 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions; +// ReSharper disable InconsistentNaming + +using EventStore.Client; + +namespace Kurrent.Client.Tests.Streams; public record SubscriptionFilter(string Name, Func Create, Func PrepareEvent) { public override string ToString() => Name; - + static readonly SubscriptionFilter StreamNamePrefix = new(nameof(StreamNamePrefix), StreamFilter.Prefix, (_, evt) => evt); static readonly SubscriptionFilter StreamNameRegex = new(nameof(StreamNameRegex), f => StreamFilter.RegularExpression(f), (_, evt) => evt); static readonly SubscriptionFilter EventTypePrefix = new(nameof(EventTypePrefix), EventTypeFilter.Prefix, (term, evt) => new(evt.EventId, term, evt.Data, evt.Metadata, evt.ContentType)); @@ -15,10 +19,10 @@ static SubscriptionFilter() { EventTypePrefix, EventTypeRegex }; - + TestCases = All.Select(x => new object[] { x }); } public static SubscriptionFilter[] All { get; } public static IEnumerable TestCases { get; } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_all_obsolete.cs b/test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToAllObsoleteTests.cs similarity index 93% rename from test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_all_obsolete.cs rename to test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToAllObsoleteTests.cs index af4b5ac43..b42cf782e 100644 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_all_obsolete.cs +++ b/test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToAllObsoleteTests.cs @@ -1,9 +1,14 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions.Obsolete; +using EventStore.Client; +using Kurrent.Client.Tests.Streams; +using Kurrent.Client.Tests.TestNode; -[Trait("Category", "Subscriptions")] -[Trait("Category", "Target:All")] +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:Subscriptions")] [Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class subscribe_to_all_obsolete(ITestOutputHelper output, SubscriptionsFixture fixture) : EventStoreTests(output, fixture) { +public class SubscribeToAllObsoleteTests(ITestOutputHelper output, SubscribeToAllObsoleteTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task receives_all_events_from_start() { var receivedAllEvents = new TaskCompletionSource(); @@ -11,9 +16,9 @@ public async Task receives_all_events_from_start() { var seedEvents = Fixture.CreateTestEvents(10).ToArray(); var pageSize = seedEvents.Length / 2; - + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - + foreach (var evt in seedEvents.Take(pageSize)) await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); @@ -34,33 +39,33 @@ public async Task receives_all_events_from_start() { subscription.Dispose(); var result = await subscriptionDropped.Task.WithTimeout(); result.ShouldBe(SubscriptionDroppedResult.Disposed()); - + return; Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); - + if (availableEvents.Count == 0) { receivedAllEvents.TrySetResult(true); Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); } - + return Task.CompletedTask; } void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => subscriptionDropped.SetResult(new(reason, ex)); } - + [Fact] public async Task receives_all_events_from_end() { var receivedAllEvents = new TaskCompletionSource(); var subscriptionDropped = new TaskCompletionSource(); var seedEvents = Fixture.CreateTestEvents(10).ToArray(); - + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - + using var subscription = await Fixture.Streams .SubscribeToAllAsync(FromAll.End, OnReceived, false, OnDropped) .WithTimeout(); @@ -79,24 +84,24 @@ public async Task receives_all_events_from_end() { subscription.Dispose(); var result = await subscriptionDropped.Task.WithTimeout(); result.ShouldBe(SubscriptionDroppedResult.Disposed()); - + return; Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); - + if (availableEvents.Count == 0) { receivedAllEvents.TrySetResult(true); Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); } - + return Task.CompletedTask; } void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => subscriptionDropped.SetResult(new(reason, ex)); } - + [Fact] public async Task receives_all_events_from_position() { var receivedAllEvents = new TaskCompletionSource(); @@ -104,16 +109,16 @@ public async Task receives_all_events_from_position() { var seedEvents = Fixture.CreateTestEvents(10).ToArray(); var pageSize = seedEvents.Length / 2; - + // only the second half of the events will be received var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); - + IWriteResult writeResult = new SuccessResult(); foreach (var evt in seedEvents.Take(pageSize)) writeResult = await Fixture.Streams.AppendToStreamAsync($"stream-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); var position = FromAll.After(writeResult.LogPosition); - + using var subscription = await Fixture.Streams .SubscribeToAllAsync(position, OnReceived, false, OnDropped) .WithTimeout(); @@ -131,24 +136,24 @@ public async Task receives_all_events_from_position() { subscription.Dispose(); var result = await subscriptionDropped.Task.WithTimeout(); result.ShouldBe(SubscriptionDroppedResult.Disposed()); - + return; Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { availableEvents.RemoveWhere(x => x == re.OriginalEvent.EventId); - + if (availableEvents.Count == 0) { receivedAllEvents.TrySetResult(true); Fixture.Log.Information("Received all {TotalEventsCount} expected events", pageSize); } - + return Task.CompletedTask; } void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => subscriptionDropped.SetResult(new(reason, ex)); } - + [Fact] public async Task receives_all_events_with_resolved_links() { var streamName = Fixture.GetStreamName(); @@ -158,13 +163,13 @@ public async Task receives_all_events_with_resolved_links() { var seedEvents = Fixture.CreateTestEvents(3).ToArray(); var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - + using var subscription = await Fixture.Streams .SubscribeToAllAsync(FromAll.Start, OnReceived, true, OnDropped) .WithTimeout(); - + await receivedAllEvents.Task.WithTimeout(); // if the subscription dropped before time, raise the reason why @@ -175,63 +180,63 @@ public async Task receives_all_events_with_resolved_links() { subscription.Dispose(); var result = await subscriptionDropped.Task.WithTimeout(); result.ShouldBe(SubscriptionDroppedResult.Disposed()); - + return; Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}"); + var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{KurrentTemporaryFixture.TestEventType}"); if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); return Task.CompletedTask; } - + if (availableEvents.Count == 0) { receivedAllEvents.TrySetResult(true); Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); } - + return Task.CompletedTask; } void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => subscriptionDropped.SetResult(new(reason, ex)); } - + [Theory] - [MemberData(nameof(SubscriptionFilter.TestCases), MemberType= typeof(SubscriptionFilter))] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] public async Task receives_all_filtered_events_from_start(SubscriptionFilter filter) { var streamPrefix = $"{nameof(receives_all_filtered_events_from_start)}-{filter.Name}-{Guid.NewGuid():N}"; - + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); - + var receivedAllEvents = new TaskCompletionSource(); var subscriptionDropped = new TaskCompletionSource(); var checkpointReached = new TaskCompletionSource(); - + var seedEvents = Fixture.CreateTestEvents(64) .Select(evt => filter.PrepareEvent(streamPrefix, evt)) .ToArray(); var pageSize = seedEvents.Length / 2; - + var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); // add noise await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); - + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); // Debugging: // await foreach (var evt in Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start)) // Fixture.Log.Debug("Read event {EventId} from {StreamId}.", evt.OriginalEvent.EventId, evt.OriginalEvent.EventStreamId); - + // add some of the events we want to see before we start the subscription foreach (var evt in seedEvents.Take(pageSize)) await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); - + using var subscription = await Fixture.Streams .SubscribeToAllAsync(FromAll.Start, OnReceived, false, OnDropped, filterOptions) .WithTimeout(); @@ -239,17 +244,17 @@ public async Task receives_all_filtered_events_from_start(SubscriptionFilter fil // add some of the events we want to see after we start the subscription foreach (var evt in seedEvents.Skip(pageSize)) await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - + // wait until all events were received and at least one checkpoint was reached? await receivedAllEvents.Task.WithTimeout(); await checkpointReached.Task.WithTimeout(); - + // await Task.WhenAll(receivedAllEvents.Task, checkpointReached.Task).WithTimeout(); // if the subscription dropped before time, raise the reason why if (subscriptionDropped.Task.IsCompleted) subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - + // stop the subscription subscription.Dispose(); var result = await subscriptionDropped.Task.WithTimeout(); @@ -266,8 +271,7 @@ Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) receivedAllEvents.TrySetException( new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") ); - } - else { + } else { Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}.", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); } @@ -290,45 +294,48 @@ void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Excepti Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { Fixture.Log.Verbose( "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", - position, seedEvents.Length - availableEvents.Count, seedEvents.Length + position, + seedEvents.Length - availableEvents.Count, + seedEvents.Length ); + checkpointReached.TrySetResult(true); return Task.CompletedTask; } } - + [Theory] - [MemberData(nameof(SubscriptionFilter.TestCases), MemberType= typeof(SubscriptionFilter))] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] public async Task receives_all_filtered_events_from_end(SubscriptionFilter filter) { var streamPrefix = $"{nameof(receives_all_filtered_events_from_end)}-{filter.Name}-{Guid.NewGuid():N}"; - + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); - + var receivedAllEvents = new TaskCompletionSource(); var subscriptionDropped = new TaskCompletionSource(); var checkpointReached = new TaskCompletionSource(); - + var seedEvents = Fixture.CreateTestEvents(64) .Select(evt => filter.PrepareEvent(streamPrefix, evt)) .ToArray(); - + var pageSize = seedEvents.Length / 2; - + // only the second half of the events will be received var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); // add noise await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); - + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); - + // add some of the events that are a match to the filter but will not be received foreach (var evt in seedEvents.Take(pageSize)) await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); - + using var subscription = await Fixture.Streams .SubscribeToAllAsync(FromAll.End, OnReceived, false, OnDropped, filterOptions) .WithTimeout(); @@ -336,20 +343,20 @@ public async Task receives_all_filtered_events_from_end(SubscriptionFilter filte // add the events we want to receive after we start the subscription foreach (var evt in seedEvents.Skip(pageSize)) await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - + // wait until all events were received and at least one checkpoint was reached? await receivedAllEvents.Task.WithTimeout(); await checkpointReached.Task.WithTimeout(); - + // if the subscription dropped before time, raise the reason why if (subscriptionDropped.Task.IsCompleted) subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - + // stop the subscription subscription.Dispose(); var result = await subscriptionDropped.Task.WithTimeout(); result.ShouldBe(SubscriptionDroppedResult.Disposed()); - + return; Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { @@ -363,8 +370,7 @@ Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) receivedAllEvents.TrySetException( new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") ); - } - else { + } else { Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); } @@ -387,48 +393,51 @@ void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Excepti Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { Fixture.Log.Verbose( "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", - position, pageSize - availableEvents.Count, pageSize + position, + pageSize - availableEvents.Count, + pageSize ); + checkpointReached.TrySetResult(true); return Task.CompletedTask; } } [Theory] - [MemberData(nameof(SubscriptionFilter.TestCases), MemberType= typeof(SubscriptionFilter))] + [MemberData(nameof(SubscriptionFilter.TestCases), MemberType = typeof(SubscriptionFilter))] public async Task receives_all_filtered_events_from_position(SubscriptionFilter filter) { var streamPrefix = $"{nameof(receives_all_filtered_events_from_position)}-{filter.Name}-{Guid.NewGuid():N}"; - + Fixture.Log.Information("Using filter {FilterName} with prefix {StreamPrefix}", filter.Name, streamPrefix); - + var receivedAllEvents = new TaskCompletionSource(); var subscriptionDropped = new TaskCompletionSource(); var checkpointReached = new TaskCompletionSource(); - + var seedEvents = Fixture.CreateTestEvents(64) .Select(evt => filter.PrepareEvent(streamPrefix, evt)) .ToArray(); - + var pageSize = seedEvents.Length / 2; - + // only the second half of the events will be received var availableEvents = new HashSet(seedEvents.Skip(pageSize).Select(x => x.EventId)); // add noise await Fixture.Streams.AppendToStreamAsync(Fixture.GetStreamName(), StreamState.NoStream, Fixture.CreateTestEvents(3)); - + var existingEventsCount = await Fixture.Streams.ReadAllAsync(Direction.Forwards, Position.Start).CountAsync(); Fixture.Log.Debug("Existing events count: {ExistingEventsCount}", existingEventsCount); - + // add some of the events that are a match to the filter but will not be received IWriteResult writeResult = new SuccessResult(); foreach (var evt in seedEvents.Take(pageSize)) writeResult = await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); var position = FromAll.After(writeResult.LogPosition); - + var filterOptions = new SubscriptionFilterOptions(filter.Create(streamPrefix), 1, CheckpointReached); - + using var subscription = await Fixture.Streams .SubscribeToAllAsync(position, OnReceived, false, OnDropped, filterOptions) .WithTimeout(); @@ -436,20 +445,20 @@ public async Task receives_all_filtered_events_from_position(SubscriptionFilter // add the events we want to receive after we start the subscription foreach (var evt in seedEvents.Skip(pageSize)) await Fixture.Streams.AppendToStreamAsync($"{streamPrefix}-{evt.EventId.ToGuid():N}", StreamState.NoStream, new[] { evt }); - + // wait until all events were received and at least one checkpoint was reached? await receivedAllEvents.Task.WithTimeout(); await checkpointReached.Task.WithTimeout(); - + // if the subscription dropped before time, raise the reason why if (subscriptionDropped.Task.IsCompleted) subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - + // stop the subscription subscription.Dispose(); var result = await subscriptionDropped.Task.WithTimeout(); result.ShouldBe(SubscriptionDroppedResult.Disposed()); - + return; Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { @@ -463,8 +472,7 @@ Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) receivedAllEvents.TrySetException( new InvalidOperationException($"Received unexpected event {re.OriginalEvent.EventId} from stream {re.OriginalEvent.EventStreamId}") ); - } - else { + } else { Fixture.Log.Verbose("Received expected event {EventId} from {StreamId}", re.OriginalEvent.EventId, re.OriginalEvent.EventStreamId); } @@ -487,13 +495,16 @@ void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Excepti Task CheckpointReached(StreamSubscription sub, Position position, CancellationToken ct) { Fixture.Log.Verbose( "Checkpoint reached {Position}. Received {ReceivedEventsCount}/{TotalEventsCount} events", - position, pageSize - availableEvents.Count, pageSize + position, + pageSize - availableEvents.Count, + pageSize ); + checkpointReached.TrySetResult(true); return Task.CompletedTask; } } - + [Fact] public async Task receives_all_filtered_events_with_resolved_links() { var streamName = Fixture.GetStreamName(); @@ -503,17 +514,15 @@ public async Task receives_all_filtered_events_with_resolved_links() { var seedEvents = Fixture.CreateTestEvents(3).ToArray(); var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - + await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - var options = new SubscriptionFilterOptions( - StreamFilter.Prefix($"$et-{EventStoreFixture.TestEventType}") - ); - + var options = new SubscriptionFilterOptions(StreamFilter.Prefix($"$et-{KurrentTemporaryFixture.TestEventType}")); + using var subscription = await Fixture.Streams .SubscribeToAllAsync(FromAll.Start, OnReceived, true, OnDropped, options) .WithTimeout(); - + await receivedAllEvents.Task.WithTimeout(); // if the subscription dropped before time, raise the reason why @@ -524,32 +533,32 @@ public async Task receives_all_filtered_events_with_resolved_links() { subscription.Dispose(); var result = await subscriptionDropped.Task.WithTimeout(); result.ShouldBe(SubscriptionDroppedResult.Disposed()); - + return; Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}"); + var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{KurrentTemporaryFixture.TestEventType}"); if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); return Task.CompletedTask; } - + if (availableEvents.Count == 0) { receivedAllEvents.TrySetResult(true); Fixture.Log.Information("Received all {TotalEventsCount} expected events", seedEvents.Length); } - + return Task.CompletedTask; } void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => subscriptionDropped.SetResult(new(reason, ex)); } - + [Fact] public async Task drops_when_disposed() { var subscriptionDropped = new TaskCompletionSource(); - + using var subscription = await Fixture.Streams .SubscribeToAllAsync( FromAll.Start, @@ -562,7 +571,7 @@ public async Task drops_when_disposed() { // if the subscription dropped before time, raise the reason why if (subscriptionDropped.Task.IsCompleted) subscriptionDropped.Task.IsCompleted.ShouldBe(false, subscriptionDropped.Task.Result.ToString()); - + // stop the subscription subscription.Dispose(); var result = await subscriptionDropped.Task.WithTimeout(); @@ -574,7 +583,7 @@ public async Task drops_when_subscriber_error() { var expectedResult = SubscriptionDroppedResult.SubscriberError(); var subscriptionDropped = new TaskCompletionSource(); - + using var subscription = await Fixture.Streams .SubscribeToAllAsync( FromAll.Start, @@ -589,4 +598,19 @@ public async Task drops_when_subscriber_error() { var result = await subscriptionDropped.Task.WithTimeout(); result.ShouldBe(expectedResult); } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { + OnSetup = async () => { + await Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.NoStream, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + await Streams.AppendToStreamAsync($"SubscriptionsFixture-Noise-{Guid.NewGuid():N}", StreamState.NoStream, CreateTestEvents(10)); + }; + } + } } diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_stream_obsolete.cs b/test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToStreamObsoleteTests.cs similarity index 91% rename from test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_stream_obsolete.cs rename to test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToStreamObsoleteTests.cs index 4e05b293b..2a2c3f10f 100644 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/Obsolete/subscribe_to_stream_obsolete.cs +++ b/test/Kurrent.Client.Tests/Streams/Subscriptions/Obsolete/SubscribeToStreamObsoleteTests.cs @@ -1,9 +1,13 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions.Obsolete; +using EventStore.Client; +using Kurrent.Client.Tests.TestNode; -[Trait("Category", "Subscriptions")] -[Trait("Category", "Target:Stream")] +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:Streams")] +[Trait("Category", "Operation:Subscriptions")] [Obsolete("Will be removed in future release when older subscriptions APIs are removed from the client")] -public class subscribe_to_stream_obsolete(ITestOutputHelper output, SubscriptionsFixture fixture) : EventStoreTests(output, fixture) { +public class SubscribeToStreamObsoleteTests(ITestOutputHelper output, SubscribeToStreamObsoleteTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task receives_all_events_from_start() { var streamName = Fixture.GetStreamName(); @@ -266,7 +270,7 @@ public async Task receives_all_events_with_resolved_links() { await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); using var subscription = await Fixture.Streams - .SubscribeToStreamAsync($"$et-{EventStoreFixture.TestEventType}", FromStream.Start, OnReceived, true, OnDropped) + .SubscribeToStreamAsync($"$et-{KurrentTemporaryFixture.TestEventType}", FromStream.Start, OnReceived, true, OnDropped) .WithTimeout(); await receivedAllEvents.Task.WithTimeout(); @@ -283,7 +287,7 @@ public async Task receives_all_events_with_resolved_links() { return; Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) { - var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}"); + var hasResolvedLink = re.OriginalEvent.EventStreamId.StartsWith($"$et-{KurrentTemporaryFixture.TestEventType}"); if (availableEvents.RemoveWhere(x => x == re.Event.EventId && hasResolvedLink) == 0) { Fixture.Log.Debug("Received unexpected event {EventId} from stream {StreamId}", re.Event.EventId, re.OriginalEvent.EventStreamId); return Task.CompletedTask; @@ -300,4 +304,19 @@ Task OnReceived(StreamSubscription sub, ResolvedEvent re, CancellationToken ct) void OnDropped(StreamSubscription sub, SubscriptionDroppedReason reason, Exception? ex) => subscriptionDropped.SetResult(new(reason, ex)); } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { + OnSetup = async () => { + await Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.NoStream, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + await Streams.AppendToStreamAsync($"SubscriptionsFixture-Noise-{Guid.NewGuid():N}", StreamState.NoStream, CreateTestEvents(10)); + }; + } + } } diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all.cs b/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscribeToAllTests.cs similarity index 93% rename from test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all.cs rename to test/Kurrent.Client.Tests/Streams/Subscriptions/SubscribeToAllTests.cs index 2e9879a4a..33d63b280 100644 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_all.cs +++ b/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscribeToAllTests.cs @@ -1,9 +1,13 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions; +using EventStore.Client; +using Kurrent.Client.Tests.Streams; +using Kurrent.Client.Tests.TestNode; + +namespace Kurrent.Client.Tests; [Trait("Category", "Subscriptions")] -[Trait("Category", "Target:All")] -public class subscribe_to_all(ITestOutputHelper output, SubscriptionsFixture fixture) - : EventStoreTests(output, fixture) { +[Trait("Category", "Target:Streams")] +public class SubscribeToAllTests(ITestOutputHelper output, SubscribeToAllTests.CustomFixture fixture) + : KurrentTemporaryTests(output, fixture) { [Fact] public async Task receives_all_events_from_start() { var seedEvents = Fixture.CreateTestEvents(10).ToArray(); @@ -445,9 +449,7 @@ public async Task receives_all_filtered_events_with_resolved_links() { await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - var filterOptions = new SubscriptionFilterOptions( - StreamFilter.Prefix($"$et-{EventStoreFixture.TestEventType}") - ); + var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix($"$et-{KurrentPermanentFixture.TestEventType}")); await using var subscription = Fixture.Streams.SubscribeToAll(FromAll.Start, true, filterOptions: filterOptions); @@ -465,7 +467,7 @@ public async Task receives_all_filtered_events_with_resolved_links() { async Task Subscribe() { while (await enumerator.MoveNextAsync()) { if (enumerator.Current is not StreamMessage.Event(var resolvedEvent) || - !resolvedEvent.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}")) { + !resolvedEvent.OriginalEvent.EventStreamId.StartsWith($"$et-{KurrentPermanentFixture.TestEventType}")) { continue; } @@ -477,4 +479,19 @@ async Task Subscribe() { } } } + + public class CustomFixture : KurrentTemporaryFixture { + public CustomFixture() : base(x => x.RunProjections()) { + OnSetup = async () => { + await Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.NoStream, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + await Streams.AppendToStreamAsync($"SubscriptionsFixture-Noise-{Guid.NewGuid():N}", StreamState.NoStream, CreateTestEvents(10)); + }; + } + } } diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream.cs b/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscribeToStreamTests.cs similarity index 80% rename from test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream.cs rename to test/Kurrent.Client.Tests/Streams/Subscriptions/SubscribeToStreamTests.cs index d38391e8f..304144e9b 100644 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/subscribe_to_stream.cs +++ b/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscribeToStreamTests.cs @@ -1,10 +1,12 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions; +using EventStore.Client; + +namespace Kurrent.Client.Tests.Streams; [Trait("Category", "Subscriptions")] -[Trait("Category", "Target:Stream")] -public class subscribe_to_stream(ITestOutputHelper output, SubscriptionsFixture fixture) - : EventStoreTests(output, fixture) { - [Fact] +[Trait("Category", "Target:Streams")] +public class SubscribeToStreamTests(ITestOutputHelper output, SubscribeToStreamTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [RetryFact] public async Task receives_all_events_from_start() { var streamName = Fixture.GetStreamName(); @@ -43,7 +45,7 @@ async Task Subscribe() { } } - [Fact] + [RetryFact] public async Task receives_all_events_from_position() { var streamName = Fixture.GetStreamName(); @@ -91,7 +93,7 @@ async Task Subscribe() { } } - [Fact] + [RetryFact] public async Task receives_all_events_from_non_existing_stream() { var streamName = Fixture.GetStreamName(); @@ -127,7 +129,7 @@ async Task Subscribe() { } } - [Fact] + [RetryFact] public async Task allow_multiple_subscriptions_to_same_stream() { var streamName = Fixture.GetStreamName(); @@ -170,7 +172,7 @@ async Task Subscribe(IAsyncEnumerator subscription) { } } - [Fact] + [RetryFact] public async Task drops_when_stream_tombstoned() { var streamName = Fixture.GetStreamName(); @@ -193,41 +195,18 @@ public async Task drops_when_stream_tombstoned() { ex.ShouldBeOfType().Stream.ShouldBe(streamName); } - [Fact] - public async Task receives_all_events_with_resolved_links() { - var streamName = Fixture.GetStreamName(); - - var seedEvents = Fixture.CreateTestEvents(3).ToArray(); - var availableEvents = new HashSet(seedEvents.Select(x => x.EventId)); - - await Fixture.Streams.AppendToStreamAsync(streamName, StreamState.NoStream, seedEvents); - - await using var subscription = - Fixture.Streams.SubscribeToStream($"$et-{EventStoreFixture.TestEventType}", FromStream.Start, true); - - await using var enumerator = subscription.Messages.GetAsyncEnumerator(); - - Assert.True(await enumerator.MoveNextAsync()); - - Assert.IsType(enumerator.Current); - - await Subscribe().WithTimeout(); - - return; - - async Task Subscribe() { - while (await enumerator.MoveNextAsync()) { - if (enumerator.Current is not StreamMessage.Event(var resolvedEvent) || - !resolvedEvent.OriginalEvent.EventStreamId.StartsWith($"$et-{EventStoreFixture.TestEventType}")) { - continue; - } - - availableEvents.Remove(resolvedEvent.Event.EventId); - - if (availableEvents.Count == 0) { - return; - } - } + public class CustomFixture : KurrentPermanentFixture { + public CustomFixture() { + OnSetup = async () => { + await Streams.SetStreamMetadataAsync( + SystemStreams.AllStream, + StreamState.Any, + new(acl: new(SystemRoles.All)), + userCredentials: TestCredentials.Root + ); + + await Streams.AppendToStreamAsync($"SubscriptionsFixture-Noise-{Guid.NewGuid():N}", StreamState.NoStream, CreateTestEvents(10)); + }; } } } diff --git a/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionDroppedResult.cs b/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscriptionDroppedResult.cs similarity index 87% rename from test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionDroppedResult.cs rename to test/Kurrent.Client.Tests/Streams/Subscriptions/SubscriptionDroppedResult.cs index 40df3eb52..6157c6cb5 100644 --- a/test/EventStore.Client.Streams.Tests/Subscriptions/SubscriptionDroppedResult.cs +++ b/test/Kurrent.Client.Tests/Streams/Subscriptions/SubscriptionDroppedResult.cs @@ -1,4 +1,6 @@ -namespace EventStore.Client.Streams.Tests.Subscriptions; +using EventStore.Client; + +namespace Kurrent.Client.Tests; public record SubscriptionDroppedResult(SubscriptionDroppedReason Reason, Exception? Error) { public Task Throw() => Task.FromException(Error!); @@ -6,11 +8,11 @@ public record SubscriptionDroppedResult(SubscriptionDroppedReason Reason, Except public static SubscriptionDroppedResult ServerError(Exception? error = null) => new(SubscriptionDroppedReason.ServerError, error ?? new Exception("Server error")); - public static SubscriptionDroppedResult SubscriberError(Exception? error = null) => + public static SubscriptionDroppedResult SubscriberError(Exception? error = null) => new(SubscriptionDroppedReason.SubscriberError, error ?? new Exception("Subscriber error")); - public static SubscriptionDroppedResult Disposed(Exception? error = null) => + public static SubscriptionDroppedResult Disposed(Exception? error = null) => new(SubscriptionDroppedReason.Disposed, error); public override string ToString() => $"{Reason} {Error?.Message ?? string.Empty}".Trim(); -} \ No newline at end of file +} diff --git a/test/Kurrent.Client.Tests/UserManagement/ChangePasswordTests.cs b/test/Kurrent.Client.Tests/UserManagement/ChangePasswordTests.cs new file mode 100644 index 000000000..f393e24da --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/ChangePasswordTests.cs @@ -0,0 +1,69 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class ChangePasswordTests(ITestOutputHelper output, KurrentPermanentFixture fixture) + : KurrentPermanentTests(output, fixture) { + [Theory, ChangePasswordNullInputCases] + public async Task changing_user_password_with_null_input_throws(string loginName, string currentPassword, string newPassword, string paramName) { + var ex = await Fixture.Users + .ChangePasswordAsync(loginName, currentPassword, newPassword, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Theory, ChangePasswordEmptyInputCases] + public async Task changing_user_password_with_empty_input_throws(string loginName, string currentPassword, string newPassword, string paramName) { + var ex = await Fixture.Users + .ChangePasswordAsync(loginName, currentPassword, newPassword, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Theory(Skip = "This can't be right")] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task changing_user_password_with_user_with_insufficient_credentials_throws(string loginName, UserCredentials userCredentials) { + await Fixture.Users.CreateUserAsync(loginName, "Full Name", Array.Empty(), "password", userCredentials: TestCredentials.Root); + + await Fixture.Users + .ChangePasswordAsync(loginName, "password", "newPassword", userCredentials: userCredentials) + .ShouldThrowAsync(); + } + + [Fact] + public async Task changing_user_password_when_the_current_password_is_wrong_throws() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .ChangePasswordAsync(user.LoginName, "wrong-password", "new-password", userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + } + + [Fact] + public async Task changing_user_password_with_correct_credentials() { + var user = await Fixture.CreateTestUser(); + + await Fixture.Users + .ChangePasswordAsync(user.LoginName, user.Password, "new-password", userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + class ChangePasswordNullInputCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [null!, Faker.Internet.Password(), Faker.Internet.Password(), "loginName"]; + yield return [Faker.Person.UserName, null!, Faker.Internet.Password(), "currentPassword"]; + yield return [Faker.Person.UserName, Faker.Internet.Password(), null!, "newPassword"]; + } + } + + class ChangePasswordEmptyInputCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [string.Empty, Faker.Internet.Password(), Faker.Internet.Password(), "loginName"]; + yield return [Faker.Person.UserName, string.Empty, Faker.Internet.Password(), "currentPassword"]; + yield return [Faker.Person.UserName, Faker.Internet.Password(), string.Empty, "newPassword"]; + } + } +} diff --git a/test/Kurrent.Client.Tests/UserManagement/CreateUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/CreateUserTests.cs new file mode 100644 index 000000000..3096e58e5 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/CreateUserTests.cs @@ -0,0 +1,93 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class CreateUserTests(ITestOutputHelper output, CreateUserTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { + [Theory, CreateUserNullInputCases] + public async Task creating_user_with_null_input_throws(string loginName, string fullName, string[] groups, string password, string paramName) { + var ex = await Fixture.Users + .CreateUserAsync(loginName, fullName, groups, password, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Theory, CreateUserEmptyInputCases] + public async Task creating_user_with_empty_input_throws(string loginName, string fullName, string[] groups, string password, string paramName) { + var ex = await Fixture.Users + .CreateUserAsync(loginName, fullName, groups, password, userCredentials: TestCredentials.Root) + .ShouldThrowAsync(); + + ex.ParamName.ShouldBe(paramName); + } + + [Fact] + public async Task creating_user_with_password_containing_ascii_chars() { + var user = Fakers.Users.Generate(); + + await Fixture.Users + .CreateUserAsync(user.LoginName, user.FullName, user.Groups, user.Password, userCredentials: TestCredentials.Root) + .ShouldNotThrowAsync(); + } + + [Theory] + [ClassData(typeof(InvalidCredentialsTestCases))] + public async Task creating_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) => + await Fixture.Users + .CreateUserAsync( + testCase.User.LoginName, + testCase.User.FullName, + testCase.User.Groups, + testCase.User.Password, + userCredentials: testCase.User.Credentials + ) + .ShouldThrowAsync(testCase.ExpectedException); + + [Fact] + public async Task creating_user_can_be_read() { + var user = Fakers.Users.Generate(); + + await Fixture.Users + .CreateUserAsync( + user.LoginName, + user.FullName, + user.Groups, + user.Password, + userCredentials: TestCredentials.Root + ) + .ShouldNotThrowAsync(); + + var actual = await Fixture.Users.GetUserAsync(user.LoginName, userCredentials: TestCredentials.Root); + + var expected = new UserDetails( + user.Details.LoginName, + user.Details.FullName, + user.Details.Groups, + user.Details.Disabled, + actual.DateLastUpdated + ); + + actual.ShouldBeEquivalentTo(expected); + } + + class CreateUserNullInputCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [null!, Faker.Person.UserName, Faker.Lorem.Words(), Faker.Internet.Password(), "loginName"]; + yield return [Faker.Person.UserName, null!, Faker.Lorem.Words(), Faker.Internet.Password(), "fullName"]; + yield return [Faker.Person.UserName, Faker.Person.FullName, null!, Faker.Internet.Password(), "groups"]; + yield return [Faker.Person.UserName, Faker.Person.FullName, Faker.Lorem.Words(), null!, "password"]; + } + } + + class CreateUserEmptyInputCases : TestCaseGenerator { + protected override IEnumerable Data() { + yield return [string.Empty, Faker.Person.UserName, Faker.Lorem.Words(), Faker.Internet.Password(), "loginName"]; + yield return [Faker.Person.UserName, string.Empty, Faker.Lorem.Words(), Faker.Internet.Password(), "fullName"]; + yield return [Faker.Person.UserName, Faker.Person.FullName, Faker.Lorem.Words(), string.Empty, "password"]; + } + } + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.UserManagement.Tests/deleting_a_user.cs b/test/Kurrent.Client.Tests/UserManagement/DeleteUserTests.cs similarity index 83% rename from test/EventStore.Client.UserManagement.Tests/deleting_a_user.cs rename to test/Kurrent.Client.Tests/UserManagement/DeleteUserTests.cs index 9f5948fb6..e8e4e4a9d 100644 --- a/test/EventStore.Client.UserManagement.Tests/deleting_a_user.cs +++ b/test/Kurrent.Client.Tests/UserManagement/DeleteUserTests.cs @@ -1,11 +1,10 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; -public class deleting_a_user : IClassFixture { - public deleting_a_user(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:UserManagement")] +public class DeleteUserTests(ITestOutputHelper output, DeleteUserTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { [Fact] public async Task with_null_input_throws() { var ex = await Fixture.Users @@ -65,4 +64,6 @@ public async Task a_second_time_throws() { ex.LoginName.ShouldBe(user.LoginName); } -} \ No newline at end of file + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.UserManagement.Tests/disabling_a_user.cs b/test/Kurrent.Client.Tests/UserManagement/DisableUserTests.cs similarity index 82% rename from test/EventStore.Client.UserManagement.Tests/disabling_a_user.cs rename to test/Kurrent.Client.Tests/UserManagement/DisableUserTests.cs index c72ab0295..d4be66f86 100644 --- a/test/EventStore.Client.UserManagement.Tests/disabling_a_user.cs +++ b/test/Kurrent.Client.Tests/UserManagement/DisableUserTests.cs @@ -1,10 +1,8 @@ -namespace EventStore.Client.Tests; - -public class disabling_a_user : IClassFixture { - public disabling_a_user(ITestOutputHelper output, InsecureClientTestFixture fixture) => Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:UserManagement")] +public class DisableUserTests(ITestOutputHelper output, DisableUserTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { [Fact] public async Task with_null_input_throws() { var ex = await Fixture.Users @@ -61,4 +59,6 @@ await Fixture.Users .DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) .ShouldNotThrowAsync(); } -} \ No newline at end of file + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.UserManagement.Tests/enabling_a_user.cs b/test/Kurrent.Client.Tests/UserManagement/EnableUserTests.cs similarity index 81% rename from test/EventStore.Client.UserManagement.Tests/enabling_a_user.cs rename to test/Kurrent.Client.Tests/UserManagement/EnableUserTests.cs index 79755e891..7392a0dbe 100644 --- a/test/EventStore.Client.UserManagement.Tests/enabling_a_user.cs +++ b/test/Kurrent.Client.Tests/UserManagement/EnableUserTests.cs @@ -1,11 +1,8 @@ -namespace EventStore.Client.Tests; - -public class enabling_a_user : IClassFixture { - public enabling_a_user(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); - - InsecureClientTestFixture Fixture { get; } +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:UserManagement")] +public class EnableUserTests(ITestOutputHelper output, EnableUserTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { [Fact] public async Task with_null_input_throws() { var ex = await Fixture.Users @@ -23,7 +20,7 @@ public async Task with_empty_input_throws() { ex.ParamName.ShouldBe("loginName"); } - + [Theory] [ClassData(typeof(InvalidCredentialsTestCases))] public async Task with_user_with_insufficient_credentials_throws(InvalidCredentialsTestCase testCase) { @@ -43,9 +40,9 @@ await Fixture.Users [Fact] public async Task that_was_disabled() { var user = await Fixture.CreateTestUser(); - + await Fixture.Users.DisableUserAsync(user.LoginName, userCredentials: TestCredentials.Root); - + await Fixture.Users .EnableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) .ShouldNotThrowAsync(); @@ -54,9 +51,11 @@ await Fixture.Users [Fact] public async Task that_is_enabled() { var user = await Fixture.CreateTestUser(); - + await Fixture.Users .EnableUserAsync(user.LoginName, userCredentials: TestCredentials.Root) .ShouldNotThrowAsync(); } -} \ No newline at end of file + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/Kurrent.Client.Tests/UserManagement/GetCurrentUserTests.cs b/test/Kurrent.Client.Tests/UserManagement/GetCurrentUserTests.cs new file mode 100644 index 000000000..bb91c1a64 --- /dev/null +++ b/test/Kurrent.Client.Tests/UserManagement/GetCurrentUserTests.cs @@ -0,0 +1,12 @@ +using EventStore.Client; + +namespace Kurrent.Client.Tests; + +[Trait("Category", "Target:UserManagement")] +public class GetCurrentUserTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { + [Fact] + public async Task returns_the_current_user() { + var user = await Fixture.Users.GetCurrentUserAsync(TestCredentials.Root); + user.LoginName.ShouldBe(TestCredentials.Root.Username); + } +} diff --git a/test/EventStore.Client.UserManagement.Tests/listing_users.cs b/test/Kurrent.Client.Tests/UserManagement/ListUserTests.cs similarity index 81% rename from test/EventStore.Client.UserManagement.Tests/listing_users.cs rename to test/Kurrent.Client.Tests/UserManagement/ListUserTests.cs index 4760e7898..8cf4d1ad1 100644 --- a/test/EventStore.Client.UserManagement.Tests/listing_users.cs +++ b/test/Kurrent.Client.Tests/UserManagement/ListUserTests.cs @@ -1,11 +1,9 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; -public class listing_users : IClassFixture { - public listing_users(ITestOutputHelper output, EventStoreFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); +namespace Kurrent.Client.Tests; - EventStoreFixture Fixture { get; } - +[Trait("Category", "Target:UserManagement")] +public class ListUserTests(ITestOutputHelper output, KurrentPermanentFixture fixture) : KurrentPermanentTests(output, fixture) { [Fact] public async Task returns_all_created_users() { var seed = await Fixture.CreateTestUsers(); @@ -39,4 +37,4 @@ public async Task returns_all_system_users() { expected.ShouldBeSubsetOf(actual); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.UserManagement.Tests/resetting_user_password.cs b/test/Kurrent.Client.Tests/UserManagement/ResettingUserPasswordTests.cs similarity index 86% rename from test/EventStore.Client.UserManagement.Tests/resetting_user_password.cs rename to test/Kurrent.Client.Tests/UserManagement/ResettingUserPasswordTests.cs index 549e7119b..39b0bf718 100644 --- a/test/EventStore.Client.UserManagement.Tests/resetting_user_password.cs +++ b/test/Kurrent.Client.Tests/UserManagement/ResettingUserPasswordTests.cs @@ -1,16 +1,15 @@ -namespace EventStore.Client.Tests; +using EventStore.Client; -public class resetting_user_password : IClassFixture { - public resetting_user_password(ITestOutputHelper output, InsecureClientTestFixture fixture) => - Fixture = fixture.With(x => x.CaptureTestRun(output)); +namespace Kurrent.Client.Tests; - InsecureClientTestFixture Fixture { get; } - +[Trait("Category", "Target:UserManagement")] +public class ResettingUserPasswordTests(ITestOutputHelper output, ResettingUserPasswordTests.CustomFixture fixture) + : KurrentPermanentTests(output, fixture) { public static IEnumerable NullInputCases() { yield return Fakers.Users.Generate().WithResult(x => new object?[] { null, x.Password, "loginName" }); yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, null, "newPassword" }); } - + [Theory] [MemberData(nameof(NullInputCases))] public async Task with_null_input_throws(string loginName, string newPassword, string paramName) { @@ -20,7 +19,7 @@ public async Task with_null_input_throws(string loginName, string newPassword, s ex.ParamName.ShouldBe(paramName); } - + public static IEnumerable EmptyInputCases() { yield return Fakers.Users.Generate().WithResult(x => new object?[] { string.Empty, x.Password, "loginName" }); yield return Fakers.Users.Generate().WithResult(x => new object?[] { x.LoginName, string.Empty, "newPassword" }); @@ -77,4 +76,6 @@ await Fixture.Users .ResetPasswordAsync(user.LoginName, "new-password", userCredentials: user.Credentials) .ShouldThrowAsync(); } -} \ No newline at end of file + + public class CustomFixture() : KurrentPermanentFixture(x => x.WithoutDefaultCredentials()); +} diff --git a/test/EventStore.Client.UserManagement.Tests/UserCredentialsTests.cs b/test/Kurrent.Client.Tests/UserManagement/UserCredentialsTests.cs similarity index 92% rename from test/EventStore.Client.UserManagement.Tests/UserCredentialsTests.cs rename to test/Kurrent.Client.Tests/UserManagement/UserCredentialsTests.cs index c8eb0a570..e4ffbe456 100644 --- a/test/EventStore.Client.UserManagement.Tests/UserCredentialsTests.cs +++ b/test/Kurrent.Client.Tests/UserManagement/UserCredentialsTests.cs @@ -1,9 +1,11 @@ using System.Net.Http.Headers; using System.Text; +using EventStore.Client; using static System.Convert; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:UserManagement")] public class UserCredentialsTests { const string JwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiI5OSIsIm5hbWUiOiJKb2huIFdpY2siLCJpYXQiOjE1MTYyMzkwMjJ9." @@ -39,4 +41,4 @@ public void from_bearer_token() { credentials.Password.ShouldBeNull(); credentials.ToString().ShouldBe($"Bearer {JwtToken}"); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/UuidTests.cs b/test/Kurrent.Client.Tests/UuidTests.cs similarity index 90% rename from test/EventStore.Client.Tests/UuidTests.cs rename to test/Kurrent.Client.Tests/UuidTests.cs index 35d833096..96550b734 100644 --- a/test/EventStore.Client.Tests/UuidTests.cs +++ b/test/Kurrent.Client.Tests/UuidTests.cs @@ -1,11 +1,13 @@ using AutoFixture; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class UuidTests : ValueObjectTests { public UuidTests() : base(new ScenarioFixture()) { } - [Fact] + [RetryFact] public void ToGuidReturnsExpectedResult() { var guid = Guid.NewGuid(); var sut = Uuid.FromGuid(guid); @@ -13,21 +15,21 @@ public void ToGuidReturnsExpectedResult() { Assert.Equal(sut.ToGuid(), guid); } - [Fact] + [RetryFact] public void ToStringProducesExpectedResult() { var sut = Uuid.NewUuid(); Assert.Equal(sut.ToGuid().ToString(), sut.ToString()); } - [Fact] + [RetryFact] public void ToFormattedStringProducesExpectedResult() { var sut = Uuid.NewUuid(); Assert.Equal(sut.ToGuid().ToString("n"), sut.ToString("n")); } - [Fact] + [RetryFact] public void ToDtoReturnsExpectedResult() { var msb = GetRandomInt64(); var lsb = GetRandomInt64(); @@ -41,7 +43,7 @@ public void ToDtoReturnsExpectedResult() { Assert.Equal(msb, result.Structured.MostSignificantBits); } - [Fact] + [RetryFact] public void ParseReturnsExpectedResult() { var guid = Guid.NewGuid(); @@ -50,7 +52,7 @@ public void ParseReturnsExpectedResult() { Assert.Equal(Uuid.FromGuid(guid), sut); } - [Fact] + [RetryFact] public void FromInt64ReturnsExpectedResult() { var guid = Guid.Parse("65678f9b-d139-4786-8305-b9166922b378"); var sut = Uuid.FromInt64(7306966819824813958L, -9005588373953137800L); @@ -70,4 +72,4 @@ static long GetRandomInt64() { class ScenarioFixture : Fixture { public ScenarioFixture() => Customize(composer => composer.FromFactory(Uuid.FromGuid)); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/ValueObjectTests.cs b/test/Kurrent.Client.Tests/ValueObjectTests.cs similarity index 79% rename from test/EventStore.Client.Tests/ValueObjectTests.cs rename to test/Kurrent.Client.Tests/ValueObjectTests.cs index cce2526f7..08d09606d 100644 --- a/test/EventStore.Client.Tests/ValueObjectTests.cs +++ b/test/Kurrent.Client.Tests/ValueObjectTests.cs @@ -1,15 +1,16 @@ using AutoFixture; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public abstract class ValueObjectTests { protected readonly Fixture _fixture; protected ValueObjectTests(Fixture fixture) => _fixture = fixture; - [Fact] + [RetryFact] public void ValueObjectIsWellBehaved() => _fixture.Create().Verify(typeof(T)); - [Fact] + [RetryFact] public void ValueObjectIsEquatable() => Assert.IsAssignableFrom>(_fixture.Create()); -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/X509CertificatesTests.cs b/test/Kurrent.Client.Tests/X509CertificatesTests.cs similarity index 72% rename from test/EventStore.Client.Tests/X509CertificatesTests.cs rename to test/Kurrent.Client.Tests/X509CertificatesTests.cs index dd32aabb0..04dce9b7a 100644 --- a/test/EventStore.Client.Tests/X509CertificatesTests.cs +++ b/test/Kurrent.Client.Tests/X509CertificatesTests.cs @@ -1,32 +1,42 @@ using System.Security.Cryptography.X509Certificates; +using EventStore.Client; -namespace EventStore.Client.Tests; +namespace Kurrent.Client.Tests; +[Trait("Category", "Target:Misc")] public class X509CertificatesTests { - [Fact] + [RetryFact] public void create_from_pem_file() { const string certPemFilePath = "certs/ca/ca.crt"; const string keyPemFilePath = "certs/ca/ca.key"; var rsa = X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); +#if NET9_0_OR_GREATER + var cert = X509CertificateLoader.LoadCertificateFromFile(certPemFilePath); +#else var cert = new X509Certificate2(certPemFilePath); +#endif rsa.Issuer.ShouldBe(cert.Issuer); rsa.SerialNumber.ShouldBe(cert.SerialNumber); } - [Fact] + [RetryFact] public void create_from_pem_file_() { const string certPemFilePath = "certs/user-admin/user-admin.crt"; const string keyPemFilePath = "certs/user-admin/user-admin.key"; var rsa = X509Certificates.CreateFromPemFile(certPemFilePath, keyPemFilePath); +#if NET9_0_OR_GREATER + var cert = X509CertificateLoader.LoadCertificateFromFile(certPemFilePath); +#else var cert = new X509Certificate2(certPemFilePath); +#endif rsa.Issuer.ShouldBe(cert.Issuer); rsa.Subject.ShouldBe(cert.Subject); rsa.SerialNumber.ShouldBe(cert.SerialNumber); } -} \ No newline at end of file +}