diff --git a/build/ci-build.yml b/build/ci-build.yml index 48cd58c8..16491336 100644 --- a/build/ci-build.yml +++ b/build/ci-build.yml @@ -21,6 +21,10 @@ parameters: - name: 'Package.Version.ManualTrigger' type: string default: 'preview' + - name: azureServiceConnection + displayName: 'Azure service connection' + type: string + default: 'Azure Codit-Arcus Service Principal' resources: repositories: @@ -30,8 +34,6 @@ resources: endpoint: arcus-azure variables: - - group: 'Arcus Messaging - Integration Testing' - - group: 'Arcus - GitHub Package Registry' - group: 'Build Configuration' - template: ./variables/build.yml - template: ./variables/test.yml @@ -93,35 +95,24 @@ stages: dotnetSdkVersion: '$(DotNet.Sdk.Version)' projectName: '$(Project).Tests.Unit' - - stage: SelfContainingIntegrationTests - displayName: Self-Containing Integration Tests + - stage: IntegrationTests + displayName: Integration Tests dependsOn: Build condition: succeeded() - variables: - - name: 'Arcus.Health.Port.Queue' - value: '42063' jobs: - - job: SelfContainingIntegrationTests - displayName: 'Run self-containing integration tests' + - job: RunIntegrationTests + displayName: 'Run integration tests' pool: vmImage: '$(Vm.Image)' steps: - - task: UseDotNet@2 - displayName: 'Import .NET Core SDK ($(DotNet.Sdk.PreviousVersion))' - inputs: - packageType: 'sdk' - version: '$(DotNet.Sdk.PreviousVersion)' - - template: test/run-integration-tests.yml@templates + - template: templates/integration-tests.yml parameters: - dotnetSdkVersion: '$(DotNet.Sdk.Version)' - includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) - projectName: '$(Project).Tests.Integration' - category: 'Integration' + azureServiceConnection: ${{ parameters.azureServiceConnection }} - - stage: ReleaseToMyget + - stage: ReleaseToMyGet displayName: 'Release to MyGet' dependsOn: - [SelfContainingIntegrationTests, UnitTests] + [IntegrationTests, UnitTests] condition: succeeded() jobs: - job: PushToMyGet diff --git a/build/deploy-test-resources.yml b/build/deploy-test-resources.yml new file mode 100644 index 00000000..eb948b14 --- /dev/null +++ b/build/deploy-test-resources.yml @@ -0,0 +1,85 @@ +name: Arcus Messaging - Deploy test resources + +trigger: none +pr: none + +parameters: + - name: azureServiceConnection + displayName: 'Azure service connection' + type: string + default: 'Azure Codit-Arcus Service Principal' + - name: resourceGroupName + displayName: 'Resource group name' + default: arcus-messaging-dev-we-rg + - name: keyVaultName + displayName: 'Key vault name' + default: 'arcus-messaging-kv' + +variables: + - template: ./variables/build.yml + - template: ./variables/test.yml + +resources: + repositories: + - repository: templates + type: github + name: arcus-azure/azure-devops-templates + endpoint: arcus-azure + +stages: + - stage: Deploy + jobs: + - job: DeployBicep + displayName: 'Deploy test resources' + pool: + vmImage: $(Vm.Image) + steps: + - task: AzureCLI@2 + inputs: + azureSubscription: '${{ parameters.azureServiceConnection }}' + addSpnToEnvironment: true + scriptType: 'pscore' + scriptLocation: 'inlineScript' + inlineScript: | + az deployment sub create ` + --location westeurope ` + --template-file ./build/templates/resource-group.bicep ` + --parameters resourceGroupName=$env:ARCUS_MESSAGING_RESOURCEGROUP_NAME ` + --parameters location=westeurope + + $objectId = (az ad sp show --id $env:servicePrincipalId | ConvertFrom-Json).id + + az deployment group create ` + --resource-group $env:ARCUS_MESSAGING_RESOURCEGROUP_NAME ` + --template-file ./build/templates/test-resources.bicep ` + --parameters serviceBusNamespace=$env:ARCUS_MESSAGING_SERVICEBUS_NAMESPACE ` + --parameters eventHubsNamespace=$env:ARCUS_MESSAGING_EVENTHUBS_NAMESPACE ` + --parameters storageAccountName=$env:ARCUS_MESSAGING_STORAGEACCOUNT_NAME ` + --parameters keyVaultName=$env:ARCUS_MESSAGING_KEYVAULT_NAME ` + --parameters servicePrincipal_objectId=$objectId + + $accountKey = (az storage account keys list --account-name $env:ARCUS_MESSAGING_STORAGEACCOUNT_NAME | ConvertFrom-Json)[0].value + az keyvault secret set ` + --name $env:ARCUS_MESSAGING_STORAGEACCOUNT_KEY_SECRETNAME ` + --value $accountKey ` + --vault-name ${{ parameters.keyVaultName }} + + $serviceBusKeys = az servicebus namespace authorization-rule keys list ` + --resource-group $env:ARCUS_MESSAGING_RESOURCEGROUP_NAME ` + --namespace-name $env:ARCUS_MESSAGING_SERVICEBUS_NAMESPACE ` + --authorization-rule-name 'RootManageSharedAccessKey' ` + | ConvertFrom-Json + az keyvault secret set ` + --name $env:ARCUS_MESSAGING_SERVICEBUS_CONNECTIONSTRING_SECRETNAME ` + --value $serviceBusKeys.primaryConnectionString ` + --vault-name ${{ parameters.keyVaultName }} + + $eventHubsKeys = az eventhubs namespace authorization-rule keys list ` + --resource-group $env:ARCUS_MESSAGING_RESOURCEGROUP_NAME ` + --namespace-name $env:ARCUS_MESSAGING_EVENTHUBS_NAMESPACE ` + --authorization-rule-name 'RootManageSharedAccessKey' ` + | ConvertFrom-Json + az keyvault secret set ` + --name $env:ARCUS_MESSAGING_EVENTHUBS_CONNECTIONSTRING_SECRETNAME ` + --value $eventHubsKeys.primaryConnectionString ` + --vault-name ${{ parameters.keyVaultName }} \ No newline at end of file diff --git a/build/nuget-release.yml b/build/nuget-release.yml index 37bc5b0f..157d51b4 100644 --- a/build/nuget-release.yml +++ b/build/nuget-release.yml @@ -6,6 +6,10 @@ pr: none parameters: - name: 'Package.Version' type: 'string' + - name: azureServiceConnection + displayName: 'Azure service connection' + type: string + default: 'Azure Codit-Arcus Service Principal' resources: repositories: @@ -15,9 +19,6 @@ resources: endpoint: arcus-azure variables: - - group: 'Arcus Messaging - Integration Testing' - - group: 'Arcus Security - Integration Testing' - - group: 'Arcus - GitHub Package Registry' - group: 'Build Configuration' - template: ./variables/build.yml - template: ./variables/test.yml @@ -79,35 +80,24 @@ stages: dotnetSdkVersion: '$(DotNet.Sdk.Version)' projectName: '$(Project).Tests.Unit' - - stage: SelfContainingIntegrationTests - displayName: Self-Containing Integration Tests + - stage: IntegrationTests + displayName: Integration Tests dependsOn: Build condition: succeeded() - variables: - - name: 'Arcus.Health.Port.Queue' - value: '42063' jobs: - - job: SelfContainingIntegrationTests - displayName: 'Run self-containing integration tests' + - job: RunIntegrationTests + displayName: 'Run integration tests' pool: vmImage: '$(Vm.Image)' steps: - - task: UseDotNet@2 - displayName: 'Import .NET Core SDK ($(DotNet.Sdk.PreviousVersion))' - inputs: - packageType: 'sdk' - version: '$(DotNet.Sdk.PreviousVersion)' - includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) - - template: test/run-integration-tests.yml@templates + - template: templates/integration-tests.yml parameters: - dotnetSdkVersion: '$(DotNet.Sdk.Version)' - projectName: '$(Project).Tests.Integration' - category: 'Integration' + azureServiceConnection: ${{ parameters.azureServiceConnection }} - stage: Release displayName: 'Release to NuGet.org' dependsOn: - [SelfContainingIntegrationTests , UnitTests] + [IntegrationTests , UnitTests] condition: succeeded() jobs: - job: PushToNuGet diff --git a/build/templates/build-and-run-az-func-container.yml b/build/templates/build-and-run-az-func-container.yml deleted file mode 100644 index 2bb1261d..00000000 --- a/build/templates/build-and-run-az-func-container.yml +++ /dev/null @@ -1,64 +0,0 @@ -parameters: - containerName: '' - projectName: '' - imageName: '' - imageTag: '' - port: '' - envVars: {} - -steps: -- bash: | - if [ -z "$IMAGE_NAME" ]; then - echo "##vso[task.logissue type=error;]Missing template parameter \"imageName\"" - echo "##vso[task.complete result=Failed;]" - fi - if [ -z "$IMAGE_NAME" ]; then - echo "##vso[task.logissue type=error;]Missing template parameter \"projectName\"" - echo "##vso[task.complete result=Failed;]" - fi - if [ -z "$CONTAINER_NAME" ]; then - echo "##vso[task.logissue type=error;]Missing template parameter \"containerName\"" - echo "##vso[task.complete result=Failed;]" - fi - if [ -z "$BUILD_VERSION" ]; then - echo "##vso[task.logissue type=error;]Missing template parameter \"imageTag\"" - echo "##vso[task.complete result=Failed;]" - fi - if [ -z "$PORT" ]; then - echo "##vso[task.logissue type=error;]Missing template parameter \"port\"" - echo "##vso[task.complete result=Failed;]" - fi - env: - CONTAINER_NAME: ${{ parameters.containerName }} - PROJECT_NAME: ${{ parameters.projectName }} - IMAGE_NAME: ${{ parameters.imageName }} - BUILD_VERSION: ${{ parameters.imageTag }} - PORT: ${{ parameters.port }} - displayName: Check for required parameters in YAML template -- task: qetza.replacetokens.replacetokens-task.replacetokens@3 - displayName: 'Replace integration test tokens in local.settings.json' - inputs: - rootDirectory: 'src/${{ parameters.projectName }}/' - targetFiles: 'local.settings.json' - encoding: 'auto' - verbosity: 'detailed' - writeBOM: true - actionOnMissing: 'fail' - keepToken: false - tokenPrefix: '#{' - tokenSuffix: '}#' -- task: Docker@1 - displayName: 'Build Docker image ''${{ parameters.imageName }}'' for ''${{ parameters.projectName }}''' - inputs: - dockerFile: src/${{ parameters.projectName }}/Dockerfile - imageName: '${{ parameters.imageName }}:${{ parameters.imageTag }}' - useDefaultContext: false - buildContext: src -- task: Docker@1 - displayName: 'Run ''${{ parameters.imageName }}'' Docker image' - inputs: - command: 'Run an image' - imageName: '${{ parameters.imageName }}:${{ parameters.imageTag }}' - containerName: '${{ parameters.containerName }}' - ports: '${{ parameters.port }}:${{ parameters.port }}' - envVars: ${{ parameters.envVars }} \ No newline at end of file diff --git a/build/templates/build-and-run-worker-container.yml b/build/templates/build-and-run-worker-container.yml deleted file mode 100644 index 6c8c6c67..00000000 --- a/build/templates/build-and-run-worker-container.yml +++ /dev/null @@ -1,52 +0,0 @@ -parameters: - containerName: '' - projectName: '' - imageName: '' - imageTag: '' - port: '' - envVars: {} - -steps: -- bash: | - if [ -z "$IMAGE_NAME" ]; then - echo "##vso[task.logissue type=error;]Missing template parameter \"imageName\"" - echo "##vso[task.complete result=Failed;]" - fi - if [ -z "$IMAGE_NAME" ]; then - echo "##vso[task.logissue type=error;]Missing template parameter \"projectName\"" - echo "##vso[task.complete result=Failed;]" - fi - if [ -z "$CONTAINER_NAME" ]; then - echo "##vso[task.logissue type=error;]Missing template parameter \"containerName\"" - echo "##vso[task.complete result=Failed;]" - fi - if [ -z "$BUILD_VERSION" ]; then - echo "##vso[task.logissue type=error;]Missing template parameter \"imageTag\"" - echo "##vso[task.complete result=Failed;]" - fi - if [ -z "PORT" ]; then - echo "##vso[task.logissue type=error;]Missing template parameter \"port\"" - echo "##vso[task.complete result=Failed;]" - fi - env: - CONTAINER_NAME: ${{ parameters.containerName }} - PROJECT_NAME: ${{ parameters.projectName }} - IMAGE_NAME: ${{ parameters.imageName }} - BUILD_VERSION: ${{ parameters.imageTag }} - PORT: ${{ parameters.port }} - displayName: Check for required parameters in YAML template -- task: Docker@1 - displayName: 'Build Docker image ''${{ parameters.imageName }}'' for ''${{ parameters.projectName }}''' - inputs: - dockerFile: src/${{ parameters.projectName }}/Dockerfile - imageName: '${{ parameters.imageName }}:${{ parameters.imageTag }}' - useDefaultContext: false - buildContext: src -- task: Docker@1 - displayName: 'Run ''${{ parameters.imageName }}'' Docker image' - inputs: - command: 'Run an image' - imageName: '${{ parameters.imageName }}:${{ parameters.imageTag }}' - containerName: '${{ parameters.containerName }}' - ports: '${{ parameters.port }}:${{ parameters.port }}' - envVars: ${{ parameters.envVars }} \ No newline at end of file diff --git a/build/templates/integration-tests.yml b/build/templates/integration-tests.yml new file mode 100644 index 00000000..4118ea84 --- /dev/null +++ b/build/templates/integration-tests.yml @@ -0,0 +1,44 @@ +parameters: + azureServiceConnection: '' + +steps: + - task: UseDotNet@2 + displayName: 'Import .NET Core SDK ($(DotNet.Sdk.PreviousVersion))' + inputs: + packageType: 'sdk' + version: '$(DotNet.Sdk.PreviousVersion)' + + - task: AzureCLI@2 + displayName: 'Import secrets from Azure Key Vault' + inputs: + azureSubscription: '${{ parameters.azureServiceConnection }}' + addSpnToEnvironment: true + scriptType: 'pscore' + scriptLocation: 'inlineScript' + inlineScript: | + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + Install-Module -Name Arcus.Scripting.DevOps -AllowClobber -MinimumVersion 1.3.0 + + $subscriptionId = (az account show | ConvertFrom-Json).id + $objectId = (az ad sp show --id $env:servicePrincipalId | ConvertFrom-Json).id + Set-AzDevOpsVariable -Name 'Arcus.Infra.SubscriptionId' -Value $subscriptionId + Set-AzDevOpsVariable -Name 'Arcus.Infra.TenantId' -Value $env:tenantId -AsSecret + Set-AzDevOpsVariable -Name 'Arcus.Infra.ServicePrincipal.ObjectId' -Value $objectId + Set-AzDevOpsVariable -Name 'Arcus.Infra.ServicePrincipal.ClientId' -Value $env:servicePrincipalId -AsSecret + Set-AzDevOpsVariable -Name 'Arcus.Infra.ServicePrincipal.ClientSecret' -Value $env:servicePrincipalKey -AsSecret + + $accountKey = az keyvault secret show --name $env:ARCUS_MESSAGING_STORAGEACCOUNT_KEY_SECRETNAME --vault-name $env:ARCUS_MESSAGING_KEYVAULT_NAME | ConvertFrom-Json + Set-AzDevOpsVariable -Name 'Arcus.Messaging.StorageAccount.Key' -Value $accountKey.value -AsSecret + + $serviceBusConnectionString = az keyvault secret show --name $env:ARCUS_MESSAGING_SERVICEBUS_CONNECTIONSTRING_SECRETNAME --vault-name $env:ARCUS_MESSAGING_KEYVAULT_NAME | ConvertFrom-Json + Set-AzDevOpsVariable -Name 'Arcus.Messaging.ServiceBus.ConnectionString' -Value $serviceBusConnectionString.value -AsSecret + + $eventHubsConnectionString = az keyvault secret show --name $env:ARCUS_MESSAGING_EVENTHUBS_CONNECTIONSTRING_SECRETNAME --vault-name $env:ARCUS_MESSAGING_KEYVAULT_NAME | ConvertFrom-Json + Set-AzDevOpsVariable -Name 'Arcus.Messaging.EventHubs.ConnectionString' -Value $eventHubsConnectionString.value -AsSecret + + - template: test/run-integration-tests.yml@templates + parameters: + dotnetSdkVersion: '$(DotNet.Sdk.Version)' + includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) + projectName: '$(Project).Tests.Integration' + category: 'Integration' \ No newline at end of file diff --git a/build/templates/resource-group.bicep b/build/templates/resource-group.bicep new file mode 100644 index 00000000..f21d0090 --- /dev/null +++ b/build/templates/resource-group.bicep @@ -0,0 +1,15 @@ +// Define the name of the resource group. +param resourceGroupName string + +// Define the location for the deployment of the components. +param location string + +targetScope='subscription' + +module resourceGroup 'br/public:avm/res/resources/resource-group:0.2.3' = { + name: 'resourceGroupDeployment' + params: { + name: resourceGroupName + location: location + } +} diff --git a/build/templates/run-docker-integration-tests.yml b/build/templates/run-docker-integration-tests.yml deleted file mode 100644 index cc913a5e..00000000 --- a/build/templates/run-docker-integration-tests.yml +++ /dev/null @@ -1,145 +0,0 @@ -steps: -- task: DownloadPipelineArtifact@2 - displayName: 'Download build artifacts' - inputs: - artifact: 'Build' - path: '$(Build.SourcesDirectory)' -- template: build-and-run-worker-container.yml - parameters: - projectName: 'Arcus.Messaging.Tests.Workers.ServiceBus.Queue' - containerName: '$(Images.ServiceBus.Queue)' - imageName: '$(Images.ServiceBus.Queue)' - imageTag: $(Build.BuildId) - port: '$(Arcus.Health.Port.Queue)' - envVars: | - ARCUS_HEALTH_PORT=$(Arcus.Health.Port.Queue) - EVENTGRID_TOPIC_URI=$(Arcus.TestInfra.EventGrid.Topic.Uri) - EVENTGRID_AUTH_KEY=$(Arcus.TestInfra.EventGrid.Auth.Key) - ARCUS_SERVICEBUS_CONNECTIONSTRING=$(Arcus.ServiceBus.Docker.ConnectionStringWithQueue) -- template: build-and-run-worker-container.yml - parameters: - projectName: 'Arcus.Messaging.Tests.Workers.ServiceBus.Topic' - containerName: '$(Images.ServiceBus.Topic)' - imageName: '$(Images.ServiceBus.Topic)' - imageTag: $(Build.BuildId) - port: '$(Arcus.Health.Port.Topic)' - envVars: | - ARCUS_HEALTH_PORT=$(Arcus.Health.Port.Topic) - EVENTGRID_TOPIC_URI=$(Arcus.TestInfra.EventGrid.Topic.Uri) - EVENTGRID_AUTH_KEY=$(Arcus.TestInfra.EventGrid.Auth.Key) - ARCUS_SERVICEBUS_CONNECTIONSTRING=$(Arcus.ServiceBus.Docker.ConnectionStringWithTopic) -# .NET 8 not available yet for Azure Functions in-process -# - template: build-and-run-az-func-container.yml -# parameters: -# projectName: 'Arcus.Messaging.Tests.Runtimes.AzureFunction.ServiceBus.Queue' -# containerName: '$(Images.AzureFunction.ServiceBus.Queue)' -# imageName: '$(Images.AzureFunction.ServiceBus.Queue)' -# imageTag: '$(Build.BuildId)' -# port: '$(Arcus.AzureFunctions.Queue.Port)' -# envVars: | -# ARCUS_EVENTGRID_TOPIC_URI=$(Arcus.TestInfra.EventGrid.Topic.Uri) -# ARCUS_EVENTGRID_AUTH_KEY=$(Arcus.TestInfra.EventGrid.Auth.Key) -# ARCUS_SERVICEBUS_CONNECTIONSTRING=$(Arcus.ServiceBus.Docker.AzureFunctions.NamespaceConnectionString) -- template: build-and-run-az-func-container.yml - parameters: - projectName: 'Arcus.Messaging.Tests.Runtimes.AzureFunction.ServiceBus.Topic' - containerName: '$(Images.AzureFunction.ServiceBus.Topic)' - imageName: '$(Images.AzureFunction.ServiceBus.Topic)' - imageTag: '$(Build.BuildId)' - port: '$(Arcus.AzureFunctions.Topic.Port)' - envVars: | - ARCUS_EVENTGRID_TOPIC_URI=$(Arcus.TestInfra.EventGrid.Topic.Uri) - ARCUS_EVENTGRID_AUTH_KEY=$(Arcus.TestInfra.EventGrid.Auth.Key) - ARCUS_SERVICEBUS_CONNECTIONSTRING=$(Arcus.ServiceBus.Docker.AzureFunctions.ConnectionStringWithTopic) - FUNCTIONS_WORKER_RUNTIME=dotnet-isolated -- template: build-and-run-az-func-container.yml - parameters: - projectName: 'Arcus.Messaging.Tests.Runtimes.AzureFunction.EventHubs' - containerName: '$(Images.AzureFunction.Isolated.EventHubs)' - imageName: '$(Images.AzureFunction.Isolated.EventHubs)' - imageTag: '$(Build.BuildId)' - port: '$(Arcus.AzureFunctions.Isolated.EventHubs.Port)' - envVars: | - EVENTGRID_TOPIC_URI=$(Arcus.TestInfra.EventGrid.Topic.Uri) - EVENTGRID_AUTH_KEY=$(Arcus.TestInfra.EventGrid.Auth.Key) - EventHubsConnectionString=$(Arcus.EventHubs.ConnectionString) - AzureWebJobsStorage=$(Arcus.EventHubs.BlobStorage.StorageAccountConnectionString) - FUNCTIONS_WORKER_RUNTIME=dotnet-isolated -# .NET 8 not available yet for Azure Functions in-process -# - template: build-and-run-az-func-container.yml -# parameters: -# projectName: 'Arcus.Messaging.Tests.Runtimes.AzureFunction.EventHubs.InProcess' -# containerName: '$(Images.AzureFunction.InProcess.EventHubs)' -# imageName: '$(Images.AzureFunction.InProcess.EventHubs)' -# imageTag: '$(Build.BuildId)' -# port: '$(Arcus.AzureFunctions.InProcess.EventHubs.Port)' -# envVars: | -# EVENTGRID_TOPIC_URI=$(Arcus.TestInfra.EventGrid.Topic.Uri) -# EVENTGRID_AUTH_KEY=$(Arcus.TestInfra.EventGrid.Auth.Key) -# EventHubsConnectionString=$(Arcus.EventHubs.ConnectionString) -# AzureWebJobsStorage=$(Arcus.EventHubs.BlobStorage.StorageAccountConnectionString) -- template: build-and-run-worker-container.yml - parameters: - projectName: 'Arcus.Messaging.Tests.Workers.EventHubs' - containerName: '$(Images.EventHubs)' - imageName: '$(Images.EventHubs)' - imageTag: $(Build.BuildId) - port: '$(Arcus.Health.Port.EventHubs)' - envVars: | - ARCUS_HEALTH_PORT=$(Arcus.Health.Port.EventHubs) - EVENTGRID_TOPIC_URI=$(Arcus.TestInfra.EventGrid.Topic.Uri) - EVENTGRID_AUTH_KEY=$(Arcus.TestInfra.EventGrid.Auth.Key) - EVENTHUBS_NAME=$(Arcus.EventHubs.Docker.EventHubsName) - EVENTHUBS_CONNECIONSTRING=$(Arcus.EventHubs.ConnectionString) - BLOBSTORAGE_CONTAINERNAME=$(Arcus.EventHubs.Docker.BlobStorage.ContainerName) - STORAGEACCOUNT_CONNECTIONSTRING=$(Arcus.EventHubs.BlobStorage.StorageAccountConnectionString) -- template: test/run-integration-tests.yml@templates - parameters: - dotnetSdkVersion: '$(DotNet.Sdk.Version)' - includePreviewVersions: $(DotNet.Sdk.IncludePreviewVersions) - projectName: '$(Project).Tests.Integration' - category: 'Docker' -- task: PowerShell@2 - displayName: 'Get Docker container logs for Service Bus Queue worker project' - inputs: - targetType: 'inline' - script: 'docker logs $(Images.ServiceBus.Queue)' - condition: failed() -- task: PowerShell@2 - displayName: 'Get Docker container logs for Service Bus Topic worker project' - inputs: - targetType: 'inline' - script: 'docker logs $(Images.ServiceBus.Topic)' - condition: failed() -# .NET 8 not available yet for Azure Functions in-process -# - task: PowerShell@2 -# displayName: 'Get Docker container logs for Azure Functions Service Bus Queue project' -# inputs: -# targetType: 'inline' -# script: 'docker logs $(Images.AzureFunction.ServiceBus.Queue)' -# condition: failed() -- task: PowerShell@2 - displayName: 'Get Docker container logs for Azure Functions Service Bus Topic project' - inputs: - targetType: 'inline' - script: 'docker logs $(Images.AzureFunction.ServiceBus.Topic)' - condition: failed() -# .NET 8 not available yet for Azure Functions in-process -# - task: PowerShell@2 -# displayName: 'Get Docker container logs for Azure Functions EventHubs project' -# inputs: -# targetType: 'inline' -# script: 'docker logs $(Images.AzureFunction.InProcess.EventHubs)' -# condition: failed() -- task: PowerShell@2 - displayName: 'Get Docker container logs for Azure Functions EventHubs (isolated) project' - inputs: - targetType: 'inline' - script: 'docker logs $(Images.AzureFunction.Isolated.EventHubs)' - condition: failed() -- task: PowerShell@2 - displayName: 'Get Docker container logs for Azure EventHubs worker project' - inputs: - targetType: 'inline' - script: 'docker logs $(Images.EventHubs)' - condition: failed() \ No newline at end of file diff --git a/build/templates/test-resources.bicep b/build/templates/test-resources.bicep new file mode 100644 index 00000000..5b84144c --- /dev/null +++ b/build/templates/test-resources.bicep @@ -0,0 +1,89 @@ +// Define the location for the deployment of the components. +param location string = resourceGroup().location + +// Define the name of the single Azure Service bus namespace. +param serviceBusNamespace string + +// Define the name of the single Azure EventHubs namespace. +param eventHubsNamespace string + +// Define the name of the storage account that will be created. +param storageAccountName string + +// Define the name of the Key Vault. +param keyVaultName string + +// Define the Service Principal ID that needs access full access to the deployed resource group. +param servicePrincipal_objectId string + +module serviceBus 'br/public:avm/res/service-bus/namespace:0.8.0' = { + name: 'serviceBusDeployment' + params: { + name: serviceBusNamespace + location: location + skuObject: { + name: 'Standard' + } + disableLocalAuth: false + roleAssignments: [ + { + principalId: servicePrincipal_objectId + roleDefinitionIdOrName: 'Azure Service Bus Data Owner' + } + ] + } +} + +module hubs 'br/public:avm/res/event-hub/namespace:0.7.0' = { + name: 'eventHubsDeployment' + params: { + name: eventHubsNamespace + location: location + skuName: 'Basic' + disableLocalAuth: false + roleAssignments: [ + { + principalId: servicePrincipal_objectId + roleDefinitionIdOrName: 'Azure Event Hubs Data Owner' + } + ] + } +} + +module storageAccount 'br/public:avm/res/storage/storage-account:0.9.1' = { + name: 'storageAccountDeployment' + params: { + name: storageAccountName + location: location + allowBlobPublicAccess: true + publicNetworkAccess: 'Enabled' + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Allow' + ipRules: [] + virtualNetworkRules: [] + } + roleAssignments: [ + { + principalId: servicePrincipal_objectId + roleDefinitionIdOrName: 'Storage Blob Data Contributor' + } + ] + } +} + +module vault 'br/public:avm/res/key-vault/vault:0.6.1' = { + name: 'vaultDeployment' + params: { + name: keyVaultName + location: location + roleAssignments: [ + { + principalId: servicePrincipal_objectId + roleDefinitionIdOrName: 'Key Vault Secrets officer' + } + ] + secrets: [ + ] + } +} diff --git a/build/variables/test.yml b/build/variables/test.yml index 5f4d819c..4ac2a076 100644 --- a/build/variables/test.yml +++ b/build/variables/test.yml @@ -1,20 +1,12 @@ variables: - Images.ServiceBus.Queue: 'workers-service-bus-queue' - Images.ServiceBus.Topic: 'workers-service-bus-topic' - Images.AzureFunction.ServiceBus.Queue: 'runtimes-az-func-service-bus-queue' - Images.AzureFunction.ServiceBus.Topic: 'runtimes-az-func-service-bus-topic' - Images.AzureFunction.Isolated.EventHubs: 'runtimes-az-func-eventhubs-isolated' - Images.AzureFunction.InProcess.EventHubs: 'runtimes-az-func-eventhubs-inprocess' - Images.EventHubs: 'workers-eventhubs' - Arcus.Health.Port.Queue: '42063' - Arcus.Health.Port.Topic: '42064' - Arcus.AzureFunctions.Queue.Port: '42065' - Arcus.AzureFunctions.Topic.Port: '42068' - Arcus.AzureFunctions.Isolated.EventHubs.Port: '42067' - Arcus.AzureFunctions.InProcess.EventHubs.Port: '42069' - Arcus.Health.Port.EventHubs: '42066' - Arcus.EventHubs.SelfContained.EventHubsName: 'orders' - Arcus.EventHubs.Docker.EventHubsName: 'orders-docker' - Arcus.EventHubs.Docker.AzureFunctions.Isolated.EventHubsName: 'orders-az-func-docker' - Arcus.EventHubs.Docker.AzureFunctions.InProcess.EventHubsName: 'orders-az-func-inprocess-docker' - Arcus.EventHubs.Docker.BlobStorage.ContainerName: 'eventhubs-docker' + Arcus.Messaging.ResourceGroup.Name: 'arcus-messaging-dev-we-rg' + Arcus.Messaging.KeyVault.Name: 'arcus-messaging-kv' + + Arcus.Messaging.StorageAccount.Name: 'arcusmessagingstorage' + Arcus.Messaging.StorageAccount.Key.SecretName: 'Arcus-Messaging-StorageAccount-Key' + + Arcus.Messaging.ServiceBus.Namespace: 'arcus-messaging-servicebus' + Arcus.Messaging.ServiceBus.ConnectionString.SecretName: 'Arcus-Messaging-ServiceBus-ConnectionString' + + Arcus.Messaging.EventHubs.Namespace: 'arcus-messaging-eventhubs' + Arcus.Messaging.EventHubs.ConnectionString.SecretName: 'Arcus-Messaging-EventHubs-ConnectionString' \ No newline at end of file diff --git a/src/Arcus.Messaging.Pumps.Abstractions/MessagePump.cs b/src/Arcus.Messaging.Pumps.Abstractions/MessagePump.cs index c2353dff..b1e471f5 100644 --- a/src/Arcus.Messaging.Pumps.Abstractions/MessagePump.cs +++ b/src/Arcus.Messaging.Pumps.Abstractions/MessagePump.cs @@ -82,7 +82,7 @@ protected MessagePump(IConfiguration configuration, IServiceProvider serviceProv /// Exception that occurred protected virtual Task HandleReceiveExceptionAsync(Exception receiveException) { - Logger.LogCritical(receiveException, "Unable to process message from {EntityPath} with client {ClientId}", EntityPath, ClientId); + Logger.LogCritical(receiveException, "Unable to process message from {EntityPath} with client {ClientId}: {Message}", EntityPath, ClientId, receiveException.Message); return Task.CompletedTask; } diff --git a/src/Arcus.Messaging.Pumps.EventHubs/AzureEventHubsMessagePump.cs b/src/Arcus.Messaging.Pumps.EventHubs/AzureEventHubsMessagePump.cs index 172ed9ea..446a60d9 100644 --- a/src/Arcus.Messaging.Pumps.EventHubs/AzureEventHubsMessagePump.cs +++ b/src/Arcus.Messaging.Pumps.EventHubs/AzureEventHubsMessagePump.cs @@ -200,9 +200,14 @@ public override async Task StopProcessingMessagesAsync(CancellationToken cancell try { Logger.LogTrace("Stopping Azure EventHubs message pump '{JobId}' on '{ConsumerGroup}/{EventHubsName}' in '{Namespace}'", JobId, ConsumerGroup, EventHubName , Namespace); - await _eventProcessor.StopProcessingAsync(cancellationToken); - _eventProcessor.ProcessEventAsync -= ProcessMessageAsync; - _eventProcessor.ProcessErrorAsync -= ProcessErrorAsync; + + if (_eventProcessor != null) + { + await _eventProcessor.StopProcessingAsync(cancellationToken); + _eventProcessor.ProcessEventAsync -= ProcessMessageAsync; + _eventProcessor.ProcessErrorAsync -= ProcessErrorAsync; + } + Logger.LogInformation("Azure EventHubs message pump '{JobId}' on '{ConsumerGroup}/{EventHubsName}' in '{Namespace}' stopped: {Time}", JobId, ConsumerGroup, EventHubName , Namespace, DateTimeOffset.UtcNow); } catch (Exception exception) diff --git a/src/Arcus.Messaging.Tests.Core/Arcus.Messaging.Tests.Core.csproj b/src/Arcus.Messaging.Tests.Core/Arcus.Messaging.Tests.Core.csproj index e7280c09..86479dc3 100644 --- a/src/Arcus.Messaging.Tests.Core/Arcus.Messaging.Tests.Core.csproj +++ b/src/Arcus.Messaging.Tests.Core/Arcus.Messaging.Tests.Core.csproj @@ -14,8 +14,6 @@ - - diff --git a/src/Arcus.Messaging.Tests.Core/Events/v1/OrderCreatedEvent.cs b/src/Arcus.Messaging.Tests.Core/Events/v1/OrderCreatedEvent.cs deleted file mode 100644 index a7fca240..00000000 --- a/src/Arcus.Messaging.Tests.Core/Events/v1/OrderCreatedEvent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Arcus.EventGrid.Contracts; -using Arcus.Messaging.Abstractions; -using Newtonsoft.Json; - -namespace Arcus.Messaging.Tests.Core.Events.v1 -{ - public class OrderCreatedEvent : EventGridEvent - { - private const string DefaultDataVersion = "1"; - private const string DefaultEventType = "Arcus.Samples.Orders.OrderCreated"; - - public OrderCreatedEvent(string eventId, string orderId, int amount, string articleNumber, string customerName, MessageCorrelationInfo correlationInfo) - : base(eventId, $"customer/{customerName}", - new OrderCreatedEventData(orderId, amount, articleNumber, customerName, correlationInfo), DefaultDataVersion, - DefaultEventType) - { - } - - [JsonConstructor] - private OrderCreatedEvent() - { - } - } -} \ No newline at end of file diff --git a/src/Arcus.Messaging.Tests.Integration/Arcus.Messaging.Tests.Integration.csproj b/src/Arcus.Messaging.Tests.Integration/Arcus.Messaging.Tests.Integration.csproj index 8767a6c8..0ffc6f30 100644 --- a/src/Arcus.Messaging.Tests.Integration/Arcus.Messaging.Tests.Integration.csproj +++ b/src/Arcus.Messaging.Tests.Integration/Arcus.Messaging.Tests.Integration.csproj @@ -6,25 +6,22 @@ - - - - - + + + - + + + - - - diff --git a/src/Arcus.Messaging.Tests.Integration/AzureFunctions/EventHubs/AzureFunctionsEventHubsTriggerDockerTests.cs b/src/Arcus.Messaging.Tests.Integration/AzureFunctions/EventHubs/AzureFunctionsEventHubsTriggerDockerTests.cs deleted file mode 100644 index f2c111c9..00000000 --- a/src/Arcus.Messaging.Tests.Integration/AzureFunctions/EventHubs/AzureFunctionsEventHubsTriggerDockerTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Arcus.Messaging.Tests.Core.Correlation; -using Arcus.Messaging.Tests.Core.Events.v1; -using Arcus.Messaging.Tests.Core.Generators; -using Arcus.Messaging.Tests.Core.Messages.v1; -using Arcus.Messaging.Tests.Integration.Fixture; -using Arcus.Messaging.Tests.Integration.MessagePump.EventHubs; -using Arcus.Messaging.Tests.Integration.MessagePump.Fixture; -using Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus; -using Azure.Messaging.EventHubs; -using Newtonsoft.Json; -using Xunit; -using Xunit.Abstractions; - -namespace Arcus.Messaging.Tests.Integration.AzureFunctions.EventHubs -{ - [Collection("Docker")] - [Trait("Category", "Docker")] - public class AzureFunctionsEventHubsTriggerDockerTests : DockerServiceBusIntegrationTest - { - private readonly TestConfig _config; - - /// - /// Initializes a new instance of the class. - /// - public AzureFunctionsEventHubsTriggerDockerTests(ITestOutputHelper outputWriter) : base(outputWriter) - { - _config = TestConfig.Create(); - } - - [Theory] - [InlineData(IntegrationTestType.DockerAzureFunctionsInProcess, Skip = ".NET 8 is not supported yet for in-process Azure Functions")] - [InlineData(IntegrationTestType.DockerAzureFunctionsIsolated)] - public async Task EventHubsMessageFunction_PublishesEventDataMessage_MessageSuccessfullyProcessed(IntegrationTestType integrationType) - { - // Arrange - var traceParent = TraceParent.Generate(); - Order order = OrderGenerator.Generate(); - EventData expected = new EventData(JsonConvert.SerializeObject(order)).WithDiagnosticId(traceParent); - - EventHubsConfig eventHubs = _config.GetEventHubsConfig(); - string eventHubsName = eventHubs.GetEventHubsName(integrationType); - var producer = new TestEventHubsMessageProducer(eventHubs.EventHubsConnectionString, eventHubsName); - - await using (var consumer = await TestServiceBusMessageEventConsumer.StartNewAsync(_config, Logger)) - { - // Act - await producer.ProduceAsync(expected); - - // Assert - OrderCreatedEventData orderCreatedEventData = consumer.ConsumeOrderEventForW3C(traceParent.TransactionId); - Assert.NotNull(orderCreatedEventData); - Assert.NotNull(orderCreatedEventData.CorrelationInfo); - Assert.Equal(order.Id, orderCreatedEventData.Id); - Assert.Equal(order.Amount, orderCreatedEventData.Amount); - Assert.Equal(order.ArticleNumber, orderCreatedEventData.ArticleNumber); - Assert.Equal(traceParent.TransactionId, orderCreatedEventData.CorrelationInfo.TransactionId); - Assert.Equal(traceParent.OperationParentId, orderCreatedEventData.CorrelationInfo.OperationParentId); - Assert.NotNull(orderCreatedEventData.CorrelationInfo.OperationId); - } - } - } -} diff --git a/src/Arcus.Messaging.Tests.Integration/AzureFunctions/ServiceBus/AzureFunctionServiceBusTriggerDockerTests.cs b/src/Arcus.Messaging.Tests.Integration/AzureFunctions/ServiceBus/AzureFunctionServiceBusTriggerDockerTests.cs deleted file mode 100644 index 1461ff5a..00000000 --- a/src/Arcus.Messaging.Tests.Integration/AzureFunctions/ServiceBus/AzureFunctionServiceBusTriggerDockerTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Threading.Tasks; -using Arcus.Messaging.Tests.Core.Correlation; -using Arcus.Messaging.Tests.Core.Events.v1; -using Arcus.Messaging.Tests.Core.Generators; -using Arcus.Messaging.Tests.Core.Messages.v1; -using Arcus.Messaging.Tests.Integration.Fixture; -using Arcus.Messaging.Tests.Integration.MessagePump.Fixture; -using Azure.Messaging.ServiceBus; -using Xunit; -using Xunit.Abstractions; - -namespace Arcus.Messaging.Tests.Integration.AzureFunctions.ServiceBus -{ - [Collection("Docker")] - [Trait("Category", "Docker")] - public class AzureFunctionServiceBusTriggerDockerTests : DockerServiceBusIntegrationTest - { - /// - /// Initializes a new instance of the class. - /// - public AzureFunctionServiceBusTriggerDockerTests(ITestOutputHelper outputWriter) : base(outputWriter) - { - } - - [Theory] - [InlineData("Arcus:ServiceBus:Docker:AzureFunctions:ConnectionStringWithQueue", Skip = ".NET 8 is not supported yet for in-process Azure Functions")] - [InlineData("Arcus:ServiceBus:Docker:AzureFunctions:ConnectionStringWithTopic")] - public async Task ServiceBusTrigger_PublishServiceBusMessage_MessageSuccessfullyProcessed(string connectionStringKey) - { - // Arrange - var traceParent = TraceParent.Generate(); - Order order = OrderGenerator.Generate(); - var orderMessage = new ServiceBusMessage(BinaryData.FromObjectAsJson(order)).WithDiagnosticId(traceParent); - - // Act - await SenderOrderToServiceBusAsync(orderMessage, connectionStringKey); - - // Assert - OrderCreatedEventData orderEventData = ReceiveOrderFromEventGrid(traceParent.TransactionId); - Assert.NotNull(orderEventData.CorrelationInfo); - Assert.Equal(order.Id, orderEventData.Id); - Assert.Equal(order.Amount, orderEventData.Amount); - Assert.Equal(order.ArticleNumber, orderEventData.ArticleNumber); - Assert.Equal(traceParent.TransactionId, orderEventData.CorrelationInfo.TransactionId); - Assert.Equal(traceParent.OperationParentId, orderEventData.CorrelationInfo.OperationParentId); - Assert.NotNull(orderEventData.CorrelationInfo.OperationId); - } - } -} diff --git a/src/Arcus.Messaging.Tests.Integration/EventHubs/AzureClientFactoryBuilderExtensionsTests.cs b/src/Arcus.Messaging.Tests.Integration/EventHubs/AzureClientFactoryBuilderExtensionsTests.cs index 9236a986..4a1ca5da 100644 --- a/src/Arcus.Messaging.Tests.Integration/EventHubs/AzureClientFactoryBuilderExtensionsTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/EventHubs/AzureClientFactoryBuilderExtensionsTests.cs @@ -4,12 +4,10 @@ using Arcus.Messaging.Tests.Core.Generators; using Arcus.Messaging.Tests.Core.Messages.v1; using Arcus.Messaging.Tests.Integration.Fixture; -using Arcus.Messaging.Tests.Integration.MessagePump.EventHubs; -using Arcus.Testing.Logging; +using Arcus.Messaging.Tests.Integration.MessagePump; +using Arcus.Testing; using Azure.Messaging.EventHubs; using Azure.Messaging.EventHubs.Producer; -using Azure.Storage.Blobs; -using Microsoft.Azure.Management.ServiceBus.Models; using Microsoft.Extensions.Azure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -21,25 +19,28 @@ namespace Arcus.Messaging.Tests.Integration.EventHubs { - public class AzureClientFactoryBuilderExtensionsTests : IAsyncLifetime + public class AzureClientFactoryBuilderExtensionsTests : IClassFixture, IAsyncLifetime { private readonly TestConfig _config; private readonly EventHubsConfig _eventHubsConfig; private readonly ILogger _logger; - private TemporaryBlobStorageContainer _blobStorageContainer; + private TemporaryManagedIdentityConnection _connection; + private TemporaryBlobContainer _blobStorageContainer; /// /// Initializes a new instance of the class. /// - public AzureClientFactoryBuilderExtensionsTests(ITestOutputHelper outputWriter) + public AzureClientFactoryBuilderExtensionsTests(EventHubsEntityFixture fixture, ITestOutputHelper outputWriter) { _config = TestConfig.Create(); _logger = new XunitTestLogger(outputWriter); - _eventHubsConfig = _config.GetEventHubsConfig(); + _eventHubsConfig = _config.GetEventHubs(); + + EventHubsName = fixture.HubName; } - private string EventHubsName => _eventHubsConfig.GetEventHubsName(IntegrationTestType.SelfContained); + private string EventHubsName { get; } [Fact] public async Task AddEventHubProducerClientWithNamespace_SendEvent_Succeeds() @@ -47,7 +48,7 @@ public async Task AddEventHubProducerClientWithNamespace_SendEvent_Succeeds() // Arrange var services = new ServiceCollection(); var connectionStringSecretName = "MyConnectionString"; - EventHubsConfig eventHubsConfig = _config.GetEventHubsConfig(); + EventHubsConfig eventHubsConfig = _config.GetEventHubs(); services.AddSecretStore(stores => stores.AddInMemory(connectionStringSecretName, eventHubsConfig.EventHubsConnectionString)); // Act @@ -120,9 +121,7 @@ private async Task RetryAssertUntilServiceBusMessageIsAvailableAsync(Action, IAsyncLifetime { private const string DependencyIdPattern = @"with ID [a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12}"; @@ -29,19 +28,22 @@ public class EventHubProducerClientExtensionsTests : IAsyncLifetime private readonly EventHubsConfig _eventHubsConfig; private readonly ILogger _logger; - private TemporaryBlobStorageContainer _blobStorageContainer; + private TemporaryManagedIdentityConnection _connection; + private TemporaryBlobContainer _blobStorageContainer; /// /// Initializes a new instance of the class. /// - public EventHubProducerClientExtensionsTests(ITestOutputHelper outputWriter) + public EventHubProducerClientExtensionsTests(EventHubsEntityFixture fixture, ITestOutputHelper outputWriter) { _config = TestConfig.Create(); - _eventHubsConfig = _config.GetEventHubsConfig(); + _eventHubsConfig = _config.GetEventHubs(); _logger = new XunitTestLogger(outputWriter); + + EventHubsName = fixture.HubName; } - private string EventHubsName => _eventHubsConfig.GetEventHubsName(IntegrationTestType.SelfContained); + private string EventHubsName { get; } [Fact] public async Task SendMessage_WithMessageCorrelation_TracksMessage() @@ -51,7 +53,8 @@ public async Task SendMessage_WithMessageCorrelation_TracksMessage() MessageCorrelationInfo correlation = GenerateMessageCorrelationInfo(); var logger = new InMemoryLogger(); - await using (var client = new EventHubProducerClient(_eventHubsConfig.EventHubsConnectionString, EventHubsName)) + + await using (EventHubProducerClient client = _eventHubsConfig.GetProducerClient(EventHubsName)) { await client.SendAsync(new [] { order }, correlation, logger); } @@ -82,7 +85,7 @@ public async Task SendMessage_WithCustomOptions_TracksMessage() string key1 = Guid.NewGuid().ToString(), value1 = Guid.NewGuid().ToString(); string key2 = Guid.NewGuid().ToString(), value2 = Guid.NewGuid().ToString(); - await using (var client = new EventHubProducerClient(_eventHubsConfig.EventHubsConnectionString, EventHubsName)) + await using (EventHubProducerClient client = _eventHubsConfig.GetProducerClient(EventHubsName)) { await client.SendAsync(new [] { order }, correlation, logger, options => { @@ -171,9 +174,7 @@ private async Task RetryAssertUntilServiceBusMessageIsAvailableAsync(Action + { + opt.TopicSubscription = TopicSubscription.Automatic; + opt.AutoComplete = true; + }); + } } } diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/DockerServiceBusIntegrationTest.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/DockerServiceBusIntegrationTest.cs deleted file mode 100644 index 7539e677..00000000 --- a/src/Arcus.Messaging.Tests.Integration/Fixture/DockerServiceBusIntegrationTest.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Threading.Tasks; -using Arcus.EventGrid; -using Arcus.EventGrid.Contracts; -using Arcus.EventGrid.Parsers; -using Arcus.EventGrid.Testing.Infrastructure.Hosts.ServiceBus; -using Arcus.Messaging.Tests.Core.Events.v1; -using Arcus.Messaging.Tests.Core.Messages.v1; -using Arcus.Messaging.Tests.Workers.ServiceBus.Fixture; -using Azure.Messaging.ServiceBus; -using CloudNative.CloudEvents; -using GuardNet; -using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; -using Xunit; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Arcus.Messaging.Tests.Integration.Fixture -{ - /// - /// Represents the general setup an teardown of an integration test, using an external running Docker container to interact with. - /// - [Trait("Category", "Docker")] - public abstract class DockerServiceBusIntegrationTest : IntegrationTest, IAsyncLifetime - { - private ServiceBusEventConsumerHost _serviceBusEventConsumerHost; - - /// - /// Initializes a new instance of the class. - /// - /// The logger instance to write diagnostic messages during the interaction with Azure Service Bus instances. - protected DockerServiceBusIntegrationTest(ITestOutputHelper outputWriter) : base(outputWriter) - { - } - - /// - /// Called immediately after the class has been created, before it is used. - /// - public async Task InitializeAsync() - { - var connectionString = Configuration.GetValue("Arcus:Infra:ServiceBus:ConnectionString"); - var topicName = Configuration.GetValue("Arcus:Infra:ServiceBus:TopicName"); - - var serviceBusEventConsumerHostOptions = new ServiceBusEventConsumerHostOptions(topicName, connectionString); - _serviceBusEventConsumerHost = await ServiceBusEventConsumerHost.StartAsync(serviceBusEventConsumerHostOptions, Logger); - } - - /// - /// Sends an message to an Azure Service Bus instance, located at the given . - /// - /// The Service Bus message representation of an . - /// The connection string key where the should be send to. - /// Thrown when the is null. - /// Thrown when the is blank. - public async Task SenderOrderToServiceBusAsync(ServiceBusMessage message, string connectionStringKey) - { - Guard.NotNull(message, nameof(message), "Requires an Azure Service Bus message representation of an 'Order' to send it to an Azure Service Bus instance"); - Guard.NotNullOrWhitespace(connectionStringKey, nameof(connectionStringKey), "Requires an Azure Service Bus connection string to send the 'Order' message to"); - - var connectionString = Configuration.GetValue(connectionStringKey); - ServiceBusConnectionStringProperties serviceBusConnectionString = ServiceBusConnectionStringProperties.Parse(connectionString); - - await using (var client = new ServiceBusClient(connectionString)) - await using (ServiceBusSender messageSender = client.CreateSender(serviceBusConnectionString.EntityPath)) - { - await messageSender.SendMessageAsync(message); - } - } - - /// - /// Receives the previously send message as an published event on Azure Event Grid. - /// - /// The transaction ID to identity the correct published . - /// Thrown when the is blank. - /// - /// Thrown when the received message event doesn't conform with the expected structure of an published event. - /// - public OrderCreatedEventData ReceiveOrderFromEventGrid(string transactionId) - { - Guard.NotNullOrWhitespace(transactionId, nameof(transactionId), "Requires an operation ID to uniquely identity the published 'Order' message"); - - CloudEvent cloudEvent = _serviceBusEventConsumerHost.GetReceivedEvent((CloudEvent ev) => - { - string json = ev.Data.ToString(); - var eventData = JsonConvert.DeserializeObject(json, new MessageCorrelationInfoJsonConverter()); - - return eventData.CorrelationInfo.TransactionId == transactionId; - }, timeout: TimeSpan.FromMinutes(1)); - - var json = cloudEvent.Data.ToString(); - Assert.NotNull(json); - var orderCreatedEventData = JsonConvert.DeserializeObject(json, new MessageCorrelationInfoJsonConverter()); - Assert.NotNull(orderCreatedEventData); - - return orderCreatedEventData; - } - - /// - /// Called when an object is no longer needed. Called just before - /// if the class also implements that. - /// - public async Task DisposeAsync() - { - await _serviceBusEventConsumerHost.StopAsync(); - } - } -} diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/EventHubsConfig.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/EventHubsConfig.cs index 5a9fa59a..99f56c3c 100644 --- a/src/Arcus.Messaging.Tests.Integration/Fixture/EventHubsConfig.cs +++ b/src/Arcus.Messaging.Tests.Integration/Fixture/EventHubsConfig.cs @@ -1,5 +1,9 @@ -using System; -using GuardNet; +using Arcus.Testing; +using Azure.Core; +using Azure.Messaging.EventHubs; +using Azure.Messaging.EventHubs.Producer; +using Azure.ResourceManager.EventHubs; +using Azure.Storage.Blobs; namespace Arcus.Messaging.Tests.Integration.Fixture { @@ -8,72 +12,53 @@ namespace Arcus.Messaging.Tests.Integration.Fixture /// public class EventHubsConfig { - private readonly string _selfContainedEventHubsName, _dockerEventHubsName, _dockerAzureFunctionsIsolatedEventHubsName, _dockerAzureFunctionsInProcessEventHubsName; - /// /// Initializes a new instance of the class. /// - /// The name of the Azure EventHubs instance used in the slf-contained integration tests. - /// The name of the Azure EventHubs instance used in the docker integration tests. - /// The name of the Azure EventHubs instance used in the docker Azure Functions isolated integration tests. - /// The name of the Azure EventHubs instance used in the docker Azure Functions in-process integration tests. - /// The connection string to connect to the on Azure. - /// - /// The connection string used to connect to the related Azure Blob storage instance where checkpoints are stored and load balancing done. - /// - /// - /// Thrown when the , , or the is blank. - /// public EventHubsConfig( - string selfContainedEventHubsName, - string dockerEventHubsName, - string dockerAzureFunctionsIsolatedEventHubsName, - string dockerAzureFunctionsInProcessEventHubsName, - string connectionString, - string storageConnectionString) + ServicePrincipal servicePrincipal, + string subscriptionId, + string resourceGroupName, + string eventHubsNamespace, + string connectionString, + StorageAccountConfig storageAccount) { - Guard.NotNullOrWhitespace(selfContainedEventHubsName, nameof(selfContainedEventHubsName), "Requires a non-blank name for the Azure EventHubs instance used in the self-contained integration tests"); - Guard.NotNullOrWhitespace(dockerEventHubsName, nameof(dockerEventHubsName), "Requires a non-blank name for the Azure EventHubs instance used in the docker integration tests"); - Guard.NotNullOrWhitespace(dockerAzureFunctionsIsolatedEventHubsName, nameof(dockerAzureFunctionsIsolatedEventHubsName), "Requires a non-blank name for the Azure EventHubs instance used in the docker Azure Functions integration tests"); - Guard.NotNullOrWhitespace(dockerAzureFunctionsInProcessEventHubsName, nameof(dockerAzureFunctionsInProcessEventHubsName), "Requires a non-blank name for the Azure EventHubs instance used in the docker Azure Functions integration tests"); - Guard.NotNullOrWhitespace(connectionString, nameof(connectionString), "Requires a non-blank connection string to connect to the Azure EventHubs instance"); - Guard.NotNullOrWhitespace(storageConnectionString, nameof(storageConnectionString), "Requires a non-blank connection string to connect to the related Azure Blob storage instance for Azure EventHubs"); - - _selfContainedEventHubsName = selfContainedEventHubsName; - _dockerEventHubsName = dockerEventHubsName; - _dockerAzureFunctionsIsolatedEventHubsName = dockerAzureFunctionsIsolatedEventHubsName; - _dockerAzureFunctionsInProcessEventHubsName = dockerAzureFunctionsInProcessEventHubsName; + ServicePrincipal = servicePrincipal; + ResourceId = EventHubsNamespaceResource.CreateResourceIdentifier(subscriptionId, resourceGroupName, eventHubsNamespace); + HostName = $"{eventHubsNamespace}.servicebus.windows.net"; + Storage = storageAccount; EventHubsConnectionString = connectionString; - StorageConnectionString = storageConnectionString; } - /// - /// Gets the connection string to connect to the Azure EventHubs instance. - /// + public ServicePrincipal ServicePrincipal { get; } + public ResourceIdentifier ResourceId { get; } + public string HostName { get; } + public StorageAccountConfig Storage { get; } public string EventHubsConnectionString { get; } - /// - /// Gets the connection string to connect to the related Azure Blob storage instance that is prepared for the integration tests. - /// - public string StorageConnectionString { get; } + public EventHubProducerClient GetProducerClient(string name) + { + return new EventHubProducerClient(HostName, name, ServicePrincipal.GetCredential()); + } - /// - /// Gets the configured Azure EventHubs name used during the integration tests. - /// - /// The type of test that request the name of the Azure EventHubs instance. - /// When the is outside the bounds of the enumeration. - public string GetEventHubsName(IntegrationTestType type) + public EventProcessorClient GetProcessorClient(string name, BlobContainerClient checkpointStore) + { + return new EventProcessorClient(checkpointStore, "$Default", HostName, name, ServicePrincipal.GetCredential()); + } + } + + public static class EventHubsTestConfigExtensions + { + public static EventHubsConfig GetEventHubs(this TestConfig config) { - switch (type) - { - case IntegrationTestType.SelfContained: return _selfContainedEventHubsName; - case IntegrationTestType.DockerWorker: return _dockerEventHubsName; - case IntegrationTestType.DockerAzureFunctionsIsolated: return _dockerAzureFunctionsIsolatedEventHubsName; - case IntegrationTestType.DockerAzureFunctionsInProcess: return _dockerAzureFunctionsInProcessEventHubsName; - default: - throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown integration test type"); - } + return new EventHubsConfig( + config.GetServicePrincipal(), + config.GetSubscriptionId(), + config.GetResourceGroupName(), + config["Arcus:EventHubs:Namespace"], + config["Arcus:EventHubs:ConnectionString"], + config.GetStorageAccount()); } } } diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/IntegrationTestType.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/IntegrationTestType.cs index 00d2ec35..26a01288 100644 --- a/src/Arcus.Messaging.Tests.Integration/Fixture/IntegrationTestType.cs +++ b/src/Arcus.Messaging.Tests.Integration/Fixture/IntegrationTestType.cs @@ -3,8 +3,5 @@ public enum IntegrationTestType { SelfContained, - DockerWorker, - DockerAzureFunctionsIsolated, - DockerAzureFunctionsInProcess } } diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/KeyVaultConfig.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/KeyVaultConfig.cs index ac855cd5..bb655db9 100644 --- a/src/Arcus.Messaging.Tests.Integration/Fixture/KeyVaultConfig.cs +++ b/src/Arcus.Messaging.Tests.Integration/Fixture/KeyVaultConfig.cs @@ -1,4 +1,6 @@ using System; +using Arcus.Testing; +using Azure.Security.KeyVault.Secrets; using GuardNet; namespace Arcus.Messaging.Tests.Integration.Fixture @@ -8,6 +10,8 @@ namespace Arcus.Messaging.Tests.Integration.Fixture /// public class KeyVaultConfig { + private readonly ServicePrincipal _servicePrincipal; + /// /// Initializes a new instance of the class. /// @@ -27,6 +31,15 @@ public KeyVaultConfig(string vaultUri, string secretName, KeyVaultEventEndpoint SecretNewVersionCreated = secretNewVersionCreated; } + /// + /// Initializes a new instance of the class. + /// + public KeyVaultConfig(ServicePrincipal servicePrincipal, string vaultName) + { + _servicePrincipal = servicePrincipal; + VaultUri = $"https://{vaultName}.vault.azure.net"; + } + /// /// Gets the URI referencing the Azure Key Vault instance. /// @@ -41,5 +54,20 @@ public KeyVaultConfig(string vaultUri, string secretName, KeyVaultEventEndpoint /// Gets the endpoint where Azure Key Vault events will be available, including 'Secret new version created' event. /// public KeyVaultEventEndpoint SecretNewVersionCreated { get; } + + public SecretClient GetClient() + { + return new SecretClient(new Uri(VaultUri), _servicePrincipal.GetCredential()); + } + } + + public static class KeyVaultConfigExtensions + { + public static KeyVaultConfig GetKeyVault(this TestConfig config) + { + return new KeyVaultConfig( + config.GetServicePrincipal(), + config["Arcus:KeyVault:Name"]); + } } } diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/ServiceBusConfig.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/ServiceBusConfig.cs new file mode 100644 index 00000000..fa7a2ed7 --- /dev/null +++ b/src/Arcus.Messaging.Tests.Integration/Fixture/ServiceBusConfig.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Arcus.Testing; +using Azure.Core; +using Azure.Messaging.ServiceBus; +using Azure.Messaging.ServiceBus.Administration; +using Azure.ResourceManager.ServiceBus; + +namespace Arcus.Messaging.Tests.Integration.Fixture +{ + public class ServiceBusConfig + { + /// + /// Initializes a new instance of the class. + /// + public ServiceBusConfig( + ServicePrincipal servicePrincipal, + string subscriptionId, + string resourceGroupName, + string @namespace, + string namespaceConnectionString = null) + { + ServicePrincipal = servicePrincipal; + NamespaceConnectionString = namespaceConnectionString; + HostName = $"{@namespace}.servicebus.windows.net"; + ResourceId = ServiceBusNamespaceResource.CreateResourceIdentifier(subscriptionId, resourceGroupName, @namespace); + } + + public string HostName { get; } + public ResourceIdentifier ResourceId { get; } + public ServicePrincipal ServicePrincipal { get; } + public string NamespaceConnectionString { get; } + + public ServiceBusClient GetClient() + { + return new ServiceBusClient(HostName, ServicePrincipal.GetCredential()); + } + + public ServiceBusAdministrationClient GetAdminClient() + { + return new ServiceBusAdministrationClient(HostName, ServicePrincipal.GetCredential()); + } + } + + public static class ServiceBusConfigExtensions + { + public static ServiceBusConfig GetServiceBus(this TestConfig config) + { + return new ServiceBusConfig( + config.GetServicePrincipal(), + config.GetSubscriptionId(), + config.GetResourceGroupName(), + config["Arcus:ServiceBus:Namespace"], + config["Arcus:ServiceBus:ConnectionString"]); + } + } +} diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/ServicePrincipal.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/ServicePrincipal.cs index a57b0971..626a6d08 100644 --- a/src/Arcus.Messaging.Tests.Integration/Fixture/ServicePrincipal.cs +++ b/src/Arcus.Messaging.Tests.Integration/Fixture/ServicePrincipal.cs @@ -1,5 +1,7 @@ using System; -using GuardNet; +using Arcus.Testing; +using Azure.Core; +using Azure.Identity; using Microsoft.IdentityModel.Clients.ActiveDirectory; namespace Arcus.Messaging.Tests.Integration.Fixture @@ -12,57 +14,63 @@ public class ServicePrincipal /// /// Initializes a new instance of the class. /// - /// The ID of the client application. - /// The secret of the client application. - /// Thrown when the or is blank. - public ServicePrincipal(string clientId, string clientSecret) + public ServicePrincipal(string tenantId, string objectId, string clientId, string clientSecret) { - Guard.NotNullOrWhitespace(clientId, nameof(clientId), "Requires a non-blank Azure service principal client ID"); - Guard.NotNullOrWhitespace(clientSecret, nameof(clientSecret), "Requires a non-blank Azure service principal client secret"); - + TenantId = tenantId; + ObjectId = Guid.Parse(objectId); ClientId = clientId; ClientSecret = clientSecret; } - - /// - /// Initializes a new instance of the class. - /// - /// The ID of the client application. - /// The secret of the client application. - /// The key to the secret of the client application. - /// Thrown when the , or is blank. - public ServicePrincipal(string clientId, string clientSecret, string clientSecretKey) - { - Guard.NotNullOrWhitespace(clientId, nameof(clientId), "Requires a non-blank Azure service principal client ID"); - Guard.NotNullOrWhitespace(clientSecret, nameof(clientSecret), "Requires a non-blank Azure service principal client secret"); - Guard.NotNullOrWhitespace(clientSecretKey, nameof(clientSecretKey), "Requires a non-blank secret Azure Key Vault key where the Azure service principal client secret is located"); - ClientId = clientId; - ClientSecret = clientSecret; - ClientSecretKey = clientSecretKey; - } + public string TenantId { get; } /// /// Gets the ID of the client application. /// public string ClientId { get; } + public Guid ObjectId { get; } + /// /// Gets the secret of the client application. /// public string ClientSecret { get; } /// - /// Gets the key to the client secret of the client application. + /// Creates an instance that combines the service principal information into an instance. + /// + public TokenCredential GetCredential() + { + return new ClientSecretCredential(TenantId, ClientId, ClientSecret); + } + } + + public static class ServicePrincipalConfigExtensions + { + /// + /// Gets the service principal that can authenticate with the Azure resources used in these integration tests. /// - public string ClientSecretKey { get; } + /// + public static ServicePrincipal GetServicePrincipal(this TestConfig config) + { + var servicePrincipal = new ServicePrincipal( + tenantId: GetTenantId(config), + objectId: config["Arcus:Infra:ServicePrincipal:ObjectId"], + clientId: config["Arcus:Infra:ServicePrincipal:ClientId"], + clientSecret: config["Arcus:Infra:ServicePrincipal:ClientSecret"]); + + return servicePrincipal; + } /// - /// Creates an instance that combines the service principal information into an instance. + /// Gets the ID of the current tenant where the Azure resources used in these integration tests are located. /// - public ClientCredential CreateCredentials() + public static string GetTenantId(this TestConfig config) { - return new ClientCredential(ClientId, ClientSecret); + return config["Arcus:Infra:TenantId"]; } + + public static string GetSubscriptionId(this TestConfig config) => config["Arcus:Infra:SubscriptionId"]; + public static string GetResourceGroupName(this TestConfig config) => config["Arcus:Infra:ResourceGroup:Name"]; } } diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/StorageAccountConfig.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/StorageAccountConfig.cs new file mode 100644 index 00000000..5a128652 --- /dev/null +++ b/src/Arcus.Messaging.Tests.Integration/Fixture/StorageAccountConfig.cs @@ -0,0 +1,30 @@ +using Arcus.Testing; + +namespace Arcus.Messaging.Tests.Integration.Fixture +{ + public class StorageAccountConfig + { + + /// + /// Initializes a new instance of the class. + /// + public StorageAccountConfig(string name, string key) + { + Name = name; + ConnectionString = $"DefaultEndpointsProtocol=https;AccountName={name};AccountKey={key};EndpointSuffix=core.windows.net"; + } + + public string Name { get; } + public string ConnectionString { get; } + } + + public static class StorageAccountConfigExtensions + { + public static StorageAccountConfig GetStorageAccount(this TestConfig config) + { + return new StorageAccountConfig( + config["Arcus:StorageAccount:Name"], + config["Arcus:StorageAccount:Key"]); + } + } +} \ No newline at end of file diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryEventHubEntity.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryEventHubEntity.cs new file mode 100644 index 00000000..ddf4c34b --- /dev/null +++ b/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryEventHubEntity.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using Azure; +using Azure.ResourceManager; +using Azure.ResourceManager.EventHubs; +using Azure.ResourceManager.EventHubs.Models; +using Microsoft.Extensions.Logging; + +namespace Arcus.Messaging.Tests.Integration.Fixture +{ + /// + /// Represents a temporary hub on an Azure EventHubs namespace that is gets deleted when the instance gets disposed. + /// + public class TemporaryEventHubEntity : IAsyncDisposable + { + private readonly EventHubsNamespaceResource _eventHubsNamespace; + private readonly ILogger _logger; + + private TemporaryEventHubEntity(string name, EventHubsNamespaceResource eventHubsNamespace, ILogger logger) + { + Name = name; + _eventHubsNamespace = eventHubsNamespace; + _logger = logger; + } + + public string Name { get; } + + public static async Task CreateAsync(string name, EventHubsConfig config, ILogger logger) + { + var client = new ArmClient(config.ServicePrincipal.GetCredential()); + + EventHubsNamespaceResource eventHubsNamespace = client.GetEventHubsNamespaceResource(config.ResourceId); + + logger.LogTrace("[Test] create EventHub '{HubName}'", name); + await eventHubsNamespace.GetEventHubs() + .CreateOrUpdateAsync(WaitUntil.Completed, name, new EventHubData + { + PartitionCount = 1, + RetentionDescription = new RetentionDescription + { + CleanupPolicy = CleanupPolicyRetentionDescription.Delete, + RetentionTimeInHours = 1, + } + }); + + return new TemporaryEventHubEntity(name, eventHubsNamespace, logger); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously. + /// + /// A task that represents the asynchronous dispose operation. + public async ValueTask DisposeAsync() + { + _logger.LogTrace("[Test] delete EventHub '{HubName}'", Name); + EventHubResource hub = await _eventHubsNamespace.GetEventHubAsync(Name); + await hub.DeleteAsync(WaitUntil.Started); + + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryKeyVaultSecret.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryKeyVaultSecret.cs new file mode 100644 index 00000000..f7b3fa9c --- /dev/null +++ b/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryKeyVaultSecret.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Azure.Security.KeyVault.Secrets; +using Microsoft.Extensions.Logging; + +namespace Arcus.Messaging.Tests.Integration.Fixture +{ + public class TemporaryKeyVaultSecret : IAsyncDisposable + { + private readonly string _secretName; + private readonly SecretClient _client; + private readonly ILogger _logger; + + private TemporaryKeyVaultSecret(string secretName, SecretClient client, ILogger logger) + { + _secretName = secretName; + _client = client; + _logger = logger; + } + + public string Name => _secretName; + + public static async Task CreateAsync(string secretName, string secretValue, KeyVaultConfig config, ILogger logger) + { + SecretClient secretClient = config.GetClient(); + + logger.LogTrace("[Test] create Key vault secret '{SecretName}'", secretName); + await secretClient.SetSecretAsync(secretName, secretValue); + + return new TemporaryKeyVaultSecret(secretName, secretClient, logger); + } + + public async Task UpdateSecretAsync(string secretValue) + { + _logger.LogTrace("[Test] update Key vault secret '{SecretName}'", _secretName); + await _client.SetSecretAsync(_secretName, secretValue); + } + + public async ValueTask DisposeAsync() + { + _logger.LogTrace("[Test] delete Key vault secret '{SecretName}'", _secretName); + await _client.StartDeleteSecretAsync(_secretName); + } + } +} diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryManagedIdentityConnection.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryManagedIdentityConnection.cs index ceac7493..35a43a99 100644 --- a/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryManagedIdentityConnection.cs +++ b/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryManagedIdentityConnection.cs @@ -1,4 +1,5 @@ using System; +using Arcus.Testing; using GuardNet; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryServiceBusEntity.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryServiceBusEntity.cs new file mode 100644 index 00000000..d26942e4 --- /dev/null +++ b/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryServiceBusEntity.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Azure; +using Azure.Identity; +using Azure.Messaging.ServiceBus; +using Azure.ResourceManager; +using Azure.ResourceManager.ServiceBus; +using Azure.ResourceManager.ServiceBus.Models; +using Microsoft.Azure.EventGrid.Models; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Sdk; + +namespace Arcus.Messaging.Tests.Integration.Fixture +{ + public class TemporaryServiceBusEntity : IAsyncDisposable + { + private readonly ServiceBusEntityType _entityType; + private readonly ServiceBusNamespaceResource _ns; + private readonly ILogger _logger; + + private TemporaryServiceBusEntity(ServiceBusEntityType entityType, string entityName, ServiceBusNamespaceResource @namespace, ILogger logger) + { + _entityType = entityType; + _ns = @namespace; + _logger = logger; + + EntityName = entityName; + } + + public string EntityName { get; } + + public static async Task CreateAsync(ServiceBusEntityType entityType, string entityName, ServiceBusConfig serviceBus, ILogger logger) + { + var armClient = new ArmClient(serviceBus.ServicePrincipal.GetCredential()); + ServiceBusNamespaceResource serviceBusNamespace = armClient.GetServiceBusNamespaceResource(serviceBus.ResourceId); + + switch (entityType) + { + case ServiceBusEntityType.Queue: + logger.LogTrace("[Test] create Service bus queue '{EntityName}'", entityName); + await serviceBusNamespace.GetServiceBusQueues() + .CreateOrUpdateAsync(WaitUntil.Completed, entityName, new ServiceBusQueueData()); + break; + + case ServiceBusEntityType.Topic: + logger.LogTrace("[Test] create Service bus topic '{EntityName}'", entityName); + await serviceBusNamespace.GetServiceBusTopics() + .CreateOrUpdateAsync(WaitUntil.Completed, entityName, new ServiceBusTopicData()); + break; + + case ServiceBusEntityType.Unknown: + default: + throw new ArgumentOutOfRangeException(nameof(entityType), entityType, "Unknown Service bus entity type"); + } + + return new TemporaryServiceBusEntity(entityType, entityName, serviceBusNamespace, logger); + } + + public async ValueTask DisposeAsync() + { + switch (_entityType) + { + case ServiceBusEntityType.Queue: + _logger.LogTrace("[Test] delete Service bus queue '{EntityName}''", EntityName); + ServiceBusQueueResource queue = await _ns.GetServiceBusQueueAsync(EntityName); + await queue.DeleteAsync(WaitUntil.Started); + break; + + case ServiceBusEntityType.Topic: + _logger.LogTrace("[Test] delete Service bus topic '{EntityName}'", EntityName); + ServiceBusTopicResource topic = await _ns.GetServiceBusTopicAsync(EntityName); + await topic.DeleteAsync(WaitUntil.Started); + break; + + case ServiceBusEntityType.Unknown: + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryServiceBusNamespace.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryServiceBusNamespace.cs new file mode 100644 index 00000000..7dce5e4f --- /dev/null +++ b/src/Arcus.Messaging.Tests.Integration/Fixture/TemporaryServiceBusNamespace.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Azure; +using Azure.Core; +using Azure.Messaging.ServiceBus.Administration; +using Azure.ResourceManager; +using Azure.ResourceManager.Authorization; +using Azure.ResourceManager.Authorization.Models; +using Azure.ResourceManager.Resources; +using Azure.ResourceManager.ServiceBus; +using Azure.ResourceManager.ServiceBus.Models; +using Microsoft.Azure.Management.ServiceBus.Models; +using Microsoft.Extensions.Logging; + +namespace Arcus.Messaging.Tests.Integration.Fixture +{ + internal class TemporaryServiceBusNamespace : IAsyncDisposable + { + private const string DefaultRuleName = "RootManageSharedAccessKey"; + + private readonly ServiceBusNamespaceResource _namespace; + private readonly ILogger _logger; + + private TemporaryServiceBusNamespace( + ServicePrincipal servicePrincipal, + ServiceBusNamespaceResource serviceBusNamespace, + ILogger logger) + { + _namespace = serviceBusNamespace; + _logger = logger; + + Config = new ServiceBusConfig(servicePrincipal, _namespace.Id.SubscriptionId, _namespace.Id.ResourceGroupName, _namespace.Id.Name); + } + + public ServiceBusConfig Config { get; } + + public static async Task CreateBasicAsync( + string subscriptionId, + string resourceGroupName, + ServicePrincipal servicePrincipal, + ILogger logger) + { + var client = new ArmClient(servicePrincipal.GetCredential()); + + string @namespace = $"arcus-message-servicebus-{Guid.NewGuid().ToString()[..10]}"; + logger.LogTrace("[Test] create Service bus namespace '{Namespace}'", @namespace); + + ResourceGroupResource resourceGroup = + await client.GetResourceGroupResource(ResourceGroupResource.CreateResourceIdentifier(subscriptionId, resourceGroupName)).GetAsync(); + + var options = new ServiceBusNamespaceData(resourceGroup.Data.Location) + { + Sku = new ServiceBusSku(ServiceBusSkuName.Basic), + DisableLocalAuth = false + }; + ArmOperation serviceBusNamespace = + await resourceGroup.GetServiceBusNamespaces() + .CreateOrUpdateAsync(WaitUntil.Completed, @namespace, options); + + ResourceIdentifier serviceBusOwner = + RoleAssignmentResource.CreateResourceIdentifier( + scope: serviceBusNamespace.Value.Id.ToString(), + "090c5cfd-751d-490a-894a-3ce6f1109419"); + + var content = new RoleAssignmentCreateOrUpdateContent(serviceBusOwner, servicePrincipal.ObjectId); + await client.GetRoleAssignments(serviceBusNamespace.Value.Id) + .CreateOrUpdateAsync(WaitUntil.Completed, roleAssignmentName: Guid.NewGuid().ToString(), content); + + return new TemporaryServiceBusNamespace(servicePrincipal, serviceBusNamespace.Value, logger); + } + + public async Task GetAccessKeysAsync() + { + ServiceBusNamespaceAuthorizationRuleResource rule = GetDefaultAuthRule(); + + ServiceBusAccessKeys keys = await rule.GetKeysAsync(); + return keys; + } + + public async Task RotateAccessKeysAsync(ServiceBusAccessKeyType keyType) + { + _logger.LogTrace("[Test] rotate {KeyType} Service bus namespace '{Namespace}' connection string", keyType, _namespace.Id.Name); + ServiceBusNamespaceAuthorizationRuleResource rule = GetDefaultAuthRule(); + + ServiceBusAccessKeys newKeys = + await rule.RegenerateKeysAsync(new ServiceBusRegenerateAccessKeyContent(keyType)); + + return newKeys; + } + + private ServiceBusNamespaceAuthorizationRuleResource GetDefaultAuthRule() + { + return _namespace.GetServiceBusNamespaceAuthorizationRule(DefaultRuleName); + } + + public async ValueTask DisposeAsync() + { + _logger.LogTrace("[Test] delete Service bus namespace '{Namespace}'", _namespace.Id.Name); + await _namespace.DeleteAsync(WaitUntil.Started); + } + } +} diff --git a/src/Arcus.Messaging.Tests.Integration/Fixture/TestConfig.cs b/src/Arcus.Messaging.Tests.Integration/Fixture/TestConfig.cs deleted file mode 100644 index 40cccd5a..00000000 --- a/src/Arcus.Messaging.Tests.Integration/Fixture/TestConfig.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System; -using System.Collections.Generic; -using GuardNet; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; - -namespace Arcus.Messaging.Tests.Integration.Fixture -{ - /// - /// Represents a test configuration model with application key/value properties specific to this integration test suite. - /// - public class TestConfig : IConfigurationRoot - { - private readonly IConfigurationRoot _config; - - /// - /// Initializes a new instance of the class. - /// - private TestConfig(IConfigurationRoot config) - { - Guard.NotNull(config, nameof(config)); - _config = config; - } - - /// - /// Creates the test configuration model for this integration test suite. - /// - /// - public static TestConfig Create() - { - var config = - new ConfigurationBuilder() - .AddJsonFile(path: "appsettings.json") - .AddJsonFile(path: "appsettings.local.json", optional: true) - .AddEnvironmentVariables() - .Build(); - - return new TestConfig(config); - } - - public string GetServiceBusTopicConnectionString() - { - return GetServiceBusConnectionString(ServiceBusEntityType.Topic); - } - - public string GetServiceBusQueueConnectionString() - { - return GetServiceBusConnectionString(ServiceBusEntityType.Queue); - } - - /// - /// Gets the Service Bus connection string based on the given . - /// - /// The type of the Service Bus entity. - public string GetServiceBusConnectionString(ServiceBusEntityType entity) - { - switch (entity) - { - case ServiceBusEntityType.Queue: return _config["Arcus:ServiceBus:SelfContained:ConnectionStringWithQueue"]; - case ServiceBusEntityType.Topic: return _config["Arcus:ServiceBus:SelfContained:ConnectionStringWithTopic"]; - default: - throw new ArgumentOutOfRangeException(nameof(entity), entity, "Unknown Service Bus entity"); - } - } - - /// - /// Gets the service principal that can authenticate with the Azure resources used in these integration tests. - /// - /// - public ServicePrincipal GetServicePrincipal() - { - var servicePrincipal = new ServicePrincipal( - clientId: _config.GetValue("Arcus:Infra:ServicePrincipal:ClientId"), - clientSecret: _config.GetValue("Arcus:Infra:ServicePrincipal:ClientSecret")); - - return servicePrincipal; - } - - /// - /// Gets the ID of the current tenant where the Azure resources used in these integration tests are located. - /// - public string GetTenantId() - { - const string tenantIdKey = "Arcus:Infra:TenantId"; - var tenantId = _config.GetValue(tenantIdKey); - Guard.For(() => string.IsNullOrWhiteSpace(tenantId), $"Requires a non-blank 'TenantId' at '{tenantIdKey}'"); - - return tenantId; - } - - /// - /// Gets all the configuration to run a complete key rotation integration test. - /// - public KeyRotationConfig GetKeyRotationConfig() - { - var azureEnv = new ServiceBusNamespace( - tenantId: _config.GetValue("Arcus:KeyRotation:ServiceBus:TenantId"), - azureSubscriptionId: _config.GetValue("Arcus:KeyRotation:ServiceBus:SubscriptionId"), - resourceGroup: _config.GetValue("Arcus:KeyRotation:ServiceBus:ResourceGroupName"), - @namespace: _config.GetValue("Arcus:KeyRotation:ServiceBus:Namespace"), - queueName: _config.GetValue("Arcus:KeyRotation:ServiceBus:QueueName"), - topicName: _config.GetValue("Arcus:KeyRotation:ServiceBus:TopicName"), - authorizationRuleName: _config.GetValue("Arcus:KeyRotation:ServiceBus:AuthorizationRuleName")); - - var servicePrincipal = new ServicePrincipal( - clientId: _config.GetValue("Arcus:KeyRotation:ServicePrincipal:ClientId"), - clientSecret: _config.GetValue("Arcus:KeyRotation:ServicePrincipal:ClientSecret"), - clientSecretKey: _config.GetValue("Arcus:KeyRotation:ServicePrincipal:ClientSecretKey")); - - var secret = new KeyVaultConfig( - vaultUri: _config.GetValue("Arcus:KeyRotation:KeyVault:VaultUri"), - secretName: _config.GetValue("Arcus:KeyRotation:KeyVault:ConnectionStringSecretName"), - secretNewVersionCreated: new KeyVaultEventEndpoint( - _config.GetValue("Arcus:KeyRotation:KeyVault:SecretNewVersionCreated:ServiceBusConnectionStringWithTopicEndpoint"))); - - return new KeyRotationConfig(secret, servicePrincipal, azureEnv); - } - - /// - /// Gets all the configuration to run the Azure EventHubs integration tests. - /// - public EventHubsConfig GetEventHubsConfig() - { - return new EventHubsConfig( - _config.GetValue("Arcus:EventHubs:SelfContained:EventHubsName"), - _config.GetValue("Arcus:EventHubs:Docker:EventHubsName"), - _config.GetValue("Arcus:EventHubs:Docker:AzureFunctions:Isolated:EventHubsName"), - _config.GetValue("Arcus:EventHubs:Docker:AzureFunctions:InProcess:EventHubsName"), - _config.GetValue("Arcus:EventHubs:ConnectionString"), - _config.GetValue("Arcus:EventHubs:BlobStorage:StorageAccountConnectionString")); - } - - /// - /// Gets a configuration sub-section with the specified key. - /// - /// The key of the configuration section. - /// The . - /// - /// This method will never return null. If no matching sub-section is found with the specified key, - /// an empty will be returned. - /// - public IConfigurationSection GetSection(string key) - { - return _config.GetSection(key); - } - - /// - /// Gets the immediate descendant configuration sub-sections. - /// - /// The configuration sub-sections. - public IEnumerable GetChildren() - { - return _config.GetChildren(); - } - - /// - /// Returns a that can be used to observe when this configuration is reloaded. - /// - /// A . - public IChangeToken GetReloadToken() - { - return _config.GetReloadToken(); - } - - /// Gets or sets a configuration value. - /// The configuration key. - /// The configuration value. - public string this[string key] - { - get => _config[key]; - set => _config[key] = value; - } - - /// - /// Force the configuration values to be reloaded from the underlying s. - /// - public void Reload() - { - _config.Reload(); - } - - /// - /// The s for this configuration. - /// - public IEnumerable Providers => _config.Providers; - } -} diff --git a/src/Arcus.Messaging.Tests.Integration/Health/TcpHealthListenerDockerTests.cs b/src/Arcus.Messaging.Tests.Integration/Health/TcpHealthListenerDockerTests.cs deleted file mode 100644 index 6ae6f716..00000000 --- a/src/Arcus.Messaging.Tests.Integration/Health/TcpHealthListenerDockerTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Xunit; -using Xunit.Abstractions; - -namespace Arcus.Messaging.Tests.Integration.Health -{ - [Trait("Category", "Docker")] - public class TcpHealthListenerDockerTests : IntegrationTest - { - private readonly int _healthTcpPort; - - /// - /// Initializes a new instance of the class. - /// - public TcpHealthListenerDockerTests(ITestOutputHelper testOutput) : base(testOutput) - { - _healthTcpPort = Configuration.GetValue("Arcus:Health:Port"); - } - - [Fact] - public async Task TcpHealthListener_ProbeForHealthReport_ResponseHealthy() - { - // Arrange - var service = new TcpHealthService(_healthTcpPort, Logger); - - // Act - HealthReport report = await service.GetHealthReportAsync(); - - // Assert - Assert.NotNull(report); - Assert.Equal(HealthStatus.Healthy, report.Status); - (string entryName, HealthReportEntry entry) = Assert.Single(report.Entries); - Assert.Equal("sample", entryName); - Assert.Equal(HealthStatus.Healthy, entry.Status); - } - } -} diff --git a/src/Arcus.Messaging.Tests.Integration/Health/TcpHealthListenerTests.cs b/src/Arcus.Messaging.Tests.Integration/Health/TcpHealthListenerTests.cs index 0aa647ce..0dafaca8 100644 --- a/src/Arcus.Messaging.Tests.Integration/Health/TcpHealthListenerTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/Health/TcpHealthListenerTests.cs @@ -4,7 +4,7 @@ using System.Net.Sockets; using System.Threading.Tasks; using Arcus.Messaging.Tests.Integration.Fixture; -using Arcus.Testing.Logging; +using Arcus.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; diff --git a/src/Arcus.Messaging.Tests.Integration/IntegrationTest.cs b/src/Arcus.Messaging.Tests.Integration/IntegrationTest.cs index c89beda3..250aa93a 100644 --- a/src/Arcus.Messaging.Tests.Integration/IntegrationTest.cs +++ b/src/Arcus.Messaging.Tests.Integration/IntegrationTest.cs @@ -1,4 +1,4 @@ -using Arcus.Testing.Logging; +using Arcus.Testing; using Microsoft.Extensions.Configuration; using Xunit.Abstractions; diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubs/TemporaryBlobStorageContainer.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubs/TemporaryBlobStorageContainer.cs deleted file mode 100644 index 12155e4c..00000000 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubs/TemporaryBlobStorageContainer.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Threading.Tasks; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using GuardNet; -using Microsoft.Extensions.Logging; - -namespace Arcus.Messaging.Tests.Integration.MessagePump.EventHubs -{ - /// - /// Represents a temporary disposable fixture that sets up an Azure Blob storage container. - /// - public class TemporaryBlobStorageContainer : IAsyncDisposable - { - private readonly BlobServiceClient _client; - private readonly ILogger _logger; - - private TemporaryBlobStorageContainer(BlobServiceClient client, string containerName, ILogger logger) - { - Guard.NotNull(client, nameof(client), "Requires an Azure Blob storage client to create an temporary container"); - Guard.NotNullOrWhitespace(containerName, nameof(containerName), "Requires a non-blank Azure Blob storage container name"); - Guard.NotNull(logger, nameof(logger), "Requires a logger instance to write diagnostic trace messages during the lifetime of the temporary Azure Blob storage container"); - - _client = client; - _logger = logger; - - ContainerName = containerName; - ContainerUri = new Uri(client.Uri, new Uri(containerName, UriKind.Relative)).OriginalString; - } - - /// - /// Gets the container name used during this temporary Azure Blob storage container. - /// - public string ContainerName { get; } - - /// - /// Gets the container primary endpoint used during this temporary Azure Blob storage container. - /// - public string ContainerUri { get; } - - /// - /// Creates an instance that creates an Azure Blob container on the Azure storage account - /// that is accessed via the given . - /// - /// The Azure storage account connection string on which a temporary Azure Blob storage container should be created. - /// The logger instance to write diagnostic trace messages during the lifetime of the temporary Azure blob storage container. - /// Thrown when the is blank. - /// Thrown when the is null. - public static async Task CreateAsync(string storageAccountConnectionString, ILogger logger) - { - Guard.NotNullOrWhitespace(storageAccountConnectionString, nameof(storageAccountConnectionString), "Requires a non-blank connection string to access the Azure storage account"); - Guard.NotNull(logger, nameof(logger), "Requires a logger instance to write diagnostic trace messages during the lifetime of the temporary Azure Blob storage container"); - - string containerName = $"eventhubs-{Guid.NewGuid()}"; - return await CreateAsync(storageAccountConnectionString, containerName, logger); - } - - /// - /// Creates an instance that creates an Azure Blob container on the Azure storage account - /// that is accessed via the given . - /// - /// The Azure storage account connection string on which a temporary Azure Blob storage container should be created. - /// The logger instance to write diagnostic trace messages during the lifetime of the temporary Azure blob storage container. - /// Thrown when the is blank. - /// Thrown when the is null. - public static async Task CreateAsync(string storageAccountConnectionString, string containerName, ILogger logger) - { - Guard.NotNullOrWhitespace(storageAccountConnectionString, nameof(storageAccountConnectionString), "Requires a non-blank connection string to access the Azure storage account"); - Guard.NotNull(logger, nameof(logger), "Requires a logger instance to write diagnostic trace messages during the lifetime of the temporary Azure Blob storage container"); - - var blobClient = new BlobServiceClient(storageAccountConnectionString); - - logger.LogTrace("Add Azure Blob storage container '{ContainerName}'", containerName); - await blobClient.CreateBlobContainerAsync(containerName, PublicAccessType.Blob); - - return new TemporaryBlobStorageContainer(blobClient, containerName, logger); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously. - /// - /// A task that represents the asynchronous dispose operation. - public async ValueTask DisposeAsync() - { - _logger.LogTrace("Remove Azure Blob storage container '{ContainerName}'", ContainerName); - await _client.DeleteBlobContainerAsync(ContainerName); - } - } -} diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubs/TestEventHubsMessageProducer.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubs/TestEventHubsMessageProducer.cs index 9a2c84b7..6074f73a 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubs/TestEventHubsMessageProducer.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubs/TestEventHubsMessageProducer.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.Serialization; using System.Threading.Tasks; +using Arcus.Messaging.Tests.Integration.Fixture; using Azure.Messaging.EventHubs; using Azure.Messaging.EventHubs.Producer; using GuardNet; @@ -12,22 +13,16 @@ namespace Arcus.Messaging.Tests.Integration.MessagePump.EventHubs /// public class TestEventHubsMessageProducer { - private readonly string _eventHubsConnectionString; - private readonly string _eventHubName; + private readonly string _name; + private readonly EventHubsConfig _config; /// /// Initializes a new instance of the class. /// - /// The connection string to access the Azure EventHubs. - /// The name of the Azure EventHubs where an event message should be placed. - /// Thrown when the or the is blank. - public TestEventHubsMessageProducer(string eventHubsConnectionString, string eventHubName) + public TestEventHubsMessageProducer(string name, EventHubsConfig config) { - Guard.NotNullOrWhitespace(eventHubsConnectionString, nameof(eventHubsConnectionString), "Requires a non-blank connection string to access the Azure EventHubs"); - Guard.NotNullOrWhitespace(eventHubName, nameof(eventHubName), "Requires a non-blank name of the Azure EventHubs"); - - _eventHubsConnectionString = eventHubsConnectionString; - _eventHubName = eventHubName; + _name = name; + _config = config; } /// @@ -43,7 +38,7 @@ public async Task ProduceAsync(EventData eventData) { Guard.NotNull(eventData, nameof(eventData), "Requires an event data instance to place on the configured Azure EventHubs"); - await using (var client = new EventHubProducerClient(_eventHubsConnectionString, _eventHubName)) + await using (EventHubProducerClient client = _config.GetProducerClient(_name)) { await client.SendAsync(new[] { eventData }); } diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePump.ConnectivityTests.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePump.ConnectivityTests.cs index 424a2c05..5621c666 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePump.ConnectivityTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePump.ConnectivityTests.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Arcus.Messaging.Pumps.Abstractions; using Arcus.Messaging.Pumps.EventHubs; -using Arcus.Messaging.Tests.Core.Correlation; using Arcus.Messaging.Tests.Core.Events.v1; using Arcus.Messaging.Tests.Core.Messages.v1; using Arcus.Messaging.Tests.Integration.Fixture; @@ -22,16 +21,20 @@ namespace Arcus.Messaging.Tests.Integration.MessagePump public partial class EventHubsMessagePumpTests { [Fact] - public async Task EventHubsMessagePumpUsingManagedIdentity_PublishesMessage_MessageSuccessfullyProcessed() + public async Task EventHubsMessagePumpUsingSecrets_PublishesMessage_MessageSuccessfullyProcessed() { - using var auth = TemporaryManagedIdentityConnection.Create(_config, _logger); + string eventHubsConnectionStringSecretName = "Arcus_EventHubs_ConnectionString", + storageAccountConnectionStringSecretName = "Arcus_StorageAccount_ConnectionString"; + await TestEventHubsMessageHandlingAsync(options => { - options.AddEventHubsMessagePumpUsingManagedIdentity( - eventHubsName: EventHubsName, - fullyQualifiedNamespace: FullyQualifiedEventHubsNamespace, - blobContainerUri: _blobStorageContainer.ContainerUri, - clientId: auth.ClientId) + options.AddSecretStore(stores => stores.AddInMemory(new Dictionary + { + [eventHubsConnectionStringSecretName] = _eventHubsConfig.EventHubsConnectionString, + [storageAccountConnectionStringSecretName] = _eventHubsConfig.Storage.ConnectionString + })); + + options.AddEventHubsMessagePump(EventHubsName, eventHubsConnectionStringSecretName, ContainerName, storageAccountConnectionStringSecretName) .WithEventHubsMessageHandler(); }); } diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePump.TelemetryTests.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePump.TelemetryTests.cs index 3f3cfc80..11b6342a 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePump.TelemetryTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePump.TelemetryTests.cs @@ -9,15 +9,16 @@ using Arcus.Messaging.Tests.Integration.Fixture; using Arcus.Messaging.Tests.Integration.Fixture.Logging; using Arcus.Messaging.Tests.Integration.MessagePump.EventHubs; -using Arcus.Messaging.Tests.Integration.MessagePump.Fixture; using Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus; using Arcus.Messaging.Tests.Workers.EventHubs.Core.MessageHandlers; +using Arcus.Testing; using Azure.Messaging.EventHubs; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.Extensions.DependencyInjection; using Serilog; using Xunit; +using static Arcus.Messaging.Tests.Integration.MessagePump.Fixture.AssertX; namespace Arcus.Messaging.Tests.Integration.MessagePump { @@ -118,15 +119,28 @@ public async Task EventHubsMessagePump_WithW3CCorrelationFormat_AutomaticallyTra await producer.ProduceAsync(eventData); // Assert - AssertX.RetryAssertUntil(() => - { - RequestTelemetry requestViaArcusEventHubs = AssertX.GetRequestFrom(spySink.Telemetries, r => r.Name == operationName && r.Context.Operation.Id == traceParent.TransactionId); - DependencyTelemetry dependencyViaArcusKeyVault = AssertX.GetDependencyFrom(spySink.Telemetries, d => d.Type == "Azure key vault" && d.Context.Operation.Id == traceParent.TransactionId); - DependencyTelemetry dependencyViaMicrosoftSql = AssertX.GetDependencyFrom(spyChannel.Telemetries, d => d.Type == "SQL" && d.Context.Operation.Id == traceParent.TransactionId); - - Assert.Equal(requestViaArcusEventHubs.Id, dependencyViaArcusKeyVault.Context.Operation.ParentId); - Assert.Equal(requestViaArcusEventHubs.Id, dependencyViaMicrosoftSql.Context.Operation.ParentId); - }, timeout: TimeSpan.FromMinutes(2), _logger); + TimeSpan timeout = TimeSpan.FromMinutes(2); + + DependencyTelemetry dependencyViaArcusKeyVault = + await Poll.Target(() => GetDependencyFrom(spySink.Telemetries, d => d.Type == "Azure key vault")) + .Until(d => d.Context.Operation.Id == traceParent.TransactionId) + .Timeout(timeout) + .FailWith("missing Key vault dependency telemetry tracking via Arcus with W3C format in spied sink"); + + DependencyTelemetry dependencyViaMicrosoftSql = + await Poll.Target(() => GetDependencyFrom(spyChannel.Telemetries, d => d.Type == "SQL")) + .Until(d => d.Context.Operation.Id == traceParent.TransactionId) + .Timeout(timeout) + .FailWith("missing SQL dependency telemetry tracking via Microsoft with W3C format in spied channel"); + + RequestTelemetry requestViaArcusEventHubs = + await Poll.Target(() => GetRequestFrom(spySink.Telemetries, r => r.Name == operationName)) + .Until(r => r.Context.Operation.Id == traceParent.TransactionId) + .Timeout(timeout) + .FailWith("missing request telemetry tracking with W3C format in spied sink"); + + Assert.Equal(requestViaArcusEventHubs.Id, dependencyViaArcusKeyVault.Context.Operation.ParentId); + Assert.Equal(requestViaArcusEventHubs.Id, dependencyViaMicrosoftSql.Context.Operation.ParentId); } [Fact] @@ -154,19 +168,19 @@ public async Task EventHubsMessagePump_WithW3CCorrelationFormatForNewParent_Auto await producer.ProduceAsync(eventData); // Assert - AssertX.RetryAssertUntil(() => - { - IEnumerable dependenciesViaArcusKeyVault = spySink.Telemetries.OfType().Where(d => d.Type == "Azure key vault"); - IEnumerable dependenciesViaMicrosoftSql = spyChannel.Telemetries.OfType().Where(d => d.Type == "SQL"); - - bool correlationSuccess = spySink.Telemetries.Any(t => - { - return t is RequestTelemetry r && r.Name == operationName - && dependenciesViaArcusKeyVault.SingleOrDefault(d => d.Context.Operation.ParentId == r.Id) != null - && dependenciesViaMicrosoftSql.SingleOrDefault(d => d.Context.Operation.ParentId == r.Id) != null; - }); - Assert.True(correlationSuccess); - }, timeout: TimeSpan.FromMinutes(1), _logger); + RequestTelemetry requestViaArcusEventHubs = + await Poll.Target(() => GetRequestFrom(spySink.Telemetries, r => r.Name == operationName)) + .Timeout(TimeSpan.FromMinutes(2)) + .FailWith("missing request telemetry with operation name in spied sink"); + + await Poll.Target(() => GetDependencyFrom(spySink.Telemetries, d => d.Type == "Azure key vault")) + .Until(d => d.Context.Operation.ParentId == requestViaArcusEventHubs.Id) + .FailWith("missing Key vault dependency telemetry tracking via Arcus in spied sink"); + + + await Poll.Target(() => GetDependencyFrom(spyChannel.Telemetries, d => d.Type == "SQL")) + .Until(d => d.Context.Operation.ParentId == requestViaArcusEventHubs.Id) + .FailWith("missing SQL dependency telemetry racking via Microsoft on spied channel"); } } } diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePumpDockerTests.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePumpDockerTests.cs deleted file mode 100644 index 7d5c43b3..00000000 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePumpDockerTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using Arcus.Messaging.Abstractions; -using Arcus.Messaging.Tests.Core.Correlation; -using Arcus.Messaging.Tests.Core.Events.v1; -using Arcus.Messaging.Tests.Core.Generators; -using Arcus.Messaging.Tests.Core.Messages.v1; -using Arcus.Messaging.Tests.Integration.Fixture; -using Arcus.Messaging.Tests.Integration.MessagePump.EventHubs; -using Arcus.Messaging.Tests.Integration.MessagePump.Fixture; -using Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus; -using Arcus.Testing.Logging; -using Azure.Messaging.EventHubs; -using Bogus; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Xunit; -using Xunit.Abstractions; - -namespace Arcus.Messaging.Tests.Integration.MessagePump -{ - [Collection("Docker")] - [Trait("Category", "Docker")] - public class EventHubsMessagePumpDockerTests : DockerServiceBusIntegrationTest - { - private readonly TestConfig _config; - - public EventHubsMessagePumpDockerTests(ITestOutputHelper outputWriter) : base(outputWriter) - { - _config = TestConfig.Create(); - } - - [Fact] - public async Task EventHubsMessagePump_PublishEventDataMessage_MessageSuccessfullyProcessed() - { - // Arrange - var traceParent = TraceParent.Generate(); - Order order = OrderGenerator.Generate(); - EventData expected = new EventData(JsonConvert.SerializeObject(order)).WithDiagnosticId(traceParent); - - EventHubsConfig eventHubs = _config.GetEventHubsConfig(); - string eventHubsName = eventHubs.GetEventHubsName(IntegrationTestType.DockerWorker); - var producer = new TestEventHubsMessageProducer(eventHubs.EventHubsConnectionString, eventHubsName); - - await using (var consumer = await TestServiceBusMessageEventConsumer.StartNewAsync(_config, Logger)) - { - // Act - await producer.ProduceAsync(expected); - - // Assert - OrderCreatedEventData orderCreatedEventData = consumer.ConsumeOrderEventForW3C(traceParent.TransactionId); - Assert.NotNull(orderCreatedEventData); - Assert.NotNull(orderCreatedEventData.CorrelationInfo); - Assert.Equal(order.Id, orderCreatedEventData.Id); - Assert.Equal(order.Amount, orderCreatedEventData.Amount); - Assert.Equal(order.ArticleNumber, orderCreatedEventData.ArticleNumber); - Assert.Equal(traceParent.TransactionId, orderCreatedEventData.CorrelationInfo.TransactionId); - Assert.Equal(traceParent.OperationParentId, orderCreatedEventData.CorrelationInfo.OperationParentId); - Assert.NotNull(orderCreatedEventData.CorrelationInfo.OperationId); - } - } - } -} diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePumpTests.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePumpTests.cs index 65587270..ebf8984d 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePumpTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/EventHubsMessagePumpTests.cs @@ -1,8 +1,8 @@ using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using Arcus.EventGrid.Testing.Logging; using Arcus.Messaging.Abstractions; using Arcus.Messaging.Abstractions.EventHubs.MessageHandling; using Arcus.Messaging.Abstractions.MessageHandling; @@ -15,11 +15,11 @@ using Arcus.Messaging.Tests.Integration.MessagePump.EventHubs; using Arcus.Messaging.Tests.Integration.MessagePump.Fixture; using Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus; -using Arcus.Testing.Logging; +using Arcus.Testing; using Azure.Messaging.EventHubs; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; @@ -28,51 +28,52 @@ namespace Arcus.Messaging.Tests.Integration.MessagePump { [Collection("Integration")] [Trait("Category", "Integration")] - public partial class EventHubsMessagePumpTests : IAsyncLifetime + public partial class EventHubsMessagePumpTests : IClassFixture, IAsyncLifetime { private readonly TestConfig _config; private readonly EventHubsConfig _eventHubsConfig; private readonly ILogger _logger; private readonly ITestOutputHelper _outputWriter; - private TemporaryBlobStorageContainer _blobStorageContainer; + private TemporaryBlobContainer _blobStorageContainer; + private TemporaryManagedIdentityConnection _connection; /// /// Initializes a new instance of the class. /// - public EventHubsMessagePumpTests(ITestOutputHelper outputWriter) + public EventHubsMessagePumpTests(EventHubsEntityFixture fixture, ITestOutputHelper outputWriter) { _outputWriter = outputWriter; _logger = new XunitTestLogger(outputWriter); _config = TestConfig.Create(); - _eventHubsConfig = _config.GetEventHubsConfig(); + _eventHubsConfig = _config.GetEventHubs(); + + EventHubsName = fixture.HubName; } - private string EventHubsName => _eventHubsConfig.GetEventHubsName(IntegrationTestType.SelfContained); - private string FullyQualifiedEventHubsNamespace => EventHubsConnectionStringProperties.Parse(_eventHubsConfig.EventHubsConnectionString).FullyQualifiedNamespace; - private string ContainerName => _blobStorageContainer.ContainerName; + private string EventHubsName { get; } + private string FullyQualifiedEventHubsNamespace => _eventHubsConfig.HostName; + private string ContainerName => _blobStorageContainer.Name; /// /// Called immediately after the class has been created, before it is used. /// public async Task InitializeAsync() { - _blobStorageContainer = await TemporaryBlobStorageContainer.CreateAsync(_eventHubsConfig.StorageConnectionString, _logger); + _connection = TemporaryManagedIdentityConnection.Create(_config, _logger); + _blobStorageContainer = await TemporaryBlobContainer.CreateIfNotExistsAsync(_eventHubsConfig.Storage.Name, $"test-{Guid.NewGuid()}", _logger); } private EventHubsMessageHandlerCollection AddEventHubsMessagePump(WorkerOptions options, Action configureOptions = null) { - string eventHubsConnectionStringSecretName = "Arcus_EventHubs_ConnectionString", - storageAccountConnectionStringSecretName = "Arcus_StorageAccount_ConnectionString"; - return options.AddXunitTestLogging(_outputWriter) - .AddSecretStore(stores => stores.AddInMemory(new Dictionary - { - [eventHubsConnectionStringSecretName] = _eventHubsConfig.EventHubsConnectionString, - [storageAccountConnectionStringSecretName] = _eventHubsConfig.StorageConnectionString - })) - .AddEventHubsMessagePump(EventHubsName, eventHubsConnectionStringSecretName, ContainerName, storageAccountConnectionStringSecretName, configureOptions); + .AddEventHubsMessagePumpUsingManagedIdentity( + eventHubsName: EventHubsName, + fullyQualifiedNamespace: FullyQualifiedEventHubsNamespace, + blobContainerUri: _blobStorageContainer.Client.Uri.ToString(), + clientId: _connection.ClientId, + configureOptions); } private async Task TestEventHubsMessageHandlingAsync( @@ -215,9 +216,7 @@ private static void AssertReceivedSensorEventDataForW3C( private TestEventHubsMessageProducer CreateEventHubsMessageProducer() { - return new TestEventHubsMessageProducer( - _eventHubsConfig.EventHubsConnectionString, - EventHubsName); + return new TestEventHubsMessageProducer(EventHubsName, _eventHubsConfig); } /// @@ -230,6 +229,26 @@ public async Task DisposeAsync() { await _blobStorageContainer.DisposeAsync(); } + + _connection?.Dispose(); + } + } + + public class EventHubsEntityFixture : IAsyncLifetime + { + private TemporaryEventHubEntity _hub; + + public string HubName { get; } = $"hub-{Guid.NewGuid()}"; + + public async Task InitializeAsync() + { + var config = TestConfig.Create(); + _hub = await TemporaryEventHubEntity.CreateAsync(HubName, config.GetEventHubs(), NullLogger.Instance); + } + + public async Task DisposeAsync() + { + await _hub.DisposeAsync(); } } } diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/DiskMessageEventConsumer.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/DiskMessageEventConsumer.cs index 16d60dea..4f7e633b 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/DiskMessageEventConsumer.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/DiskMessageEventConsumer.cs @@ -11,10 +11,10 @@ namespace Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus { public static class DiskMessageEventConsumer { - public static async Task ConsumeOrderCreatedAsync(string messageId) + public static async Task ConsumeOrderCreatedAsync(string messageId, TimeSpan? timeout = null) { return await ConsumeEventAsync(messageId, - $"order created event does not seem to be delivered in time as the file '{messageId}.json' cannot be found on disk"); + $"order created event does not seem to be delivered in time as the file '{messageId}.json' cannot be found on disk", timeout); } public static async Task ConsumeSensorReadAsync(string messageId) @@ -23,7 +23,7 @@ public static async Task ConsumeSensorReadAsync(string mess $"sensor read event does not seem to be delivered in time as the file '{messageId}.json' cannot be found on disk"); } - private static async Task ConsumeEventAsync(string messageId, string errorMessage) + private static async Task ConsumeEventAsync(string messageId, string errorMessage, TimeSpan? timeout = null) { var dir = new DirectoryInfo(Directory.GetCurrentDirectory()); @@ -31,7 +31,7 @@ private static async Task ConsumeEventAsync(string messageId, await Poll.Target(() => Assert.Single(dir.GetFiles($"{messageId}.json", SearchOption.AllDirectories))) .Until(files => files.Length > 0) .Every(TimeSpan.FromMilliseconds(100)) - .Timeout(TimeSpan.FromSeconds(10)) + .Timeout(timeout ?? TimeSpan.FromSeconds(10)) .FailWith(errorMessage); string json = await File.ReadAllTextAsync(file.FullName); diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/TestServiceBusMessageEventConsumer.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/TestServiceBusMessageEventConsumer.cs deleted file mode 100644 index 59bc78c6..00000000 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/TestServiceBusMessageEventConsumer.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Threading.Tasks; -using Arcus.EventGrid; -using Arcus.EventGrid.Contracts; -using Arcus.EventGrid.Parsers; -using Arcus.EventGrid.Testing.Infrastructure.Hosts.ServiceBus; -using Arcus.Messaging.Tests.Core.Events.v1; -using Arcus.Messaging.Tests.Workers.ServiceBus.Fixture; -using CloudNative.CloudEvents; -using GuardNet; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Newtonsoft.Json; -using Xunit; -using TestConfig = Arcus.Messaging.Tests.Integration.Fixture.TestConfig; - -namespace Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus -{ - /// - /// Represents an event consumer which receives events from an Azure Service Bus. - /// - public class TestServiceBusMessageEventConsumer : IAsyncDisposable - { - private readonly ServiceBusEventConsumerHost _serviceBusEventConsumerHost; - - private TestServiceBusMessageEventConsumer(ServiceBusEventConsumerHost consumerHost) - { - Guard.NotNull(consumerHost, nameof(consumerHost), "Requires an Azure Service Bus consumer host instance to consume messages"); - _serviceBusEventConsumerHost = consumerHost; - } - - /// - /// Starts an new event consumer which receives events from an Azure Service Bus entity. - /// - /// The test configuration to retrieve the Azure Service Bus test infrastructure. - /// The logger to write diagnostic messages during consuming the messages. - /// Thrown when the is null. - public static async Task StartNewAsync(TestConfig configuration, ILogger logger) - { - Guard.NotNull(configuration, nameof(configuration), "Requires a test configuration to retrieve the Azure Service Bus test infrastructure"); - - logger = logger ?? NullLogger.Instance; - - var topicName = configuration.GetValue("Arcus:Infra:ServiceBus:TopicName"); - var connectionString = configuration.GetValue("Arcus:Infra:ServiceBus:ConnectionString"); - var serviceBusEventConsumerHostOptions = new ServiceBusEventConsumerHostOptions(topicName, connectionString); - - var serviceBusEventConsumerHost = await ServiceBusEventConsumerHost.StartAsync(serviceBusEventConsumerHostOptions, logger); - return new TestServiceBusMessageEventConsumer(serviceBusEventConsumerHost); - } - - /// - /// Receives an event produced on the Azure Service Bus. - /// - /// The ID to identity the produced event. - /// Thrown when the is blank. - public OrderCreatedEventData ConsumeOrderEventForHierarchical(string eventId) - { - Guard.NotNullOrWhitespace(eventId, nameof(eventId), "Requires a non-blank event ID to identity the produced event on the Azure Service Bus"); - - string receivedEvent = _serviceBusEventConsumerHost.GetReceivedEvent(eventId, retryCount: 10); - Assert.NotEmpty(receivedEvent); - - EventBatch eventBatch = EventParser.Parse(receivedEvent); - Assert.NotNull(eventBatch); - Event @event = Assert.Single(eventBatch.Events); - Assert.NotNull(@event); - - var data = @event.Data.ToString(); - Assert.NotNull(data); - - var eventData = JsonConvert.DeserializeObject(data, new MessageCorrelationInfoJsonConverter()); - return eventData; - } - - /// - /// Receives an event produced on the Azure Service Bus. - /// - /// The ID to identity the produced event. - /// The optional time-out in seconds for the event to be arrived. - /// Thrown when the is blank. - public OrderCreatedEventData ConsumeOrderEventForW3C(string transactionId, int timeoutInSeconds = 60) - { - Guard.NotNullOrWhitespace(transactionId, nameof(transactionId), "Requires a non-blank transaction ID to identity the produced event on the Azure Service Bus"); - Guard.NotLessThan(timeoutInSeconds, 0, nameof(timeoutInSeconds), "Requires a time-out in seconds of at least 1 second"); - - // TODO: will be simplified, once all the message handlers are using the same event publishing (https://github.com/arcus-azure/arcus.messaging/issues/343). - CloudEvent receivedEvent = _serviceBusEventConsumerHost.GetReceivedEvent((CloudEvent ev) => - { - var data = ev.Data.ToString(); - var eventData = JsonConvert.DeserializeObject(data, new MessageCorrelationInfoJsonConverter()); - - return eventData.CorrelationInfo.TransactionId == transactionId; - }, timeout: TimeSpan.FromSeconds(timeoutInSeconds)); - - var data = receivedEvent.Data.ToString(); - - var eventData = JsonConvert.DeserializeObject(data, new MessageCorrelationInfoJsonConverter()); - return eventData; - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously. - /// - /// A task that represents the asynchronous dispose operation. - public async ValueTask DisposeAsync() - { - await _serviceBusEventConsumerHost.StopAsync(); - } - } -} \ No newline at end of file diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/TestServiceBusMessageProducer.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/TestServiceBusMessageProducer.cs index ddfaaeeb..0f20ad22 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/TestServiceBusMessageProducer.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/TestServiceBusMessageProducer.cs @@ -1,9 +1,9 @@ using System; using System.Threading.Tasks; using Arcus.Messaging.Tests.Integration.Fixture; +using Arcus.Testing; using Azure.Messaging.ServiceBus; using GuardNet; -using Microsoft.Extensions.Logging; namespace Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus { @@ -13,50 +13,34 @@ namespace Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus public class TestServiceBusMessageProducer { private readonly string _connectionString; + private readonly string _entityName; + private readonly ServiceBusConfig _config; /// /// Initializes a new instance of the class. /// - /// The Azure Service Bus entity-scoped connection string to send messages to. - /// Thrown when the is blank. - public TestServiceBusMessageProducer(string connectionString) - { - Guard.NotNullOrWhitespace(connectionString, nameof(connectionString), "Requires a non-blank Azure Service Bus entity-scoped connection string"); - _connectionString = connectionString; - } - - /// - /// Creates an instance which sends events to an Azure Service Bus topic subscription. - /// - /// The test configuration used in this test suite. - public static TestServiceBusMessageProducer CreateForTopic(TestConfig configuration) + public TestServiceBusMessageProducer(string entityName, ServiceBusConfig config) { - Guard.NotNull(configuration, nameof(configuration), "Requires a test configuration to retrieve the Azure Service Bus topic entity-scoped connection string"); - return CreateFor(configuration, ServiceBusEntityType.Topic); + _entityName = entityName; + _config = config ?? throw new ArgumentNullException(nameof(config)); } /// - /// Creates an instance which sends events to an Azure Service Bus queue. + /// Initializes a new instance of the class. /// - /// The test configuration used in this test suite. - public static TestServiceBusMessageProducer CreateForQueue(TestConfig configuration) + public TestServiceBusMessageProducer(string connectionString) { - Guard.NotNull(configuration, nameof(configuration), "Requires a test configuration to retrieve the Azure Service Bus queue entity-scoped connection string"); - return CreateFor(configuration, ServiceBusEntityType.Queue); + _connectionString = connectionString; } /// /// Creates an instance which sends events to an Azure Service Bus. /// - /// The test configuration used in this test suite. - /// - /// Thrown when the is null. - public static TestServiceBusMessageProducer CreateFor(TestConfig configuration, ServiceBusEntityType entityType) + public static TestServiceBusMessageProducer CreateFor(string entityName, TestConfig configuration) { Guard.NotNull(configuration, nameof(configuration), "Requires a test configuration to retrieve the Azure Service Bus entity-scoped connection string"); - string connectionString = configuration.GetServiceBusConnectionString(entityType); - return new TestServiceBusMessageProducer(connectionString); + return new TestServiceBusMessageProducer(entityName, configuration.GetServiceBus()); } /// @@ -68,20 +52,12 @@ public async Task ProduceAsync(params ServiceBusMessage[] messages) { Guard.NotNull(messages, nameof(messages), "Requires an Azure Service Bus message to send"); - var connectionStringProperties = ServiceBusConnectionStringProperties.Parse(_connectionString); - await using (var client = new ServiceBusClient(_connectionString)) - { - ServiceBusSender messageSender = client.CreateSender(connectionStringProperties.EntityPath); - - try - { - await messageSender.SendMessagesAsync(messages); - } - finally - { - await messageSender.CloseAsync(); - } - } + await using var client = _connectionString is null + ? new ServiceBusClient(_config.HostName, _config.ServicePrincipal.GetCredential()) + : new ServiceBusClient(_connectionString); + + await using ServiceBusSender messageSender = client.CreateSender(_entityName); + await messageSender.SendMessagesAsync(messages); } } } \ No newline at end of file diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/TestServiceMessageConsumer.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/TestServiceMessageConsumer.cs index 302c0cb5..b86c945c 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/TestServiceMessageConsumer.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBus/TestServiceMessageConsumer.cs @@ -2,14 +2,13 @@ using System.Collections.Generic; using System.Threading.Tasks; using Arcus.Messaging.Tests.Core.Events.v1; +using Arcus.Messaging.Tests.Integration.Fixture; using Arcus.Messaging.Tests.Workers.ServiceBus.Fixture; using Arcus.Testing; using Azure.Messaging.ServiceBus; -using GuardNet; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Xunit; -using TestConfig = Arcus.Messaging.Tests.Integration.Fixture.TestConfig; namespace Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus { @@ -18,31 +17,23 @@ namespace Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus /// public class TestServiceMessageConsumer { - private readonly string _connectionString; + private readonly string _entityName; + private readonly ServiceBusConfig _config; private readonly ILogger _logger; - private TestServiceMessageConsumer(string connectionString, ILogger logger) + private TestServiceMessageConsumer(string entityName, ServiceBusConfig config, ILogger logger) { - Guard.NotNullOrWhitespace(connectionString, nameof(connectionString), "Requires an Azure Service Bus connection string so dead-lettered messages can be consumed"); - Guard.NotNull(logger, nameof(logger), "Requires a logger instance to write informational messages during the dead-lettered message consuming from an Azure Service Bus queue"); - - _connectionString = connectionString; + _entityName = entityName; + _config = config; _logger = logger; } /// /// Creates an instance that consumes dead-lettered messages from an Azure Service Bus queue. /// - /// The integration test configuration where the connection string to the Azure Service Bus queue is present. - /// The logger instance to write informational messages during the message consuming. - /// Thrown when the or the is null. - public static TestServiceMessageConsumer CreateForQueue(TestConfig config, ILogger logger) + public static TestServiceMessageConsumer CreateForQueue(string entityName, ServiceBusConfig config, ILogger logger) { - Guard.NotNull(config, nameof(config), "Requires an integration test configuration to retrieve the Azure Service Bus queue connection string so dead-lettered messages can be consumed"); - Guard.NotNull(logger, nameof(logger), "Requires a logger instance to write informational messages during the dead-lettered message consuming from an Azure Service Bus queue"); - - string connectionString = config.GetServiceBusQueueConnectionString(); - return new TestServiceMessageConsumer(connectionString, logger); + return new TestServiceMessageConsumer(entityName, config, logger); } /// @@ -50,9 +41,8 @@ public static TestServiceMessageConsumer CreateForQueue(TestConfig config, ILogg /// public async Task AssertCompletedMessageAsync(string messageId) { - var properties = ServiceBusConnectionStringProperties.Parse(_connectionString); - await using var client = new ServiceBusClient(_connectionString); - await using var receiver = client.CreateReceiver(properties.EntityPath); + await using ServiceBusClient client = _config.GetClient(); + await using ServiceBusReceiver receiver = client.CreateReceiver(_entityName); IReadOnlyList messages = await receiver.ReceiveMessagesAsync(1, maxWaitTime: TimeSpan.FromSeconds(2)); Assert.DoesNotContain(messages, msg => msg.MessageId == messageId); @@ -64,9 +54,8 @@ public async Task AssertCompletedMessageAsync(string messageId) /// Thrown when no abandoned messages can be consumed within the configured time-out. public async Task AssertAbandonMessageAsync(string messageId) { - var properties = ServiceBusConnectionStringProperties.Parse(_connectionString); - await using var client = new ServiceBusClient(_connectionString); - await using var receiver = client.CreateReceiver(properties.EntityPath); + await using ServiceBusClient client = _config.GetClient(); + await using ServiceBusReceiver receiver = client.CreateReceiver(_entityName); ServiceBusReceivedMessage message = await Poll.Target(() => receiver.ReceiveMessageAsync()) @@ -84,19 +73,18 @@ await Poll.Target(() => receiver.ReceiveMessageAsync()) /// Thrown when no dead-lettered messages can be consumed within the configured time-out. public async Task AssertDeadLetterMessageAsync(string messageId) { - var properties = ServiceBusConnectionStringProperties.Parse(_connectionString); var options = new ServiceBusReceiverOptions { SubQueue = SubQueue.DeadLetter }; async Task ReceiveMessageAsync() { - await using var client = new ServiceBusClient(_connectionString); - await using var receiver = client.CreateReceiver(properties.EntityPath, options); + await using ServiceBusClient client = _config.GetClient(); + await using ServiceBusReceiver receiver = client.CreateReceiver(_entityName, options); - _logger.LogTrace("Start looking for dead-lettered message '{MessageId}' for Azure Service bus queue '{QueueName}'", messageId, properties.EntityPath); + _logger.LogTrace("Start looking for dead-lettered message '{MessageId}' for Azure Service bus queue '{QueueName}'", messageId, _entityName); ServiceBusReceivedMessage message = await receiver.ReceiveMessageAsync(); if (message != null) { - _logger.LogTrace("Found dead-lettered message '{MessageId}' for Azure Service bus queue '{QueueName}'", messageId, properties.EntityPath); + _logger.LogTrace("Found dead-lettered message '{MessageId}' for Azure Service bus queue '{QueueName}'", messageId, _entityName); await receiver.CompleteMessageAsync(message); } diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.ConnectivityTests.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.ConnectivityTests.cs index bd408b39..c21e1b06 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.ConnectivityTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.ConnectivityTests.cs @@ -1,21 +1,14 @@ using System; using System.Threading.Tasks; -using Arcus.Messaging.Abstractions.MessageHandling; -using Arcus.Messaging.Pumps.ServiceBus; using Arcus.Messaging.Tests.Core.Events.v1; using Arcus.Messaging.Tests.Core.Messages.v1; using Arcus.Messaging.Tests.Integration.Fixture; using Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus; -using Arcus.Messaging.Tests.Integration.ServiceBus; using Arcus.Messaging.Tests.Workers.MessageHandlers; -using Arcus.Security.Core.Caching.Configuration; -using Azure.Identity; using Azure.Messaging.ServiceBus; -using Azure.Security.KeyVault.Secrets; -using Microsoft.Azure.Management.ServiceBus.Models; +using Azure.ResourceManager.ServiceBus.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Xunit; using static Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus.DiskMessageEventConsumer; using static Microsoft.Extensions.Logging.ServiceBusEntityType; @@ -24,20 +17,6 @@ namespace Arcus.Messaging.Tests.Integration.MessagePump { public partial class ServiceBusMessagePumpTests { - [Fact] - public async Task ServiceBusQueueMessagePumpWithEntityScopedConnectionString_PublishServiceBusMessageForHierarchical_MessageSuccessfullyProcessed() - { - await TestServiceBusMessageHandlingAsync(Queue, format: MessageCorrelationFormat.Hierarchical, configureOptions: options => - { - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => - { - opt.AutoComplete = true; - opt.Routing.Correlation.Format = MessageCorrelationFormat.Hierarchical; - }) - .WithServiceBusMessageHandler(); - }); - } - [Fact] public async Task ServiceBusQueueMessagePumpWithEntityScopedConnectionString_PublishServiceBusMessage_MessageSuccessfullyProcessed() { @@ -51,14 +30,9 @@ await TestServiceBusMessageHandlingAsync(Queue, options => [Fact] public async Task ServiceBusQueueMessagePumpWithNamespaceScopedConnectionString_PublishesServiceBusMessage_MessageSuccessfullyProcessed() { - // Arrange - var properties = ServiceBusConnectionStringProperties.Parse(QueueConnectionString); - string namespaceConnectionString = properties.GetNamespaceConnectionString(); - - // Act / Assert await TestServiceBusMessageHandlingAsync(Queue, options => { - options.AddServiceBusQueueMessagePump(properties.EntityPath, _ => namespaceConnectionString, opt => opt.AutoComplete = true) + options.AddServiceBusQueueMessagePump(QueueName, _ => NamespaceConnectionString, opt => opt.AutoComplete = true) .WithServiceBusMessageHandler(); }); } @@ -74,127 +48,71 @@ await TestServiceBusMessageHandlingAsync(Topic, options => } [Fact] - public async Task ServiceBusTopicMessagePumpWithNamespaceScopedConnectionString_PublishesServiceBusMessage_MessageSuccessfullyProcessed() - { - // Arrange - var properties = ServiceBusConnectionStringProperties.Parse(TopicConnectionString); - string namespaceConnectionString = properties.GetNamespaceConnectionString(); - - // Act / Assert - await TestServiceBusMessageHandlingAsync(Topic, options => - { - options.AddServiceBusTopicMessagePump( - topicName: properties.EntityPath, - subscriptionName: Guid.NewGuid().ToString(), - getConnectionStringFromConfigurationFunc: _ => namespaceConnectionString, - configureMessagePump: opt => - { - opt.AutoComplete = true; - opt.TopicSubscription = TopicSubscription.Automatic; - }) - .WithServiceBusMessageHandler(); - }); - } - - [Fact] - public async Task ServiceBusTopicMessagePumpUsingManagedIdentity_PublishServiceBusMessage_MessageSuccessfullyProcessed() - { - // Arrange - ServiceBusConnectionStringProperties properties = ServiceBusConnectionStringProperties.Parse(TopicConnectionString); - using var auth = TemporaryManagedIdentityConnection.Create(_config, _logger); - - // Act / Assert - await TestServiceBusMessageHandlingAsync(Topic, options => - { - options.AddServiceBusTopicMessagePumpUsingManagedIdentity( - topicName: properties.EntityPath, - subscriptionName: Guid.NewGuid().ToString(), - serviceBusNamespace: properties.FullyQualifiedNamespace, - clientId: auth.ClientId, - configureMessagePump: opt => - { - opt.AutoComplete = true; - opt.TopicSubscription = TopicSubscription.Automatic; - }) - .WithServiceBusMessageHandler(); - }); - } - - [Fact] - public async Task ServiceBusQueueMessagePumpUsingManagedIdentity_PublishServiceBusMessage_MessageSuccessfullyProcessed() - { - // Arrange - ServiceBusConnectionStringProperties properties = ServiceBusConnectionStringProperties.Parse(QueueConnectionString); - using var auth = TemporaryManagedIdentityConnection.Create(_config, _logger); - - // Act / Assert - await TestServiceBusMessageHandlingAsync(Queue, options => - { - options.AddServiceBusQueueMessagePumpUsingManagedIdentity( - queueName: properties.EntityPath, - serviceBusNamespace: properties.FullyQualifiedNamespace, - clientId: auth.ClientId, - configureMessagePump: opt => opt.AutoComplete = true) - .WithServiceBusMessageHandler(); - }); - } - - [Fact] public async Task ServiceBusMessagePump_RotateServiceBusConnectionKeys_MessagePumpRestartsThenMessageSuccessfullyProcessed() { // Arrange - string tenantId = _config.GetTenantId(); - KeyRotationConfig keyRotationConfig = _config.GetKeyRotationConfig(); - _logger.LogInformation("Using Service Principal [ClientID: '{ClientId}']", keyRotationConfig.ServicePrincipal.ClientId); - - var client = new ServiceBusConfiguration(keyRotationConfig, _logger); - string freshConnectionString = await client.RotateConnectionStringKeysForQueueAsync(KeyType.PrimaryKey); + await using TemporaryServiceBusNamespace serviceBus = await CreateServiceBusNamespaceAsync(); + await using TemporaryServiceBusEntity queue = await CreateServiceBusQueueAsync(serviceBus.Config); - SecretClient secretClient = CreateSecretClient(tenantId, keyRotationConfig); - await SetConnectionStringInKeyVaultAsync(secretClient, keyRotationConfig, freshConnectionString); + ServiceBusAccessKeys keys = await serviceBus.GetAccessKeysAsync(); + await using TemporaryKeyVaultSecret secret = await CreateKeyVaultSecretAsync(keys.PrimaryConnectionString); var options = new WorkerOptions(); - options.AddSecretStore(stores => stores.AddAzureKeyVaultWithServicePrincipal( - rawVaultUri: keyRotationConfig.KeyVault.VaultUri, - tenantId: tenantId, - clientId: keyRotationConfig.ServicePrincipal.ClientId, - clientKey: keyRotationConfig.ServicePrincipal.ClientSecret, - cacheConfiguration: CacheConfiguration.Default)) - .AddServiceBusQueueMessagePump(keyRotationConfig.KeyVault.SecretName, opt => opt.AutoComplete = true) + options.AddXunitTestLogging(_outputWriter) + .AddSecretStore(stores => AddAzureKeyVaultWithServicePrincipal(stores, _serviceBusConfig.ServicePrincipal)) + .AddServiceBusQueueMessagePump(queue.EntityName, secret.Name) .WithServiceBusMessageHandler(); - ServiceBusMessage message = CreateOrderServiceBusMessageForW3C(); + await using var worker = await Worker.StartNewAsync(options); - await using (var worker = await Worker.StartNewAsync(options)) - { - string newSecondaryConnectionString = await client.RotateConnectionStringKeysForQueueAsync(KeyType.SecondaryKey); - await SetConnectionStringInKeyVaultAsync(secretClient, keyRotationConfig, newSecondaryConnectionString); + // Act + ServiceBusAccessKeys newKeys = await serviceBus.RotateAccessKeysAsync(ServiceBusAccessKeyType.PrimaryKey); - // Act - string newPrimaryConnectionString = await client.RotateConnectionStringKeysForQueueAsync(KeyType.PrimaryKey); + // Assert + ServiceBusMessage message = CreateOrderServiceBusMessageForW3C(); + var producer = new TestServiceBusMessageProducer(queue.EntityName, serviceBus.Config); + await producer.ProduceAsync(message); - // Assert - var producer = new TestServiceBusMessageProducer(newPrimaryConnectionString); - await producer.ProduceAsync(message); + await secret.UpdateSecretAsync(newKeys.PrimaryConnectionString); + + OrderCreatedEventData actual = await ConsumeOrderCreatedAsync(message.MessageId, TimeSpan.FromSeconds(20)); + AssertReceivedOrderEventDataForW3C(message, actual); + } - OrderCreatedEventData eventData = await ConsumeOrderCreatedAsync(message.MessageId); - AssertReceivedOrderEventDataForW3C(message, eventData); - } + private async Task CreateServiceBusNamespaceAsync() + { + return await TemporaryServiceBusNamespace.CreateBasicAsync( + _config.GetSubscriptionId(), + _config.GetResourceGroupName(), + _config.GetServicePrincipal(), + _logger); } - private static SecretClient CreateSecretClient(string tenantId, KeyRotationConfig keyRotationConfig) + private async Task CreateServiceBusQueueAsync(ServiceBusConfig serviceBus) { - var clientCredential = new ClientSecretCredential(tenantId, - keyRotationConfig.ServicePrincipal.ClientId, - keyRotationConfig.ServicePrincipal.ClientSecret); - - var secretClient = new SecretClient(new Uri(keyRotationConfig.KeyVault.VaultUri), clientCredential); - return secretClient; + return await TemporaryServiceBusEntity.CreateAsync( + Queue, + $"queue-{Guid.NewGuid()}", + serviceBus, + _logger); + } + + private async Task CreateKeyVaultSecretAsync(string secretValue) + { + return await TemporaryKeyVaultSecret.CreateAsync( + $"Queue-ConnectionString-{Guid.NewGuid()}", + secretValue, + _config.GetKeyVault(), + _logger); } - private static async Task SetConnectionStringInKeyVaultAsync(SecretClient keyVaultClient, KeyRotationConfig keyRotationConfig, string rotatedConnectionString) + private void AddAzureKeyVaultWithServicePrincipal(SecretStoreBuilder stores, ServicePrincipal servicePrincipal) { - await keyVaultClient.SetSecretAsync(keyRotationConfig.KeyVault.SecretName, rotatedConnectionString); + stores.AddAzureKeyVaultWithServicePrincipal( + _config.GetKeyVault().VaultUri, + servicePrincipal.TenantId, + servicePrincipal.ClientId, + servicePrincipal.ClientSecret); } } } diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.ResiliencyTests.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.ResiliencyTests.cs index 21819508..46252e74 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.ResiliencyTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.ResiliencyTests.cs @@ -38,7 +38,7 @@ public async Task ServiceBusTopicMessagePump_PauseViaCircuitBreaker_RestartsAgai options.AddXunitTestLogging(_outputWriter) .AddServiceBusTopicMessagePump( subscriptionName: "circuit-breaker-" + Guid.NewGuid(), - _ => _config.GetServiceBusTopicConnectionString(), + _ => TopicConnectionString, opt => opt.TopicSubscription = TopicSubscription.Automatic) .WithServiceBusMessageHandler( implementationFactory: provider => new CircuitBreakerAzureServiceBusMessageHandler( @@ -50,7 +50,7 @@ public async Task ServiceBusTopicMessagePump_PauseViaCircuitBreaker_RestartsAgai }, provider.GetRequiredService())); - var producer = TestServiceBusMessageProducer.CreateFor(_config, ServiceBusEntityType.Topic); + var producer = TestServiceBusMessageProducer.CreateFor(QueueName, _config); await using var worker = await Worker.StartNewAsync(options); // Act @@ -107,18 +107,18 @@ private static ServiceBusMessage[] GenerateShipmentMessages(int count) }).ToArray(); } - [Fact] + [Fact(Skip = "TODO: fixed upon circuit breaker changes")] public async Task ServiceBusMessagePump_PauseViaLifetime_RestartsAgain() { // Arrange - string connectionString = _config.GetServiceBusTopicConnectionString(); string jobId = Guid.NewGuid().ToString(); var options = new WorkerOptions(); options.AddXunitTestLogging(_outputWriter) - .AddServiceBusTopicMessagePump( + .AddServiceBusTopicMessagePumpUsingManagedIdentity( + TopicName, subscriptionName: Guid.NewGuid().ToString(), - _ => connectionString, - opt => + HostName, + configureMessagePump: opt => { opt.JobId = jobId; opt.TopicSubscription = TopicSubscription.Automatic; @@ -128,7 +128,7 @@ public async Task ServiceBusMessagePump_PauseViaLifetime_RestartsAgain() ServiceBusMessage message = CreateOrderServiceBusMessageForW3C(); - var producer = TestServiceBusMessageProducer.CreateFor(_config, ServiceBusEntityType.Topic); + var producer = TestServiceBusMessageProducer.CreateFor(QueueName, _config); await using var worker = await Worker.StartNewAsync(options); var lifetime = worker.Services.GetRequiredService(); diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.RouterTests.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.RouterTests.cs index 6fab5e69..95f1c47d 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.RouterTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.RouterTests.cs @@ -25,7 +25,7 @@ public async Task ServiceBusTopicMessagePumpWithBodyFiltering_RoutesServiceBusMe { await TestServiceBusMessageHandlingAsync(Topic, options => { - options.AddServiceBusTopicMessagePump(TopicConnectionString) + options.AddServiceBusTopicMessagePumpUsingManagedIdentity(TopicName, HostName) .WithServiceBusMessageHandler((Customer body) => body is null) .WithServiceBusMessageHandler((Order body) => body.Id != null) .WithMessageHandler((Order _) => false); @@ -37,7 +37,7 @@ public async Task ServiceBusQueueMessagePumpWithContextAndBodyFilteringWithSeria { await TestServiceBusMessageHandlingAsync(Queue, options => { - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => opt.AutoComplete = true) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => opt.AutoComplete = true) .WithServiceBusMessageHandler(messageContextFilter: _ => false) .WithServiceBusMessageHandler(messageBodyFilter: _ => true) .WithServiceBusMessageHandler( @@ -56,7 +56,7 @@ public async Task ServiceBusQueueMessagePumpWithContextTypeFiltering_RoutesServi { await TestServiceBusMessageHandlingAsync(Queue, options => { - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => opt.AutoComplete = true) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => opt.AutoComplete = true) .WithServiceBusMessageHandler() .WithServiceBusMessageHandler(); }); @@ -67,7 +67,7 @@ public async Task ServiceBusQueueMessagePumpWithContextAndBodyFiltering_RoutesSe { // Arrange var options = new WorkerOptions(); - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => opt.AutoComplete = true) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => opt.AutoComplete = true) .WithServiceBusMessageHandler(context => context.Properties.ContainsKey("NotExisting"), _ => false) .WithServiceBusMessageHandler( context => context.Properties["Topic"].ToString() == "Orders", @@ -89,7 +89,7 @@ public async Task ServiceBusTopicMessagePumpWithContextFiltering_RoutesServiceBu { // Arrange var options = new WorkerOptions(); - options.AddServiceBusTopicMessagePump(TopicConnectionString) + options.AddServiceBusTopicMessagePumpUsingManagedIdentity(TopicName, HostName) .WithServiceBusMessageHandler(context => context.Properties.TryGetValue("Topic", out object value) && value.ToString() == "Customers") .WithServiceBusMessageHandler(context => context.Properties.TryGetValue("Topic", out object value) && value.ToString() == "Orders") .WithMessageHandler((AzureServiceBusMessageContext _) => false); @@ -110,7 +110,7 @@ public async Task ServiceBusQueueMessagePumpWithBatchedMessages_PublishServiceBu { await TestServiceBusMessageHandlingAsync(Queue, options => { - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => opt.AutoComplete = true) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => opt.AutoComplete = true) .WithServiceBusMessageHandler( messageBodySerializerImplementationFactory: serviceProvider => { @@ -125,10 +125,11 @@ public async Task ServiceBusQueueMessagePumpWithIgnoringMissingMembersDeserializ { await TestServiceBusMessageHandlingAsync(Queue, options => { - options.AddServiceBusQueueMessagePump( - _ => QueueConnectionString, - opt => opt.Routing.Deserialization.AdditionalMembers = AdditionalMemberHandling.Ignore) - .WithServiceBusMessageHandler(); + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => + { + opt.Routing.Deserialization.AdditionalMembers = AdditionalMemberHandling.Ignore; + + }).WithServiceBusMessageHandler(); }); } @@ -150,7 +151,7 @@ public async Task ServiceBusQueueMessagePump_PublishesEncodedServiceBusMessage_M { // Arrange var options = new WorkerOptions(); - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => opt.AutoComplete = true) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => opt.AutoComplete = true) .WithServiceBusMessageHandler(); ServiceBusMessage message = CreateOrderServiceBusMessageForW3C(encoding: encoding); @@ -170,7 +171,7 @@ public async Task ServiceBusTopicMessagePump_PublishesEncodedServiceBusMessage_M { // Arrange var options = new WorkerOptions(); - options.AddServiceBusTopicMessagePump(TopicConnectionString) + options.AddServiceBusTopicMessagePumpUsingManagedIdentity(TopicName, HostName) .WithServiceBusMessageHandler(); ServiceBusMessage message = CreateOrderServiceBusMessageForW3C(encoding: encoding); @@ -189,7 +190,7 @@ public async Task ServiceBusMessagePumpWithFallback_PublishServiceBusMessage_Mes { await TestServiceBusMessageHandlingAsync(Queue, options => { - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => opt.AutoComplete = true) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => opt.AutoComplete = true) .WithServiceBusMessageHandler((AzureServiceBusMessageContext _) => false) .WithFallbackMessageHandler(); }); @@ -200,7 +201,7 @@ public async Task ServiceBusMessagePumpWithServiceBusFallback_PublishServiceBusM { await TestServiceBusMessageHandlingAsync(Queue, options => { - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => opt.AutoComplete = true) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => opt.AutoComplete = true) .WithServiceBusMessageHandler((AzureServiceBusMessageContext _) => false) .WithServiceBusFallbackMessageHandler(); }); diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.ServiceBusTests.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.ServiceBusTests.cs index 4a70c2c5..afcf52bb 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.ServiceBusTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.ServiceBusTests.cs @@ -10,8 +10,8 @@ using Azure.Messaging.ServiceBus; using Azure.Messaging.ServiceBus.Administration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Xunit; +using static Microsoft.Extensions.Logging.ServiceBusEntityType; namespace Arcus.Messaging.Tests.Integration.MessagePump { @@ -22,7 +22,7 @@ public async Task ServiceBusQueueMessagePumpWithServiceBusDeadLetter_PublishServ { await TestServiceBusQueueDeadLetteredMessageAsync(options => { - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => opt.AutoComplete = false) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => opt.AutoComplete = false) .WithServiceBusMessageHandler(context => context.Properties["Topic"].ToString() == "Customers") .WithServiceBusMessageHandler() .WithMessageHandler((AzureServiceBusMessageContext _) => false); @@ -34,7 +34,7 @@ public async Task ServiceBusQueueMessagePumpWithServiceBusDeadLetterOnFallback_P { await TestServiceBusQueueDeadLetteredMessageAsync(options => { - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => opt.AutoComplete = false) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => opt.AutoComplete = false) .WithServiceBusMessageHandler((AzureServiceBusMessageContext _) => true) .WithServiceBusFallbackMessageHandler(); }); @@ -48,9 +48,9 @@ private async Task TestServiceBusQueueDeadLetteredMessageAsync(Action + await TestServiceBusMessageHandlingAsync(options, Queue, message, async () => { - var consumer = TestServiceMessageConsumer.CreateForQueue(_config, _logger); + TestServiceMessageConsumer consumer = CreateQueueConsumer(); await consumer.AssertDeadLetterMessageAsync(message.MessageId); }); } @@ -60,7 +60,7 @@ public async Task ServiceBusQueueMessagePumpWithServiceBusAbandon_PublishService { await TestServiceBusQueueAbandonMessageAsync(options => { - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName) .WithServiceBusMessageHandler((AzureServiceBusMessageContext _) => false) .WithServiceBusMessageHandler((AzureServiceBusMessageContext _) => true); }); @@ -71,7 +71,7 @@ public async Task ServiceBusQueueMessagePumpWithServiceBusAbandonOnFallback_Publ { await TestServiceBusQueueAbandonMessageAsync(options => { - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString) + options.AddServiceBusQueueMessagePump(QueueName, HostName) .WithServiceBusMessageHandler() .WithServiceBusFallbackMessageHandler(); }); @@ -86,10 +86,10 @@ private async Task TestServiceBusQueueAbandonMessageAsync(Action ServiceBusMessage message = CreateOrderServiceBusMessageForW3C(); // Act - await TestServiceBusMessageHandlingAsync(options, ServiceBusEntityType.Queue, message, async () => + await TestServiceBusMessageHandlingAsync(options, Queue, message, async () => { // Assert - var consumer = TestServiceMessageConsumer.CreateForQueue(_config, _logger); + TestServiceMessageConsumer consumer = CreateQueueConsumer(); await consumer.AssertAbandonMessageAsync(message.MessageId); }); } @@ -99,17 +99,17 @@ public async Task ServiceBusQueueMessagePumpWithCustomCompleteOnFallback_Publish { // Arrange var options = new WorkerOptions(); - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => opt.AutoComplete = false) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => opt.AutoComplete = false) .WithServiceBusMessageHandler() .WithServiceBusFallbackMessageHandler(); ServiceBusMessage message = CreateOrderServiceBusMessageForW3C(); // Act - await TestServiceBusMessageHandlingAsync(options, ServiceBusEntityType.Queue, message, async () => + await TestServiceBusMessageHandlingAsync(options, Queue, message, async () => { // Assert - var consumer = TestServiceMessageConsumer.CreateForQueue(_config, _logger); + TestServiceMessageConsumer consumer = CreateQueueConsumer(); await consumer.AssertCompletedMessageAsync(message.MessageId); }); } @@ -119,20 +119,26 @@ public async Task ServiceBusTopicMessagePumpWithCustomComplete_PublishServiceBus { // Arrange var options = new WorkerOptions(); - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => opt.AutoComplete = false) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => opt.AutoComplete = false) .WithServiceBusMessageHandler(); ServiceBusMessage message = CreateOrderServiceBusMessageForW3C(); // Act - await TestServiceBusMessageHandlingAsync(options, ServiceBusEntityType.Queue, message, async () => + await TestServiceBusMessageHandlingAsync(options, Queue, message, async () => { // Assert - var consumer = TestServiceMessageConsumer.CreateForQueue(_config, _logger); + TestServiceMessageConsumer consumer = CreateQueueConsumer(); await consumer.AssertCompletedMessageAsync(message.MessageId); }); } + private TestServiceMessageConsumer CreateQueueConsumer() + { + var consumer = TestServiceMessageConsumer.CreateForQueue(QueueName, _serviceBusConfig, _logger); + return consumer; + } + [Theory] [InlineData(TopicSubscription.None, false)] [InlineData(TopicSubscription.Automatic, true)] @@ -141,32 +147,32 @@ public async Task ServiceBusTopicMessagePump_WithNoneTopicSubscription_DoesNotCr // Arrange var options = new WorkerOptions(); var subscriptionName = $"Subscription-{Guid.NewGuid():N}"; - options.AddServiceBusTopicMessagePump( + options.AddServiceBusTopicMessagePumpUsingManagedIdentity( + TopicName, subscriptionName, - _ => TopicConnectionString, - opt => opt.TopicSubscription = topicSubscription) + HostName, + configureMessagePump: opt => opt.TopicSubscription = topicSubscription) .WithServiceBusMessageHandler(); // Act await using var worker = await Worker.StartNewAsync(options); // Assert - var client = new ServiceBusAdministrationClient(TopicConnectionString); - var properties = ServiceBusConnectionStringProperties.Parse(TopicConnectionString); - - Response subscriptionExistsResponse = await client.SubscriptionExistsAsync(properties.EntityPath, subscriptionName); + ServiceBusAdministrationClient client = _serviceBusConfig.GetAdminClient(); + Response subscriptionExistsResponse = await client.SubscriptionExistsAsync(TopicName, subscriptionName); Assert.Equal(doesSubscriptionExists, subscriptionExistsResponse.Value); } [Fact] public async Task ServiceBusTopicMessagePumpWithSubscriptionNameOver50_PublishServiceBusMessage_MessageSuccessfullyProcessed() { - await TestServiceBusMessageHandlingAsync(ServiceBusEntityType.Topic, options => + await TestServiceBusMessageHandlingAsync(Topic, options => { - options.AddServiceBusTopicMessagePump( + options.AddServiceBusTopicMessagePumpUsingManagedIdentity( + TopicName, subscriptionName: "Test-Receive-All-Topic-Only-with-an-azure-servicebus-topic-subscription-name-over-50-characters", - _ => TopicConnectionString, - opt => + HostName, + configureMessagePump: opt => { opt.AutoComplete = true; opt.TopicSubscription = TopicSubscription.Automatic; diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.TelemetryTests.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.TelemetryTests.cs index 4e6e0f33..a6f6dd2c 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.TelemetryTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePump.TelemetryTests.cs @@ -39,10 +39,11 @@ public async Task ServiceBusMessagePump_WithW3CCorrelationFormat_AutomaticallyTr var options = new WorkerOptions(); string operationName = Guid.NewGuid().ToString(); - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => { opt.AutoComplete = true; opt.Routing.Telemetry.OperationName = operationName; + }).WithServiceBusMessageHandler(); var spySink = new InMemoryApplicationInsightsTelemetryConverter(); @@ -73,10 +74,11 @@ public async Task ServiceBusMessagePump_WithW3CCorrelationFormatForNewParent_Aut var options = new WorkerOptions(); string operationName = Guid.NewGuid().ToString(); - options.AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => { opt.Routing.Telemetry.OperationName = operationName; opt.AutoComplete = true; + }).WithServiceBusMessageHandler(); var spySink = new InMemoryApplicationInsightsTelemetryConverter(); @@ -143,12 +145,12 @@ public async Task ServiceBusMessagePump_FailureDuringMessageHandling_TracksCorre var spySink = new InMemoryLogSink(); var options = new WorkerOptions(); options.ConfigureSerilog(config => config.WriteTo.Sink(spySink)) - .AddServiceBusQueueMessagePump(_ => QueueConnectionString, opt => + .AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => { opt.AutoComplete = true; opt.Routing.Correlation.Format = MessageCorrelationFormat.Hierarchical; - }) - .WithServiceBusMessageHandler(); + + }).WithServiceBusMessageHandler(); string operationId = $"operation-{Guid.NewGuid()}", transactionId = $"transaction-{Guid.NewGuid()}"; Order order = OrderGenerator.Generate(); @@ -180,15 +182,15 @@ public async Task ServiceBusTopicMessagePump_WithCustomTransactionIdProperty_Ret // Arrange var customTransactionIdPropertyName = "MyTransactionId"; var options = new WorkerOptions(); - options.AddServiceBusTopicMessagePump($"MySubscription-{Guid.NewGuid():N}", _ => TopicConnectionString, - opt => - { - opt.AutoComplete = true; - opt.TopicSubscription = TopicSubscription.Automatic; - opt.Routing.Correlation.Format = MessageCorrelationFormat.Hierarchical; - opt.Routing.Correlation.TransactionIdPropertyName = customTransactionIdPropertyName; - }) - .WithServiceBusMessageHandler(); + options.AddServiceBusTopicMessagePumpUsingManagedIdentity(TopicName, $"MySubscription-{Guid.NewGuid():N}", HostName, + configureMessagePump: opt => + { + opt.AutoComplete = true; + opt.TopicSubscription = TopicSubscription.Automatic; + opt.Routing.Correlation.Format = MessageCorrelationFormat.Hierarchical; + opt.Routing.Correlation.TransactionIdPropertyName = customTransactionIdPropertyName; + + }).WithServiceBusMessageHandler(); ServiceBusMessage message = CreateOrderServiceBusMessageForHierarchical(customTransactionIdPropertyName); @@ -206,20 +208,19 @@ public async Task ServiceBusQueueMessagePump_WithCustomOperationParentIdProperty // Arrange var customOperationParentIdPropertyName = "MyOperationParentId"; var options = new WorkerOptions(); - options.AddServiceBusQueueMessagePump( - _ => QueueConnectionString, - opt => - { - opt.AutoComplete = true; - opt.Routing.Correlation.Format = MessageCorrelationFormat.Hierarchical; - opt.Routing.Correlation.OperationParentIdPropertyName = customOperationParentIdPropertyName; - }) - .WithServiceBusMessageHandler(); + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, + configureMessagePump: opt => + { + opt.AutoComplete = true; + opt.Routing.Correlation.Format = MessageCorrelationFormat.Hierarchical; + opt.Routing.Correlation.OperationParentIdPropertyName = customOperationParentIdPropertyName; + + }).WithServiceBusMessageHandler(); ServiceBusMessage message = CreateOrderServiceBusMessageForHierarchical(operationParentIdPropertyName: customOperationParentIdPropertyName); // Act / Assert - await TestServiceBusMessageHandlingAsync(options, Queue, message, async () => + await TestServiceBusMessageHandlingAsync(options, ServiceBusEntityType.Topic, message, async () => { OrderCreatedEventData eventData = await ConsumeOrderCreatedAsync(message.MessageId); AssertReceivedOrderEventDataForHierarchical(message, eventData, operationParentIdPropertyName: customOperationParentIdPropertyName); diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePumpDockerTests.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePumpDockerTests.cs deleted file mode 100644 index e9127c6d..00000000 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePumpDockerTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Threading.Tasks; -using Arcus.Messaging.Tests.Core.Correlation; -using Arcus.Messaging.Tests.Core.Events.v1; -using Arcus.Messaging.Tests.Core.Generators; -using Azure.Messaging.ServiceBus; -using Arcus.Messaging.Tests.Core.Messages.v1; -using Arcus.Messaging.Tests.Integration.Fixture; -using Arcus.Messaging.Tests.Integration.MessagePump.Fixture; -using Xunit; -using Xunit.Abstractions; - -namespace Arcus.Messaging.Tests.Integration.MessagePump -{ - [Collection("Docker")] - [Trait("Category", "Docker")] - public class ServiceBusMessagePumpDockerTests : DockerServiceBusIntegrationTest - { - /// - /// Initializes a new instance of the class. - /// - public ServiceBusMessagePumpDockerTests(ITestOutputHelper testOutput) : base(testOutput) - { - } - - [Theory] - [InlineData("Arcus:ServiceBus:Docker:ConnectionStringWithQueue")] - [InlineData("Arcus:ServiceBus:Docker:ConnectionStringWithTopic")] - public async Task ServiceBusTopicMessagePump_PublishServiceBusMessage_MessageSuccessfullyProcessed(string connectionString) - { - // Arrange - var traceParent = TraceParent.Generate(); - Order order = OrderGenerator.Generate(); - - var orderMessage = new ServiceBusMessage(BinaryData.FromObjectAsJson(order)) - .WithDiagnosticId(traceParent); - - // Act - await SenderOrderToServiceBusAsync(orderMessage, connectionString); - - // Assert - OrderCreatedEventData orderCreatedEventData = ReceiveOrderFromEventGrid(traceParent.TransactionId); - Assert.NotNull(orderCreatedEventData); - Assert.NotNull(orderCreatedEventData.CorrelationInfo); - Assert.Equal(order.Id, orderCreatedEventData.Id); - Assert.Equal(order.Amount, orderCreatedEventData.Amount); - Assert.Equal(order.ArticleNumber, orderCreatedEventData.ArticleNumber); - Assert.Equal(traceParent.TransactionId, orderCreatedEventData.CorrelationInfo.TransactionId); - Assert.Equal(traceParent.OperationParentId, orderCreatedEventData.CorrelationInfo.OperationParentId); - Assert.NotNull(orderCreatedEventData.CorrelationInfo.OperationId); - } - } -} \ No newline at end of file diff --git a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePumpTests.cs b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePumpTests.cs index 526dc549..55bf56b2 100644 --- a/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePumpTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/MessagePump/ServiceBusMessagePumpTests.cs @@ -1,9 +1,11 @@ using System; +using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Arcus.Messaging.Abstractions; using Arcus.Messaging.Abstractions.MessageHandling; +using Arcus.Messaging.Pumps.ServiceBus; using Arcus.Messaging.Tests.Core.Correlation; using Arcus.Messaging.Tests.Core.Events.v1; using Arcus.Messaging.Tests.Core.Generators; @@ -12,15 +14,15 @@ using Arcus.Messaging.Tests.Integration.MessagePump.Fixture; using Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus; using Arcus.Messaging.Tests.Workers.MessageHandlers; +using Arcus.Testing; using Azure.Messaging.ServiceBus; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; using Xunit; using Xunit.Abstractions; -using ArgumentOutOfRangeException = System.ArgumentOutOfRangeException; -using TestConfig = Arcus.Messaging.Tests.Integration.Fixture.TestConfig; -using XunitTestLogger = Arcus.Testing.Logging.XunitTestLogger; using static Microsoft.Extensions.Logging.ServiceBusEntityType; using static Arcus.Messaging.Tests.Integration.MessagePump.ServiceBus.DiskMessageEventConsumer; @@ -28,37 +30,45 @@ namespace Arcus.Messaging.Tests.Integration.MessagePump { [Collection("Integration")] [Trait("Category", "Integration")] - public partial class ServiceBusMessagePumpTests + public partial class ServiceBusMessagePumpTests : IClassFixture, IDisposable { private readonly TestConfig _config; + private readonly ServiceBusConfig _serviceBusConfig; + private readonly TemporaryManagedIdentityConnection _connection; private readonly ILogger _logger; private readonly ITestOutputHelper _outputWriter; /// /// Initializes a new instance of the class. /// - public ServiceBusMessagePumpTests(ITestOutputHelper outputWriter) + public ServiceBusMessagePumpTests(ServiceBusEntityFixture entity, ITestOutputHelper outputWriter) { _config = TestConfig.Create(); + _serviceBusConfig = _config.GetServiceBus(); + _outputWriter = outputWriter; _logger = new XunitTestLogger(outputWriter); + _connection = TemporaryManagedIdentityConnection.Create(_config, _logger); + + QueueName = entity.QueueName; + TopicName = entity.TopicName; } - private string QueueConnectionString => _config.GetServiceBusQueueConnectionString(); - private string TopicConnectionString => _config.GetServiceBusTopicConnectionString(); + private string QueueName { get; } + private string TopicName { get; } + private string HostName => _serviceBusConfig.HostName; + private string NamespaceConnectionString => _serviceBusConfig.NamespaceConnectionString; + private string QueueConnectionString => $"{_serviceBusConfig.NamespaceConnectionString};EntityPath={QueueName}"; + private string TopicConnectionString => $"{_serviceBusConfig.NamespaceConnectionString};EntityPath={TopicName}"; [Fact(Skip = ".NET application cannot start multiple blocking background tasks, see https://github.com/dotnet/runtime/issues/36063")] public async Task ServiceBusMessagePumpWithQueueAndTopic_PublishServiceBusMessage_MessageSuccessfullyProcessed() { // Arrange - string connectionString = _config.GetServiceBusQueueConnectionString(); var options = new WorkerOptions(); - options.AddServiceBusQueueMessagePump(_ => connectionString, opt => opt.AutoComplete = true) + options.AddServiceBusQueueMessagePumpUsingManagedIdentity(QueueName, HostName, configureMessagePump: opt => opt.AutoComplete = true) .WithServiceBusMessageHandler(); - options.AddServiceBusTopicMessagePump( - subscriptionName: Guid.NewGuid().ToString(), - _ => _config.GetServiceBusConnectionString(Topic), - opt => opt.AutoComplete = true) + options.AddServiceBusTopicMessagePumpUsingManagedIdentity(TopicName, HostName) .WithServiceBusMessageHandler(); // Act / Assert @@ -125,7 +135,14 @@ private async Task TestServiceBusMessageHandlingAsync( options.AddXunitTestLogging(_outputWriter); await using var worker = await Worker.StartNewAsync(options, memberName); - var producer = TestServiceBusMessageProducer.CreateFor(_config, entityType); + ServiceBusEntityType registeredEntityType = DetermineRegisteredEntityType(worker); + + var producer = TestServiceBusMessageProducer.CreateFor(registeredEntityType switch + { + Queue => QueueName, + Topic => TopicName, + _ => throw new ArgumentOutOfRangeException(nameof(entityType), entityType, "Unknown Service bus entity type") + }, _config); // Act await producer.ProduceAsync(message); @@ -134,6 +151,19 @@ private async Task TestServiceBusMessageHandlingAsync( await assertionAsync(); } + private static ServiceBusEntityType DetermineRegisteredEntityType(Worker worker) + { + ServiceBusEntityType registeredEntityType = + Assert.Single( + worker.Services.GetServices() + .Where(h => h is AzureServiceBusMessagePump) + .Cast() + .Select(m => m.Settings.ServiceBusEntity) + .ToArray()); + + return registeredEntityType; + } + private static ServiceBusMessage CreateOrderServiceBusMessageForHierarchical( string transactionIdPropertyName = PropertyNames.TransactionId, string operationParentIdPropertyName = PropertyNames.OperationParentId, @@ -221,5 +251,35 @@ private static void AssertReceivedOrderEventDataForW3C( Assert.NotNull(receivedEventData.CorrelationInfo.OperationId); Assert.Equal(operationParentId, receivedEventData.CorrelationInfo.OperationParentId); } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + _connection?.Dispose(); + } + } + + public class ServiceBusEntityFixture : IAsyncLifetime + { + private TemporaryServiceBusEntity _queue, _topic; + + public string QueueName { get; } = $"queue-{Guid.NewGuid()}"; + public string TopicName { get; } = $"topic-{Guid.NewGuid()}"; + + public async Task InitializeAsync() + { + var config = TestConfig.Create().GetServiceBus(); + _topic = await TemporaryServiceBusEntity.CreateAsync(Topic, TopicName, config, NullLogger.Instance); + _queue = await TemporaryServiceBusEntity.CreateAsync(Queue, QueueName, config, NullLogger.Instance); + } + + public async Task DisposeAsync() + { + await using var disposables = new DisposableCollection(NullLogger.Instance); + disposables.Add(_queue); + disposables.Add(_topic); + } } } \ No newline at end of file diff --git a/src/Arcus.Messaging.Tests.Integration/ServiceBus/AzureClientFactoryBuilderExtensionsTests.cs b/src/Arcus.Messaging.Tests.Integration/ServiceBus/AzureClientFactoryBuilderExtensionsTests.cs index e38d31cd..504074ee 100644 --- a/src/Arcus.Messaging.Tests.Integration/ServiceBus/AzureClientFactoryBuilderExtensionsTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/ServiceBus/AzureClientFactoryBuilderExtensionsTests.cs @@ -5,26 +5,34 @@ using Arcus.Messaging.Tests.Core.Generators; using Arcus.Messaging.Tests.Core.Messages.v1; using Arcus.Messaging.Tests.Integration.Fixture; +using Arcus.Testing; using Azure.Messaging.ServiceBus; using Microsoft.Extensions.Azure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Polly; using Polly.Wrap; using Xunit; +using Xunit.Abstractions; namespace Arcus.Messaging.Tests.Integration.ServiceBus { - public class AzureClientFactoryBuilderExtensionsTests + public class AzureClientFactoryBuilderExtensionsTests : IAsyncLifetime { private readonly TestConfig _config; + private readonly ILogger _logger; + + private TemporaryManagedIdentityConnection _connection; + private TemporaryServiceBusEntity _queue; /// /// Initializes a new instance of the class. /// - public AzureClientFactoryBuilderExtensionsTests() + public AzureClientFactoryBuilderExtensionsTests(ITestOutputHelper outputWriter) { _config = TestConfig.Create(); + _logger = new XunitTestLogger(outputWriter); } [Fact] @@ -33,7 +41,7 @@ public async Task AddServiceBusClient_SendsMessage_Succeeds() // Arrange var services = new ServiceCollection(); var connectionStringSecretName = "MyConnectionString"; - string connectionString = _config.GetServiceBusQueueConnectionString(); + string connectionString = _config.GetServiceBus().NamespaceConnectionString + ";EntityPath=" + _queue.EntityName; var connectionStringProperties = ServiceBusConnectionStringProperties.Parse(connectionString); services.AddSecretStore(stores => stores.AddInMemory(connectionStringSecretName, connectionString)); @@ -91,5 +99,17 @@ await policy.ExecuteAsync(async () => }); } } + + public async Task InitializeAsync() + { + _connection = TemporaryManagedIdentityConnection.Create(_config, _logger); + _queue = await TemporaryServiceBusEntity.CreateAsync(ServiceBusEntityType.Queue, $"queue-{Guid.NewGuid()}", _config.GetServiceBus(), _logger); + } + + public async Task DisposeAsync() + { + await _queue.DisposeAsync(); + _connection.Dispose(); + } } } diff --git a/src/Arcus.Messaging.Tests.Integration/ServiceBus/ServiceBusConfiguration.cs b/src/Arcus.Messaging.Tests.Integration/ServiceBus/ServiceBusConfiguration.cs index da9c03c9..2c303435 100644 --- a/src/Arcus.Messaging.Tests.Integration/ServiceBus/ServiceBusConfiguration.cs +++ b/src/Arcus.Messaging.Tests.Integration/ServiceBus/ServiceBusConfiguration.cs @@ -110,11 +110,12 @@ private async Task CreateServiceManagementClientAsy string tenantId = _configuration.ServiceBusNamespace.TenantId; var context = new AuthenticationContext($"https://login.microsoftonline.com/{tenantId}"); - ClientCredential clientCredentials = _configuration.ServicePrincipal.CreateCredentials(); AuthenticationResult result = await context.AcquireTokenAsync( - "https://management.azure.com/", - clientCredentials); + "https://management.azure.com/", + new ClientCredential( + _configuration.ServicePrincipal.ClientId, + _configuration.ServicePrincipal.ClientSecret)); var tokenCredentials = new TokenCredentials(result.AccessToken); string subscriptionId = _configuration.ServiceBusNamespace.SubscriptionId; diff --git a/src/Arcus.Messaging.Tests.Integration/ServiceBus/ServiceBusSenderExtensionsTests.cs b/src/Arcus.Messaging.Tests.Integration/ServiceBus/ServiceBusSenderExtensionsTests.cs index 42543ab2..61a56d56 100644 --- a/src/Arcus.Messaging.Tests.Integration/ServiceBus/ServiceBusSenderExtensionsTests.cs +++ b/src/Arcus.Messaging.Tests.Integration/ServiceBus/ServiceBusSenderExtensionsTests.cs @@ -6,7 +6,8 @@ using Arcus.Messaging.Tests.Core.Generators; using Arcus.Messaging.Tests.Core.Messages.v1; using Arcus.Messaging.Tests.Integration.Fixture; -using Arcus.Testing.Logging; +using Arcus.Messaging.Tests.Integration.MessagePump; +using Arcus.Testing; using Azure.Messaging.ServiceBus; using Polly; using Polly.Wrap; @@ -14,7 +15,7 @@ namespace Arcus.Messaging.Tests.Integration.ServiceBus { - public class ServiceBusSenderExtensionsTests + public class ServiceBusSenderExtensionsTests : IClassFixture { private const string DependencyIdPattern = @"with ID [a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12}"; @@ -23,11 +24,15 @@ public class ServiceBusSenderExtensionsTests /// /// Initializes a new instance of the class. /// - public ServiceBusSenderExtensionsTests() + public ServiceBusSenderExtensionsTests(ServiceBusEntityFixture fixture) { _config = TestConfig.Create(); + + QueueName = fixture.QueueName; } + private string QueueName { get; } + [Fact] public async Task SendMessage_WithMessageCorrelation_TracksMessage() { @@ -36,31 +41,26 @@ public async Task SendMessage_WithMessageCorrelation_TracksMessage() MessageCorrelationInfo correlation = GenerateMessageCorrelationInfo(); var logger = new InMemoryLogger(); - string connectionString = _config.GetServiceBusQueueConnectionString(); - var connectionStringProperties = ServiceBusConnectionStringProperties.Parse(connectionString); - - await using (var client = new ServiceBusClient(connectionString)) + await using ServiceBusClient client = _config.GetServiceBus().GetClient(); + await using (ServiceBusSender sender = client.CreateSender(QueueName)) { - await using (ServiceBusSender sender = client.CreateSender(connectionStringProperties.EntityPath)) - { - // Act - await sender.SendMessageAsync(order, correlation, logger); - } - - // Assert - await RetryAssertUntilServiceBusMessageIsAvailableAsync(client, connectionStringProperties.EntityPath, message => - { - var actual = message.Body.ToObjectFromJson(); - Assert.Equal(order.Id, actual.Id); - - Assert.Equal(message.ApplicationProperties[PropertyNames.TransactionId], correlation.TransactionId); - Assert.False(string.IsNullOrWhiteSpace(message.ApplicationProperties[PropertyNames.OperationParentId].ToString())); - - string logMessage = Assert.Single(logger.Messages); - Assert.Contains("Dependency", logMessage); - Assert.Matches(DependencyIdPattern, logMessage); - }); + // Act + await sender.SendMessageAsync(order, correlation, logger); } + + // Assert + await RetryAssertUntilServiceBusMessageIsAvailableAsync(client, QueueName, message => + { + var actual = message.Body.ToObjectFromJson(); + Assert.Equal(order.Id, actual.Id); + + Assert.Equal(message.ApplicationProperties[PropertyNames.TransactionId], correlation.TransactionId); + Assert.False(string.IsNullOrWhiteSpace(message.ApplicationProperties[PropertyNames.OperationParentId].ToString())); + + string logMessage = Assert.Single(logger.Messages); + Assert.Contains("Dependency", logMessage); + Assert.Matches(DependencyIdPattern, logMessage); + }); } [Fact] @@ -75,38 +75,33 @@ public async Task SendMessage_WithCustomOptions_TracksMessage() var telemetryContext = new Dictionary { [key] = value }; var logger = new InMemoryLogger(); - string connectionString = _config.GetServiceBusQueueConnectionString(); - var connectionStringProperties = ServiceBusConnectionStringProperties.Parse(connectionString); - - await using (var client = new ServiceBusClient(connectionString)) + await using ServiceBusClient client = _config.GetServiceBus().GetClient(); + await using (ServiceBusSender sender = client.CreateSender(QueueName)) { - await using (ServiceBusSender sender = client.CreateSender(connectionStringProperties.EntityPath)) - { - // Act - await sender.SendMessageAsync(order, correlation, logger, options => - { - options.TransactionIdPropertyName = transactionIdPropertyName; - options.UpstreamServicePropertyName = upstreamServicePropertyName; - options.GenerateDependencyId = () => dependencyId; - options.AddTelemetryContext(telemetryContext); - }); - } - - // Assert - string logMessage = Assert.Single(logger.Messages); - Assert.Contains("Dependency", logMessage); - Assert.Matches($"with ID {dependencyId}", logMessage); - Assert.Contains(key, logMessage); - Assert.Contains(value, logMessage); - await RetryAssertUntilServiceBusMessageIsAvailableAsync(client, connectionStringProperties.EntityPath, message => + // Act + await sender.SendMessageAsync(order, correlation, logger, options => { - var actual = message.Body.ToObjectFromJson(); - Assert.Equal(order.Id, actual.Id); - - Assert.Equal(correlation.TransactionId, message.ApplicationProperties[transactionIdPropertyName]); - Assert.Equal(dependencyId, message.ApplicationProperties[upstreamServicePropertyName]); + options.TransactionIdPropertyName = transactionIdPropertyName; + options.UpstreamServicePropertyName = upstreamServicePropertyName; + options.GenerateDependencyId = () => dependencyId; + options.AddTelemetryContext(telemetryContext); }); } + + // Assert + string logMessage = Assert.Single(logger.Messages); + Assert.Contains("Dependency", logMessage); + Assert.Matches($"with ID {dependencyId}", logMessage); + Assert.Contains(key, logMessage); + Assert.Contains(value, logMessage); + await RetryAssertUntilServiceBusMessageIsAvailableAsync(client, QueueName, message => + { + var actual = message.Body.ToObjectFromJson(); + Assert.Equal(order.Id, actual.Id); + + Assert.Equal(correlation.TransactionId, message.ApplicationProperties[transactionIdPropertyName]); + Assert.Equal(dependencyId, message.ApplicationProperties[upstreamServicePropertyName]); + }); } private static MessageCorrelationInfo GenerateMessageCorrelationInfo() diff --git a/src/Arcus.Messaging.Tests.Integration/appsettings.json b/src/Arcus.Messaging.Tests.Integration/appsettings.json index 8fd649fd..6832e6d7 100644 --- a/src/Arcus.Messaging.Tests.Integration/appsettings.json +++ b/src/Arcus.Messaging.Tests.Integration/appsettings.json @@ -1,81 +1,35 @@ { "Arcus": { - "Health": { - "Port": "#{Arcus.Health.Port.Queue}#" - }, "Infra": { - "ServiceBus": { - "TopicName": "#{Arcus.TestInfra.ServiceBus.Topic.Name}#", - "ConnectionString": "#{Arcus.TestInfra.ServiceBus.Topic.ConnectionString}#" - }, - "EventGrid": { - "TopicUri": "#{Arcus.TestInfra.EventGrid.Topic.Uri}#", - "AuthKey": "#{Arcus.TestInfra.EventGrid.Auth.Key}#" - }, + "SubscriptionId": "#{Arcus.Infra.SubscriptionId}#", "TenantId": "#{Arcus.Infra.TenantId}#", + "ResourceGroup": { + "Name": "#{Arcus.Messaging.ResourceGroup.Name}#" + }, "ServicePrincipal": { "ClientId": "#{Arcus.Infra.ServicePrincipal.ClientId}#", + "ObjectId": "#{Arcus.Infra.ServicePrincipal.ObjectId}#", "ClientSecret": "#{Arcus.Infra.ServicePrincipal.ClientSecret}#" } }, + + "StorageAccount": { + "Name": "#{Arcus.Messaging.StorageAccount.Name}#", + "Key": "#{Arcus.Messaging.StorageAccount.Key}#" + }, + + "KeyVault": { + "Name": "#{Arcus.Messaging.KeyVault.Name}#" + }, + "ServiceBus": { - "Docker": { - "ConnectionStringWithQueue": "#{Arcus.ServiceBus.Docker.ConnectionStringWithQueue}#", - "ConnectionStringWithTopic": "#{Arcus.ServiceBus.Docker.ConnectionStringWithTopic}#", - "NamespaceConnectionString": "#{Arcus.ServiceBus.Docker.AzureFunctions.NamespaceConnectionString}#", - "AzureFunctions": { - "ConnectionStringWithQueue": "#{Arcus.ServiceBus.Docker.AzureFunctions.ConnectionStringWithQueue}#", - "ConnectionStringWithTopic": "#{Arcus.ServiceBus.Docker.AzureFunctions.ConnectionStringWithTopic}#" - } - }, - "SelfContained": { - "ConnectionStringWithQueue": "#{Arcus.ServiceBus.ConnectionStringWithQueue}#", - "ConnectionStringWithTopic": "#{Arcus.ServiceBus.ConnectionStringWithTopic}#" - } + "Namespace": "#{Arcus.Messaging.ServiceBus.Namespace}#", + "ConnectionString": "#{Arcus.Messaging.ServiceBus.ConnectionString}#" }, + "EventHubs": { - "ConnectionString": "#{Arcus.EventHubs.ConnectionString}#", - "BlobStorage": { - "StorageAccountConnectionString": "#{Arcus.EventHubs.BlobStorage.StorageAccountConnectionString}#" - }, - "SelfContained": { - "EventHubsName": "#{Arcus.EventHubs.SelfContained.EventHubsName}#" - }, - "Docker": { - "EventHubsName": "#{Arcus.EventHubs.Docker.EventHubsName}#", - "AzureFunctions": { - "Isolated": { - "EventHubsName": "#{Arcus.EventHubs.Docker.AzureFunctions.Isolated.EventHubsName}#" - }, - "InProcess": { - "EventHubsName": "#{Arcus.EventHubs.Docker.AzureFunctions.InProcess.EventHubsName}#" - } - } - } - }, - "KeyRotation": { - "ServicePrincipal": { - "ClientId": "#{Arcus.KeyRotation.ServicePrincipal.ClientId}#", - "ClientSecret": "#{Arcus.KeyRotation.ServicePrincipal.ClientSecret}#", - "ClientSecretKey": "#{Arcus.KeyRotation.ServicePrincipal.ClientSecretKey}#" - }, - "ServiceBus": { - "ResourceGroupName": "#{Arcus.KeyRotation.ServiceBus.ResourceGroupName}#", - "TenantId": "#{Arcus.KeyRotation.ServiceBus.TenantId}#", - "SubscriptionId": "#{Arcus.KeyRotation.ServiceBus.SubscriptionId}#", - "Namespace": "#{Arcus.KeyRotation.ServiceBus.Namespace}#", - "QueueName": "#{Arcus.KeyRotation.ServiceBus.QueueName}#", - "TopicName": "#{Arcus.KeyRotation.ServiceBus.TopicName}#", - "AuthorizationRuleName": "#{Arcus.KeyRotation.ServiceBus.AuthorizationRuleName}#" - }, - "KeyVault": { - "VaultUri": "#{Arcus.KeyRotation.KeyVault.VaultUri}#", - "ConnectionStringSecretName": "#{Arcus.KeyRotation.KeyVault.ConnectionStringSecretName}#", - "SecretNewVersionCreated": { - "ServiceBusConnectionStringWithTopicEndpoint": "#{Arcus.KeyRotation.KeyVault.SecretNewVersionCreated.ServiceBusConnectionStringWithTopicEndpoint}#" - } - } - } - }, - "Build.SourcesDirectory": "#{Build.SourcesDirectory}#" + "Namespace": "#{Arcus.Messaging.EventHubs.Namespace}#", + "ConnectionString": "#{Arcus.Messaging.EventHubs.ConnectionString}#" + } + } } diff --git a/src/Arcus.Messaging.Tests.Unit/Arcus.Messaging.Tests.Unit.csproj b/src/Arcus.Messaging.Tests.Unit/Arcus.Messaging.Tests.Unit.csproj index 24e189d2..e7690bb1 100644 --- a/src/Arcus.Messaging.Tests.Unit/Arcus.Messaging.Tests.Unit.csproj +++ b/src/Arcus.Messaging.Tests.Unit/Arcus.Messaging.Tests.Unit.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Arcus.Messaging.Tests.Unit/MessageHandling/MessageRouterTests.cs b/src/Arcus.Messaging.Tests.Unit/MessageHandling/MessageRouterTests.cs index 772e3d31..e2158796 100644 --- a/src/Arcus.Messaging.Tests.Unit/MessageHandling/MessageRouterTests.cs +++ b/src/Arcus.Messaging.Tests.Unit/MessageHandling/MessageRouterTests.cs @@ -8,15 +8,9 @@ using Arcus.Messaging.Tests.Core.Messages.v1; using Arcus.Messaging.Tests.Core.Messages.v2; using Arcus.Messaging.Tests.Unit.Fixture; -using Arcus.Observability.Telemetry.Core; -using Arcus.Testing.Logging; -using Azure.Messaging.ServiceBus; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; -using Serilog; -using Serilog.Events; using Xunit; using Order = Arcus.Messaging.Tests.Core.Messages.v1.Order; diff --git a/src/Arcus.Messaging.sln b/src/Arcus.Messaging.sln index 16415d78..c79cb62d 100644 --- a/src/Arcus.Messaging.sln +++ b/src/Arcus.Messaging.sln @@ -19,12 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.ServiceBus. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Abstractions", "Arcus.Messaging.Abstractions\Arcus.Messaging.Abstractions.csproj", "{B53BF9F5-AC72-4462-87DE-C19CA7C74C9D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Tests.Workers.ServiceBus.Queue", "Arcus.Messaging.Tests.Workers.ServiceBus.Queue\Arcus.Messaging.Tests.Workers.ServiceBus.Queue.csproj", "{799D5D4C-2EED-400B-BF6B-134E8722B628}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Tests.Workers", "Arcus.Messaging.Tests.Workers\Arcus.Messaging.Tests.Workers.csproj", "{3C4C0426-284D-4279-8A68-B1B396EFEC57}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Tests.Workers.ServiceBus.Topic", "Arcus.Messaging.Tests.Workers.ServiceBus.Topic\Arcus.Messaging.Tests.Workers.ServiceBus.Topic.csproj", "{0BAF4A9C-8F6B-4CF9-B654-95629C2B38DF}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Tests.Unit", "Arcus.Messaging.Tests.Unit\Arcus.Messaging.Tests.Unit.csproj", "{9EED9AD7-B69D-45D5-870F-D4F63A1C3495}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Tests.Core", "Arcus.Messaging.Tests.Core\Arcus.Messaging.Tests.Core.csproj", "{55DE6D12-4C54-4570-BDFA-00B0FFDE5AB6}" @@ -33,8 +29,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Abstraction EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.AzureFunctions.ServiceBus", "Arcus.Messaging.AzureFunctions.ServiceBus\Arcus.Messaging.AzureFunctions.ServiceBus.csproj", "{7386C41A-6F27-42B7-BBC1-B6CB41200A68}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Tests.Runtimes.AzureFunction.ServiceBus.Queue", "Arcus.Messaging.Tests.Runtimes.AzureFunction.ServiceBus.Queue\Arcus.Messaging.Tests.Runtimes.AzureFunction.ServiceBus.Queue.csproj", "{903E1D94-3157-47BB-9642-8B92755AB8B5}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Pumps.EventHubs", "Arcus.Messaging.Pumps.EventHubs\Arcus.Messaging.Pumps.EventHubs.csproj", "{F5A14425-FEF5-425D-875C-2601C988E6FA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Abstractions.EventHubs", "Arcus.Messaging.Abstractions.EventHubs\Arcus.Messaging.Abstractions.EventHubs.csproj", "{040DE2D5-0400-4B4A-A333-60C45CBB0204}" @@ -45,20 +39,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServiceBus", "ServiceBus", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EventHubs", "EventHubs", "{31250C96-0F22-482F-B3D7-127898A28CF3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Tests.Workers.EventHubs", "Arcus.Messaging.Tests.Workers.EventHubs\Arcus.Messaging.Tests.Workers.EventHubs.csproj", "{D98DF5E5-1701-43B4-AC84-2F28D3AE1176}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Tests.Workers.ServiceBus", "Arcus.Messaging.Tests.Workers.ServiceBus\Arcus.Messaging.Tests.Workers.ServiceBus.csproj", "{F1B24FD4-099E-489D-B45D-A1A992CA5C3A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Tests.Workers.EventHubs.Core", "Arcus.Messaging.Tests.Workers.EventHubs.Core\Arcus.Messaging.Tests.Workers.EventHubs.Core.csproj", "{0D6C38A7-B684-43BA-9BCF-1EF4C1A943CD}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.Tests.Runtimes.AzureFunction.EventHubs", "Arcus.Messaging.Tests.Runtimes.AzureFunction.EventHubs\Arcus.Messaging.Tests.Runtimes.AzureFunction.EventHubs.csproj", "{6FE22274-0A2D-4AAE-BF64-968987AD7435}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Arcus.Messaging.Tests.Runtimes.AzureFunction.ServiceBus.Topic", "Arcus.Messaging.Tests.Runtimes.AzureFunction.ServiceBus.Topic\Arcus.Messaging.Tests.Runtimes.AzureFunction.ServiceBus.Topic.csproj", "{728C9611-3B33-4751-8D75-5ABB44041531}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Messaging.AzureFunctions.EventHubs", "Arcus.Messaging.AzureFunctions.EventHubs\Arcus.Messaging.AzureFunctions.EventHubs.csproj", "{619AA74F-CDE4-48F6-B6E4-4B7C191CCB1B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Arcus.Messaging.Tests.Runtimes.AzureFunction.EventHubs.InProcess", "Arcus.Messaging.Tests.Runtimes.AzureFunction.EventHubs.InProcess\Arcus.Messaging.Tests.Runtimes.AzureFunction.EventHubs.InProcess.csproj", "{4466DA72-0636-481F-A186-D87735679582}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -93,18 +79,10 @@ Global {B53BF9F5-AC72-4462-87DE-C19CA7C74C9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {B53BF9F5-AC72-4462-87DE-C19CA7C74C9D}.Release|Any CPU.ActiveCfg = Release|Any CPU {B53BF9F5-AC72-4462-87DE-C19CA7C74C9D}.Release|Any CPU.Build.0 = Release|Any CPU - {799D5D4C-2EED-400B-BF6B-134E8722B628}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {799D5D4C-2EED-400B-BF6B-134E8722B628}.Debug|Any CPU.Build.0 = Debug|Any CPU - {799D5D4C-2EED-400B-BF6B-134E8722B628}.Release|Any CPU.ActiveCfg = Release|Any CPU - {799D5D4C-2EED-400B-BF6B-134E8722B628}.Release|Any CPU.Build.0 = Release|Any CPU {3C4C0426-284D-4279-8A68-B1B396EFEC57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3C4C0426-284D-4279-8A68-B1B396EFEC57}.Debug|Any CPU.Build.0 = Debug|Any CPU {3C4C0426-284D-4279-8A68-B1B396EFEC57}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C4C0426-284D-4279-8A68-B1B396EFEC57}.Release|Any CPU.Build.0 = Release|Any CPU - {0BAF4A9C-8F6B-4CF9-B654-95629C2B38DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0BAF4A9C-8F6B-4CF9-B654-95629C2B38DF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0BAF4A9C-8F6B-4CF9-B654-95629C2B38DF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0BAF4A9C-8F6B-4CF9-B654-95629C2B38DF}.Release|Any CPU.Build.0 = Release|Any CPU {9EED9AD7-B69D-45D5-870F-D4F63A1C3495}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9EED9AD7-B69D-45D5-870F-D4F63A1C3495}.Debug|Any CPU.Build.0 = Debug|Any CPU {9EED9AD7-B69D-45D5-870F-D4F63A1C3495}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -121,10 +99,6 @@ Global {7386C41A-6F27-42B7-BBC1-B6CB41200A68}.Debug|Any CPU.Build.0 = Debug|Any CPU {7386C41A-6F27-42B7-BBC1-B6CB41200A68}.Release|Any CPU.ActiveCfg = Release|Any CPU {7386C41A-6F27-42B7-BBC1-B6CB41200A68}.Release|Any CPU.Build.0 = Release|Any CPU - {903E1D94-3157-47BB-9642-8B92755AB8B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {903E1D94-3157-47BB-9642-8B92755AB8B5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {903E1D94-3157-47BB-9642-8B92755AB8B5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {903E1D94-3157-47BB-9642-8B92755AB8B5}.Release|Any CPU.Build.0 = Release|Any CPU {F5A14425-FEF5-425D-875C-2601C988E6FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F5A14425-FEF5-425D-875C-2601C988E6FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {F5A14425-FEF5-425D-875C-2601C988E6FA}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -137,10 +111,6 @@ Global {05EA4B3A-C619-44BB-88C4-F067FCCF83AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {05EA4B3A-C619-44BB-88C4-F067FCCF83AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {05EA4B3A-C619-44BB-88C4-F067FCCF83AA}.Release|Any CPU.Build.0 = Release|Any CPU - {D98DF5E5-1701-43B4-AC84-2F28D3AE1176}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D98DF5E5-1701-43B4-AC84-2F28D3AE1176}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D98DF5E5-1701-43B4-AC84-2F28D3AE1176}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D98DF5E5-1701-43B4-AC84-2F28D3AE1176}.Release|Any CPU.Build.0 = Release|Any CPU {F1B24FD4-099E-489D-B45D-A1A992CA5C3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F1B24FD4-099E-489D-B45D-A1A992CA5C3A}.Debug|Any CPU.Build.0 = Debug|Any CPU {F1B24FD4-099E-489D-B45D-A1A992CA5C3A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -149,22 +119,10 @@ Global {0D6C38A7-B684-43BA-9BCF-1EF4C1A943CD}.Debug|Any CPU.Build.0 = Debug|Any CPU {0D6C38A7-B684-43BA-9BCF-1EF4C1A943CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {0D6C38A7-B684-43BA-9BCF-1EF4C1A943CD}.Release|Any CPU.Build.0 = Release|Any CPU - {6FE22274-0A2D-4AAE-BF64-968987AD7435}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6FE22274-0A2D-4AAE-BF64-968987AD7435}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6FE22274-0A2D-4AAE-BF64-968987AD7435}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6FE22274-0A2D-4AAE-BF64-968987AD7435}.Release|Any CPU.Build.0 = Release|Any CPU - {728C9611-3B33-4751-8D75-5ABB44041531}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {728C9611-3B33-4751-8D75-5ABB44041531}.Debug|Any CPU.Build.0 = Debug|Any CPU - {728C9611-3B33-4751-8D75-5ABB44041531}.Release|Any CPU.ActiveCfg = Release|Any CPU - {728C9611-3B33-4751-8D75-5ABB44041531}.Release|Any CPU.Build.0 = Release|Any CPU {619AA74F-CDE4-48F6-B6E4-4B7C191CCB1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {619AA74F-CDE4-48F6-B6E4-4B7C191CCB1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {619AA74F-CDE4-48F6-B6E4-4B7C191CCB1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {619AA74F-CDE4-48F6-B6E4-4B7C191CCB1B}.Release|Any CPU.Build.0 = Release|Any CPU - {4466DA72-0636-481F-A186-D87735679582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4466DA72-0636-481F-A186-D87735679582}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4466DA72-0636-481F-A186-D87735679582}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4466DA72-0636-481F-A186-D87735679582}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -174,24 +132,17 @@ Global {49D85A35-F341-47A3-887F-68DEC06CEBCA} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} {B21CD154-120D-49C5-AC2B-AC3ABDE97641} = {2CD090E7-7306-49A0-9680-6ED78CFECAE1} {71FCD6E8-B586-4F4B-AA26-0C2F698B837E} = {2CD090E7-7306-49A0-9680-6ED78CFECAE1} - {799D5D4C-2EED-400B-BF6B-134E8722B628} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} {3C4C0426-284D-4279-8A68-B1B396EFEC57} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} - {0BAF4A9C-8F6B-4CF9-B654-95629C2B38DF} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} {9EED9AD7-B69D-45D5-870F-D4F63A1C3495} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} {55DE6D12-4C54-4570-BDFA-00B0FFDE5AB6} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} {864C12DF-DE3D-421F-8687-EC3918FFB8BE} = {2CD090E7-7306-49A0-9680-6ED78CFECAE1} {7386C41A-6F27-42B7-BBC1-B6CB41200A68} = {2CD090E7-7306-49A0-9680-6ED78CFECAE1} - {903E1D94-3157-47BB-9642-8B92755AB8B5} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} {F5A14425-FEF5-425D-875C-2601C988E6FA} = {31250C96-0F22-482F-B3D7-127898A28CF3} {040DE2D5-0400-4B4A-A333-60C45CBB0204} = {31250C96-0F22-482F-B3D7-127898A28CF3} {05EA4B3A-C619-44BB-88C4-F067FCCF83AA} = {31250C96-0F22-482F-B3D7-127898A28CF3} - {D98DF5E5-1701-43B4-AC84-2F28D3AE1176} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} {F1B24FD4-099E-489D-B45D-A1A992CA5C3A} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} {0D6C38A7-B684-43BA-9BCF-1EF4C1A943CD} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} - {6FE22274-0A2D-4AAE-BF64-968987AD7435} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} {619AA74F-CDE4-48F6-B6E4-4B7C191CCB1B} = {31250C96-0F22-482F-B3D7-127898A28CF3} - {4466DA72-0636-481F-A186-D87735679582} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} - {728C9611-3B33-4751-8D75-5ABB44041531} = {A1369CCD-42D1-43F6-98BC-D8EDA62C2B13} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {066FD85A-3DDE-4615-B550-BF67ACCDAA51}