diff --git a/.gitignore b/.gitignore index f46d8166..d0fd8f93 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* +tests-results/ # NUnit *.VisualState.xml diff --git a/DockerfileSolutionRestore.txt b/DockerfileSolutionRestore.txt index 0d5f7e3e..3df29144 100644 Binary files a/DockerfileSolutionRestore.txt and b/DockerfileSolutionRestore.txt differ diff --git a/README.md b/README.md index 1e6852b2..c570d483 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,30 @@ Note that it will take a little while to start all containers. eShopOnDapr inclu When all microservices are healthy, you can navigate to http://localhost:5104 to view the eShopOnDapr UI. +#### Unit and integration testing + +The tests in eShopOnDapr are structured in the following structure, per type: + +- Tests per microservice + + - Unit Tests + - Functional/Integration Tests + + To run the tests per microservice from the CLI, run the following command from the root folder: + + ```terminal + docker compose -f docker-compose-tests.yml -f docker-compose-tests.override.yml up + ``` + +- Global application tests + - Microservices Functional/Integration Tests across the whole application + + To run the global application test from the CLI, run the following command from the root folder: + + ```terminal + docker compose -f docker-compose-integration-tests.yml -f docker-compose-integration-tests.override.yml up + ``` + ### Attributions Model photo by [Angelo Pantazis](https://unsplash.com/@angelopantazis?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) diff --git a/dapr/components-test/eshop-email.yaml b/dapr/components-test/eshop-email.yaml new file mode 100644 index 00000000..c2908012 --- /dev/null +++ b/dapr/components-test/eshop-email.yaml @@ -0,0 +1,27 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: sendmail + namespace: eshop +spec: + type: bindings.smtp + version: v1 + metadata: + - name: host + value: maildev-test + - name: port + value: 1025 + - name: user + secretKeyRef: + name: Smtp:User + key: Smtp:User + - name: password + secretKeyRef: + name: Smtp:Password + key: Smtp:Password + - name: skipTLSVerify + value: true +auth: + secretStore: eshopondapr-secretstore +scopes: +- ordering-api-test \ No newline at end of file diff --git a/dapr/components-test/eshop-pubsub.yaml b/dapr/components-test/eshop-pubsub.yaml new file mode 100644 index 00000000..5e5aaa50 --- /dev/null +++ b/dapr/components-test/eshop-pubsub.yaml @@ -0,0 +1,21 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: eshopondapr-pubsub + namespace: eshop +spec: + type: pubsub.rabbitmq + version: v1 + metadata: + - name: host + value: "amqp://rabbitmq-test:5672" + - name: durable + value: "false" + - name: deletedWhenUnused + value: "false" + - name: autoAck + value: "false" + - name: reconnectWait + value: "0" + - name: concurrency + value: parallel diff --git a/dapr/components-test/eshop-secrets.json b/dapr/components-test/eshop-secrets.json new file mode 100644 index 00000000..3823b054 --- /dev/null +++ b/dapr/components-test/eshop-secrets.json @@ -0,0 +1,14 @@ +{ + "ConnectionStrings": { + "CatalogDB": "Server=sqldata-test;Database=Microsoft.eShopOnDapr.Services.CatalogDb;User Id=sa;Password=Pass@word;TrustServerCertificate=true", + "IdentityDB": "Server=sqldata-test;Database=Microsoft.eShopOnDapr.Service.IdentityDb;User Id=sa;Password=Pass@word;TrustServerCertificate=true", + "OrderingDB": "Server=sqldata-test;Database=Microsoft.eShopOnDapr.Services.OrderingDb;User Id=sa;Password=Pass@word;TrustServerCertificate=true" + }, + "Smtp": { + "User": "_username", + "Password": "_password" + }, + "State": { + "RedisPassword": "" + } +} diff --git a/dapr/components-test/eshop-secretstore.yaml b/dapr/components-test/eshop-secretstore.yaml new file mode 100644 index 00000000..94fbaf42 --- /dev/null +++ b/dapr/components-test/eshop-secretstore.yaml @@ -0,0 +1,13 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: eshopondapr-secretstore + namespace: eshop +spec: + type: secretstores.local.file + version: v1 + metadata: + - name: secretsFile + value: /components/eshop-secrets.json + - name: nestedSeparator + value: ":" diff --git a/dapr/components-test/eshop-statestore.yaml b/dapr/components-test/eshop-statestore.yaml new file mode 100644 index 00000000..5207e860 --- /dev/null +++ b/dapr/components-test/eshop-statestore.yaml @@ -0,0 +1,22 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: eshopondapr-statestore + namespace: eshop +spec: + type: state.redis + version: v1 + metadata: + - name: redisHost + value: redis-test:6379 + - name: redisPassword + secretKeyRef: + name: State:RedisPassword + key: State:RedisPassword + - name: actorStateStore + value: "true" +auth: + secretStore: eshopondapr-secretstore +scopes: +- basket-api-test +- ordering-api-test \ No newline at end of file diff --git a/docker-compose-integration-tests.override.yml b/docker-compose-integration-tests.override.yml new file mode 100644 index 00000000..35862912 --- /dev/null +++ b/docker-compose-integration-tests.override.yml @@ -0,0 +1,84 @@ +version: '3.4' + +services: + maildev-test: + ports: + - "5500:1080" + - "1025:1025" + + rabbitmq-test: + ports: + - "5672:5672" + + redis-test: + ports: + - "5379:6379" + + sqldata-test: + environment: + - SA_PASSWORD=Pass@word + - ACCEPT_EULA=Y + ports: + - "5433:1433" + + integration-test: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_BASKET_URLS=http://0.0.0.0:81 + - ASPNETCORE_BASKET_DAPR_HTTP_ENDPOINT=http://127.0.0.1:3501 + - ASPNETCORE_BASKET_DAPR_GRPC_ENDPOINT=http://127.0.0.1:50002 + - ASPNETCORE_ORDERING_URLS=http://0.0.0.0:82 + - ASPNETCORE_ORDERING_DAPR_HTTP_ENDPOINT=http://127.0.0.1:3502 + - ASPNETCORE_ORDERING_DAPR_GRPC_ENDPOINT=http://127.0.0.1:50003 + - SeqServerUrl=http://seq-test + - RetryMigrations=true + - IssuerUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 + - BlazorClientUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5104 + - BasketApiUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103 + - OrderingApiUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102 + - ShoppingAggregatorApiUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5121 + - IdentityUrl=http://identity-api-test + - Serilog__MinimumLevel__Override__Microsoft=Information + entrypoint: + - dotnet + - test + - --logger + - trx;LogFileName=/tests/integration-test-results.xml + + basket-api-dapr-integration-test: + command: ["./daprd", + "-app-id", "basket-api-test", + "-app-port", "81", + "-dapr-http-port", "3501", + "-dapr-grpc-port", "50002", + "-metrics-port", "9091", + "-log-level", "debug", + "--enable-api-logging", + "-components-path", "/components", + "-config", "/configuration/eshop-config.yaml" + ] + volumes: + - "./dapr/components-test/:/components" + - "./dapr/configuration/:/configuration" + + ordering-api-dapr-integration-test: + command: ["./daprd", + "-app-id", "ordering-api-test", + "-app-port", "82", + "-dapr-http-port", "3502", + "-dapr-grpc-port", "50003", + "-metrics-port", "9092", + "-log-level", "debug", + "--enable-api-logging", + "-placement-host-address", "dapr-placement-test:50000", + "-components-path", "/components", + "-config", "/configuration/eshop-config.yaml" + ] + volumes: + - "./dapr/components-test/:/components" + - "./dapr/configuration/:/configuration" + + dapr-placement-test: + command: ["./placement", "-port", "50000", "-log-level", "debug"] + ports: + - "50000:50000" \ No newline at end of file diff --git a/docker-compose-integration-tests.yml b/docker-compose-integration-tests.yml new file mode 100644 index 00000000..85924fae --- /dev/null +++ b/docker-compose-integration-tests.yml @@ -0,0 +1,40 @@ +version: '3.4' + +services: + maildev-test: + image: maildev/maildev:latest + + rabbitmq-test: + image: rabbitmq:3-management-alpine + + redis-test: + image: redis:alpine + + sqldata-test: + image: mcr.microsoft.com/azure-sql-edge + + integration-test: + image: ${REGISTRY:-eshopdapr}/integration-test:${TAG:-latest} + build: + context: . + dockerfile: src/Tests/Services/Application.FunctionalTests/Dockerfile + depends_on: + - sqldata-test + - redis-test + volumes: + - ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests + + basket-api-dapr-integration-test: + image: "daprio/daprd:1.9.4" + network_mode: "service:integration-test" + depends_on: + - integration-test + + ordering-api-dapr-integration-test: + image: "daprio/daprd:1.9.4" + network_mode: "service:integration-test" + depends_on: + - integration-test + + dapr-placement-test: + image: "daprio/dapr:1.9.4" \ No newline at end of file diff --git a/docker-compose-tests.override.yml b/docker-compose-tests.override.yml new file mode 100644 index 00000000..921ae75f --- /dev/null +++ b/docker-compose-tests.override.yml @@ -0,0 +1,117 @@ +version: '3.4' + +services: + rabbitmq-test: + ports: + - "5672:5672" + + redis-test: + ports: + - "5379:6379" + + sqldata-test: + environment: + - SA_PASSWORD=Pass@word + - ACCEPT_EULA=Y + ports: + - "5433:1433" + + basket-api-functional-test: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:80 + - IdentityUrl=http://identity-api-test + - IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 + - SeqServerUrl=http://seq-test + - SuppressCheckForUnhandledSecurityMetadata=true + - IsTest=true + entrypoint: + - dotnet + - test + - --logger + - trx;LogFileName=/tests/basket-api-functional-test-results.xml + + basket-api-dapr-test: + command: ["./daprd", + "-app-id", "basket-api-test", + "-app-port", "80", + "-log-level", "debug", + "-components-path", "/components", + "-config", "/configuration/eshop-config.yaml" + ] + volumes: + - "./dapr/components-test/:/components" + - "./dapr/configuration/:/configuration" + + basket-api-unit-test: + entrypoint: + - dotnet + - test + - --logger + - trx;LogFileName=/tests/basket-api-unit-test-results.xml + + catalog-api-functional-test: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:80 + - RetryMigrations=true + - SeqServerUrl=http://seq-test + entrypoint: + - dotnet + - test + - --logger + - trx;LogFileName=/tests/catalog-api-functional-test-results.xml + + catalog-api-dapr-test: + command: ["./daprd", + "-app-id", "catalog-api-test", + "-app-port", "80", + "-log-level", "debug", + "-components-path", "/components", + "-config", "/configuration/eshop-config.yaml" + ] + volumes: + - "./dapr/components-test/:/components" + - "./dapr/configuration/:/configuration" + + catalog-api-unit-test: + entrypoint: + - dotnet + - test + - --logger + - trx;LogFileName=/tests/catalog-api-unit-test-results.xml + + ordering-api-functional-test: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:80 + - IdentityUrl=http://identity-api-test + - IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 + - RetryMigrations=true + - SeqServerUrl=http://seq-test + - SuppressCheckForUnhandledSecurityMetadata=true + - IsTest=true + entrypoint: + - dotnet + - test + - --logger + - trx;LogFileName=/tests/ordering-api-functional-test-results.xml + + ordering-api-dapr-test: + command: ["./daprd", + "-app-id", "ordering-api-test", + "-app-port", "80", + "-log-level", "debug", + "-components-path", "/components", + "-config", "/configuration/eshop-config.yaml" + ] + volumes: + - "./dapr/components-test/:/components" + - "./dapr/configuration/:/configuration" + + ordering-api-unit-test: + entrypoint: + - dotnet + - test + - --logger + - trx;LogFileName=/tests/ordering-api-unit-test-results.xml \ No newline at end of file diff --git a/docker-compose-tests.yml b/docker-compose-tests.yml new file mode 100644 index 00000000..6b70afa2 --- /dev/null +++ b/docker-compose-tests.yml @@ -0,0 +1,90 @@ +version: '3.4' + +services: + rabbitmq-test: + image: rabbitmq:3-management-alpine + + redis-test: + image: redis:alpine + + sqldata-test: + image: mcr.microsoft.com/azure-sql-edge + + basket-api-functional-test: + image: ${REGISTRY:-eshopdapr}/basket-api-test:${TAG:-latest} + build: + context: . + dockerfile: src/Services/Basket/Basket.API/Dockerfile + target: functionaltest + depends_on: + - sqldata-test + - redis-test + volumes: + - ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests + + basket-api-dapr-test: + image: "daprio/daprd:1.9.4" + network_mode: "service:basket-api-functional-test" + depends_on: + - basket-api-functional-test + + basket-api-unit-test: + image: ${REGISTRY:-eshopdapr}/basket-api-unit-test:${TAG:-latest} + build: + context: . + dockerfile: src/Services/Basket/Basket.API/Dockerfile + target: unittest + volumes: + - ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests + + catalog-api-functional-test: + image: ${REGISTRY:-eshopdapr}/catalog-api-test:${TAG:-latest} + build: + context: . + dockerfile: src/Services/Catalog/Catalog.API/Dockerfile + target: functionaltest + depends_on: + - sqldata-test + volumes: + - ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests + + catalog-api-dapr-test: + image: "daprio/daprd:1.9.4" + network_mode: "service:catalog-api-functional-test" + depends_on: + - catalog-api-functional-test + + catalog-api-unit-test: + image: ${REGISTRY:-eshopdapr}/catalog-unit-test:${TAG:-latest} + build: + context: . + dockerfile: src/Services/Catalog/Catalog.API/Dockerfile + target: unittest + volumes: + - ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests + + ordering-api-functional-test: + image: ${REGISTRY:-eshopdapr}/ordering-api-test:${TAG:-latest} + build: + context: . + dockerfile: src/Services/Ordering/Ordering.API/Dockerfile + target: functionaltest + depends_on: + - sqldata-test + volumes: + - ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests + + ordering-api-dapr-test: + image: "daprio/daprd:1.9.4" + network_mode: "service:ordering-api-functional-test" + depends_on: + - ordering-api-functional-test + + ordering-api-unit-test: + image: ${REGISTRY:-eshopdapr}/ordering-unit-test:${TAG:-latest} + build: + context: . + dockerfile: src/Services/Ordering/Ordering.API/Dockerfile + target: unittest + volumes: + - ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests \ No newline at end of file diff --git a/eShopOnDapr.sln b/eShopOnDapr.sln index a5fd39e9..eee92c50 100644 --- a/eShopOnDapr.sln +++ b/eShopOnDapr.sln @@ -85,6 +85,30 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Healthchecks", "src\Buildin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebStatus", "src\Web\WebStatus\WebStatus.csproj", "{1489AC81-8724-4077-9241-D0384BA0CB6C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{A5E6DFFF-326A-4E61-B6F2-2D687F016C95}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Basket.UnitTests", "src\Services\Basket\Basket.UnitTests\Basket.UnitTests.csproj", "{72AF2D18-ECAC-4CC5-9540-64B60E07FB8E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Basket.FunctionalTests", "src\Services\Basket\Basket.FunctionalTests\Basket.FunctionalTests.csproj", "{E6B8BBD4-A3D4-4E8B-9760-85389340272C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{35591FC2-D97E-4B24-85BD-5B1AAC820D17}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Catalog.UnitTests", "src\Services\Catalog\Catalog.UnitTests\Catalog.UnitTests.csproj", "{A37AE278-11A6-4694-886D-C42AE8470094}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Catalog.FunctionalTests", "src\Services\Catalog\Catalog.FunctionalTests\Catalog.FunctionalTests.csproj", "{9B6AF0F3-956D-442D-B105-F2CFB9E0264F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{A3279718-CBA1-4235-BF37-99E15B8A1534}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ordering.UnitTests", "src\Services\Ordering\Ordering.UnitTests\Ordering.UnitTests.csproj", "{60CF84FF-41E8-4B99-9410-D4A43F1FC7B7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ordering.FunctionalTests", "src\Services\Ordering\Ordering.FunctionalTests\Ordering.FunctionalTests.csproj", "{C0F48240-D82A-4748-A3CB-6780C5166938}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{DDA8BAB3-C7DA-4EE8-B740-6E8DD05D906C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServiceTests", "ServiceTests", "{D0A88BCC-B318-4676-8AC7-D44A58537658}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application.FunctionalTests", "src\Tests\Services\Application.FunctionalTests\Application.FunctionalTests.csproj", "{5A336D27-22D7-4D12-BF12-38BCA5E8E2BB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -139,6 +163,34 @@ Global {1489AC81-8724-4077-9241-D0384BA0CB6C}.Debug|Any CPU.Build.0 = Debug|Any CPU {1489AC81-8724-4077-9241-D0384BA0CB6C}.Release|Any CPU.ActiveCfg = Release|Any CPU {1489AC81-8724-4077-9241-D0384BA0CB6C}.Release|Any CPU.Build.0 = Release|Any CPU + {72AF2D18-ECAC-4CC5-9540-64B60E07FB8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72AF2D18-ECAC-4CC5-9540-64B60E07FB8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72AF2D18-ECAC-4CC5-9540-64B60E07FB8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72AF2D18-ECAC-4CC5-9540-64B60E07FB8E}.Release|Any CPU.Build.0 = Release|Any CPU + {E6B8BBD4-A3D4-4E8B-9760-85389340272C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6B8BBD4-A3D4-4E8B-9760-85389340272C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6B8BBD4-A3D4-4E8B-9760-85389340272C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6B8BBD4-A3D4-4E8B-9760-85389340272C}.Release|Any CPU.Build.0 = Release|Any CPU + {A37AE278-11A6-4694-886D-C42AE8470094}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A37AE278-11A6-4694-886D-C42AE8470094}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A37AE278-11A6-4694-886D-C42AE8470094}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A37AE278-11A6-4694-886D-C42AE8470094}.Release|Any CPU.Build.0 = Release|Any CPU + {9B6AF0F3-956D-442D-B105-F2CFB9E0264F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B6AF0F3-956D-442D-B105-F2CFB9E0264F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B6AF0F3-956D-442D-B105-F2CFB9E0264F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B6AF0F3-956D-442D-B105-F2CFB9E0264F}.Release|Any CPU.Build.0 = Release|Any CPU + {60CF84FF-41E8-4B99-9410-D4A43F1FC7B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60CF84FF-41E8-4B99-9410-D4A43F1FC7B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60CF84FF-41E8-4B99-9410-D4A43F1FC7B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60CF84FF-41E8-4B99-9410-D4A43F1FC7B7}.Release|Any CPU.Build.0 = Release|Any CPU + {C0F48240-D82A-4748-A3CB-6780C5166938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0F48240-D82A-4748-A3CB-6780C5166938}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0F48240-D82A-4748-A3CB-6780C5166938}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0F48240-D82A-4748-A3CB-6780C5166938}.Release|Any CPU.Build.0 = Release|Any CPU + {5A336D27-22D7-4D12-BF12-38BCA5E8E2BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A336D27-22D7-4D12-BF12-38BCA5E8E2BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A336D27-22D7-4D12-BF12-38BCA5E8E2BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A336D27-22D7-4D12-BF12-38BCA5E8E2BB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -170,6 +222,17 @@ Global {60441B2A-69B7-4BD1-B4FE-CCA68DFF4311} = {6E3302E8-41FD-490C-920B-D5929B5DE0D9} {8AA9B06A-8D20-4F3A-BD64-6DE81940B1E7} = {60441B2A-69B7-4BD1-B4FE-CCA68DFF4311} {1489AC81-8724-4077-9241-D0384BA0CB6C} = {EF69A3A6-A184-4C23-B9FB-034DAD2913C6} + {A5E6DFFF-326A-4E61-B6F2-2D687F016C95} = {58D1C64F-B283-45CB-97C9-5BC4F6589CE8} + {72AF2D18-ECAC-4CC5-9540-64B60E07FB8E} = {A5E6DFFF-326A-4E61-B6F2-2D687F016C95} + {E6B8BBD4-A3D4-4E8B-9760-85389340272C} = {A5E6DFFF-326A-4E61-B6F2-2D687F016C95} + {35591FC2-D97E-4B24-85BD-5B1AAC820D17} = {E5D12115-EAF4-43F4-83C8-D8DAE7C17285} + {A37AE278-11A6-4694-886D-C42AE8470094} = {35591FC2-D97E-4B24-85BD-5B1AAC820D17} + {9B6AF0F3-956D-442D-B105-F2CFB9E0264F} = {35591FC2-D97E-4B24-85BD-5B1AAC820D17} + {A3279718-CBA1-4235-BF37-99E15B8A1534} = {6E06245D-F575-4D67-AECE-EEC5D70AE53B} + {60CF84FF-41E8-4B99-9410-D4A43F1FC7B7} = {A3279718-CBA1-4235-BF37-99E15B8A1534} + {C0F48240-D82A-4748-A3CB-6780C5166938} = {A3279718-CBA1-4235-BF37-99E15B8A1534} + {D0A88BCC-B318-4676-8AC7-D44A58537658} = {DDA8BAB3-C7DA-4EE8-B740-6E8DD05D906C} + {5A336D27-22D7-4D12-BF12-38BCA5E8E2BB} = {D0A88BCC-B318-4676-8AC7-D44A58537658} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBED0B95-87F8-439B-BC1E-382C8ABD4F2E} diff --git a/src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Dockerfile b/src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Dockerfile index 43362249..193db40b 100644 --- a/src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Dockerfile +++ b/src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Dockerfile @@ -12,10 +12,17 @@ COPY ["src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Web.Shopping.Http COPY ["src/BuildingBlocks/EventBus/EventBus.csproj", "src/BuildingBlocks/EventBus/"] COPY ["src/BuildingBlocks/Healthchecks/Healthchecks.csproj", "src/BuildingBlocks/Healthchecks/"] COPY ["src/Services/Basket/Basket.API/Basket.API.csproj", "src/Services/Basket/Basket.API/"] +COPY ["src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj", "src/Services/Basket/Basket.FunctionalTests/"] +COPY ["src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj", "src/Services/Basket/Basket.UnitTests/"] COPY ["src/Services/Catalog/Catalog.API/Catalog.API.csproj", "src/Services/Catalog/Catalog.API/"] +COPY ["src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj", "src/Services/Catalog/Catalog.FunctionalTests/"] +COPY ["src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj", "src/Services/Catalog/Catalog.UnitTests/"] COPY ["src/Services/Identity/Identity.API/Identity.API.csproj", "src/Services/Identity/Identity.API/"] COPY ["src/Services/Ordering/Ordering.API/Ordering.API.csproj", "src/Services/Ordering/Ordering.API/"] +COPY ["src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj", "src/Services/Ordering/Ordering.FunctionalTests/"] +COPY ["src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj", "src/Services/Ordering/Ordering.UnitTests/"] COPY ["src/Services/Payment/Payment.API/Payment.API.csproj", "src/Services/Payment/Payment.API/"] +COPY ["src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj", "src/Tests/Services/Application.FunctionalTests/"] COPY ["src/Web/BlazorClient.Host/BlazorClient.Host.csproj", "src/Web/BlazorClient.Host/"] COPY ["src/Web/BlazorClient/BlazorClient.csproj", "src/Web/BlazorClient/"] COPY ["src/Web/WebStatus/WebStatus.csproj", "src/Web/WebStatus/"] diff --git a/src/Services/Basket/Basket.API/Dockerfile b/src/Services/Basket/Basket.API/Dockerfile index 95c42e8c..99753673 100644 --- a/src/Services/Basket/Basket.API/Dockerfile +++ b/src/Services/Basket/Basket.API/Dockerfile @@ -12,22 +12,35 @@ COPY ["src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Web.Shopping.Http COPY ["src/BuildingBlocks/EventBus/EventBus.csproj", "src/BuildingBlocks/EventBus/"] COPY ["src/BuildingBlocks/Healthchecks/Healthchecks.csproj", "src/BuildingBlocks/Healthchecks/"] COPY ["src/Services/Basket/Basket.API/Basket.API.csproj", "src/Services/Basket/Basket.API/"] +COPY ["src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj", "src/Services/Basket/Basket.FunctionalTests/"] +COPY ["src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj", "src/Services/Basket/Basket.UnitTests/"] COPY ["src/Services/Catalog/Catalog.API/Catalog.API.csproj", "src/Services/Catalog/Catalog.API/"] +COPY ["src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj", "src/Services/Catalog/Catalog.FunctionalTests/"] +COPY ["src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj", "src/Services/Catalog/Catalog.UnitTests/"] COPY ["src/Services/Identity/Identity.API/Identity.API.csproj", "src/Services/Identity/Identity.API/"] COPY ["src/Services/Ordering/Ordering.API/Ordering.API.csproj", "src/Services/Ordering/Ordering.API/"] +COPY ["src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj", "src/Services/Ordering/Ordering.FunctionalTests/"] +COPY ["src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj", "src/Services/Ordering/Ordering.UnitTests/"] COPY ["src/Services/Payment/Payment.API/Payment.API.csproj", "src/Services/Payment/Payment.API/"] +COPY ["src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj", "src/Tests/Services/Application.FunctionalTests/"] COPY ["src/Web/BlazorClient.Host/BlazorClient.Host.csproj", "src/Web/BlazorClient.Host/"] COPY ["src/Web/BlazorClient/BlazorClient.csproj", "src/Web/BlazorClient/"] COPY ["src/Web/WebStatus/WebStatus.csproj", "src/Web/WebStatus/"] COPY ["docker-compose.dcproj", "./"] COPY ["NuGet.config", "./"] COPY ["eShopOnDapr.sln", "./"] -RUN dotnet restore "src/Services/Basket/Basket.API/Basket.API.csproj" +RUN dotnet restore "eShopOnDapr.sln" COPY . . WORKDIR "/src/src/Services/Basket/Basket.API" # RUN dotnet build "Basket.API.csproj" -c Release -o /app/build +FROM build as unittest +WORKDIR /src/src/Services/Basket/Basket.UnitTests + +FROM build as functionaltest +WORKDIR /src/src/Services/Basket/Basket.FunctionalTests + FROM build AS publish RUN dotnet publish --no-restore "Basket.API.csproj" -c Release -o /app/publish diff --git a/src/Services/Basket/Basket.API/GlobalUsings.cs b/src/Services/Basket/Basket.API/GlobalUsings.cs index 4da62bb9..9b9245ee 100644 --- a/src/Services/Basket/Basket.API/GlobalUsings.cs +++ b/src/Services/Basket/Basket.API/GlobalUsings.cs @@ -1,8 +1,10 @@ -global using Dapr; +global using System.ComponentModel.DataAnnotations; +global using System.IdentityModel.Tokens.Jwt; +global using System.Net; +global using Dapr; global using Dapr.Client; global using HealthChecks.UI.Client; global using Microsoft.AspNetCore.Authorization; -global using Microsoft.AspNetCore.Diagnostics.HealthChecks; global using Microsoft.AspNetCore.Mvc; global using Microsoft.eShopOnDapr.BuildingBlocks.EventBus; global using Microsoft.eShopOnDapr.BuildingBlocks.EventBus.Abstractions; @@ -12,11 +14,9 @@ global using Microsoft.eShopOnDapr.Services.Basket.API.Infrastructure.Repositories; global using Microsoft.eShopOnDapr.Services.Basket.API.IntegrationEvents.EventHandling; global using Microsoft.eShopOnDapr.Services.Basket.API.IntegrationEvents.Events; +global using Microsoft.eShopOnDapr.Services.Basket.API.Middlewares; global using Microsoft.eShopOnDapr.Services.Basket.API.Model; global using Microsoft.eShopOnDapr.Services.Basket.API.Services; global using Microsoft.Extensions.Diagnostics.HealthChecks; global using Microsoft.OpenApi.Models; -global using Swashbuckle.AspNetCore.SwaggerGen; -global using System.ComponentModel.DataAnnotations; -global using System.IdentityModel.Tokens.Jwt; -global using System.Net; +global using Swashbuckle.AspNetCore.SwaggerGen; \ No newline at end of file diff --git a/src/Services/Basket/Basket.API/Middlewares/AuthMiddleware.cs b/src/Services/Basket/Basket.API/Middlewares/AuthMiddleware.cs new file mode 100644 index 00000000..f1ab6233 --- /dev/null +++ b/src/Services/Basket/Basket.API/Middlewares/AuthMiddleware.cs @@ -0,0 +1,9 @@ +namespace Microsoft.eShopOnDapr.Services.Basket.API.Middlewares; + +public class AuthMiddleware : IAuthMiddleware +{ + public void UseAuth(IApplicationBuilder app) + { + app.UseAuthentication(); + } +} \ No newline at end of file diff --git a/src/Services/Basket/Basket.API/Middlewares/IAuthMiddleware.cs b/src/Services/Basket/Basket.API/Middlewares/IAuthMiddleware.cs new file mode 100644 index 00000000..cbaa73ac --- /dev/null +++ b/src/Services/Basket/Basket.API/Middlewares/IAuthMiddleware.cs @@ -0,0 +1,6 @@ +namespace Microsoft.eShopOnDapr.Services.Basket.API.Middlewares; + +public interface IAuthMiddleware +{ + public void UseAuth(IApplicationBuilder app); +} \ No newline at end of file diff --git a/src/Services/Basket/Basket.API/Program.cs b/src/Services/Basket/Basket.API/Program.cs index 02ed109b..311a3c23 100644 --- a/src/Services/Basket/Basket.API/Program.cs +++ b/src/Services/Basket/Basket.API/Program.cs @@ -1,5 +1,5 @@ var appName = "Basket API"; -var builder = WebApplication.CreateBuilder(); +var builder = WebApplication.CreateBuilder(args); builder.AddCustomSerilog(); builder.AddCustomSwagger(); @@ -25,7 +25,10 @@ app.UseCloudEvents(); -app.UseAuthentication(); +using var scope = app.Services.CreateScope(); +{ + scope.ServiceProvider.GetRequiredService().UseAuth(app); +} app.UseAuthorization(); app.UseCors("CorsPolicy"); @@ -47,3 +50,7 @@ { Serilog.Log.CloseAndFlush(); } + +public partial class Program +{ +} \ No newline at end of file diff --git a/src/Services/Basket/Basket.API/ProgramExtensions.cs b/src/Services/Basket/Basket.API/ProgramExtensions.cs index 9b1ae731..0cf2f655 100644 --- a/src/Services/Basket/Basket.API/ProgramExtensions.cs +++ b/src/Services/Basket/Basket.API/ProgramExtensions.cs @@ -64,7 +64,27 @@ public static void UseCustomSwagger(this WebApplication app) public static void AddCustomMvc(this WebApplicationBuilder builder) { // TODO DaprClient good enough? - builder.Services.AddControllers().AddDapr(); + builder.Services.AddControllers() + .AddDapr(b => b.UseEndpoints(builder.Configuration)); + } + + public static DaprClientBuilder UseEndpoints(this DaprClientBuilder builder, ConfigurationManager configuration) + { + var daprHttpEndpoint = configuration["DAPR_HTTP_ENDPOINT"]; + + if (!string.IsNullOrEmpty(daprHttpEndpoint)) + { + builder.UseHttpEndpoint(daprHttpEndpoint); + } + + var daprGrpcEndpoint = configuration["DAPR_GRPC_ENDPOINT"]; + + if (!string.IsNullOrEmpty(daprGrpcEndpoint)) + { + builder.UseGrpcEndpoint(daprGrpcEndpoint); + } + + return builder; } public static void AddCustomAuthentication(this WebApplicationBuilder builder) @@ -109,5 +129,7 @@ public static void AddCustomApplicationServices(this WebApplicationBuilder build builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); + + builder.Services.AddTransient(); } -} +} \ No newline at end of file diff --git a/src/Services/Basket/Basket.FunctionalTests/AutoAuthorizeMiddleware.cs b/src/Services/Basket/Basket.FunctionalTests/AutoAuthorizeMiddleware.cs new file mode 100644 index 00000000..86a4c8e2 --- /dev/null +++ b/src/Services/Basket/Basket.FunctionalTests/AutoAuthorizeMiddleware.cs @@ -0,0 +1,26 @@ +namespace Basket.FunctionalTests; + +internal class AutoAuthorizeMiddleware +{ + public const string IDENTITY_ID = "9e3163b9-1ae6-4652-9dc6-7898ab7b7a00"; + + private readonly RequestDelegate _next; + + public AutoAuthorizeMiddleware(RequestDelegate rd) + { + _next = rd; + } + + public async Task Invoke(HttpContext httpContext) + { + var identity = new ClaimsIdentity("cookies"); + + identity.AddClaim(new Claim("sub", IDENTITY_ID)); + identity.AddClaim(new Claim("scope", "basket")); + identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID)); + + httpContext.User.AddIdentity(identity); + + await _next.Invoke(httpContext); + } +} \ No newline at end of file diff --git a/src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj b/src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj new file mode 100644 index 00000000..ce502cf6 --- /dev/null +++ b/src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj @@ -0,0 +1,32 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Services/Basket/Basket.FunctionalTests/BasketScenarios.cs b/src/Services/Basket/Basket.FunctionalTests/BasketScenarios.cs new file mode 100644 index 00000000..1f83468e --- /dev/null +++ b/src/Services/Basket/Basket.FunctionalTests/BasketScenarios.cs @@ -0,0 +1,75 @@ +namespace Basket.FunctionalTests; + +public class BasketScenarios : IClassFixture +{ + private readonly BasketWebApplicationFactory _factory; + + public BasketScenarios(BasketWebApplicationFactory factory) + => _factory = factory; + + [Fact] + public async Task Post_basket_and_response_ok_status_code() + { + var content = new StringContent(BuildBasket(), Encoding.UTF8, "application/json"); + var response = await _factory.CreateClient() + .PostAsync(Post.Basket, content); + + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task Get_basket_and_response_ok_status_code() + { + var response = await _factory.CreateClient() + .GetAsync(Get.Basket); + + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task Send_Checkout_basket_and_response_ok_status_code() + { + var contentBasket = new StringContent(BuildBasket(), Encoding.UTF8, "application/json"); + + await _factory.CreateClient() + .PostAsync(Post.Basket, contentBasket); + + var contentCheckout = new StringContent(BuildCheckout(), Encoding.UTF8, "application/json"); + + var response = await _factory.CreateIdempotentClient() + .PostAsync(Post.CheckoutOrder, contentCheckout); + + response.EnsureSuccessStatusCode(); + } + + private string BuildBasket() + { + CustomerBasket order = new(AutoAuthorizeMiddleware.IDENTITY_ID); + + order.Items.Add(new BasketItem + { + ProductId = 1, + ProductName = ".NET Bot Black Hoodie", + UnitPrice = 10, + Quantity = 1 + }); + + return JsonSerializer.Serialize(order); + } + + private string BuildCheckout() + { + BasketCheckout checkoutBasket = new( + "buyer@email.com", + "city", + "street", + "state", + "coutry", + "1234567890123456", + "CardHolderName", + DateTime.UtcNow.AddDays(1), + "123"); + + return JsonSerializer.Serialize(checkoutBasket); + } +} \ No newline at end of file diff --git a/src/Services/Basket/Basket.FunctionalTests/BasketWebApplicationFactory.cs b/src/Services/Basket/Basket.FunctionalTests/BasketWebApplicationFactory.cs new file mode 100644 index 00000000..64abc259 --- /dev/null +++ b/src/Services/Basket/Basket.FunctionalTests/BasketWebApplicationFactory.cs @@ -0,0 +1,32 @@ +namespace Basket.FunctionalTests; + +public class BasketWebApplicationFactory : WebApplicationFactory +{ + private const string ApiUrlBase = "api/v1/basket"; + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices((context, services) => + { + // Added to avoid the Authorize data annotation in test environment. + // Property "SuppressCheckForUnhandledSecurityMetadata" in appsettings.json + services.Configure(context.Configuration); + }); + + builder.ConfigureTestServices(services => + { + services.AddTransient(); + }); + } + + public static class Get + { + public static string Basket = ApiUrlBase; + } + + public static class Post + { + public static string Basket = ApiUrlBase; + public static string CheckoutOrder = $"{ApiUrlBase}/checkout"; + } +} \ No newline at end of file diff --git a/src/Services/Basket/Basket.FunctionalTests/GlobalUsings.cs b/src/Services/Basket/Basket.FunctionalTests/GlobalUsings.cs new file mode 100644 index 00000000..6bdd73c5 --- /dev/null +++ b/src/Services/Basket/Basket.FunctionalTests/GlobalUsings.cs @@ -0,0 +1,14 @@ +global using System.Security.Claims; +global using System.Text; +global using System.Text.Json; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc.Testing; +global using Microsoft.AspNetCore.Routing; +global using Microsoft.AspNetCore.TestHost; +global using Microsoft.eShopOnDapr.Services.Basket.API.Middlewares; +global using Microsoft.eShopOnDapr.Services.Basket.API.Model; +global using Microsoft.Extensions.DependencyInjection; +global using Xunit; +global using static Basket.FunctionalTests.BasketWebApplicationFactory; \ No newline at end of file diff --git a/src/Services/Basket/Basket.FunctionalTests/HttpClientExtensions.cs b/src/Services/Basket/Basket.FunctionalTests/HttpClientExtensions.cs new file mode 100644 index 00000000..ebfdbab2 --- /dev/null +++ b/src/Services/Basket/Basket.FunctionalTests/HttpClientExtensions.cs @@ -0,0 +1,13 @@ +namespace Basket.FunctionalTests; + +internal static class HttpClientExtensions +{ + public static HttpClient CreateIdempotentClient(this BasketWebApplicationFactory server) + { + var client = server.CreateClient(); + + client.DefaultRequestHeaders.Add("X-Request-Id", Guid.NewGuid().ToString()); + + return client; + } +} \ No newline at end of file diff --git a/src/Services/Basket/Basket.FunctionalTests/TestAuthMiddleware.cs b/src/Services/Basket/Basket.FunctionalTests/TestAuthMiddleware.cs new file mode 100644 index 00000000..84c234ba --- /dev/null +++ b/src/Services/Basket/Basket.FunctionalTests/TestAuthMiddleware.cs @@ -0,0 +1,9 @@ +namespace Basket.FunctionalTests; + +internal class TestAuthMiddleware : IAuthMiddleware +{ + public void UseAuth(IApplicationBuilder app) + { + app.UseMiddleware(); + } +} \ No newline at end of file diff --git a/src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj b/src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj new file mode 100644 index 00000000..f13ce299 --- /dev/null +++ b/src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Services/Basket/Basket.UnitTests/BasketWebApiTest.cs b/src/Services/Basket/Basket.UnitTests/BasketWebApiTest.cs new file mode 100644 index 00000000..41474481 --- /dev/null +++ b/src/Services/Basket/Basket.UnitTests/BasketWebApiTest.cs @@ -0,0 +1,143 @@ +namespace Basket.UnitTests; + +public class BasketWebApiTest +{ + private readonly Mock _basketRepositoryMock; + private readonly Mock _identityServiceMock; + private readonly Mock _serviceBusMock; + private readonly Mock> _loggerMock; + + public BasketWebApiTest() + { + _basketRepositoryMock = new Mock(); + _identityServiceMock = new Mock(); + _serviceBusMock = new Mock(); + _loggerMock = new Mock>(); + } + + [Fact] + public async Task Get_customer_basket_success() + { + //Arrange + var fakeCustomerId = "1"; + var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId); + + _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny())) + .Returns(Task.FromResult(fakeCustomerBasket)); + _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); + + _serviceBusMock.Setup(x => x.PublishAsync(It.IsAny())); + + //Act + var basketController = new BasketController( + _basketRepositoryMock.Object, + _identityServiceMock.Object, + _serviceBusMock.Object, + _loggerMock.Object); + + var actionResult = await basketController.GetBasketAsync(); + var result = actionResult.Result as OkObjectResult; + + //Assert + Assert.Equal(result?.StatusCode, (int)System.Net.HttpStatusCode.OK); + var value = result!.Value as CustomerBasket; + Assert.Equal(value?.BuyerId, fakeCustomerId); + } + + [Fact] + public async Task Post_customer_basket_success() + { + //Arrange + var fakeCustomerId = "1"; + var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId); + + _basketRepositoryMock.Setup(x => x.UpdateBasketAsync(It.IsAny())) + .Returns(Task.FromResult(fakeCustomerBasket)); + _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); + _serviceBusMock.Setup(x => x.PublishAsync(It.IsAny())); + + //Act + var basketController = new BasketController( + _basketRepositoryMock.Object, + _identityServiceMock.Object, + _serviceBusMock.Object, + _loggerMock.Object); + + var actionResult = await basketController.UpdateBasketAsync(fakeCustomerBasket); + var result = actionResult.Result as OkObjectResult; + + //Assert + Assert.Equal(result?.StatusCode, (int)System.Net.HttpStatusCode.OK); + var value = result!.Value as CustomerBasket; + Assert.Equal(value?.BuyerId, fakeCustomerId); + } + + [Fact] + public async Task Doing_Checkout_Without_Basket_Should_Return_Bad_Request() + { + var fakeCustomerId = "2"; + _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny())) + .Returns(Task.FromResult((CustomerBasket)null!)); + _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); + + //Act + var basketController = new BasketController( + _basketRepositoryMock.Object, + _identityServiceMock.Object, + _serviceBusMock.Object, + _loggerMock.Object); + + var result = await basketController.CheckoutAsync(new BasketCheckout(null!, null!, null!, null!, null!, null!, null!, default, null!), Guid.NewGuid().ToString()) as BadRequestResult; + Assert.NotNull(result); + } + + [Fact] + public async Task Doing_Checkout_Wit_Basket_Should_Publish_UserCheckoutAccepted_Integration_Event() + { + var fakeCustomerId = "1"; + var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId); + + _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny())) + .Returns(Task.FromResult(fakeCustomerBasket)); + + _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); + + var basketController = new BasketController( + _basketRepositoryMock.Object, + _identityServiceMock.Object, + _serviceBusMock.Object, + _loggerMock.Object) + { + ControllerContext = new ControllerContext() + { + HttpContext = new DefaultHttpContext() + { + User = new ClaimsPrincipal( + new ClaimsIdentity(new Claim[] { + new Claim("sub", "testuser"), + new Claim("scope", "basket"), + new Claim(ClaimTypes.Name, "testuser") + })) + } + } + }; + + //Act + var result = await basketController.CheckoutAsync(new BasketCheckout(null!, null!, null!, null!, null!, null!, null!, default, null!), Guid.NewGuid().ToString()) as AcceptedResult; + + _serviceBusMock.Verify(mock => mock.PublishAsync(It.IsAny()), Times.Once); + + Assert.NotNull(result); + } + + private static CustomerBasket GetCustomerBasketFake(string fakeCustomerId) + { + return new CustomerBasket(fakeCustomerId) + { + Items = new List() + { + new BasketItem() + } + }; + } +} \ No newline at end of file diff --git a/src/Services/Basket/Basket.UnitTests/GlobalUsings.cs b/src/Services/Basket/Basket.UnitTests/GlobalUsings.cs new file mode 100644 index 00000000..172f2832 --- /dev/null +++ b/src/Services/Basket/Basket.UnitTests/GlobalUsings.cs @@ -0,0 +1,11 @@ +global using System.Security.Claims; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.eShopOnDapr.BuildingBlocks.EventBus.Abstractions; +global using Microsoft.eShopOnDapr.Services.Basket.API.Controllers; +global using Microsoft.eShopOnDapr.Services.Basket.API.IntegrationEvents.Events; +global using Microsoft.eShopOnDapr.Services.Basket.API.Model; +global using Microsoft.Extensions.Logging; +global using Moq; +global using Xunit; +global using IBasketIdentityService = Microsoft.eShopOnDapr.Services.Basket.API.Services.IIdentityService; \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.API/Dockerfile b/src/Services/Catalog/Catalog.API/Dockerfile index 5247960a..e649d961 100644 --- a/src/Services/Catalog/Catalog.API/Dockerfile +++ b/src/Services/Catalog/Catalog.API/Dockerfile @@ -12,10 +12,17 @@ COPY ["src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Web.Shopping.Http COPY ["src/BuildingBlocks/EventBus/EventBus.csproj", "src/BuildingBlocks/EventBus/"] COPY ["src/BuildingBlocks/Healthchecks/Healthchecks.csproj", "src/BuildingBlocks/Healthchecks/"] COPY ["src/Services/Basket/Basket.API/Basket.API.csproj", "src/Services/Basket/Basket.API/"] +COPY ["src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj", "src/Services/Basket/Basket.FunctionalTests/"] +COPY ["src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj", "src/Services/Basket/Basket.UnitTests/"] COPY ["src/Services/Catalog/Catalog.API/Catalog.API.csproj", "src/Services/Catalog/Catalog.API/"] +COPY ["src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj", "src/Services/Catalog/Catalog.FunctionalTests/"] +COPY ["src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj", "src/Services/Catalog/Catalog.UnitTests/"] COPY ["src/Services/Identity/Identity.API/Identity.API.csproj", "src/Services/Identity/Identity.API/"] COPY ["src/Services/Ordering/Ordering.API/Ordering.API.csproj", "src/Services/Ordering/Ordering.API/"] +COPY ["src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj", "src/Services/Ordering/Ordering.FunctionalTests/"] +COPY ["src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj", "src/Services/Ordering/Ordering.UnitTests/"] COPY ["src/Services/Payment/Payment.API/Payment.API.csproj", "src/Services/Payment/Payment.API/"] +COPY ["src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj", "src/Tests/Services/Application.FunctionalTests/"] COPY ["src/Web/BlazorClient.Host/BlazorClient.Host.csproj", "src/Web/BlazorClient.Host/"] COPY ["src/Web/BlazorClient/BlazorClient.csproj", "src/Web/BlazorClient/"] COPY ["src/Web/WebStatus/WebStatus.csproj", "src/Web/WebStatus/"] @@ -28,6 +35,12 @@ COPY . . WORKDIR "/src/src/Services/Catalog/Catalog.API" # RUN dotnet build "Catalog.API.csproj" -c Release -o /app/build +FROM build as unittest +WORKDIR /src/src/Services/Catalog/Catalog.UnitTests + +FROM build as functionaltest +WORKDIR /src/src/Services/Catalog/Catalog.FunctionalTests + FROM build AS publish RUN dotnet publish --no-restore "Catalog.API.csproj" -c Release -o /app/publish diff --git a/src/Services/Catalog/Catalog.API/Program.cs b/src/Services/Catalog/Catalog.API/Program.cs index 3043dab6..bf939340 100644 --- a/src/Services/Catalog/Catalog.API/Program.cs +++ b/src/Services/Catalog/Catalog.API/Program.cs @@ -54,3 +54,7 @@ { Serilog.Log.CloseAndFlush(); } + +public partial class Program +{ +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj b/src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj new file mode 100644 index 00000000..5dc7bb38 --- /dev/null +++ b/src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Services/Catalog/Catalog.FunctionalTests/CatalogScenarios.cs b/src/Services/Catalog/Catalog.FunctionalTests/CatalogScenarios.cs new file mode 100644 index 00000000..d8e96a40 --- /dev/null +++ b/src/Services/Catalog/Catalog.FunctionalTests/CatalogScenarios.cs @@ -0,0 +1,74 @@ +namespace Catalog.FunctionalTests; + +public class CatalogScenarios : IClassFixture +{ + private readonly CatalogWebApplicationFactory _factory; + + public CatalogScenarios(CatalogWebApplicationFactory factory) + => _factory = factory; + + [Fact] + public async Task Get_get_all_catalogitems_and_response_ok_status_code() + { + var response = await _factory.CreateClient() + .GetAsync(Get.Items()); + + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task Get_get_catalogitem_by_id_and_response_ok_status_code() + { + var response = await _factory.CreateClient() + .GetAsync(Get.ItemByIds(1)); + + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task Get_get_paginated_catalog_items_and_response_ok_status_code() + { + const bool paginated = true; + var response = await _factory.CreateClient() + .GetAsync(Get.Items(paginated)); + + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task Get_get_filtered_catalog_items_and_response_ok_status_code() + { + var response = await _factory.CreateClient() + .GetAsync(Get.Filtered(1, 1)); + + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task Get_get_paginated_filtered_catalog_items_and_response_ok_status_code() + { + const bool paginated = true; + var response = await _factory.CreateClient() + .GetAsync(Get.Filtered(1, 1, paginated)); + + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task Get_catalog_types_response_ok_status_code() + { + var response = await _factory.CreateClient() + .GetAsync(Get.Types); + + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task Get_catalog_brands_response_ok_status_code() + { + var response = await _factory.CreateClient() + .GetAsync(Get.Brands); + + response.EnsureSuccessStatusCode(); + } +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.FunctionalTests/CatalogWebApplicationFactory.cs b/src/Services/Catalog/Catalog.FunctionalTests/CatalogWebApplicationFactory.cs new file mode 100644 index 00000000..2376d1b7 --- /dev/null +++ b/src/Services/Catalog/Catalog.FunctionalTests/CatalogWebApplicationFactory.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc.Testing; + +namespace Catalog.FunctionalTests; + +public class CatalogWebApplicationFactory : WebApplicationFactory +{ + private const string ApiUrlBase = $"api/v1/catalog"; + + public static class Get + { + private const int PageIndex = 0; + private const int PageCount = 4; + + public static string Items(bool paginated = false) + { + return paginated + ? $"{ApiUrlBase}/items/by_page" + Paginated(PageIndex, PageCount) + : $"{ApiUrlBase}/items/by_page"; + } + + public static string ItemByIds(params int[] ids) + { + return $"{ApiUrlBase}/items/by_ids?ids={string.Join(",", ids)}"; + } + + public static string Types = $"{ApiUrlBase}/types"; + + public static string Brands = $"{ApiUrlBase}/brands"; + + public static string Filtered(int catalogTypeId, int catalogBrandId, bool paginated = false) + { + return paginated + ? $"{ApiUrlBase}/items/by_page" + Paginated(PageIndex, PageCount) + $"&typeId={catalogTypeId}&brandId={catalogBrandId}" + : $"{ApiUrlBase}/items/by_page?typeId={catalogTypeId}&brandId={catalogBrandId}"; + } + + private static string Paginated(int pageIndex, int pageCount) + { + return $"?pageIndex={pageIndex}&pageSize={pageCount}"; + } + } +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.FunctionalTests/GlobalUsings.cs b/src/Services/Catalog/Catalog.FunctionalTests/GlobalUsings.cs new file mode 100644 index 00000000..c32faf3b --- /dev/null +++ b/src/Services/Catalog/Catalog.FunctionalTests/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using Xunit; +global using static Catalog.FunctionalTests.CatalogWebApplicationFactory; \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj b/src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj new file mode 100644 index 00000000..6f60e2ef --- /dev/null +++ b/src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Services/Catalog/Catalog.UnitTests/CatalogControllerTest.cs b/src/Services/Catalog/Catalog.UnitTests/CatalogControllerTest.cs new file mode 100644 index 00000000..38fabb88 --- /dev/null +++ b/src/Services/Catalog/Catalog.UnitTests/CatalogControllerTest.cs @@ -0,0 +1,55 @@ +namespace Catalog.UnitTests; + +public class CatalogControllerTest +{ + private readonly DbContextOptions _dbOptions; + + public CatalogControllerTest() + { + _dbOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "in-memory") + .Options; + + using var dbContext = new CatalogDbContext(_dbOptions); + dbContext.AddRange(GetFakeCatalog()); + dbContext.SaveChanges(); + } + + [Fact] + public async Task Get_catalog_items_success() + { + //Arrange + var brandFilterApplied = 1; + var typesFilterApplied = 2; + var pageSize = 4; + var pageIndex = 1; + + var expectedItemsInPage = 2; + var expectedTotalItems = 6; + + var catalogContext = new CatalogDbContext(_dbOptions); + + //Act + var catalogController = new CatalogController(catalogContext); + var page = await catalogController.ItemsAsync(typesFilterApplied, brandFilterApplied, pageSize, pageIndex); + + //Assert + Assert.Equal(expectedTotalItems, page.Count); + Assert.Equal(pageIndex, page.PageIndex); + Assert.Equal(pageSize, page.PageSize); + Assert.Equal(expectedItemsInPage, page.Items.Count()); + } + + private static List GetFakeCatalog() + { + return new List() + { + new(1, "fakeItemA", 1m, "fakeItemA.png", 2, 1, 10), + new(2, "fakeItemB", 1m, "fakeItemB.png", 2, 1, 10), + new(3, "fakeItemC", 1m, "fakeItemC.png", 2, 1, 10), + new(4, "fakeItemD", 1m, "fakeItemD.png", 2, 1, 10), + new(5, "fakeItemE", 1m, "fakeItemE.png", 2, 1, 10), + new(6, "fakeItemF", 1m, "fakeItemF.png", 2, 1, 10), + }; + } +} \ No newline at end of file diff --git a/src/Services/Catalog/Catalog.UnitTests/GlobalUsings.cs b/src/Services/Catalog/Catalog.UnitTests/GlobalUsings.cs new file mode 100644 index 00000000..fdcf8c1c --- /dev/null +++ b/src/Services/Catalog/Catalog.UnitTests/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using Microsoft.EntityFrameworkCore; +global using Microsoft.eShopOnDapr.Services.Catalog.API.Controllers; +global using Microsoft.eShopOnDapr.Services.Catalog.API.Infrastructure; +global using Microsoft.eShopOnDapr.Services.Catalog.API.Model; +global using Xunit; \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/Dockerfile b/src/Services/Identity/Identity.API/Dockerfile index 28f69f8b..88d3d3e6 100644 --- a/src/Services/Identity/Identity.API/Dockerfile +++ b/src/Services/Identity/Identity.API/Dockerfile @@ -12,17 +12,24 @@ COPY ["src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Web.Shopping.Http COPY ["src/BuildingBlocks/EventBus/EventBus.csproj", "src/BuildingBlocks/EventBus/"] COPY ["src/BuildingBlocks/Healthchecks/Healthchecks.csproj", "src/BuildingBlocks/Healthchecks/"] COPY ["src/Services/Basket/Basket.API/Basket.API.csproj", "src/Services/Basket/Basket.API/"] +COPY ["src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj", "src/Services/Basket/Basket.FunctionalTests/"] +COPY ["src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj", "src/Services/Basket/Basket.UnitTests/"] COPY ["src/Services/Catalog/Catalog.API/Catalog.API.csproj", "src/Services/Catalog/Catalog.API/"] +COPY ["src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj", "src/Services/Catalog/Catalog.FunctionalTests/"] +COPY ["src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj", "src/Services/Catalog/Catalog.UnitTests/"] COPY ["src/Services/Identity/Identity.API/Identity.API.csproj", "src/Services/Identity/Identity.API/"] COPY ["src/Services/Ordering/Ordering.API/Ordering.API.csproj", "src/Services/Ordering/Ordering.API/"] +COPY ["src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj", "src/Services/Ordering/Ordering.FunctionalTests/"] +COPY ["src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj", "src/Services/Ordering/Ordering.UnitTests/"] COPY ["src/Services/Payment/Payment.API/Payment.API.csproj", "src/Services/Payment/Payment.API/"] +COPY ["src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj", "src/Tests/Services/Application.FunctionalTests/"] COPY ["src/Web/BlazorClient.Host/BlazorClient.Host.csproj", "src/Web/BlazorClient.Host/"] COPY ["src/Web/BlazorClient/BlazorClient.csproj", "src/Web/BlazorClient/"] COPY ["src/Web/WebStatus/WebStatus.csproj", "src/Web/WebStatus/"] COPY ["docker-compose.dcproj", "./"] COPY ["NuGet.config", "./"] COPY ["eShopOnDapr.sln", "./"] -RUN dotnet restore "src/Services/Identity/Identity.API/Identity.API.csproj" +RUN dotnet restore "eShopOnDapr.sln" COPY . . WORKDIR "/src/src/Services/Identity/Identity.API" diff --git a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs index c1192616..091ee63a 100644 --- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs +++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs @@ -7,13 +7,16 @@ public class OrdersController : ControllerBase { private readonly IOrderRepository _orderRepository; private readonly IIdentityService _identityService; + private readonly IActorProxyFactory _actorProxyFactory; public OrdersController( - IOrderRepository orderRepository, - IIdentityService identityService) + IOrderRepository orderRepository, + IIdentityService identityService, + IActorProxyFactory actorProxyFactory) { _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); _identityService = identityService ?? throw new ArgumentNullException(nameof(identityService)); + _actorProxyFactory = actorProxyFactory ?? throw new ArgumentNullException(nameof(actorProxyFactory)); } [Route("{orderNumber:int}/cancel")] @@ -57,7 +60,7 @@ public async Task ShipOrderAsync(int orderNumber, [FromHeader(Nam [Route("{orderNumber:int}")] [HttpGet] - [ProducesResponseType(typeof(Model.Order),(int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(Order), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task GetOrderAsync(int orderNumber) { @@ -92,6 +95,6 @@ private async Task GetOrderingProcessActorAsync(int order } var actorId = new ActorId(order.Id.ToString()); - return ActorProxy.Create(actorId, nameof(OrderingProcessActor)); + return _actorProxyFactory.CreateActorProxy(actorId, nameof(OrderingProcessActor)); } -} +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Dockerfile b/src/Services/Ordering/Ordering.API/Dockerfile index 0f0604d9..97b99f5a 100644 --- a/src/Services/Ordering/Ordering.API/Dockerfile +++ b/src/Services/Ordering/Ordering.API/Dockerfile @@ -12,22 +12,35 @@ COPY ["src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Web.Shopping.Http COPY ["src/BuildingBlocks/EventBus/EventBus.csproj", "src/BuildingBlocks/EventBus/"] COPY ["src/BuildingBlocks/Healthchecks/Healthchecks.csproj", "src/BuildingBlocks/Healthchecks/"] COPY ["src/Services/Basket/Basket.API/Basket.API.csproj", "src/Services/Basket/Basket.API/"] +COPY ["src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj", "src/Services/Basket/Basket.FunctionalTests/"] +COPY ["src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj", "src/Services/Basket/Basket.UnitTests/"] COPY ["src/Services/Catalog/Catalog.API/Catalog.API.csproj", "src/Services/Catalog/Catalog.API/"] +COPY ["src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj", "src/Services/Catalog/Catalog.FunctionalTests/"] +COPY ["src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj", "src/Services/Catalog/Catalog.UnitTests/"] COPY ["src/Services/Identity/Identity.API/Identity.API.csproj", "src/Services/Identity/Identity.API/"] COPY ["src/Services/Ordering/Ordering.API/Ordering.API.csproj", "src/Services/Ordering/Ordering.API/"] +COPY ["src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj", "src/Services/Ordering/Ordering.FunctionalTests/"] +COPY ["src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj", "src/Services/Ordering/Ordering.UnitTests/"] COPY ["src/Services/Payment/Payment.API/Payment.API.csproj", "src/Services/Payment/Payment.API/"] +COPY ["src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj", "src/Tests/Services/Application.FunctionalTests/"] COPY ["src/Web/BlazorClient.Host/BlazorClient.Host.csproj", "src/Web/BlazorClient.Host/"] COPY ["src/Web/BlazorClient/BlazorClient.csproj", "src/Web/BlazorClient/"] COPY ["src/Web/WebStatus/WebStatus.csproj", "src/Web/WebStatus/"] COPY ["docker-compose.dcproj", "./"] COPY ["NuGet.config", "./"] COPY ["eShopOnDapr.sln", "./"] -RUN dotnet restore "src/Services/Ordering/Ordering.API/Ordering.API.csproj" +RUN dotnet restore "eShopOnDapr.sln" COPY . . WORKDIR "/src/src/Services/Ordering/Ordering.API" # RUN dotnet build "Ordering.API.csproj" -c Release -o /app/build +FROM build as unittest +WORKDIR /src/src/Services/Ordering/Ordering.UnitTests + +FROM build as functionaltest +WORKDIR /src/src/Services/Ordering/Ordering.FunctionalTests + FROM build AS publish RUN dotnet publish --no-restore "Ordering.API.csproj" -c Release -o /app/publish diff --git a/src/Services/Ordering/Ordering.API/GlobalUsings.cs b/src/Services/Ordering/Ordering.API/GlobalUsings.cs index e77f6b3a..b0e4412d 100644 --- a/src/Services/Ordering/Ordering.API/GlobalUsings.cs +++ b/src/Services/Ordering/Ordering.API/GlobalUsings.cs @@ -1,4 +1,8 @@ -global using Dapr; +global using System.IdentityModel.Tokens.Jwt; +global using System.Net; +global using System.Text; +global using System.Text.Json; +global using Dapr; global using Dapr.Actors; global using Dapr.Actors.Client; global using Dapr.Actors.Runtime; @@ -6,7 +10,6 @@ global using Dapr.Extensions.Configuration; global using HealthChecks.UI.Client; global using Microsoft.AspNetCore.Authorization; -global using Microsoft.AspNetCore.Diagnostics.HealthChecks; global using Microsoft.AspNetCore.Mvc; global using Microsoft.AspNetCore.SignalR; global using Microsoft.Data.SqlClient; @@ -24,13 +27,10 @@ global using Microsoft.eShopOnDapr.Services.Ordering.API.Infrastructure.Repositories; global using Microsoft.eShopOnDapr.Services.Ordering.API.Infrastructure.Services; global using Microsoft.eShopOnDapr.Services.Ordering.API.IntegrationEvents; +global using Microsoft.eShopOnDapr.Services.Ordering.API.Middlewares; global using Microsoft.eShopOnDapr.Services.Ordering.API.Model; global using Microsoft.Extensions.Diagnostics.HealthChecks; global using Microsoft.Extensions.Options; global using Microsoft.OpenApi.Models; global using Polly; -global using Swashbuckle.AspNetCore.SwaggerGen; -global using System.IdentityModel.Tokens.Jwt; -global using System.Net; -global using System.Text; -global using System.Text.Json; +global using Swashbuckle.AspNetCore.SwaggerGen; \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Middlewares/AuthMiddleware.cs b/src/Services/Ordering/Ordering.API/Middlewares/AuthMiddleware.cs new file mode 100644 index 00000000..06c310aa --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Middlewares/AuthMiddleware.cs @@ -0,0 +1,9 @@ +namespace Microsoft.eShopOnDapr.Services.Ordering.API.Middlewares; + +public class AuthMiddleware : IAuthMiddleware +{ + public void UseAuth(IApplicationBuilder app) + { + app.UseAuthentication(); + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Middlewares/IAuthMiddleware.cs b/src/Services/Ordering/Ordering.API/Middlewares/IAuthMiddleware.cs new file mode 100644 index 00000000..d8a1455e --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Middlewares/IAuthMiddleware.cs @@ -0,0 +1,6 @@ +namespace Microsoft.eShopOnDapr.Services.Ordering.API.Middlewares; + +public interface IAuthMiddleware +{ + public void UseAuth(IApplicationBuilder app); +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Program.cs b/src/Services/Ordering/Ordering.API/Program.cs index 7f45a016..d6b892b4 100644 --- a/src/Services/Ordering/Ordering.API/Program.cs +++ b/src/Services/Ordering/Ordering.API/Program.cs @@ -5,15 +5,16 @@ builder.AddCustomSerilog(); builder.AddCustomSwagger(); builder.AddCustomAuthentication(); -builder.AddCustomAuthorization(); +builder.AddCustomAuthorization(); builder.AddCustomHealthChecks(); builder.AddCustomApplicationServices(); builder.AddCustomDatabase(); -builder.Services.AddDaprClient(); +builder.Services.AddDaprClient(b => b.UseEndpoints(builder.Configuration)); builder.Services.AddControllers(); builder.Services.AddActors(options => { + options.UseEndpoint(builder.Configuration); options.Actors.RegisterActor(); }); builder.Services.AddSignalR(); @@ -34,7 +35,10 @@ app.UseCloudEvents(); -app.UseAuthentication(); +using var scope = app.Services.CreateScope(); +{ + scope.ServiceProvider.GetRequiredService().UseAuth(app); +} app.UseAuthorization(); app.MapGet("/", () => Results.LocalRedirect("~/swagger")); @@ -62,5 +66,6 @@ Serilog.Log.CloseAndFlush(); } - - +public partial class Program +{ +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/ProgramExtensions.cs b/src/Services/Ordering/Ordering.API/ProgramExtensions.cs index 20e17ef7..fa1b515b 100644 --- a/src/Services/Ordering/Ordering.API/ProgramExtensions.cs +++ b/src/Services/Ordering/Ordering.API/ProgramExtensions.cs @@ -11,7 +11,38 @@ public static void AddCustomConfiguration(this WebApplicationBuilder builder) { builder.Configuration.AddDaprSecretStore( "eshopondapr-secretstore", - new DaprClientBuilder().Build()); + new DaprClientBuilder() + .UseEndpoints(builder.Configuration) + .Build()); + } + + public static DaprClientBuilder UseEndpoints(this DaprClientBuilder builder, ConfigurationManager configuration) + { + var daprHttpEndpoint = configuration["DAPR_HTTP_ENDPOINT"]; + + if (!string.IsNullOrEmpty(daprHttpEndpoint)) + { + builder.UseHttpEndpoint(daprHttpEndpoint); + } + + var daprGrpcEndpoint = configuration["DAPR_GRPC_ENDPOINT"]; + + if (!string.IsNullOrEmpty(daprGrpcEndpoint)) + { + builder.UseGrpcEndpoint(daprGrpcEndpoint); + } + + return builder; + } + + public static void UseEndpoint(this ActorRuntimeOptions options, ConfigurationManager configuration) + { + var daprHttpEndpoint = configuration["DAPR_HTTP_ENDPOINT"]; + + if (!string.IsNullOrEmpty(daprHttpEndpoint)) + { + options.HttpEndpoint = daprHttpEndpoint; + } } public static void AddCustomSerilog(this WebApplicationBuilder builder) @@ -101,7 +132,7 @@ public static void AddCustomHealthChecks(this WebApplicationBuilder builder) => .AddSqlServer( builder.Configuration["ConnectionStrings:OrderingDB"]!, name: "OrderingDB-check", - tags: new [] { "orderdb" }); + tags: new[] { "orderdb" }); public static void AddCustomApplicationServices(this WebApplicationBuilder builder) { @@ -154,4 +185,4 @@ private static Policy CreateRetryPolicy(IConfiguration configuration, Serilog.IL return Policy.NoOp(); } -} +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.FunctionalTests/AutoAuthorizeMiddleware.cs b/src/Services/Ordering/Ordering.FunctionalTests/AutoAuthorizeMiddleware.cs new file mode 100644 index 00000000..bb811883 --- /dev/null +++ b/src/Services/Ordering/Ordering.FunctionalTests/AutoAuthorizeMiddleware.cs @@ -0,0 +1,25 @@ +namespace Ordering.FunctionalTests; + +internal class AutoAuthorizeMiddleware +{ + public const string IDENTITY_ID = "9e3163b9-1ae6-4652-9dc6-7898ab7b7a00"; + + private readonly RequestDelegate _next; + + public AutoAuthorizeMiddleware(RequestDelegate rd) + { + _next = rd; + } + + public async Task Invoke(HttpContext httpContext) + { + var identity = new ClaimsIdentity("cookies"); + + identity.AddClaim(new Claim("sub", IDENTITY_ID)); + identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID)); + + httpContext.User.AddIdentity(identity); + + await _next.Invoke(httpContext); + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.FunctionalTests/GlobalUsings.cs b/src/Services/Ordering/Ordering.FunctionalTests/GlobalUsings.cs new file mode 100644 index 00000000..781c6806 --- /dev/null +++ b/src/Services/Ordering/Ordering.FunctionalTests/GlobalUsings.cs @@ -0,0 +1,12 @@ +global using System.Net; +global using System.Security.Claims; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc.Testing; +global using Microsoft.AspNetCore.Routing; +global using Microsoft.AspNetCore.TestHost; +global using Microsoft.eShopOnDapr.Services.Ordering.API.Middlewares; +global using Microsoft.Extensions.DependencyInjection; +global using Xunit; +global using static Ordering.FunctionalTests.OrderingWebApplicationFactory; \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.FunctionalTests/HttpClientExtensions.cs b/src/Services/Ordering/Ordering.FunctionalTests/HttpClientExtensions.cs new file mode 100644 index 00000000..acc5cec5 --- /dev/null +++ b/src/Services/Ordering/Ordering.FunctionalTests/HttpClientExtensions.cs @@ -0,0 +1,13 @@ +namespace Ordering.FunctionalTests; + +internal static class HttpClientExtensions +{ + public static HttpClient CreateIdempotentClient(this OrderingWebApplicationFactory server) + { + var client = server.CreateClient(); + + client.DefaultRequestHeaders.Add("x-requestid", Guid.NewGuid().ToString()); + + return client; + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj b/src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj new file mode 100644 index 00000000..4276d154 --- /dev/null +++ b/src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs b/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs new file mode 100644 index 00000000..6c3c4a0a --- /dev/null +++ b/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs @@ -0,0 +1,36 @@ +namespace Ordering.FunctionalTests; + +public class OrderingScenarios : IClassFixture +{ + private readonly OrderingWebApplicationFactory _factory; + + public OrderingScenarios(OrderingWebApplicationFactory factory) + => _factory = factory; + + [Fact] + public async Task Get_get_all_stored_orders_and_response_ok_status_code() + { + var response = await _factory.CreateClient() + .GetAsync(Get.Orders); + + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task Cancel_order_no_order_internal_server_error_response() + { + var response = await _factory.CreateIdempotentClient() + .PutAsync(Put.CancelOrder(1), null); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + } + + [Fact] + public async Task Ship_order_no_order_internal_server_error_response() + { + var response = await _factory.CreateIdempotentClient() + .PutAsync(Put.ShipOrder(1), null); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.FunctionalTests/OrderingWebApplicationFactory.cs b/src/Services/Ordering/Ordering.FunctionalTests/OrderingWebApplicationFactory.cs new file mode 100644 index 00000000..742be66a --- /dev/null +++ b/src/Services/Ordering/Ordering.FunctionalTests/OrderingWebApplicationFactory.cs @@ -0,0 +1,44 @@ +namespace Ordering.FunctionalTests; + +public class OrderingWebApplicationFactory : WebApplicationFactory +{ + private const string ApiUrlBase = "api/v1/orders"; + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureServices((context, services) => + { + // Added to avoid the Authorize data annotation in test environment. + // Property "SuppressCheckForUnhandledSecurityMetadata" in appsettings.json + services.Configure(context.Configuration); + }); + + builder.ConfigureTestServices(services => + { + services.AddTransient(); + }); + } + + public static class Get + { + public static string Orders = ApiUrlBase; + + public static string OrderBy(int id) + { + return $"{ApiUrlBase}/{id}"; + } + } + + public static class Put + { + public static string CancelOrder(int id) + { + return $"{ApiUrlBase}/{id}/cancel"; + } + + public static string ShipOrder(int id) + { + return $"{ApiUrlBase}/{id}/ship"; + } + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.FunctionalTests/TestAuthMiddleware.cs b/src/Services/Ordering/Ordering.FunctionalTests/TestAuthMiddleware.cs new file mode 100644 index 00000000..a3601b43 --- /dev/null +++ b/src/Services/Ordering/Ordering.FunctionalTests/TestAuthMiddleware.cs @@ -0,0 +1,9 @@ +namespace Ordering.FunctionalTests; + +internal class TestAuthMiddleware : IAuthMiddleware +{ + public void UseAuth(IApplicationBuilder app) + { + app.UseMiddleware(); + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.UnitTests/GlobalUsings.cs b/src/Services/Ordering/Ordering.UnitTests/GlobalUsings.cs new file mode 100644 index 00000000..16f2ac8f --- /dev/null +++ b/src/Services/Ordering/Ordering.UnitTests/GlobalUsings.cs @@ -0,0 +1,9 @@ +global using Dapr.Actors; +global using Dapr.Actors.Client; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.eShopOnDapr.Services.Ordering.API.Actors; +global using Microsoft.eShopOnDapr.Services.Ordering.API.Controllers; +global using Microsoft.eShopOnDapr.Services.Ordering.API.Infrastructure.Services; +global using Microsoft.eShopOnDapr.Services.Ordering.API.Model; +global using Moq; +global using Xunit; \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj b/src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj new file mode 100644 index 00000000..dd1336db --- /dev/null +++ b/src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Services/Ordering/Ordering.UnitTests/OrdersWebApiTest.cs b/src/Services/Ordering/Ordering.UnitTests/OrdersWebApiTest.cs new file mode 100644 index 00000000..fb7eb36e --- /dev/null +++ b/src/Services/Ordering/Ordering.UnitTests/OrdersWebApiTest.cs @@ -0,0 +1,133 @@ +namespace Ordering.UnitTests; + +public class OrdersWebApiTest +{ + private readonly Mock _orderRepositoryMock; + private readonly Mock _identityServiceMock; + private readonly Mock _orderingProcessActorMock; + private readonly Mock _proxyFactoryMock; + + public OrdersWebApiTest() + { + _orderRepositoryMock = new(); + _identityServiceMock = new(); + _orderingProcessActorMock = new(); + _proxyFactoryMock = new(); + + _proxyFactoryMock.Setup(x => x.CreateActorProxy(It.IsAny(), nameof(OrderingProcessActor), null)) + .Returns(_orderingProcessActorMock.Object); + } + + [Fact] + public async Task Cancel_order_with_requestId_success() + { + //Arrange + var fakeDynamicResult = new Order(); + _orderRepositoryMock.Setup(x => x.GetOrderByOrderNumberAsync(It.Is(n => n == 1))) + .Returns(Task.FromResult(fakeDynamicResult ?? null)); + _orderingProcessActorMock.Setup(x => x.CancelAsync()) + .Returns(Task.FromResult(true)); + + //Act + var orderController = new OrdersController(_orderRepositoryMock.Object, _identityServiceMock.Object, _proxyFactoryMock.Object); + var actionResult = await orderController.CancelOrderAsync(1) as OkResult; + + //Assert + Assert.Equal((int)System.Net.HttpStatusCode.OK, actionResult?.StatusCode); + } + + [Fact] + public async Task Cancel_order_bad_request() + { + //Arrange + var fakeDynamicResult = new Order(); + _orderRepositoryMock.Setup(x => x.GetOrderByOrderNumberAsync(It.Is(n => n == 1))) + .Returns(Task.FromResult(fakeDynamicResult ?? null)); + _orderingProcessActorMock.Setup(x => x.CancelAsync()) + .Returns(Task.FromResult(false)); + + //Act + var orderController = new OrdersController(_orderRepositoryMock.Object, _identityServiceMock.Object, _proxyFactoryMock.Object); + var actionResult = await orderController.CancelOrderAsync(1) as BadRequestResult; + + //Assert + Assert.Equal((int)System.Net.HttpStatusCode.BadRequest, actionResult?.StatusCode); + } + + [Fact] + public async Task Ship_order_with_requestId_success() + { + //Arrange + var fakeDynamicResult = new Order(); + _orderRepositoryMock.Setup(x => x.GetOrderByOrderNumberAsync(It.Is(n => n == 1))) + .Returns(Task.FromResult(fakeDynamicResult ?? null)); + _orderingProcessActorMock.Setup(x => x.ShipAsync()) + .Returns(Task.FromResult(true)); + + //Act + var orderController = new OrdersController(_orderRepositoryMock.Object, _identityServiceMock.Object, _proxyFactoryMock.Object); + var actionResult = await orderController.ShipOrderAsync(1, Guid.NewGuid().ToString()) as OkResult; + + //Assert + Assert.Equal((int)System.Net.HttpStatusCode.OK, actionResult?.StatusCode); + } + + [Fact] + public async Task Ship_order_bad_request() + { + //Arrange + var fakeDynamicResult = new Order(); + _orderRepositoryMock.Setup(x => x.GetOrderByOrderNumberAsync(It.Is(n => n == 1))) + .Returns(Task.FromResult(fakeDynamicResult ?? null)); + _orderingProcessActorMock.Setup(x => x.ShipAsync()) + .Returns(Task.FromResult(false)); + + //Act + var orderController = new OrdersController(_orderRepositoryMock.Object, _identityServiceMock.Object, _proxyFactoryMock.Object); + var actionResult = await orderController.ShipOrderAsync(1, Guid.NewGuid().ToString()) as BadRequestResult; + + //Assert + Assert.Equal((int)System.Net.HttpStatusCode.BadRequest, actionResult?.StatusCode); + } + + [Fact] + public async Task Get_orders_success() + { + //Arrange + var fakeDynamicResult = Enumerable.Empty(); + + _identityServiceMock.Setup(x => x.GetUserIdentity()) + .Returns(Guid.NewGuid().ToString()); + + _orderRepositoryMock.Setup(x => x.GetOrdersFromBuyerAsync(Guid.NewGuid().ToString())) + .Returns(Task.FromResult(fakeDynamicResult)); + + //Act + var orderController = new OrdersController(_orderRepositoryMock.Object, _identityServiceMock.Object, _proxyFactoryMock.Object); + var actionResult = await orderController.GetOrdersAsync(); + + //Assert + Assert.Equal((int)System.Net.HttpStatusCode.OK, (actionResult.Result as OkObjectResult)?.StatusCode); + } + + [Fact] + public async Task Get_order_success() + { + //Arrange + var fakeOrderNumber = 123; + var fakeDynamicResult = new Order(); + + _identityServiceMock.Setup(x => x.GetUserIdentity()) + .Returns(string.Empty); + + _orderRepositoryMock.Setup(x => x.GetOrderByOrderNumberAsync(It.IsAny())) + .Returns(Task.FromResult(fakeDynamicResult ?? null)); + + //Act + var orderController = new OrdersController(_orderRepositoryMock.Object, _identityServiceMock.Object, _proxyFactoryMock.Object); + var actionResult = await orderController.GetOrderAsync(fakeOrderNumber) as OkObjectResult; + + //Assert + Assert.Equal((int)System.Net.HttpStatusCode.OK, actionResult?.StatusCode); + } +} \ No newline at end of file diff --git a/src/Services/Payment/Payment.API/Dockerfile b/src/Services/Payment/Payment.API/Dockerfile index 5fc68e0b..9fa409b4 100644 --- a/src/Services/Payment/Payment.API/Dockerfile +++ b/src/Services/Payment/Payment.API/Dockerfile @@ -12,10 +12,17 @@ COPY ["src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Web.Shopping.Http COPY ["src/BuildingBlocks/EventBus/EventBus.csproj", "src/BuildingBlocks/EventBus/"] COPY ["src/BuildingBlocks/Healthchecks/Healthchecks.csproj", "src/BuildingBlocks/Healthchecks/"] COPY ["src/Services/Basket/Basket.API/Basket.API.csproj", "src/Services/Basket/Basket.API/"] +COPY ["src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj", "src/Services/Basket/Basket.FunctionalTests/"] +COPY ["src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj", "src/Services/Basket/Basket.UnitTests/"] COPY ["src/Services/Catalog/Catalog.API/Catalog.API.csproj", "src/Services/Catalog/Catalog.API/"] +COPY ["src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj", "src/Services/Catalog/Catalog.FunctionalTests/"] +COPY ["src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj", "src/Services/Catalog/Catalog.UnitTests/"] COPY ["src/Services/Identity/Identity.API/Identity.API.csproj", "src/Services/Identity/Identity.API/"] COPY ["src/Services/Ordering/Ordering.API/Ordering.API.csproj", "src/Services/Ordering/Ordering.API/"] +COPY ["src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj", "src/Services/Ordering/Ordering.FunctionalTests/"] +COPY ["src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj", "src/Services/Ordering/Ordering.UnitTests/"] COPY ["src/Services/Payment/Payment.API/Payment.API.csproj", "src/Services/Payment/Payment.API/"] +COPY ["src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj", "src/Tests/Services/Application.FunctionalTests/"] COPY ["src/Web/BlazorClient.Host/BlazorClient.Host.csproj", "src/Web/BlazorClient.Host/"] COPY ["src/Web/BlazorClient/BlazorClient.csproj", "src/Web/BlazorClient/"] COPY ["src/Web/WebStatus/WebStatus.csproj", "src/Web/WebStatus/"] diff --git a/src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj b/src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj new file mode 100644 index 00000000..f8106a87 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj @@ -0,0 +1,40 @@ + + + + net7.0 + enable + enable + + false + true + FunctionalTests + Linux + ..\..\..\.. + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + basket + + + ordering + + + + + diff --git a/src/Tests/Services/Application.FunctionalTests/Dockerfile b/src/Tests/Services/Application.FunctionalTests/Dockerfile new file mode 100644 index 00000000..87c988b7 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Dockerfile @@ -0,0 +1,35 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. +ARG NET_IMAGE=7.0-bullseye-slim +FROM mcr.microsoft.com/dotnet/aspnet:${NET_IMAGE} AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:${NET_IMAGE} AS build +WORKDIR /src + +# Create this "restore-solution" section by running ./Create-DockerfileSolutionRestore.ps1, to optimize build cache reuse +COPY ["src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Web.Shopping.HttpAggregator.csproj", "src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/"] +COPY ["src/BuildingBlocks/EventBus/EventBus.csproj", "src/BuildingBlocks/EventBus/"] +COPY ["src/BuildingBlocks/Healthchecks/Healthchecks.csproj", "src/BuildingBlocks/Healthchecks/"] +COPY ["src/Services/Basket/Basket.API/Basket.API.csproj", "src/Services/Basket/Basket.API/"] +COPY ["src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj", "src/Services/Basket/Basket.FunctionalTests/"] +COPY ["src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj", "src/Services/Basket/Basket.UnitTests/"] +COPY ["src/Services/Catalog/Catalog.API/Catalog.API.csproj", "src/Services/Catalog/Catalog.API/"] +COPY ["src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj", "src/Services/Catalog/Catalog.FunctionalTests/"] +COPY ["src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj", "src/Services/Catalog/Catalog.UnitTests/"] +COPY ["src/Services/Identity/Identity.API/Identity.API.csproj", "src/Services/Identity/Identity.API/"] +COPY ["src/Services/Ordering/Ordering.API/Ordering.API.csproj", "src/Services/Ordering/Ordering.API/"] +COPY ["src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj", "src/Services/Ordering/Ordering.FunctionalTests/"] +COPY ["src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj", "src/Services/Ordering/Ordering.UnitTests/"] +COPY ["src/Services/Payment/Payment.API/Payment.API.csproj", "src/Services/Payment/Payment.API/"] +COPY ["src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj", "src/Tests/Services/Application.FunctionalTests/"] +COPY ["src/Web/BlazorClient.Host/BlazorClient.Host.csproj", "src/Web/BlazorClient.Host/"] +COPY ["src/Web/BlazorClient/BlazorClient.csproj", "src/Web/BlazorClient/"] +COPY ["src/Web/WebStatus/WebStatus.csproj", "src/Web/WebStatus/"] +COPY ["docker-compose.dcproj", "./"] +COPY ["NuGet.config", "./"] +COPY ["eShopOnDapr.sln", "./"] +RUN dotnet restore "eShopOnDapr.sln" + +COPY . . +WORKDIR /src/src/Tests/Services/Application.FunctionalTests \ No newline at end of file diff --git a/src/Tests/Services/Application.FunctionalTests/Extensions/HttpClientExtensions.cs b/src/Tests/Services/Application.FunctionalTests/Extensions/HttpClientExtensions.cs new file mode 100644 index 00000000..07bbfbc0 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Extensions/HttpClientExtensions.cs @@ -0,0 +1,22 @@ +namespace FunctionalTests.Extensions; + +internal static class HttpClientExtensions +{ + public static HttpClient CreateIdempotentClient(this BasketWebApplicationFactory server) + { + var client = server.CreateClient(); + + client.DefaultRequestHeaders.Add("X-Request-Id", AutoAuthorizeMiddleware.IDENTITY_ID); + + return client; + } + + public static HttpClient CreateIdempotentClient(this OrderingWebApplicationFactory server) + { + var client = server.CreateClient(); + + client.DefaultRequestHeaders.Add("x-requestid", AutoAuthorizeMiddleware.IDENTITY_ID); + + return client; + } +} \ No newline at end of file diff --git a/src/Tests/Services/Application.FunctionalTests/GlobalUsings.cs b/src/Tests/Services/Application.FunctionalTests/GlobalUsings.cs new file mode 100644 index 00000000..97330c4e --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/GlobalUsings.cs @@ -0,0 +1,20 @@ +global using System.Security.Claims; +global using System.Text; +global using System.Text.Json; +global using Dapr.Client; +global using FunctionalTests.Extensions; +global using FunctionalTests.Middlewares; +global using FunctionalTests.Services.Basket; +global using FunctionalTests.Services.Ordering; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc.Testing; +global using Microsoft.AspNetCore.TestHost; +global using Microsoft.eShopOnDapr.BlazorClient.Ordering; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Xunit; +global using static FunctionalTests.Services.Basket.BasketWebApplicationFactory; +global using static FunctionalTests.Services.Ordering.OrderingWebApplicationFactory; \ No newline at end of file diff --git a/src/Tests/Services/Application.FunctionalTests/Middleware/AutoAuthorizeMiddleware.cs b/src/Tests/Services/Application.FunctionalTests/Middleware/AutoAuthorizeMiddleware.cs new file mode 100644 index 00000000..7d96c202 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Middleware/AutoAuthorizeMiddleware.cs @@ -0,0 +1,26 @@ +namespace FunctionalTests.Middlewares; + +internal class AutoAuthorizeMiddleware +{ + public const string IDENTITY_ID = "9e3163b9-1ae6-4652-9dc6-7898ab7b7a00"; + + private readonly RequestDelegate _next; + + public AutoAuthorizeMiddleware(RequestDelegate rd) + { + _next = rd; + } + + public async Task Invoke(HttpContext httpContext) + { + var identity = new ClaimsIdentity("cookies"); + + identity.AddClaim(new Claim("sub", IDENTITY_ID)); + identity.AddClaim(new Claim("scope", "basket")); + identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID)); + + httpContext.User.AddIdentity(identity); + + await _next.Invoke(httpContext); + } +} \ No newline at end of file diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketWebApplicationFactory.cs b/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketWebApplicationFactory.cs new file mode 100644 index 00000000..249fe835 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketWebApplicationFactory.cs @@ -0,0 +1,33 @@ +extern alias basket; + +using basket.Microsoft.eShopOnDapr.Services.Basket.API.Middlewares; + +namespace FunctionalTests.Services.Basket; + +public class BasketWebApplicationFactory : WebApplicationWithKestrelFactory +{ + private const string ApiUrlBase = "api/v1/basket"; + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseConfiguration(new ConfigurationBuilder() + .AddEnvironmentVariables("ASPNETCORE_BASKET_") + .Build()); + + builder.ConfigureTestServices(services => + { + services.AddTransient(); + }); + } + + public static class Get + { + public static string Basket = ApiUrlBase; + } + + public static class Post + { + public static string CreateBasket = ApiUrlBase; + public static string CheckoutOrder = $"{ApiUrlBase}/checkout"; + } +} \ No newline at end of file diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Basket/TestAuthMiddleware.cs b/src/Tests/Services/Application.FunctionalTests/Services/Basket/TestAuthMiddleware.cs new file mode 100644 index 00000000..5444fdca --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/Basket/TestAuthMiddleware.cs @@ -0,0 +1,13 @@ +extern alias basket; + +using basket.Microsoft.eShopOnDapr.Services.Basket.API.Middlewares; + +namespace FunctionalTests.Services.Basket; + +internal class TestAuthMiddleware : IAuthMiddleware +{ + public void UseAuth(IApplicationBuilder app) + { + app.UseMiddleware(); + } +} \ No newline at end of file diff --git a/src/Tests/Services/Application.FunctionalTests/Services/IntegrationEventsScenarios.cs b/src/Tests/Services/Application.FunctionalTests/Services/IntegrationEventsScenarios.cs new file mode 100644 index 00000000..c053eb77 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/IntegrationEventsScenarios.cs @@ -0,0 +1,179 @@ +extern alias basket; + +using basket.Microsoft.eShopOnDapr.Services.Basket.API.Model; + +namespace FunctionalTests.Services.Ordering; + +public class IntegrationEventsScenarios : IClassFixture, IClassFixture +{ + private readonly OrderingWebApplicationFactory _orderingWebFactory; + private readonly BasketWebApplicationFactory _basketWebfactory; + + public IntegrationEventsScenarios(OrderingWebApplicationFactory orderingWebFactory, BasketWebApplicationFactory basketWebfactory) + { + _orderingWebFactory = orderingWebFactory; + _basketWebfactory = basketWebfactory; + } + + [Fact] + public async Task Cancel_basket_and_check_order_status_cancelled() + { + // Expected data + var cityExpected = $"city-{Guid.NewGuid()}"; + var orderStatusExpected = "Cancelled"; + + var orderClient = _orderingWebFactory.CreateIdempotentClient(); + var basketClient = _basketWebfactory.CreateIdempotentClient(); + + // GIVEN a basket is created + var contentBasket = new StringContent(BuildBasket(), Encoding.UTF8, "application/json"); + await basketClient.PostAsync(Post.CreateBasket, contentBasket); + + // AND basket checkout is sent + await basketClient.PostAsync(Post.CheckoutOrder, new StringContent(BuildCheckout(cityExpected), Encoding.UTF8, "application/json")); + + // WHEN Order is created in Ordering.api + var newOrder = await TryGetNewOrderCreated(cityExpected, orderClient); + + // AND Order is cancelled in Ordering.api + await orderClient.PutAsync(Put.CancelOrder(newOrder.OrderNumber), null); + + // AND the basket is retrieved + var basket = await TryGetBasket(basketClient); + + // AND the requested order is retrieved + var order = await TryGetOrder(newOrder.OrderNumber, orderStatusExpected, orderClient); + + // THEN check basket and order status + Assert.Empty(basket!.Items); + Assert.Equal(orderStatusExpected, order?.OrderStatus); + } + + private async Task TryGetBasket(HttpClient basketClient) + { + var counter = 0; + CustomerBasket? basket = null; + + while (counter < 20) + { + var basketGetResponse = await basketClient.GetStringAsync(BasketWebApplicationFactory.Get.Basket); + basket = JsonSerializer.Deserialize(basketGetResponse, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + })!; + + if (!basket.Items.Any()) + { + break; + } + + counter++; + await Task.Delay(100); + } + + return basket; + } + + private async Task TryGetOrder(int orderNumber, string orderStatus, HttpClient orderClient) + { + var counter = 0; + Order? order = null; + + while (counter < 20) + { + var ordersGetResponse = await orderClient.GetStringAsync(OrderingWebApplicationFactory.Get.Orders); + var orders = JsonSerializer.Deserialize>(ordersGetResponse, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + })!; + + order = orders.Single(o => o.OrderNumber == orderNumber); + + if (order.OrderStatus == orderStatus) + { + break; + } + + counter++; + await Task.Delay(100); + } + + return order; + } + + private async Task TryGetNewOrderCreated(string city, HttpClient orderClient) + { + var counter = 0; + Order? order = null; + + while (counter < 20) + { + //get the orders and verify that the new order has been created + var ordersGetResponse = await orderClient.GetStringAsync(OrderingWebApplicationFactory.Get.Orders); + var orders = JsonSerializer.Deserialize>(ordersGetResponse, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + if (orders?.Count > 0) + { + var lastOrder = orders.OrderByDescending(o => o.OrderDate).First(); + var id = lastOrder.OrderNumber; + var orderDetails = await orderClient.GetStringAsync(OrderingWebApplicationFactory.Get.OrderBy(id)); + order = JsonSerializer.Deserialize(orderDetails, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + })!; + + if (IsOrderCreated(order, city)) + { + break; + } + } + + counter++; + await Task.Delay(100); + } + + return order!; + } + + private bool IsOrderCreated(Order order, string city) + { + return order.Address.City == city; + } + + private string BuildBasket() + { + CustomerBasket order = new(AutoAuthorizeMiddleware.IDENTITY_ID) + { + Items = new List() + { + new BasketItem() + { + ProductName = "ProductName", + ProductId = 1, + UnitPrice = 10, + Quantity = 1 + } + } + }; + return JsonSerializer.Serialize(order); + } + + private string BuildCheckout(string cityExpected) + { + basket.Microsoft.eShopOnDapr.Services.Basket.API.Model.BasketCheckout checkoutBasket = new( + "buyer@email.com", + cityExpected, + "street", + "state", + "coutry", + "1111111111111", + "CardHolderName", + DateTime.Now.AddYears(1), + "123"); + + return JsonSerializer.Serialize(checkoutBasket); + } +} \ No newline at end of file diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingWebApplicationFactory.cs b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingWebApplicationFactory.cs new file mode 100644 index 00000000..6a145d15 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingWebApplicationFactory.cs @@ -0,0 +1,50 @@ +extern alias ordering; + +using ordering.Microsoft.eShopOnDapr.Services.Ordering.API.Middlewares; + +namespace FunctionalTests.Services.Ordering; + +public class OrderingWebApplicationFactory : WebApplicationWithKestrelFactory +{ + private const string ApiUrlBase = "api/v1/orders"; + + protected override IHost CreateHost(IHostBuilder builder) + { + var host = base.CreateHost(builder); + + // Give dapr sidecar some time to establish connection with dapr placement + Task.Delay(4000).Wait(); + + return host; + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseConfiguration(new ConfigurationBuilder() + .AddEnvironmentVariables("ASPNETCORE_ORDERING_") + .Build()); + + builder.ConfigureTestServices(services => + { + services.AddTransient(); + }); + } + + public static class Get + { + public static string Orders = ApiUrlBase; + + public static string OrderBy(int id) + { + return $"{ApiUrlBase}/{id}"; + } + } + + public static class Put + { + public static string CancelOrder(int id) + { + return $"{ApiUrlBase}/{id}/cancel"; + } + } +} \ No newline at end of file diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Ordering/TestAuthMiddleware.cs b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/TestAuthMiddleware.cs new file mode 100644 index 00000000..4b1b2d71 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/TestAuthMiddleware.cs @@ -0,0 +1,13 @@ +extern alias ordering; + +using ordering.Microsoft.eShopOnDapr.Services.Ordering.API.Middlewares; + +namespace FunctionalTests.Services.Ordering; + +internal class TestAuthMiddleware : IAuthMiddleware +{ + public void UseAuth(IApplicationBuilder app) + { + app.UseMiddleware(); + } +} \ No newline at end of file diff --git a/src/Tests/Services/Application.FunctionalTests/Services/WebApplicationWithKestrelFactory.cs b/src/Tests/Services/Application.FunctionalTests/Services/WebApplicationWithKestrelFactory.cs new file mode 100644 index 00000000..3cfd4059 --- /dev/null +++ b/src/Tests/Services/Application.FunctionalTests/Services/WebApplicationWithKestrelFactory.cs @@ -0,0 +1,56 @@ +namespace FunctionalTests.Services; + +public class WebApplicationWithKestrelFactory : WebApplicationFactory + where TEntryPoint : class +{ + private IHost? _host; + + protected override IHost CreateHost(IHostBuilder builder) + { + // Create the host for TestServer now before we + // modify the builder to use Kestrel instead. + var testHost = builder.Build(); + testHost.Start(); + + // Modify the host builder to use Kestrel instead + // of TestServer so we can listen on a real address. + builder.ConfigureWebHost(webHostBuilder => webHostBuilder.UseKestrel()); + + // Create and start the Kestrel server before the test server, + // otherwise due to the way the deferred host builder works + // for minimal hosting, the server will not get "initialized + // enough" for the address it is listening on to be available. + // See https://github.com/dotnet/aspnetcore/issues/33846. + _host = builder.Build(); + _host.Start(); + + // Ensure Dapr is ready + using var scope = _host.Services.CreateScope(); + var configuration = scope.ServiceProvider.GetRequiredService(); + using var daprClient = new DaprClientBuilder() + .UseHttpEndpoint(configuration["DAPR_HTTP_ENDPOINT"]) + .Build(); + while (true) + { + var isDaprReady = daprClient.CheckHealthAsync().Result; + + if (isDaprReady) + { + break; + } + + Task.Delay(1000).Wait(); + } + + // Return the host that uses TestServer, rather than the real one. + // Otherwise the internals will complain about the host's server + // not being an instance of the concrete type TestServer. + // See https://github.com/dotnet/aspnetcore/pull/34702. + return testHost; + } + + protected override void Dispose(bool disposing) + { + _host?.Dispose(); + } +} \ No newline at end of file diff --git a/src/Web/BlazorClient.Host/Dockerfile b/src/Web/BlazorClient.Host/Dockerfile index 35bf495e..dcfd5f97 100644 --- a/src/Web/BlazorClient.Host/Dockerfile +++ b/src/Web/BlazorClient.Host/Dockerfile @@ -12,17 +12,24 @@ COPY ["src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Web.Shopping.Http COPY ["src/BuildingBlocks/EventBus/EventBus.csproj", "src/BuildingBlocks/EventBus/"] COPY ["src/BuildingBlocks/Healthchecks/Healthchecks.csproj", "src/BuildingBlocks/Healthchecks/"] COPY ["src/Services/Basket/Basket.API/Basket.API.csproj", "src/Services/Basket/Basket.API/"] +COPY ["src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj", "src/Services/Basket/Basket.FunctionalTests/"] +COPY ["src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj", "src/Services/Basket/Basket.UnitTests/"] COPY ["src/Services/Catalog/Catalog.API/Catalog.API.csproj", "src/Services/Catalog/Catalog.API/"] +COPY ["src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj", "src/Services/Catalog/Catalog.FunctionalTests/"] +COPY ["src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj", "src/Services/Catalog/Catalog.UnitTests/"] COPY ["src/Services/Identity/Identity.API/Identity.API.csproj", "src/Services/Identity/Identity.API/"] COPY ["src/Services/Ordering/Ordering.API/Ordering.API.csproj", "src/Services/Ordering/Ordering.API/"] +COPY ["src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj", "src/Services/Ordering/Ordering.FunctionalTests/"] +COPY ["src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj", "src/Services/Ordering/Ordering.UnitTests/"] COPY ["src/Services/Payment/Payment.API/Payment.API.csproj", "src/Services/Payment/Payment.API/"] +COPY ["src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj", "src/Tests/Services/Application.FunctionalTests/"] COPY ["src/Web/BlazorClient.Host/BlazorClient.Host.csproj", "src/Web/BlazorClient.Host/"] COPY ["src/Web/BlazorClient/BlazorClient.csproj", "src/Web/BlazorClient/"] COPY ["src/Web/WebStatus/WebStatus.csproj", "src/Web/WebStatus/"] COPY ["docker-compose.dcproj", "./"] COPY ["NuGet.config", "./"] COPY ["eShopOnDapr.sln", "./"] -RUN dotnet restore "src/Web/BlazorClient.Host/BlazorClient.Host.csproj" +RUN dotnet restore "eShopOnDapr.sln" COPY . . WORKDIR "/src/src/Web/BlazorClient.Host" diff --git a/src/Web/WebStatus/Dockerfile b/src/Web/WebStatus/Dockerfile index f8640b8a..2f948b0f 100644 --- a/src/Web/WebStatus/Dockerfile +++ b/src/Web/WebStatus/Dockerfile @@ -12,10 +12,17 @@ COPY ["src/ApiGateways/Aggregators/Web.Shopping.HttpAggregator/Web.Shopping.Http COPY ["src/BuildingBlocks/EventBus/EventBus.csproj", "src/BuildingBlocks/EventBus/"] COPY ["src/BuildingBlocks/Healthchecks/Healthchecks.csproj", "src/BuildingBlocks/Healthchecks/"] COPY ["src/Services/Basket/Basket.API/Basket.API.csproj", "src/Services/Basket/Basket.API/"] +COPY ["src/Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj", "src/Services/Basket/Basket.FunctionalTests/"] +COPY ["src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj", "src/Services/Basket/Basket.UnitTests/"] COPY ["src/Services/Catalog/Catalog.API/Catalog.API.csproj", "src/Services/Catalog/Catalog.API/"] +COPY ["src/Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj", "src/Services/Catalog/Catalog.FunctionalTests/"] +COPY ["src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj", "src/Services/Catalog/Catalog.UnitTests/"] COPY ["src/Services/Identity/Identity.API/Identity.API.csproj", "src/Services/Identity/Identity.API/"] COPY ["src/Services/Ordering/Ordering.API/Ordering.API.csproj", "src/Services/Ordering/Ordering.API/"] +COPY ["src/Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj", "src/Services/Ordering/Ordering.FunctionalTests/"] +COPY ["src/Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj", "src/Services/Ordering/Ordering.UnitTests/"] COPY ["src/Services/Payment/Payment.API/Payment.API.csproj", "src/Services/Payment/Payment.API/"] +COPY ["src/Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj", "src/Tests/Services/Application.FunctionalTests/"] COPY ["src/Web/BlazorClient.Host/BlazorClient.Host.csproj", "src/Web/BlazorClient.Host/"] COPY ["src/Web/BlazorClient/BlazorClient.csproj", "src/Web/BlazorClient/"] COPY ["src/Web/WebStatus/WebStatus.csproj", "src/Web/WebStatus/"]