Repository: microsoft/contosotraders-cloudtesting Branch: main Commit: 6039159c0c63 Files: 296 Total size: 828.1 KB Directory structure: gitextract_0zyx3uia/ ├── .azurepipelines/ │ ├── aks-cost-optimization.yml │ └── contoso-traders-cloud-testing.yml ├── .devcontainer/ │ └── devcontainer.json ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── pull_request_template.md │ └── workflows/ │ ├── aks-cost-optimization.yml │ ├── codeql.yml │ └── contoso-traders-cloud-testing.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── demo-scripts/ │ ├── azure-chaos-studio/ │ │ └── walkthrough.md │ ├── azure-load-testing/ │ │ ├── aks-cost-optimization.md │ │ ├── private-endpoints.md │ │ └── walkthrough.md │ ├── dev-workflow/ │ │ └── walkthrough.md │ └── testing-with-playwright/ │ └── walkthrough.md ├── docs/ │ ├── architecture/ │ │ ├── .$contoso-traders-enhancements.drawio.bkp │ │ ├── .$contoso-traders-enhancements.drawio.dtmp │ │ └── contoso-traders-enhancements.drawio │ ├── deployment-instructions-azure-pipelines.md │ ├── deployment-instructions.md │ └── running-locally.md ├── iac/ │ ├── createPrivateDnsZone.bicep │ ├── createResourceGroup.bicep │ ├── createResources.bicep │ ├── createResources.parameters.json │ ├── dashboard.json │ ├── rsa │ ├── rsa.pub │ └── scripts/ │ └── enable-static-website.ps1 ├── loadtests/ │ ├── contoso-traders-carts-internal.yaml │ ├── contoso-traders-carts.jmx │ ├── contoso-traders-carts.yaml │ ├── contoso-traders-products.jmx │ └── contoso-traders-products.yaml └── src/ ├── .dockerignore ├── ContosoTraders.Api.Carts/ │ ├── ContosoTraders.Api.Carts.csproj │ ├── Controllers/ │ │ ├── ProfilesController.cs │ │ └── ShoppingCartController.cs │ ├── Dockerfile │ ├── Program.cs │ ├── Properties/ │ │ ├── ServiceDependencies/ │ │ │ └── tailwind-traders-cart - Zip Deploy/ │ │ │ └── profile.arm.json │ │ └── launchSettings.json │ └── Usings.cs ├── ContosoTraders.Api.Core/ │ ├── Constants/ │ │ ├── AuthConstants.cs │ │ ├── CosmosConstants.cs │ │ ├── KeyVaultConstants.cs │ │ └── RequestHeaderConstants.cs │ ├── ContosoTraders.Api.Core.csproj │ ├── Controllers/ │ │ └── ContosoTradersControllerBase.cs │ ├── DependencyInjection.cs │ ├── Exceptions/ │ │ ├── CartNotFoundException.cs │ │ ├── ContosoTradersBaseException.cs │ │ ├── MatchingProductsNotFoundException.cs │ │ ├── ProductNotFoundException.cs │ │ ├── ProfileNotFoundException.cs │ │ └── StockNotFoundException.cs │ ├── Models/ │ │ ├── AutoMapperProfile.cs │ │ ├── Implementations/ │ │ │ ├── Dao/ │ │ │ │ ├── Brand.cs │ │ │ │ ├── CartDao.cs │ │ │ │ ├── Feature.cs │ │ │ │ ├── Product.cs │ │ │ │ ├── Profile.cs │ │ │ │ ├── StockDao.cs │ │ │ │ ├── Tag.cs │ │ │ │ └── Type.cs │ │ │ └── Dto/ │ │ │ ├── AccessToken.cs │ │ │ ├── CartDto.cs │ │ │ ├── ProductDto.cs │ │ │ ├── StockDto.cs │ │ │ └── TokenRequest.cs │ │ └── Interfaces/ │ │ └── ICosmosDao.cs │ ├── Repositories/ │ │ ├── Implementations/ │ │ │ ├── CartRepository.cs │ │ │ ├── CosmosGenericRepositoryBase.cs │ │ │ └── StockRepository.cs │ │ ├── Interfaces/ │ │ │ ├── ICartRepository.cs │ │ │ ├── ICosmosGenericRepository.cs │ │ │ └── IStockRepository.cs │ │ ├── ProductsDbContext.cs │ │ └── ProfilesDbContext.cs │ ├── Requests/ │ │ ├── Definitions/ │ │ │ ├── AddItemToCartRequest.cs │ │ │ ├── DecrementStockCountRequest.cs │ │ │ ├── GetCartRequest.cs │ │ │ ├── GetPopularProductsRequest.cs │ │ │ ├── GetProductRequest.cs │ │ │ ├── GetProductsRequest.cs │ │ │ ├── GetProfileRequest.cs │ │ │ ├── GetProfilesRequest.cs │ │ │ ├── GetStockRequest.cs │ │ │ ├── LoadTestRequest.cs │ │ │ ├── PostImageRequest.cs │ │ │ ├── RemoveItemFromCartRequest.cs │ │ │ ├── SearchTextRequest.cs │ │ │ └── UpdateCartItemQuantityRequest.cs │ │ ├── Handlers/ │ │ │ ├── AddItemToCartRequestHandler.cs │ │ │ ├── DecrementStockCountRequestHandler.cs │ │ │ ├── GetCartRequestHandler.cs │ │ │ ├── GetPopularProductsRequestHandler.cs │ │ │ ├── GetProductRequestHandler.cs │ │ │ ├── GetProductsRequestHandler.cs │ │ │ ├── GetProfileRequestHandler.cs │ │ │ ├── GetProfilesRequestHandler.cs │ │ │ ├── GetStockRequestHandler.cs │ │ │ ├── LoadTestRequestHandler.cs │ │ │ ├── PostImageRequestHandler.cs │ │ │ ├── RemoveItemFromCartRequestHandler.cs │ │ │ ├── SearchTextRequestHandler.cs │ │ │ └── UpdateCartItemQuantityRequestHandler.cs │ │ └── Validators/ │ │ ├── AddItemToCartRequestValidator.cs │ │ ├── DecrementStockCountRequestValidator.cs │ │ ├── GetCartRequestValidator.cs │ │ ├── GetPopularProductsRequestValidator.cs │ │ ├── GetProductRequestValidator.cs │ │ ├── GetProductsRequestValidator.cs │ │ ├── GetProfileRequestValidator.cs │ │ ├── GetProfilesRequestValidator.cs │ │ ├── GetStockRequestValidator.cs │ │ ├── PostImageRequestValidator.cs │ │ ├── RemoveItemFromCartRequestValidator.cs │ │ ├── SearchTextRequestValidator.cs │ │ └── UpdateCartItemQuantityRequestValidator.cs │ ├── Services/ │ │ ├── ContosoTradersServiceBase.cs │ │ ├── Implementations/ │ │ │ ├── CartService.cs │ │ │ ├── ImageAnalysisService.cs │ │ │ ├── ImageSearchService.cs │ │ │ ├── ProductService.cs │ │ │ ├── ProfileService.cs │ │ │ └── StockService.cs │ │ └── Interfaces/ │ │ ├── ICartService.cs │ │ ├── IImageAnalysisService.cs │ │ ├── IImageSearchService.cs │ │ ├── IProductService.cs │ │ ├── IProfileService.cs │ │ └── IStockService.cs │ └── Usings.cs ├── ContosoTraders.Api.Products/ │ ├── ContosoTraders.Api.Products.csproj │ ├── Controllers/ │ │ ├── LoginController.cs │ │ ├── ProductsController.cs │ │ ├── ProfilesController.cs │ │ └── StocksController.cs │ ├── Dockerfile │ ├── Manifests/ │ │ ├── Certificate.yaml │ │ ├── ClusterIssuer.yaml │ │ ├── ClusterRole.yaml │ │ ├── Deployment.yaml │ │ ├── Ingress.yaml │ │ ├── NamespaceCertManager.yaml │ │ ├── NamespaceChaosTesting.yaml │ │ └── Service.yaml │ ├── Migration/ │ │ └── productsdb.sql │ ├── Program.cs │ ├── Properties/ │ │ ├── ServiceDependencies/ │ │ │ ├── tailwind-traders-product - Web Deploy/ │ │ │ │ └── profile.arm.json │ │ │ └── tailwind-traders-product - Web Deploy1/ │ │ │ └── profile.arm.json │ │ └── launchSettings.json │ └── Usings.cs ├── ContosoTraders.Api.Profiles/ │ ├── .gitignore │ ├── ContosoTraders.Api.Profiles.csproj │ ├── Controllers/ │ │ └── ProfilesController.cs │ ├── Properties/ │ │ ├── serviceDependencies.json │ │ └── serviceDependencies.local.json │ ├── Usings.cs │ └── host.json ├── ContosoTraders.Ui.Website/ │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── playwright.config.ts │ ├── public/ │ │ ├── browserconfig.xml │ │ ├── index.html │ │ ├── manifest.json │ │ └── site.webmanifest │ ├── src/ │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── actions/ │ │ │ └── actions.js │ │ ├── components/ │ │ │ ├── Input/ │ │ │ │ └── checkbox.js │ │ │ ├── accordion/ │ │ │ │ ├── accordion.js │ │ │ │ ├── accordion.scss │ │ │ │ └── sidebarAccordion.js │ │ │ ├── breadcrumb/ │ │ │ │ ├── breadcrumb.js │ │ │ │ └── breadcrumb.scss │ │ │ ├── corousel/ │ │ │ │ ├── corousel.js │ │ │ │ └── corousel.scss │ │ │ ├── dropdowns/ │ │ │ │ ├── categories.js │ │ │ │ └── categories.scss │ │ │ ├── footer/ │ │ │ │ ├── footer.js │ │ │ │ └── footer.scss │ │ │ ├── header/ │ │ │ │ ├── appbar.js │ │ │ │ ├── header.js │ │ │ │ ├── header.scss │ │ │ │ └── headerMessage.js │ │ │ ├── imageSlider/ │ │ │ │ ├── imageSlider.js │ │ │ │ └── imageSlider.scss │ │ │ ├── loadingSpinner/ │ │ │ │ ├── loadingSpinner.js │ │ │ │ └── loadingSpinner.scss │ │ │ ├── minimalSelect/ │ │ │ │ ├── minimalSelect.js │ │ │ │ ├── minimalSelect.scss │ │ │ │ └── minimalSelect.styles.js │ │ │ ├── productCard/ │ │ │ │ ├── product.js │ │ │ │ └── product.scss │ │ │ ├── quantityCounter/ │ │ │ │ ├── productCounter.js │ │ │ │ └── productCounter.scss │ │ │ ├── shared/ │ │ │ │ └── index.js │ │ │ ├── slider/ │ │ │ │ ├── slider.js │ │ │ │ └── slider.scss │ │ │ └── uploadFile/ │ │ │ ├── uploadFile.js │ │ │ └── uploadFile.scss │ │ ├── helpers/ │ │ │ ├── errorsHandler.js │ │ │ ├── localStorage.js │ │ │ ├── refreshJWTHelper.js │ │ │ ├── toast.js │ │ │ └── tokensHelper.js │ │ ├── index.css │ │ ├── index.js │ │ ├── main.scss │ │ ├── pages/ │ │ │ ├── arrivals/ │ │ │ │ ├── arrivals.js │ │ │ │ └── arrivals.scss │ │ │ ├── cart/ │ │ │ │ ├── cart.js │ │ │ │ └── cart.scss │ │ │ ├── detail/ │ │ │ │ ├── detail.scss │ │ │ │ ├── detailContainer.js │ │ │ │ └── productDetails.js │ │ │ ├── error/ │ │ │ │ ├── errorPage.js │ │ │ │ └── errorPage.scss │ │ │ ├── home/ │ │ │ │ ├── home.js │ │ │ │ ├── home.scss │ │ │ │ ├── homeContainer.js │ │ │ │ └── sections/ │ │ │ │ ├── banner.js │ │ │ │ ├── finalSection.js │ │ │ │ ├── gridSection.js │ │ │ │ └── hero.js │ │ │ ├── index.js │ │ │ ├── legals/ │ │ │ │ ├── aboutUs.js │ │ │ │ ├── legals.scss │ │ │ │ ├── refundPolicy.js │ │ │ │ └── termsOfService.js │ │ │ ├── list/ │ │ │ │ ├── list.js │ │ │ │ ├── list.scss │ │ │ │ ├── listContainer.js │ │ │ │ └── sections/ │ │ │ │ ├── banner/ │ │ │ │ │ └── offerBanner.js │ │ │ │ ├── index.js │ │ │ │ ├── listAside/ │ │ │ │ │ └── listAside.js │ │ │ │ └── listGrid/ │ │ │ │ └── listGrid.js │ │ │ ├── profile/ │ │ │ │ ├── myAddressBook.js │ │ │ │ ├── myOrders.js │ │ │ │ ├── myWishlist.js │ │ │ │ ├── personalInformation.js │ │ │ │ ├── profile.scss │ │ │ │ └── profileForm.js │ │ │ └── suggestedProductsList/ │ │ │ ├── suggestedProductsList.js │ │ │ └── suggestedproductslist.scss │ │ ├── reducers/ │ │ │ ├── login.reducer.js │ │ │ └── reducers.js │ │ ├── reportWebVitals.js │ │ ├── services/ │ │ │ ├── authB2CService.js │ │ │ ├── cartService.js │ │ │ ├── configService.js │ │ │ ├── index.js │ │ │ ├── productsService.js │ │ │ ├── telemetryClient.js │ │ │ └── userService.js │ │ ├── setupTests.js │ │ ├── store.js │ │ ├── styles/ │ │ │ ├── abstracts/ │ │ │ │ ├── _variables.scss │ │ │ │ └── mixins/ │ │ │ │ ├── _ellipsis.scss │ │ │ │ ├── _font-placeholders.scss │ │ │ │ ├── _font-scale.scss │ │ │ │ ├── _fonts.scss │ │ │ │ └── _loader.scss │ │ │ ├── base/ │ │ │ │ ├── _base.scss │ │ │ │ ├── _typography.scss │ │ │ │ └── _utilities.scss │ │ │ └── vendor/ │ │ │ └── _normalize.scss │ │ └── types/ │ │ └── types.js │ ├── tests/ │ │ ├── account.ts │ │ ├── api/ │ │ │ ├── cart.spec.ts │ │ │ ├── data.spec.ts │ │ │ └── products.spec.ts │ │ ├── auth.setup.ts │ │ ├── cart.spec.ts │ │ ├── darkmode.spec.ts │ │ ├── discounts.spec.ts │ │ ├── fileupload.spec.ts │ │ ├── map.spec.ts │ │ ├── mocks.spec.ts │ │ ├── pages.spec.ts │ │ └── test-data.csv │ └── tsconfig.json ├── ContosoTraders.sln └── ContosoTraders.sln.DotSettings ================================================ FILE CONTENTS ================================================ ================================================ FILE: .azurepipelines/aks-cost-optimization.yml ================================================ name: aks-cost-optimization trigger: none pr: none variables: - group: contosotraders-cloudtesting-variable-group - name: ACR_NAME value: contosotradersacr - name: AKS_CLUSTER_NAME value: contoso-traders-aks - name: AKS_CPU_LIMIT value: 250m - name: AKS_MEMORY_LIMIT value: 256Mi - name: AKS_REPLICAS value: "1" - name: AKS_SECRET_NAME_ACR_PASSWORD value: contoso-traders-acr-password - name: KV_NAME value: contosotraderskv - name: LOAD_TEST_SERVICE_NAME value: contoso-traders-loadtest - name: PRODUCTS_ACR_REPOSITORY_NAME value: contosotradersapiproducts - name: RESOURCE_GROUP_NAME value: contoso-traders-rg pool: vmImage: ubuntu-latest stages: - stage: default jobs: - job: aks_cost_optimization strategy: matrix: Cpu250m_Mem256Mi: AKS_REPLICAS: "1" AKS_CPU_LIMIT: "250m" AKS_MEMORY_LIMIT: "256Mi" Cpu250m_Mem128Mi: AKS_REPLICAS: "1" AKS_CPU_LIMIT: "250m" AKS_MEMORY_LIMIT: "128Mi" Cpu100m_Mem256Mi: AKS_REPLICAS: "1" AKS_CPU_LIMIT: "100m" AKS_MEMORY_LIMIT: "256Mi" Cpu100m_Mem128Mi: AKS_REPLICAS: "1" AKS_CPU_LIMIT: "100m" AKS_MEMORY_LIMIT: "128Mi" maxParallel: 1 steps: - task: AzureCLI@1 displayName: get products api endpoint name: getProductsApiEndpoint inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo "##vso[task.setvariable variable=productsApiEndpoint;isOutput=true]$(az keyvault secret show --vault-name $(KV_NAME)$(SUFFIX) --name productsApiEndpoint --query value -o tsv)" - task: replacetokens@5 displayName: substitute tokens in deployment manifest inputs: targetFiles: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml tokenPattern: custom tokenPrefix: "{" tokenSuffix: "}" inlineVariables: | SUFFIX: "$(SUFFIX)" AKS_REPLICAS: "$(AKS_REPLICAS)" AKS_CPU_LIMIT: "$(AKS_CPU_LIMIT)" AKS_MEMORY_LIMIT: "$(AKS_MEMORY_LIMIT)" - task: KubernetesManifest@1 displayName: apply deployment manifest inputs: connectionType: azureResourceManager azureSubscriptionConnection: SERVICEPRINCIPAL azureResourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) kubernetesCluster: $(AKS_CLUSTER_NAME)$(SUFFIX) action: deploy manifests: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml containers: $(ACR_NAME)$(SUFFIX).azurecr.io/$(PRODUCTS_ACR_REPOSITORY_NAME):latest imagePullSecrets: $(AKS_SECRET_NAME_ACR_PASSWORD) - task: AzureLoadTest@1 displayName: load test (products API) inputs: azureSubscription: SERVICEPRINCIPAL # Path of the YAML file. Should be fully qualified path or relative to the default working directory loadtestConfigFile: ./loadtests/contoso-traders-products.yaml resourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) loadtestResource: $(LOAD_TEST_SERVICE_NAME)$(SUFFIX) env: | [ { "name": "domain", "value": "$(getProductsApiEndpoint.productsApiEndpoint)" }, { "name": "protocol", "value": "https" }, { "name": "path", "value": "v1/Products/1" }, { "name": "threads_per_engine", "value": "25" }, { "name": "ramp_up_time", "value": "0" }, { "name": "duration_in_sec", "value": "45" } ] - job: reset_aks dependsOn: [aks_cost_optimization] condition: always() steps: - task: AzureCLI@1 displayName: get products api endpoint name: getProductsApiEndpoint inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo "##vso[task.setvariable variable=productsApiEndpoint;isOutput=true]$(az keyvault secret show --vault-name $(KV_NAME)$(SUFFIX) --name productsApiEndpoint --query value -o tsv)" - task: replacetokens@5 displayName: substitute tokens in deployment manifest inputs: targetFiles: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml tokenPattern: custom tokenPrefix: "{" tokenSuffix: "}" inlineVariables: | SUFFIX: "$(SUFFIX)" AKS_REPLICAS: "$(AKS_REPLICAS)" AKS_CPU_LIMIT: "$(AKS_CPU_LIMIT)" AKS_MEMORY_LIMIT: "$(AKS_MEMORY_LIMIT)" - task: KubernetesManifest@1 displayName: apply deployment manifest inputs: connectionType: azureResourceManager azureSubscriptionConnection: SERVICEPRINCIPAL azureResourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) kubernetesCluster: $(AKS_CLUSTER_NAME)$(SUFFIX) action: deploy manifests: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml containers: $(ACR_NAME)$(SUFFIX).azurecr.io/$(PRODUCTS_ACR_REPOSITORY_NAME):latest imagePullSecrets: $(AKS_SECRET_NAME_ACR_PASSWORD) ================================================ FILE: .azurepipelines/contoso-traders-cloud-testing.yml ================================================ name: contoso-traders-cloud-testing trigger: - main pr: branches: include: - main paths: exclude: - "docs/**" - "demo-scripts/**" variables: - group: contosotraders-cloudtesting-variable-group - name: ACR_NAME value: contosotradersacr - name: AKS_CLUSTER_NAME value: contoso-traders-aks - name: AKS_CPU_LIMIT value: 250m - name: AKS_DNS_LABEL value: contoso-traders-products - name: AKS_MEMORY_LIMIT value: 256Mi - name: AKS_NODES_RESOURCE_GROUP_NAME value: contoso-traders-aks-nodes-rg - name: AKS_REPLICAS value: "1" - name: AKS_SECRET_NAME_ACR_PASSWORD value: contoso-traders-acr-password - name: AKS_SECRET_NAME_KV_ENDPOINT value: contoso-traders-kv-endpoint - name: AKS_SECRET_NAME_MI_CLIENTID value: contoso-traders-mi-clientid - name: AZURE_AD_APP_NAME value: contoso-traders-cloud-testing-app - name: CARTS_ACA_NAME value: contoso-traders-carts - name: CARTS_ACR_REPOSITORY_NAME value: contosotradersapicarts - name: CARTS_INTERNAL_ACA_NAME value: contoso-traders-intcarts - name: CDN_PROFILE_NAME value: contoso-traders-cdn - name: CHAOS_AKS_EXPERIMENT_NAME value: contoso-traders-chaos-aks-experiment - name: KV_NAME value: contosotraderskv - name: LOAD_TEST_SERVICE_NAME value: contoso-traders-loadtest - name: MSGRAPH_API_ID value: 00000003-0000-0000-c000-000000000000 - name: MSGRAPH_API_PERMISSION_EMAIL value: 64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0=Scope - name: MSGRAPH_API_PERMISSION_USER_READ value: e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope - name: PRODUCTS_ACR_REPOSITORY_NAME value: contosotradersapiproducts - name: PRODUCTS_DB_NAME value: productsdb - name: PRODUCTS_DB_SERVER_NAME value: contoso-traders-products - name: PRODUCTS_DB_USER_NAME value: localadmin - name: PRODUCT_DETAILS_CONTAINER_NAME value: product-details - name: PRODUCT_IMAGES_STORAGE_ACCOUNT_NAME value: contosotradersimg - name: PRODUCT_LIST_CONTAINER_NAME value: product-list - name: PRODUCTS_CDN_ENDPOINT_NAME value: contoso-traders-images - name: RESOURCE_GROUP_NAME value: contoso-traders-rg - name: STORAGE_ACCOUNT_NAME value: contosotradersimg - name: UI_CDN_ENDPOINT_NAME value: contoso-traders-ui2 - name: UI_STORAGE_ACCOUNT_NAME value: contosotradersui2 - name: USER_ASSIGNED_MANAGED_IDENTITY_NAME value: contoso-traders-mi-kv-access pool: vmImage: ubuntu-latest stages: - stage: default jobs: - job: provision steps: # section #0: optional configuration of the Azure AD app. # create the Azure AD application (and update it if it already exists). # note: This is an idempotent operation. - task: AzureCLI@1 displayName: create/update azure active directory app condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az ad app create --display-name $(AZURE_AD_APP_NAME)$(SUFFIX) --sign-in-audience AzureADandPersonalMicrosoftAccount - task: AzureCLI@1 displayName: get azure ad app's object id condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) name: getAzureAdAppObjId inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo "##vso[task.setvariable variable=azureAdAppObjId;isOutput=true]$(az ad app list --display-name $(AZURE_AD_APP_NAME)$(SUFFIX) --query [].id -o tsv)" - task: AzureCLI@1 displayName: get azure ad app's client id condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) name: getAzureAdAppClientId inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo "##vso[task.setvariable variable=azureAdAppClientId;isOutput=true]$(az ad app list --display-name $(AZURE_AD_APP_NAME)$(SUFFIX) --query [].appId -o tsv)" - task: AzureCLI@1 displayName: register app as a spa condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: | az rest \ --method PATCH \ --uri https://graph.microsoft.com/v1.0/applications/$(getAzureAdAppObjId.azureAdAppObjId) \ --headers 'Content-Type=application/json' \ --body '{"spa":{"redirectUris":["https://localhost:3000/authcallback","http://localhost:3000/authcallback","https://production.contosotraders.com/authcallback","https://cloudtesting.contosotraders.com/authcallback"]}}' - task: AzureCLI@1 displayName: enable issuance of id, access tokens condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az ad app update --id $(getAzureAdAppObjId.azureAdAppObjId) --enable-access-token-issuance true --enable-id-token-issuance true - task: AzureCLI@1 displayName: enable email claim in access token condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az ad app update --id $(getAzureAdAppObjId.azureAdAppObjId) --optional-claims "{\"accessToken\":[{\"name\":\"email\",\"essential\":false}]}" # note: requesting MS Graph permissions in Azure AD app unfortunately isn't idempotent. # Even, if you have already requested the permissions, it'll keep adding to the list of requested permissions until you hit limit on max permissions requested. # Details: https://github.com/Azure/azure-cli/issues/24512 - task: AzureCLI@1 displayName: delete any requested Microsoft Graph permissions condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: | az ad app permission delete \ --id $(getAzureAdAppObjId.azureAdAppObjId) \ --api $(MSGRAPH_API_ID) - task: AzureCLI@1 displayName: request Microsoft Graph permissions condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: | az ad app permission add \ --id $(getAzureAdAppObjId.azureAdAppObjId) \ --api $(MSGRAPH_API_ID) \ --api-permissions $(MSGRAPH_API_PERMISSION_USER_READ) $(MSGRAPH_API_PERMISSION_EMAIL) # # section #1: provisioning the resources on Azure using bicep templates # # The first step is to create the resource group: `contoso-traders-rg`. # The below step can also be manually executed as follows: # az deployment sub create --location {LOCATION} --template-file .\createResourceGroup.bicep # Note: You can specify any location for `{LOCATION}`. It's the region where the deployment metadata will be stored, and not # where the resource groups will be deployed. - task: AzureResourceManagerTemplateDeployment@3 displayName: create resource group inputs: deploymentScope: Subscription azureResourceManagerConnection: SERVICEPRINCIPAL location: $(DEPLOYMENTREGION) csmFile: ./iac/createResourceGroup.bicep overrideParameters: -rgName $(RESOURCE_GROUP_NAME) -suffix $(SUFFIX) -rgLocation $(DEPLOYMENTREGION) # Next step is to deploy the Azure resources to the resource group `contoso-traders-rg` created above. The deployed resources # include storage accounts, function apps, app services cosmos db, and service bus etc. # The below step can also be manually executed as follows: # az deployment group create -g contoso-traders-rg --template-file .\createResources.bicep --parameters .\createResources.parameters.json # Note: The `createResources.parameters.json` file contains the parameters for the deployment; specifically the environment name. # You can modify the parameters to customize the deployment. # Note: The bicep template outputs are not shown in the logs. You can extract the outputs as shown here: # https://github.com/Azure/arm-deploy#another-example-on-how-to-use-this-action-to-get-the-output-of-arm-template - task: AzureResourceManagerTemplateDeployment@3 displayName: create resources inputs: deploymentScope: Resource Group azureResourceManagerConnection: SERVICEPRINCIPAL location: $(DEPLOYMENTREGION) resourceGroupName: "$(RESOURCE_GROUP_NAME)$(SUFFIX)" csmFile: ./iac/createResources.bicep csmParametersFile: ./iac/createResources.parameters.json overrideParameters: -suffix $(SUFFIX) -sqlPassword $(SQLPASSWORD) -deployPrivateEndpoints $(DEPLOYPRIVATEENDPOINTS) # Add the logged-in service principal to the key vault access policy - task: AzureCLI@1 displayName: add service principal to kv access policy inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az keyvault set-policy -n $(KV_NAME)$(SUFFIX) -g $(RESOURCE_GROUP_NAME)$(SUFFIX) --secret-permissions get list set --object-id $(az ad sp show --id $(az account show --query "user.name" -o tsv) --query "id" -o tsv) # The AKS agent pool needs to be assigned the user-assigned managed identity created (which has kv access) - task: AzureCLI@1 displayName: assign user-assigned managed-identity to aks agentpool inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: | az vmss identity assign \ --identities $(az identity show -g $(RESOURCE_GROUP_NAME)$(SUFFIX) --name $(USER_ASSIGNED_MANAGED_IDENTITY_NAME)$(SUFFIX) --query "id" -o tsv) \ --ids $(az vmss list -g $(AKS_NODES_RESOURCE_GROUP_NAME)$(SUFFIX) --query "[0].id" -o tsv) \ # Seed the DBs and storage accounts - script: sqlcmd -S $(PRODUCTS_DB_SERVER_NAME)$(SUFFIX).database.windows.net -U $(PRODUCTS_DB_USER_NAME) -P $(SQLPASSWORD) -d $(PRODUCTS_DB_NAME) -i ./src/ContosoTraders.Api.Products/Migration/productsdb.sql displayName: seed products db - task: AzureCLI@1 displayName: seed product image (product details) inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az storage blob sync --account-name '$(PRODUCT_IMAGES_STORAGE_ACCOUNT_NAME)$(SUFFIX)' -c '$(PRODUCT_DETAILS_CONTAINER_NAME)' -s 'src/ContosoTraders.Api.Images/product-details' - task: AzureCLI@1 displayName: seed product image (product list) inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az storage blob sync --account-name '$(PRODUCT_IMAGES_STORAGE_ACCOUNT_NAME)$(SUFFIX)' -c '$(PRODUCT_LIST_CONTAINER_NAME)' -s 'src/ContosoTraders.Api.Images/product-list' - task: AzureCLI@1 displayName: purge product images cdn endpoint inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az cdn endpoint purge --no-wait --content-paths '/*' -n '$(PRODUCTS_CDN_ENDPOINT_NAME)$(SUFFIX)' -g '$(RESOURCE_GROUP_NAME)$(SUFFIX)' --profile-name '$(CDN_PROFILE_NAME)$(SUFFIX)' - task: AzureCLI@1 displayName: extract acr password name: extractAcrPassword inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo "##vso[task.setvariable variable=acrPassword;isOutput=true;issecret=true]$(az acr credential show -n $(ACR_NAME)$(SUFFIX) -g $(RESOURCE_GROUP_NAME)$(SUFFIX) --query "passwords[0].value" --output tsv)" - script: docker login $(ACR_NAME)$(SUFFIX).azurecr.io --username $(ACR_NAME)$(SUFFIX) --password $(extractAcrPassword.acrPassword) displayName: azure container registry login - task: AzureCLI@1 displayName: set aks context inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az aks get-credentials --resource-group $(RESOURCE_GROUP_NAME)$(SUFFIX) --name $(AKS_CLUSTER_NAME)$(SUFFIX) # # section #2: deploy the carts api # - script: docker build src -f ./src/ContosoTraders.Api.Carts/Dockerfile -t $(ACR_NAME)$(SUFFIX).azurecr.io/$(CARTS_ACR_REPOSITORY_NAME):latest -t $(ACR_NAME)$(SUFFIX).azurecr.io/$(CARTS_ACR_REPOSITORY_NAME):$(Build.SourceVersion) displayName: docker build - script: docker push --all-tags $(ACR_NAME)$(SUFFIX).azurecr.io/$(CARTS_ACR_REPOSITORY_NAME) displayName: docker push (to acr) - task: AzureCLI@1 displayName: deploy to aca inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: | az config set extension.use_dynamic_install=yes_without_prompt az containerapp update -n $(CARTS_ACA_NAME)$(SUFFIX) -g $(RESOURCE_GROUP_NAME)$(SUFFIX) --image $(ACR_NAME)$(SUFFIX).azurecr.io/$(CARTS_ACR_REPOSITORY_NAME):$(Build.SourceVersion) - task: AzureCLI@1 displayName: deploy to aca (internal) condition: eq(variables['DEPLOYPRIVATEENDPOINTS'], 'true') inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: | az config set extension.use_dynamic_install=yes_without_prompt az containerapp update -n $(CARTS_INTERNAL_ACA_NAME)$(SUFFIX) -g $(RESOURCE_GROUP_NAME)$(SUFFIX) --image $(ACR_NAME)$(SUFFIX).azurecr.io/$(CARTS_ACR_REPOSITORY_NAME):$(Build.SourceVersion) - task: AzureCLI@1 displayName: get carts api endpoint name: getCartsApiEndpoint inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo "##vso[task.setvariable variable=cartsApiEndpoint;isOutput=true]$(az keyvault secret show --vault-name $(KV_NAME)$(SUFFIX) --name cartsApiEndpoint --query value -o tsv)" # # section #3: deploy the products api # - task: HelmInstaller@1 displayName: install helm name: installHelm inputs: helmVersionToInstall: 3.9.0 - script: docker build src -f ./src/ContosoTraders.Api.Products/Dockerfile -t $(ACR_NAME)$(SUFFIX).azurecr.io/$(PRODUCTS_ACR_REPOSITORY_NAME):latest -t $(ACR_NAME)$(SUFFIX).azurecr.io/$(PRODUCTS_ACR_REPOSITORY_NAME):$(Build.SourceVersion) displayName: docker build - script: docker push --all-tags $(ACR_NAME)$(SUFFIX).azurecr.io/$(PRODUCTS_ACR_REPOSITORY_NAME) displayName: docker push (to acr) # more details: https://stackoverflow.com/a/45881324 - script: kubectl delete secret $(AKS_SECRET_NAME_ACR_PASSWORD) --ignore-not-found displayName: delete any existing kubernetes secret (acr password) - script: | kubectl create secret docker-registry $(AKS_SECRET_NAME_ACR_PASSWORD) \ --docker-server=$(ACR_NAME)$(SUFFIX).azurecr.io \ --docker-username=$(ACR_NAME)$(SUFFIX) \ --docker-password=$(extractAcrPassword.acrPassword) \ --save-config \ -o yaml | kubectl apply -f - displayName: create kubernetes secret (acr password) - task: AzureCLI@1 displayName: get managedIdentityClientId name: getManagedIdentityClientId inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo "##vso[task.setvariable variable=managedIdentityClientId;isOutput=true]$(az identity show -g $(RESOURCE_GROUP_NAME)$(SUFFIX) --name $(USER_ASSIGNED_MANAGED_IDENTITY_NAME)$(SUFFIX) --query "clientId" -o tsv)" - task: KubernetesManifest@1 displayName: create kubernetes secret (kv endpoint) inputs: connectionType: azureResourceManager azureSubscriptionConnection: SERVICEPRINCIPAL azureResourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) kubernetesCluster: $(AKS_CLUSTER_NAME)$(SUFFIX) action: createSecret secretName: $(AKS_SECRET_NAME_KV_ENDPOINT) secretType: generic secretArguments: --from-literal=$(AKS_SECRET_NAME_KV_ENDPOINT)="https://$(KV_NAME)$(SUFFIX).vault.azure.net/" - task: KubernetesManifest@1 displayName: create kubernetes secret (managed identity client id) inputs: connectionType: azureResourceManager azureSubscriptionConnection: SERVICEPRINCIPAL azureResourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) kubernetesCluster: $(AKS_CLUSTER_NAME)$(SUFFIX) action: createSecret secretName: $(AKS_SECRET_NAME_MI_CLIENTID) secretType: generic secretArguments: --from-literal=$(AKS_SECRET_NAME_MI_CLIENTID)=$(getManagedIdentityClientId.managedIdentityClientId) - task: replacetokens@5 displayName: substitute tokens in deployment manifest inputs: targetFiles: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml tokenPattern: custom tokenPrefix: "{" tokenSuffix: "}" inlineVariables: | SUFFIX: "$(SUFFIX)" AKS_REPLICAS: "$(AKS_REPLICAS)" AKS_CPU_LIMIT: "$(AKS_CPU_LIMIT)" AKS_MEMORY_LIMIT: "$(AKS_MEMORY_LIMIT)" - task: KubernetesManifest@1 displayName: apply deployment manifest inputs: connectionType: azureResourceManager azureSubscriptionConnection: SERVICEPRINCIPAL azureResourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) kubernetesCluster: $(AKS_CLUSTER_NAME)$(SUFFIX) action: deploy manifests: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml containers: $(ACR_NAME)$(SUFFIX).azurecr.io/$(PRODUCTS_ACR_REPOSITORY_NAME):$(Build.SourceVersion) imagePullSecrets: $(AKS_SECRET_NAME_ACR_PASSWORD) - task: KubernetesManifest@1 displayName: apply service manifest inputs: connectionType: azureResourceManager azureSubscriptionConnection: SERVICEPRINCIPAL azureResourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) kubernetesCluster: $(AKS_CLUSTER_NAME)$(SUFFIX) action: deploy manifests: ./src/ContosoTraders.Api.Products/Manifests/Service.yaml # setup chaos mesh - task: KubernetesManifest@1 displayName: apply namespace manifest (chaos-testing) inputs: connectionType: azureResourceManager azureSubscriptionConnection: SERVICEPRINCIPAL azureResourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) kubernetesCluster: $(AKS_CLUSTER_NAME)$(SUFFIX) action: deploy manifests: ./src/ContosoTraders.Api.Products/Manifests/NamespaceChaosTesting.yaml - script: | az aks get-credentials --resource-group $(RESOURCE_GROUP_NAME)$(SUFFIX) --name $(AKS_CLUSTER_NAME)$(SUFFIX) helm repo add chaos-mesh https://charts.chaos-mesh.org helm repo update helm upgrade --install chaos-mesh chaos-mesh/chaos-mesh --namespace=chaos-testing --set chaosDaemon.runtime=containerd --set chaosDaemon.socketPath=/run/containerd/containerd.sock displayName: setup chaos mesh # create the ingress controller - script: | az aks get-credentials --resource-group $(RESOURCE_GROUP_NAME)$(SUFFIX) --name $(AKS_CLUSTER_NAME)$(SUFFIX) helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm upgrade --install --wait --timeout=1h nginx-ingress ingress-nginx/ingress-nginx \ --set controller.replicaCount=1 \ --set controller.nodeSelector."kubernetes\.io/os"=linux \ --set defaultBackend.nodeSelector."kubernetes\.io/os"=linux \ --set controller.admissionWebhooks.patch.nodeSelector."kubernetes\.io/os"=linux \ --set controller.service.externalTrafficPolicy=Local displayName: create ingress controller - task: AzureCLI@1 displayName: set dns label on public ip inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az network public-ip update --dns-name $(AKS_DNS_LABEL)$(SUFFIX) -g $(AKS_NODES_RESOURCE_GROUP_NAME)$(SUFFIX) -n $(az network public-ip list --query "[?starts_with(name,'kubernetes-') ].name" -o tsv -g $(AKS_NODES_RESOURCE_GROUP_NAME)$(SUFFIX)) # hack: extract the full fqdn / dns label of the aks app's public IP address - task: AzureCLI@1 displayName: get aks-fqdn name: getAksFqdn inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript # note: There should be a whitespace between ')' and ']'. More details: https://stackoverflow.com/a/59154958 inlineScript: echo "##vso[task.setvariable variable=aksFqdn;isOutput=true]$(az network public-ip list --query "[?starts_with(name,'kubernetes-') ].dnsSettings.fqdn" -o tsv -g $(AKS_NODES_RESOURCE_GROUP_NAME)$(SUFFIX))" # install cert-manager - task: KubernetesManifest@1 displayName: apply namespace manifest (cert-manager) inputs: connectionType: azureResourceManager azureSubscriptionConnection: SERVICEPRINCIPAL azureResourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) kubernetesCluster: $(AKS_CLUSTER_NAME)$(SUFFIX) action: deploy manifests: ./src/ContosoTraders.Api.Products/Manifests/NamespaceCertManager.yaml - script: | az aks get-credentials --resource-group $(RESOURCE_GROUP_NAME)$(SUFFIX) --name $(AKS_CLUSTER_NAME)$(SUFFIX) kubectl apply --validate=false -f https://github.com/cert-manager/cert-manager/releases/download/v1.9.1/cert-manager.yaml displayName: install cert-manager - bash: sleep 30s displayName: sleep for 30 seconds - task: KubernetesManifest@1 displayName: apply clusterIssuer manifest inputs: connectionType: azureResourceManager azureSubscriptionConnection: SERVICEPRINCIPAL azureResourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) kubernetesCluster: $(AKS_CLUSTER_NAME)$(SUFFIX) action: deploy manifests: ./src/ContosoTraders.Api.Products/Manifests/ClusterIssuer.yaml - task: replacetokens@5 displayName: substitute tokens in certificate manifest inputs: targetFiles: ./src/ContosoTraders.Api.Products/Manifests/Certificate.yaml tokenPattern: custom tokenPrefix: "{" tokenSuffix: "}" inlineVariables: "AKS_FQDN: $(getAksFqdn.aksFqdn)" - task: KubernetesManifest@1 displayName: apply certificate manifest inputs: connectionType: azureResourceManager azureSubscriptionConnection: SERVICEPRINCIPAL azureResourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) kubernetesCluster: $(AKS_CLUSTER_NAME)$(SUFFIX) action: deploy manifests: ./src/ContosoTraders.Api.Products/Manifests/Certificate.yaml - task: replacetokens@5 displayName: substitute tokens in ingress manifest inputs: targetFiles: ./src/ContosoTraders.Api.Products/Manifests/Ingress.yaml tokenPattern: custom tokenPrefix: "{" tokenSuffix: "}" inlineVariables: "AKS_FQDN: $(getAksFqdn.aksFqdn)" - task: KubernetesManifest@1 displayName: apply ingress manifest inputs: connectionType: azureResourceManager azureSubscriptionConnection: SERVICEPRINCIPAL azureResourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) kubernetesCluster: $(AKS_CLUSTER_NAME)$(SUFFIX) action: deploy manifests: ./src/ContosoTraders.Api.Products/Manifests/Ingress.yaml - task: KubernetesManifest@1 displayName: apply clusterRole manifest inputs: connectionType: azureResourceManager azureSubscriptionConnection: SERVICEPRINCIPAL azureResourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) kubernetesCluster: $(AKS_CLUSTER_NAME)$(SUFFIX) action: deploy manifests: ./src/ContosoTraders.Api.Products/Manifests/ClusterRole.yaml - task: AzureCLI@1 displayName: set productsApiEndpoint in kv inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az keyvault secret set --vault-name $(KV_NAME)$(SUFFIX) --name productsApiEndpoint --value $(getAksFqdn.aksFqdn) --description "endpoint url (fqdn) of the products api" - task: AzureCLI@1 displayName: get products api endpoint name: getProductsApiEndpoint inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo "##vso[task.setvariable variable=productsApiEndpoint;isOutput=true]$(az keyvault secret show --vault-name $(KV_NAME)$(SUFFIX) --name productsApiEndpoint --query value -o tsv)" # # section #4: deploy the ui # - script: echo "##vso[task.setvariable variable=REACT_APP_APIURLSHOPPINGCART]https://$(getCartsApiEndpoint.cartsApiEndpoint)/v1" displayName: set REACT_APP_APIURLSHOPPINGCART - script: echo "##vso[task.setvariable variable=REACT_APP_APIURL]https://$(getProductsApiEndpoint.productsApiEndpoint)/v1" displayName: set REACT_APP_APIURL - script: echo "##vso[task.setvariable variable=REACT_APP_B2CCLIENTID]$(getAzureAdAppClientId.azureAdAppClientId)" displayName: set REACT_APP_B2CCLIENTID condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) - task: NodeTool@0 displayName: install nodejs inputs: versionSpec: 18.x - script: npm ci displayName: npm ci workingDirectory: src/ContosoTraders.Ui.Website - script: echo "##vso[task.setvariable variable=REACT_APP_BINGMAPSKEY]$(BINGMAPSKEY)" displayName: set REACT_APP_BINGMAPSKEY - script: npm run build displayName: npm run build workingDirectory: src/ContosoTraders.Ui.Website - task: AzureCLI@1 displayName: deploy ui to storage inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az storage blob sync --account-name '$(UI_STORAGE_ACCOUNT_NAME)$(SUFFIX)' -c '$web' -s 'src/ContosoTraders.Ui.Website/build' - task: AzureCLI@1 displayName: purge ui cdn endpoint inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az cdn endpoint purge --no-wait --content-paths '/*' -n '$(UI_CDN_ENDPOINT_NAME)$(SUFFIX)' -g '$(RESOURCE_GROUP_NAME)$(SUFFIX)' --profile-name '$(CDN_PROFILE_NAME)$(SUFFIX)' - task: AzureCLI@1 displayName: get ui cdn endpoint name: getUiCdnEndpoint inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo "##vso[task.setvariable variable=uiCdnEndpoint;isOutput=true]$(az keyvault secret show --vault-name $(KV_NAME)$(SUFFIX) --name uiCdnEndpoint --query value -o tsv)" - task: AzureCLI@1 displayName: register auth callback (UI CDN) condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: | az rest \ --method PATCH \ --uri https://graph.microsoft.com/v1.0/applications/$(getAzureAdAppObjId.azureAdAppObjId) \ --headers 'Content-Type=application/json' \ --body '{"spa":{"redirectUris":["https://localhost:3000/authcallback","http://localhost:3000/authcallback","https://staging.contosotraders.com/authcallback","https://production.contosotraders.com/authcallback","https://cloudtesting.contosotraders.com/authcallback","https://$(getUiCdnEndpoint.uiCdnEndpoint)/authcallback"]}}' - task: AzureCLI@1 displayName: display ui cdn endpoint inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo UI CDN endpoint accessible at https://$(az keyvault secret show --vault-name $(KV_NAME)$(SUFFIX) --name uiCdnEndpoint --query value -o tsv) - job: load_tests_with_chaos_products_api dependsOn: [provision, playwright_tests_ui] variables: productsApiEndpoint: $[ dependencies.provision.outputs['getProductsApiEndpoint.productsApiEndpoint'] ] steps: - task: AzureCLI@1 displayName: get chaos experiment resource id name: getChaosAksExperimentResourceId inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo "##vso[task.setvariable variable=chaosAksExperimentResourceId;isOutput=true]$(az resource show --resource-group $(RESOURCE_GROUP_NAME)$(SUFFIX) --namespace Microsoft.Chaos --resource-type Experiments --name $(CHAOS_AKS_EXPERIMENT_NAME)$(SUFFIX) --query "id" -o tsv)" - task: AzureCLI@1 displayName: start chaos experiment (pod failure) inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: az rest --method post --uri https://management.azure.com$(getChaosAksExperimentResourceId.chaosAksExperimentResourceId)/start?api-version=2021-09-15-preview - bash: sleep 30s displayName: sleep for 30 seconds - task: AzureLoadTest@1 displayName: load test (products API) inputs: azureSubscription: SERVICEPRINCIPAL # Path of the YAML file. Should be fully qualified path or relative to the default working directory loadtestConfigFile: ./loadtests/contoso-traders-products.yaml resourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) loadtestResource: $(LOAD_TEST_SERVICE_NAME)$(SUFFIX) env: | [ { "name": "domain", "value": "$(productsApiEndpoint)" }, { "name": "protocol", "value": "https" }, { "name": "path", "value": "v1/Products/1" }, { "name": "threads_per_engine", "value": "5" }, { "name": "ramp_up_time", "value": "0" }, { "name": "duration_in_sec", "value": "120" } ] - job: load_tests_carts_internal_api condition: eq(variables['DEPLOYPRIVATEENDPOINTS'], 'true') dependsOn: [provision, playwright_tests_ui] steps: - task: AzureCLI@1 displayName: get carts api endpoint (internal) name: getCartsInternalApiEndpoint inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo "##vso[task.setvariable variable=cartsInternalApiEndpoint;isOutput=true]$(az keyvault secret show --vault-name $(KV_NAME)$(SUFFIX) --name cartsInternalApiEndpoint --query value -o tsv)" - task: AzureCLI@1 displayName: get vnetAcaSubnetId name: getVnetAcaSubnetId inputs: azureSubscription: SERVICEPRINCIPAL scriptLocation: inlineScript inlineScript: echo "##vso[task.setvariable variable=vnetAcaSubnetId;isOutput=true]$(az keyvault secret show --vault-name $(KV_NAME)$(SUFFIX) -n vnetAcaSubnetId --query "value" -o tsv)" - task: replacetokens@5 displayName: substitute tokens in load test config file inputs: targetFiles: ./loadtests/contoso-traders-carts-internal.yaml tokenPattern: doublebraces inlineVariables: "LOAD_TEST_SUBNET_ID: $(getVnetAcaSubnetId.vnetAcaSubnetId)" - task: AzureLoadTest@1 displayName: load test (carts API) inputs: azureSubscription: SERVICEPRINCIPAL # Path of the YAML file. Should be fully qualified path or relative to the default working directory loadtestConfigFile: ./loadtests/contoso-traders-carts-internal.yaml resourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) loadtestResource: $(LOAD_TEST_SERVICE_NAME)$(SUFFIX) env: | [ { "name": "domain", "value": "$(getCartsInternalApiEndpoint.cartsInternalApiEndpoint)" }, { "name": "protocol", "value": "https" }, { "name": "path", "value": "v1/ShoppingCart/loadtest" }, { "name": "threads_per_engine", "value": "5" }, { "name": "ramp_up_time", "value": "0" }, { "name": "duration_in_sec", "value": "120" } ] - job: load_tests_carts_api dependsOn: [provision, playwright_tests_ui] variables: cartsApiEndpoint: $[ dependencies.provision.outputs['getCartsApiEndpoint.cartsApiEndpoint'] ] steps: - task: AzureLoadTest@1 displayName: load test (carts API) inputs: azureSubscription: SERVICEPRINCIPAL # Path of the YAML file. Should be fully qualified path or relative to the default working directory loadtestConfigFile: ./loadtests/contoso-traders-carts.yaml resourceGroup: $(RESOURCE_GROUP_NAME)$(SUFFIX) loadtestResource: $(LOAD_TEST_SERVICE_NAME)$(SUFFIX) env: | [ { "name": "domain", "value": "$(cartsApiEndpoint)" }, { "name": "protocol", "value": "https" }, { "name": "path", "value": "v1/ShoppingCart/loadtest" }, { "name": "threads_per_engine", "value": "5" }, { "name": "ramp_up_time", "value": "0" }, { "name": "duration_in_sec", "value": "120" } ] - job: playwright_tests_ui dependsOn: [provision] container: mcr.microsoft.com/playwright:v1.43.1-jammy variables: APIURLSHOPPINGCART: $[ dependencies.provision.outputs['getCartsApiEndpoint.cartsApiEndpoint'] ] APIURL: $[ dependencies.provision.outputs['getProductsApiEndpoint.productsApiEndpoint'] ] BASEURLFORPLAYWRIGHTTESTING: $[ dependencies.provision.outputs['getUiCdnEndpoint.uiCdnEndpoint'] ] B2CCLIENTID: $[ dependencies.provision.outputs['getAzureAdAppClientId.azureAdAppClientId'] ] steps: - task: NodeTool@0 displayName: install nodejs inputs: versionSpec: 18.x - script: echo "##vso[task.setvariable variable=REACT_APP_APIURLSHOPPINGCART]https://$(APIURLSHOPPINGCART)/v1" displayName: set REACT_APP_APIURLSHOPPINGCART - script: echo "##vso[task.setvariable variable=REACT_APP_APIURL]https://$(APIURL)/v1" displayName: set REACT_APP_APIURL - script: echo "##vso[task.setvariable variable=REACT_APP_BASEURLFORPLAYWRIGHTTESTING]https://$(BASEURLFORPLAYWRIGHTTESTING)" displayName: set REACT_APP_BASEURLFORPLAYWRIGHTTESTING - script: echo "##vso[task.setvariable variable=REACT_APP_B2CCLIENTID]$(B2CCLIENTID)" displayName: set REACT_APP_B2CCLIENTID condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) - script: echo "##vso[task.setvariable variable=REACT_APP_AADUSERNAME]$(AADUSERNAME)" displayName: set REACT_APP_AADUSERNAME condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) - script: echo "##vso[task.setvariable variable=REACT_APP_AADPASSWORD]$(AADPASSWORD)" displayName: set REACT_APP_AADPASSWORD condition: and(ne(variables['AADUSERNAME'], ''), ne(variables['AADPASSWORD'], '')) - script: npm ci displayName: install dependencies workingDirectory: src/ContosoTraders.Ui.Website - script: | echo "##vso[task.setvariable variable=CI;]1" npx playwright test displayName: run playwright tests workingDirectory: src/ContosoTraders.Ui.Website timeoutInMinutes: 20 - task: PublishTestResults@2 displayName: publish test results condition: succeededOrFailed() inputs: searchFolder: src/ContosoTraders.Ui.Website/playwright-report-junit testResultsFormat: JUnit testResultsFiles: e2e-junit-results.xml mergeTestResults: true failTaskOnFailedTests: true testRunTitle: Playwright Tests - task: PublishBuildArtifacts@1 displayName: upload playwright report condition: failed() inputs: PathtoPublish: 'src/ContosoTraders.Ui.Website/playwright-report' ArtifactName: 'playwright-report' publishLocation: 'Container' ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "Playwright", "image": "mcr.microsoft.com/playwright:v1.43.1-jammy", "features": { "ghcr.io/devcontainers/features/node:1": { "nodeGypDependencies": true, "version": "18" }, "ghcr.io/devcontainers-contrib/features/npm-package:1": { "package": "typescript", "version": "latest" } } } ================================================ FILE: .gitattributes ================================================ *.sh eol=lf mvnw eol=lf ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/pull_request_template.md ================================================ # Change Description > Please include a summary of the changes. ## Linked GitHub Issue > Link to any related github issue number (e.g. #56). Fixes # (issue) ## Checklist Please check all options that are relevant. - [ ] I have made all necessary updates to the documentation. - [ ] I have made all necessary updates to the provisioning scripts (bicep templates, github workflows). - [ ] This is not a breaking change. ================================================ FILE: .github/workflows/aks-cost-optimization.yml ================================================ name: aks-cost-optimization on: workflow_dispatch: env: ACR_NAME: contosotradersacr AKS_CLUSTER_NAME: contoso-traders-aks AKS_CPU_LIMIT: 250m AKS_MEMORY_LIMIT: 256Mi AKS_REPLICAS: "1" AKS_SECRET_NAME_ACR_PASSWORD: contoso-traders-acr-password KV_NAME: contosotraderskv LOAD_TEST_SERVICE_NAME: contoso-traders-loadtest PRODUCTS_ACR_REPOSITORY_NAME: contosotradersapiproducts RESOURCE_GROUP_NAME: contoso-traders-rg jobs: aks-cost-optimization: strategy: fail-fast: false matrix: AKS_CPU_LIMIT: ["250m", "100m"] AKS_MEMORY_LIMIT: ["256Mi", "128Mi"] max-parallel: 1 runs-on: ubuntu-latest steps: - name: checkout code uses: actions/checkout@v4 - name: azure login uses: azure/login@v1 with: creds: ${{ secrets.SERVICEPRINCIPAL }} - name: extract acr password uses: azure/CLI@v1 id: extract-acr-password with: inlineScript: | acrPassword=$(az acr credential show -n ${{ env.ACR_NAME }}${{ vars.SUFFIX }} -g ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --query "passwords[0].value" --output tsv) echo "::add-mask::$acrPassword" echo acrPassword=$acrPassword >> $GITHUB_OUTPUT - name: azure container registry login uses: azure/docker-login@v1 with: login-server: ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io username: ${{ env.ACR_NAME }}${{ vars.SUFFIX }} password: ${{ steps.extract-acr-password.outputs.acrPassword }} - name: set aks context uses: azure/aks-set-context@v3 with: resource-group: ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} cluster-name: ${{ env.AKS_CLUSTER_NAME }}${{ vars.SUFFIX }} - name: get products api endpoint uses: azure/CLI@v1 id: get-productsApiEndpoint with: inlineScript: echo "productsApiEndpoint"="$(az keyvault secret show --vault-name ${{ env.KV_NAME }}${{ vars.SUFFIX }} --name productsApiEndpoint --query value -o tsv)" >> $GITHUB_OUTPUT - name: substitute tokens in deployment manifest uses: cschleiden/replace-tokens@v1.2 with: tokenPrefix: "{" tokenSuffix: "}" files: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml env: SUFFIX: ${{ vars.SUFFIX }} AKS_REPLICAS: ${{ env.AKS_REPLICAS }} AKS_CPU_LIMIT: ${{ matrix.AKS_CPU_LIMIT }} AKS_MEMORY_LIMIT: ${{ matrix.AKS_MEMORY_LIMIT }} - name: apply deployment manifest uses: Azure/k8s-deploy@v4 with: manifests: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml images: ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io/${{ env.PRODUCTS_ACR_REPOSITORY_NAME }}:latest imagepullsecrets: ${{ env.AKS_SECRET_NAME_ACR_PASSWORD }} force: true - name: sleep for 30 seconds run: sleep 30s shell: bash - name: load test (products API) uses: Azure/load-testing@v1.1.19 with: # Path of the YAML file. Should be fully qualified path or relative to the default working directory loadtestConfigFile: ./loadtests/contoso-traders-products.yaml loadtestResource: ${{ env.LOAD_TEST_SERVICE_NAME }}${{ vars.SUFFIX }} resourceGroup: ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} env: | [ { "name": "domain", "value": "${{ steps.get-productsApiEndpoint.outputs.productsApiEndpoint }}" }, { "name": "protocol", "value": "https" }, { "name": "path", "value": "v1/Products/1" }, { "name": "threads_per_engine", "value": "25" }, { "name": "ramp_up_time", "value": "0" }, { "name": "duration_in_sec", "value": "45" } ] reset-aks: runs-on: ubuntu-latest needs: [aks-cost-optimization] if: always() steps: - name: checkout code uses: actions/checkout@v4 - name: azure login uses: azure/login@v1 with: creds: ${{ secrets.SERVICEPRINCIPAL }} - name: extract acr password uses: azure/CLI@v1 id: extract-acr-password with: inlineScript: | acrPassword=$(az acr credential show -n ${{ env.ACR_NAME }}${{ vars.SUFFIX }} -g ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --query "passwords[0].value" --output tsv) echo "::add-mask::$acrPassword" echo acrPassword=$acrPassword >> $GITHUB_OUTPUT - name: azure container registry login uses: azure/docker-login@v1 with: login-server: ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io username: ${{ env.ACR_NAME }}${{ vars.SUFFIX }} password: ${{ steps.extract-acr-password.outputs.acrPassword }} - name: set aks context uses: azure/aks-set-context@v3 with: resource-group: ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} cluster-name: ${{ env.AKS_CLUSTER_NAME }}${{ vars.SUFFIX }} - name: get products api endpoint uses: azure/CLI@v1 id: get-productsApiEndpoint with: inlineScript: echo "productsApiEndpoint"="$(az keyvault secret show --vault-name ${{ env.KV_NAME }}${{ vars.SUFFIX }} --name productsApiEndpoint --query value -o tsv)" >> $GITHUB_OUTPUT - name: substitute tokens in deployment manifest uses: cschleiden/replace-tokens@v1.2 with: tokenPrefix: "{" tokenSuffix: "}" files: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml env: SUFFIX: ${{ vars.SUFFIX }} AKS_REPLICAS: ${{ env.AKS_REPLICAS }} AKS_CPU_LIMIT: ${{ env.AKS_CPU_LIMIT }} AKS_MEMORY_LIMIT: ${{ env.AKS_MEMORY_LIMIT }} - name: apply deployment manifest uses: Azure/k8s-deploy@v4 with: manifests: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml images: ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io/${{ env.PRODUCTS_ACR_REPOSITORY_NAME }}:latest imagepullsecrets: ${{ env.AKS_SECRET_NAME_ACR_PASSWORD }} force: true ================================================ FILE: .github/workflows/codeql.yml ================================================ name: "CodeQL" on: workflow_dispatch: jobs: analyze: name: Analyze (${{ matrix.language }}) runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: security-events: write packages: read actions: read contents: read strategy: fail-fast: false matrix: include: - language: csharp build-mode: autobuild - language: javascript-typescript build-mode: none steps: - name: Checkout repository uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/contoso-traders-cloud-testing.yml ================================================ name: contoso-traders-cloud-testing on: workflow_dispatch: push: branches: ["main"] paths-ignore: ["docs/**", "demo-scripts/**"] env: ACR_NAME: contosotradersacr AKS_CLUSTER_NAME: contoso-traders-aks AKS_CPU_LIMIT: 250m AKS_DNS_LABEL: contoso-traders-products AKS_MEMORY_LIMIT: 256Mi AKS_NODES_RESOURCE_GROUP_NAME: contoso-traders-aks-nodes-rg AKS_REPLICAS: "1" AKS_SECRET_NAME_ACR_PASSWORD: contoso-traders-acr-password AKS_SECRET_NAME_KV_ENDPOINT: contoso-traders-kv-endpoint AKS_SECRET_NAME_MI_CLIENTID: contoso-traders-mi-clientid AZURE_AD_APP_NAME: contoso-traders-cloud-testing-app CARTS_ACA_NAME: contoso-traders-carts CARTS_ACR_REPOSITORY_NAME: contosotradersapicarts CARTS_INTERNAL_ACA_NAME: contoso-traders-intcarts CDN_PROFILE_NAME: contoso-traders-cdn CHAOS_AKS_EXPERIMENT_NAME: contoso-traders-chaos-aks-experiment KV_NAME: contosotraderskv LOAD_TEST_SERVICE_NAME: contoso-traders-loadtest MSGRAPH_API_ID: 00000003-0000-0000-c000-000000000000 MSGRAPH_API_PERMISSION_EMAIL: 64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0=Scope MSGRAPH_API_PERMISSION_USER_READ: e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope PRODUCTS_ACR_REPOSITORY_NAME: contosotradersapiproducts PRODUCTS_DB_NAME: productsdb PRODUCTS_DB_SERVER_NAME: contoso-traders-products PRODUCTS_DB_USER_NAME: localadmin PRODUCT_DETAILS_CONTAINER_NAME: product-details PRODUCT_IMAGES_STORAGE_ACCOUNT_NAME: contosotradersimg PRODUCT_LIST_CONTAINER_NAME: product-list PRODUCTS_CDN_ENDPOINT_NAME: contoso-traders-images RESOURCE_GROUP_NAME: contoso-traders-rg STORAGE_ACCOUNT_NAME: contosotradersimg UI_CDN_ENDPOINT_NAME: contoso-traders-ui2 UI_STORAGE_ACCOUNT_NAME: contosotradersui2 USER_ASSIGNED_MANAGED_IDENTITY_NAME: contoso-traders-mi-kv-access jobs: provision: runs-on: ubuntu-22.04 env: AADUSERNAME: ${{ secrets.AADUSERNAME }} AADPASSWORD: ${{ secrets.AADPASSWORD }} outputs: azureAdAppClientId: ${{ steps.get-azureAdAppClientId.outputs.azureAdAppClientId }} azureAdAppObjId: ${{ steps.get-azureAdAppObjId.outputs.azureAdAppObjId }} cartsApiEndpoint: ${{ steps.get-cartsApiEndpoint.outputs.cartsApiEndpoint }} productsApiEndpoint: ${{ steps.get-productsApiEndpoint.outputs.productsApiEndpoint }} uiCdnEndpoint: ${{ steps.get-uiCdnEndpoint.outputs.uiCdnEndpoint }} concurrency: group: provision cancel-in-progress: true steps: - name: checkout code uses: actions/checkout@v4 - name: azure login uses: azure/login@v1 with: creds: ${{ secrets.SERVICEPRINCIPAL }} # section #0: optional configuration of the Azure AD app. # create the Azure AD application (and update it if it already exists). # note: This is an idempotent operation. - name: create/update azure active directory app uses: azure/CLI@v1 if: ${{ env.AADUSERNAME != '' && env.AADPASSWORD != '' }} with: inlineScript: az ad app create --display-name ${{ env.AZURE_AD_APP_NAME}}${{ vars.SUFFIX }} --sign-in-audience AzureADandPersonalMicrosoftAccount - name: get azure ad app's object id uses: azure/CLI@v1 if: ${{ env.AADUSERNAME != '' && env.AADPASSWORD != '' }} id: get-azureAdAppObjId with: inlineScript: echo "azureAdAppObjId"="$(az ad app list --display-name ${{ env.AZURE_AD_APP_NAME }}${{ vars.SUFFIX }} --query [].id -o tsv)" >> $GITHUB_OUTPUT - name: get azure ad app's client id uses: azure/CLI@v1 if: ${{ env.AADUSERNAME != '' && env.AADPASSWORD != '' }} id: get-azureAdAppClientId with: inlineScript: echo "azureAdAppClientId"="$(az ad app list --display-name ${{ env.AZURE_AD_APP_NAME }}${{ vars.SUFFIX }} --query [].appId -o tsv)" >> $GITHUB_OUTPUT - name: register app as a spa uses: azure/CLI@v1 if: ${{ env.AADUSERNAME != '' && env.AADPASSWORD != '' }} with: inlineScript: | az rest \ --method PATCH \ --uri https://graph.microsoft.com/v1.0/applications/${{ steps.get-azureAdAppObjId.outputs.azureAdAppObjId }} \ --headers 'Content-Type=application/json' \ --body '{"spa":{"redirectUris":["https://localhost:3000/authcallback","http://localhost:3000/authcallback","https://production.contosotraders.com/authcallback","https://cloudtesting.contosotraders.com/authcallback"]}}' - name: enable issuance of id, access tokens uses: azure/CLI@v1 if: ${{ env.AADUSERNAME != '' && env.AADPASSWORD != '' }} with: inlineScript: az ad app update --id ${{ steps.get-azureAdAppObjId.outputs.azureAdAppObjId }} --enable-access-token-issuance true --enable-id-token-issuance true - name: enable email claim in access token uses: azure/CLI@v1 if: ${{ env.AADUSERNAME != '' && env.AADPASSWORD != '' }} with: inlineScript: az ad app update --id ${{ steps.get-azureAdAppObjId.outputs.azureAdAppObjId }} --optional-claims "{\"accessToken\":[{\"name\":\"email\",\"essential\":false}]}" # note: requesting MS Graph permissions in Azure AD app unfortunately isn't idempotent. # Even, if you have already requested the permissions, it'll keep adding to the list of requested permissions until you hit limit on max permissions requested. # Details: https://github.com/Azure/azure-cli/issues/24512 - name: delete any requested Microsoft Graph permissions uses: azure/CLI@v1 if: ${{ env.AADUSERNAME != '' && env.AADPASSWORD != '' }} with: inlineScript: | az ad app permission delete \ --id ${{ steps.get-azureAdAppObjId.outputs.azureAdAppObjId }} \ --api ${{ env.MSGRAPH_API_ID }} - name: request Microsoft Graph permissions uses: azure/CLI@v1 if: ${{ env.AADUSERNAME != '' && env.AADPASSWORD != '' }} with: inlineScript: | az ad app permission add \ --id ${{ steps.get-azureAdAppObjId.outputs.azureAdAppObjId }} \ --api ${{ env.MSGRAPH_API_ID }} \ --api-permissions ${{ env.MSGRAPH_API_PERMISSION_USER_READ }} ${{ env.MSGRAPH_API_PERMISSION_EMAIL }} # # section #1: provisioning the resources on Azure using bicep templates # # The first step is to create the resource group: `contoso-traders-rg`. # The below step can also be manually executed as follows: # az deployment sub create --location {LOCATION} --template-file .\createResourceGroup.bicep # Note: You can specify any location for `{LOCATION}`. It's the region where the deployment metadata will be stored, and not # where the resource groups will be deployed. - name: create resource group uses: Azure/arm-deploy@v1 with: scope: subscription region: ${{ vars.DEPLOYMENTREGION }} template: ./iac/createResourceGroup.bicep parameters: rgName=${{ env.RESOURCE_GROUP_NAME }} suffix=${{ vars.SUFFIX }} rgLocation=${{ vars.DEPLOYMENTREGION }} # Next step is to deploy the Azure resources to the resource group `contoso-traders-rg` created above. The deployed resources # include storage accounts, function apps, app services cosmos db, and service bus etc. # The below step can also be manually executed as follows: # az deployment group create -g contoso-traders-rg --template-file .\createResources.bicep --parameters .\createResources.parameters.json # Note: The `createResources.parameters.json` file contains the parameters for the deployment; specifically the environment name. # You can modify the parameters to customize the deployment. # Note: The bicep template outputs are not shown in the logs. You can extract the outputs as shown here: # https://github.com/Azure/arm-deploy#another-example-on-how-to-use-this-action-to-get-the-output-of-arm-template - name: create resources uses: Azure/arm-deploy@v1 with: scope: resourcegroup region: ${{ vars.DEPLOYMENTREGION }} resourceGroupName: ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} template: ./iac/createResources.bicep parameters: ./iac/createResources.parameters.json suffix=${{ vars.SUFFIX }} sqlPassword=${{ secrets.SQLPASSWORD }} deployPrivateEndpoints=${{ vars.DEPLOYPRIVATEENDPOINTS }} # Add the logged-in service principal to the key vault access policy - name: add service principal to kv access policy uses: azure/CLI@v1 with: inlineScript: az keyvault set-policy -n ${{ env.KV_NAME }}${{ vars.SUFFIX }} -g ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --secret-permissions get list set --object-id $(az ad sp show --id $(az account show --query "user.name" -o tsv) --query "id" -o tsv) # The AKS agent pool needs to be assigned the user-assigned managed identity created (which has kv access) - name: assign user-assigned managed-identity to aks agentpool uses: azure/CLI@v1 with: inlineScript: | az vmss identity assign \ --identities $(az identity show -g ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --name ${{ env.USER_ASSIGNED_MANAGED_IDENTITY_NAME }}${{ vars.SUFFIX }} --query "id" -o tsv) \ --ids $(az vmss list -g ${{ env.AKS_NODES_RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --query "[0].id" -o tsv) \ # Seed the DBs and storage accounts - name: seed products db uses: azure/sql-action@v2.2 with: connection-string: Server=tcp:${{ env.PRODUCTS_DB_SERVER_NAME }}${{ vars.SUFFIX }}.database.windows.net,1433;Initial Catalog=${{ env.PRODUCTS_DB_NAME }};Persist Security Info=False;User ID=${{ env.PRODUCTS_DB_USER_NAME }};Password=${{ secrets.SQLPASSWORD }};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30; path: ./src/ContosoTraders.Api.Products/Migration/productsdb.sql - name: seed product image (product details) uses: azure/CLI@v1 with: inlineScript: az storage blob sync --account-name '${{ env.PRODUCT_IMAGES_STORAGE_ACCOUNT_NAME }}${{ vars.SUFFIX }}' -c '${{ env.PRODUCT_DETAILS_CONTAINER_NAME }}' -s 'src/ContosoTraders.Api.Images/product-details' - name: seed product image (product list) uses: azure/CLI@v1 with: inlineScript: az storage blob sync --account-name '${{ env.PRODUCT_IMAGES_STORAGE_ACCOUNT_NAME }}${{ vars.SUFFIX }}' -c '${{ env.PRODUCT_LIST_CONTAINER_NAME }}' -s 'src/ContosoTraders.Api.Images/product-list' - name: purge product images cdn endpoint uses: azure/CLI@v1 with: inlineScript: az cdn endpoint purge --no-wait --content-paths '/*' -n '${{ env.PRODUCTS_CDN_ENDPOINT_NAME }}${{ vars.SUFFIX }}' -g '${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }}' --profile-name '${{ env.CDN_PROFILE_NAME }}${{ vars.SUFFIX }}' - name: extract acr password uses: azure/CLI@v1 id: extract-acr-password with: inlineScript: | acrPassword=$(az acr credential show -n ${{ env.ACR_NAME }}${{ vars.SUFFIX }} -g ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --query "passwords[0].value" --output tsv) echo "::add-mask::$acrPassword" echo acrPassword=$acrPassword >> $GITHUB_OUTPUT - name: azure container registry login uses: azure/docker-login@v1 with: login-server: ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io username: ${{ env.ACR_NAME }}${{ vars.SUFFIX }} password: ${{ steps.extract-acr-password.outputs.acrPassword }} - name: set aks context uses: azure/aks-set-context@v3 with: resource-group: ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} cluster-name: ${{ env.AKS_CLUSTER_NAME }}${{ vars.SUFFIX }} # # section #2: deploy the carts api # - name: docker build run: docker build src -f ./src/ContosoTraders.Api.Carts/Dockerfile -t ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io/${{ env.CARTS_ACR_REPOSITORY_NAME }}:latest -t ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io/${{ env.CARTS_ACR_REPOSITORY_NAME }}:${{ github.sha }} - name: docker push (to acr) run: docker push --all-tags ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io/${{ env.CARTS_ACR_REPOSITORY_NAME }} - name: deploy to aca uses: azure/CLI@v1 with: inlineScript: | az config set extension.use_dynamic_install=yes_without_prompt az containerapp update -n ${{ env.CARTS_ACA_NAME }}${{ vars.SUFFIX }} -g ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --image ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io/${{ env.CARTS_ACR_REPOSITORY_NAME }}:${{ github.sha }} - name: deploy to aca (internal) if: ${{ vars.DEPLOYPRIVATEENDPOINTS == 'true' }} uses: azure/CLI@v1 with: inlineScript: | az config set extension.use_dynamic_install=yes_without_prompt az containerapp update -n ${{ env.CARTS_INTERNAL_ACA_NAME }}${{ vars.SUFFIX }} -g ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --image ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io/${{ env.CARTS_ACR_REPOSITORY_NAME }}:${{ github.sha }} - name: get carts api endpoint uses: azure/CLI@v1 id: get-cartsApiEndpoint with: inlineScript: echo "cartsApiEndpoint"="$(az keyvault secret show --vault-name ${{ env.KV_NAME }}${{ vars.SUFFIX }} --name cartsApiEndpoint --query value -o tsv)" >> $GITHUB_OUTPUT # # section #3: deploy the products api # - name: install helm uses: Azure/setup-helm@v3 id: install-helm with: version: v3.9.0 - name: docker build run: docker build src -f ./src/ContosoTraders.Api.Products/Dockerfile -t ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io/${{ env.PRODUCTS_ACR_REPOSITORY_NAME }}:latest -t ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io/${{ env.PRODUCTS_ACR_REPOSITORY_NAME }}:${{ github.sha }} - name: docker push (to acr) run: docker push --all-tags ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io/${{ env.PRODUCTS_ACR_REPOSITORY_NAME }} - name: setup kubectl uses: azure/setup-kubectl@v3 - name: create kubernetes secret (acr password) uses: Azure/k8s-create-secret@v4 with: secret-name: ${{ env.AKS_SECRET_NAME_ACR_PASSWORD }} container-registry-url: ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io container-registry-username: ${{ env.ACR_NAME }}${{ vars.SUFFIX }} container-registry-password: ${{ steps.extract-acr-password.outputs.acrPassword }} - name: get managedIdentityClientId uses: azure/CLI@v1 id: get-managedIdentityClientId with: inlineScript: echo "managedIdentityClientId"="$(az identity show -g ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --name ${{ env.USER_ASSIGNED_MANAGED_IDENTITY_NAME }}${{ vars.SUFFIX }} --query "clientId" -o tsv)" >> $GITHUB_OUTPUT - name: create kubernetes secret (kv endpoint) uses: Azure/k8s-create-secret@v4 with: secret-type: "generic" secret-name: ${{ env.AKS_SECRET_NAME_KV_ENDPOINT }} string-data: '{ "${{ env.AKS_SECRET_NAME_KV_ENDPOINT }}" : "https://${{ env.KV_NAME }}${{ vars.SUFFIX }}.vault.azure.net/" }' - name: create kubernetes secret (managed identity client id) uses: Azure/k8s-create-secret@v4 with: secret-type: "generic" secret-name: ${{ env.AKS_SECRET_NAME_MI_CLIENTID }} string-data: '{ "${{ env.AKS_SECRET_NAME_MI_CLIENTID }}" : "${{ steps.get-managedIdentityClientId.outputs.managedIdentityClientId }}" }' - name: substitute tokens in deployment manifest uses: cschleiden/replace-tokens@v1.2 with: tokenPrefix: "{" tokenSuffix: "}" files: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml env: SUFFIX: ${{ vars.SUFFIX }} AKS_REPLICAS: ${{ env.AKS_REPLICAS }} AKS_CPU_LIMIT: ${{ env.AKS_CPU_LIMIT }} AKS_MEMORY_LIMIT: ${{ env.AKS_MEMORY_LIMIT }} - name: lint deployment manifest uses: azure/k8s-lint@v2.0 with: manifests: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml - name: apply deployment manifest uses: Azure/k8s-deploy@v4 with: manifests: ./src/ContosoTraders.Api.Products/Manifests/Deployment.yaml images: ${{ env.ACR_NAME }}${{ vars.SUFFIX }}.azurecr.io/${{ env.PRODUCTS_ACR_REPOSITORY_NAME }}:${{ github.sha }} imagepullsecrets: ${{ env.AKS_SECRET_NAME_ACR_PASSWORD }} force: true - name: apply service manifest uses: Azure/k8s-deploy@v4 with: pull-images: false manifests: ./src/ContosoTraders.Api.Products/Manifests/Service.yaml force: true # setup chaos mesh - name: apply namespace manifest (chaos-testing) uses: Azure/k8s-deploy@v4 with: pull-images: false manifests: ./src/ContosoTraders.Api.Products/Manifests/NamespaceChaosTesting.yaml force: true - name: setup chaos mesh run: | az aks get-credentials --resource-group ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --name ${{ env.AKS_CLUSTER_NAME }}${{ vars.SUFFIX }} ${{ steps.install-helm.outputs.helm-path }} repo add chaos-mesh https://charts.chaos-mesh.org ${{ steps.install-helm.outputs.helm-path }} repo update ${{ steps.install-helm.outputs.helm-path }} upgrade --install chaos-mesh chaos-mesh/chaos-mesh --namespace=chaos-testing --set chaosDaemon.runtime=containerd --set chaosDaemon.socketPath=/run/containerd/containerd.sock # create the ingress controller - name: create ingress controller run: | az aks get-credentials --resource-group ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --name ${{ env.AKS_CLUSTER_NAME }}${{ vars.SUFFIX }} ${{ steps.install-helm.outputs.helm-path }} repo add ingress-nginx https://kubernetes.github.io/ingress-nginx ${{ steps.install-helm.outputs.helm-path }} repo update ${{ steps.install-helm.outputs.helm-path }} upgrade --install --wait --timeout=1h nginx-ingress ingress-nginx/ingress-nginx \ --set controller.replicaCount=1 \ --set controller.nodeSelector."kubernetes\.io/os"=linux \ --set defaultBackend.nodeSelector."kubernetes\.io/os"=linux \ --set controller.admissionWebhooks.patch.nodeSelector."kubernetes\.io/os"=linux \ --set controller.service.externalTrafficPolicy=Local - name: set dns label on public ip uses: azure/CLI@v1 with: inlineScript: az network public-ip update --dns-name ${{ env.AKS_DNS_LABEL }}${{ vars.SUFFIX }} -g ${{ env.AKS_NODES_RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} -n $(az network public-ip list --query "[?starts_with(name,'kubernetes-') ].name" -o tsv -g ${{ env.AKS_NODES_RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }}) # hack: extract the full fqdn / dns label of the aks app's public IP address - name: get aks-fqdn uses: azure/CLI@v1 id: get-aks-fqdn with: # note: There should be a whitespace between ')' and ']'. More details: https://stackoverflow.com/a/59154958 inlineScript: echo "aksFqdn"="$(az network public-ip list --query "[?starts_with(name,'kubernetes-') ].dnsSettings.fqdn" -o tsv -g ${{ env.AKS_NODES_RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }})" >> $GITHUB_OUTPUT # install cert-manager - name: apply namespace manifest (cert-manager) uses: Azure/k8s-deploy@v4 with: pull-images: false manifests: ./src/ContosoTraders.Api.Products/Manifests/NamespaceCertManager.yaml force: true - name: install cert-manager run: | az aks get-credentials --resource-group ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --name ${{ env.AKS_CLUSTER_NAME }}${{ vars.SUFFIX }} kubectl apply --validate=false -f https://github.com/cert-manager/cert-manager/releases/download/v1.9.1/cert-manager.yaml - name: sleep for 30 seconds run: sleep 30s shell: bash - name: apply clusterIssuer manifest uses: Azure/k8s-deploy@v4 with: pull-images: false manifests: ./src/ContosoTraders.Api.Products/Manifests/ClusterIssuer.yaml force: true - name: substitute tokens in certificate manifest uses: cschleiden/replace-tokens@v1.2 with: tokenPrefix: "{" tokenSuffix: "}" files: ./src/ContosoTraders.Api.Products/Manifests/Certificate.yaml env: AKS_FQDN: ${{ steps.get-aks-fqdn.outputs.aksFqdn }} - name: apply certificate manifest uses: Azure/k8s-deploy@v4 with: pull-images: false manifests: ./src/ContosoTraders.Api.Products/Manifests/Certificate.yaml force: true - name: substitute tokens in ingress manifest uses: cschleiden/replace-tokens@v1.2 with: tokenPrefix: "{" tokenSuffix: "}" files: ./src/ContosoTraders.Api.Products/Manifests/Ingress.yaml env: AKS_FQDN: ${{ steps.get-aks-fqdn.outputs.aksFqdn }} - name: apply ingress manifest uses: Azure/k8s-deploy@v4 with: pull-images: false manifests: ./src/ContosoTraders.Api.Products/Manifests/Ingress.yaml force: true - name: apply clusterRole manifest uses: Azure/k8s-deploy@v4 with: pull-images: false manifests: ./src/ContosoTraders.Api.Products/Manifests/ClusterRole.yaml force: true - name: set productsApiEndpoint in kv uses: azure/CLI@v1 with: inlineScript: az keyvault secret set --vault-name ${{ env.KV_NAME }}${{ vars.SUFFIX }} --name productsApiEndpoint --value ${{ steps.get-aks-fqdn.outputs.aksFqdn }} --description "endpoint url (fqdn) of the products api" - name: get products api endpoint uses: azure/CLI@v1 id: get-productsApiEndpoint with: inlineScript: echo "productsApiEndpoint"="$(az keyvault secret show --vault-name ${{ env.KV_NAME }}${{ vars.SUFFIX }} --name productsApiEndpoint --query value -o tsv)" >> $GITHUB_OUTPUT # # section #4: deploy the ui # - name: set REACT_APP_APIURLSHOPPINGCART run: echo "REACT_APP_APIURLSHOPPINGCART"="https://${{ steps.get-cartsApiEndpoint.outputs.cartsApiEndpoint }}/v1" >> $GITHUB_ENV - name: set REACT_APP_APIURL run: echo "REACT_APP_APIURL"="https://${{ steps.get-productsApiEndpoint.outputs.productsApiEndpoint }}/v1" >> $GITHUB_ENV - name: set REACT_APP_B2CCLIENTID if: ${{ env.AADUSERNAME != '' && env.AADPASSWORD != '' }} run: echo "REACT_APP_B2CCLIENTID"="${{ steps.get-azureAdAppClientId.outputs.azureAdAppClientId }}" >> $GITHUB_ENV - uses: actions/setup-node@v3 with: node-version: 18 cache: npm cache-dependency-path: src/ContosoTraders.Ui.Website/package-lock.json - name: npm ci run: npm ci working-directory: src/ContosoTraders.Ui.Website - name: npm run build run: npm run build env: REACT_APP_BINGMAPSKEY: ${{ secrets.BINGMAPSKEY }} working-directory: src/ContosoTraders.Ui.Website - name: deploy ui to storage uses: azure/CLI@v1 with: inlineScript: az storage blob sync --account-name '${{ env.UI_STORAGE_ACCOUNT_NAME }}${{ vars.SUFFIX }}' -c '$web' -s 'src/ContosoTraders.Ui.Website/build' - name: purge ui cdn endpoint uses: azure/CLI@v1 with: inlineScript: az cdn endpoint purge --no-wait --content-paths '/*' -n '${{ env.UI_CDN_ENDPOINT_NAME }}${{ vars.SUFFIX }}' -g '${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }}' --profile-name '${{ env.CDN_PROFILE_NAME }}${{ vars.SUFFIX }}' - name: get ui cdn endpoint uses: azure/CLI@v1 id: get-uiCdnEndpoint with: inlineScript: echo "uiCdnEndpoint"="$(az keyvault secret show --vault-name ${{ env.KV_NAME }}${{ vars.SUFFIX }} --name uiCdnEndpoint --query value -o tsv)" >> $GITHUB_OUTPUT - name: register auth callback (UI CDN) if: ${{ env.AADUSERNAME != '' && env.AADPASSWORD != '' }} uses: azure/CLI@v1 with: inlineScript: | az rest \ --method PATCH \ --uri https://graph.microsoft.com/v1.0/applications/${{ steps.get-azureAdAppObjId.outputs.azureAdAppObjId }} \ --headers 'Content-Type=application/json' \ --body '{"spa":{"redirectUris":["https://localhost:3000/authcallback","http://localhost:3000/authcallback","https://staging.contosotraders.com/authcallback","https://production.contosotraders.com/authcallback","https://cloudtesting.contosotraders.com/authcallback","https://${{ steps.get-uiCdnEndpoint.outputs.uiCdnEndpoint }}/authcallback"]}}' - name: display ui cdn endpoint uses: azure/CLI@v1 with: inlineScript: echo UI CDN endpoint accessible at https://$(az keyvault secret show --vault-name ${{ env.KV_NAME }}${{ vars.SUFFIX }} --name uiCdnEndpoint --query value -o tsv) load-tests-with-chaos-products-api: needs: [provision, playwright-tests-ui] runs-on: ubuntu-22.04 concurrency: group: load-tests-with-chaos-products-api cancel-in-progress: true steps: - name: checkout code uses: actions/checkout@v4 - name: azure login uses: azure/login@v1 with: creds: ${{ secrets.SERVICEPRINCIPAL }} - name: get chaos experiment resource id uses: azure/CLI@v1 id: get-chaosAksExperimentResourceId with: inlineScript: echo "chaosAksExperimentResourceId"="$(az resource show --resource-group ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} --namespace Microsoft.Chaos --resource-type Experiments --name ${{ env.CHAOS_AKS_EXPERIMENT_NAME }}${{ vars.SUFFIX }} --query "id" -o tsv)" >> $GITHUB_OUTPUT - name: start chaos experiment (pod failure) uses: azure/CLI@v1 with: inlineScript: az rest --method post --uri https://management.azure.com${{ steps.get-chaosAksExperimentResourceId.outputs.chaosAksExperimentResourceId }}/start?api-version=2021-09-15-preview - name: sleep for 30 seconds run: sleep 30s shell: bash - name: load test (products API) uses: Azure/load-testing@v1.1.19 with: # Path of the YAML file. Should be fully qualified path or relative to the default working directory loadtestConfigFile: ./loadtests/contoso-traders-products.yaml loadtestResource: ${{ env.LOAD_TEST_SERVICE_NAME }}${{ vars.SUFFIX }} resourceGroup: ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} env: | [ { "name": "domain", "value": "${{ needs.provision.outputs.productsApiEndpoint }}" }, { "name": "protocol", "value": "https" }, { "name": "path", "value": "v1/Products/1" }, { "name": "threads_per_engine", "value": "5" }, { "name": "ramp_up_time", "value": "0" }, { "name": "duration_in_sec", "value": "120" } ] load-tests-carts-internal-api: if: ${{ vars.DEPLOYPRIVATEENDPOINTS == 'true' }} needs: [provision, playwright-tests-ui] runs-on: ubuntu-22.04 concurrency: group: load-tests-carts-internal-api cancel-in-progress: true steps: - name: checkout code uses: actions/checkout@v4 - name: azure login uses: azure/login@v1 with: creds: ${{ secrets.SERVICEPRINCIPAL }} - name: get carts api endpoint (internal) uses: azure/CLI@v1 id: get-cartsInternalApiEndpoint with: inlineScript: echo "cartsInternalApiEndpoint"="$(az keyvault secret show --vault-name ${{ env.KV_NAME }}${{ vars.SUFFIX }} --name cartsInternalApiEndpoint --query value -o tsv)" >> $GITHUB_OUTPUT - name: get vnetAcaSubnetId uses: azure/CLI@v1 id: get-vnetAcaSubnetId with: inlineScript: echo "vnetAcaSubnetId"="$(az keyvault secret show --vault-name ${{ env.KV_NAME }}${{ vars.SUFFIX }} -n vnetAcaSubnetId --query "value" -o tsv)" >> $GITHUB_OUTPUT - name: substitute tokens in load test config file uses: cschleiden/replace-tokens@v1.2 with: tokenPrefix: "{{" tokenSuffix: "}}" files: ./loadtests/contoso-traders-carts-internal.yaml env: LOAD_TEST_SUBNET_ID: ${{ steps.get-vnetAcaSubnetId.outputs.vnetAcaSubnetId }} - name: load test (carts internal API) uses: Azure/load-testing@v1.1.19 with: # Path of the YAML file. Should be fully qualified path or relative to the default working directory loadtestConfigFile: ./loadtests/contoso-traders-carts-internal.yaml loadtestResource: ${{ env.LOAD_TEST_SERVICE_NAME }}${{ vars.SUFFIX }} resourceGroup: ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} env: | [ { "name": "domain", "value": "${{ steps.get-cartsInternalApiEndpoint.outputs.cartsInternalApiEndpoint }}" }, { "name": "protocol", "value": "https" }, { "name": "path", "value": "v1/ShoppingCart/loadtest" }, { "name": "threads_per_engine", "value": "5" }, { "name": "ramp_up_time", "value": "0" }, { "name": "duration_in_sec", "value": "120" } ] load-tests-carts-api: needs: [provision, playwright-tests-ui] runs-on: ubuntu-22.04 concurrency: group: load-tests-carts-api cancel-in-progress: true steps: - name: checkout code uses: actions/checkout@v4 - name: azure login uses: azure/login@v1 with: creds: ${{ secrets.SERVICEPRINCIPAL }} - name: load test (carts API) uses: Azure/load-testing@v1.1.19 with: # Path of the YAML file. Should be fully qualified path or relative to the default working directory loadtestConfigFile: ./loadtests/contoso-traders-carts.yaml loadtestResource: ${{ env.LOAD_TEST_SERVICE_NAME }}${{ vars.SUFFIX }} resourceGroup: ${{ env.RESOURCE_GROUP_NAME }}${{ vars.SUFFIX }} env: | [ { "name": "domain", "value": "${{ needs.provision.outputs.cartsApiEndpoint }}" }, { "name": "protocol", "value": "https" }, { "name": "path", "value": "v1/ShoppingCart/loadtest" }, { "name": "threads_per_engine", "value": "5" }, { "name": "ramp_up_time", "value": "0" }, { "name": "duration_in_sec", "value": "120" } ] playwright-tests-ui: needs: [provision] timeout-minutes: 20 runs-on: ubuntu-22.04 container: image: mcr.microsoft.com/playwright:v1.43.1-jammy defaults: run: working-directory: src/ContosoTraders.Ui.Website env: AADUSERNAME: ${{ secrets.AADUSERNAME }} AADPASSWORD: ${{ secrets.AADPASSWORD }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: 18 cache: npm cache-dependency-path: src/ContosoTraders.Ui.Website/package-lock.json - name: Set env variables for testing endpoints run: | echo "REACT_APP_APIURLSHOPPINGCART"="https://${{ needs.provision.outputs.cartsApiEndpoint }}/v1" >> $GITHUB_ENV echo "REACT_APP_APIURL"="https://${{ needs.provision.outputs.productsApiEndpoint }}/v1" >> $GITHUB_ENV echo "REACT_APP_BASEURLFORPLAYWRIGHTTESTING"="https://${{ needs.provision.outputs.uiCdnEndpoint }}" >> $GITHUB_ENV - name: Set env variables for testing login if: ${{ env.AADUSERNAME != '' && env.AADPASSWORD != '' }} run: | echo "REACT_APP_B2CCLIENTID"="${{ needs.provision.outputs.azureAdAppClientId }}" >> $GITHUB_ENV echo "REACT_APP_AADUSERNAME"="${{ env.AADUSERNAME }}" >> $GITHUB_ENV echo "REACT_APP_AADPASSWORD"="${{ env.AADPASSWORD }}" >> $GITHUB_ENV - name: install dependencies run: npm ci - name: run playwright tests id: test run: HOME=/root npx playwright test - name: upload playwright report uses: actions/upload-artifact@v3 if: always() with: name: playwright-report path: src/ContosoTraders.Ui.Website/playwright-report/ retention-days: 30 ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore auth.json # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015/2017 cache/options directory .vs/ .vscode/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ Deploy/helm/__values/* !Deploy/helm/__values/readme.txt Source\Services\Contoso.Traders.Rewards.Registration.Api\Properties\PublishProfiles\*.pubxml Source/Services/Contoso.Traders.Cart.Api/.env /Source/.env _gvalues.dev.yaml ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Microsoft Open Source Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Resources: - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns ================================================ FILE: CONTRIBUTING.md ================================================ # How to contribute to ContosoTraders This repo is a reference and learning resource and everyone is invited to contribute, however not all PRs will be accepted into the main branch (**`main`**). There's a general development strategy that's driven by @marcusfelling, who chooses, or defines criteria for choosing, the issues to include in the codebase, given a bunch of constraints and other guidelines. ## Coding Standards There are no explicit coding standards so pay attention to the general coding style, that's (mostly) used everywhere. ## Forks and Branches All contributions must be submitted as a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/) so you need to [fork this repo](https://help.github.com/articles/fork-a-repo/) on your GitHub account. The main branche is **`main`**: - **`main`**: Contains the latest code **and it is the branch actively developed**. **All PRs must be against `main` branch to be considered**. - Any other branch is considered temporary and could be deleted at any time. Do not submit any PR to them! ## DISCLAIMER - This is not a PRODUCTION-READY TEMPLATE for microservices ContosoTraders is a reference application to **showcase architectural patterns** for developing microservices applications using various Azure Services. **IT IS NOT A PRODUCTION-READY TEMPLATE** to start real-world application. In fact, the application is in a **permanent beta state**, as it’s also used to test new potentially interesting technologies as they show up. Since this is a learning resource, some design decisions have favored simplicity to convey a pattern, over production-grade robustness. ## Suggestions We hope this helps us all to work better and avoid some of the problems/frustrations of working in such a large community. We'd also appreciate any comments or ideas to improve this. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE ================================================ FILE: README.md ================================================ # Contoso Traders - Cloud testing tools demo app The Contoso Traders app is a sample application showcasing [Playwright](https://playwright.dev), [Azure Load Testing](https://aka.ms/malt-docs), [Azure Chaos Studio](https://aka.ms/CHAOS-docs) and more. This repo contains the source code, deployment templates, and demo scripts for exploring these cloud testing tools. ## Overview Video [![Watch the overview video](https://img.youtube.com/vi/7JletmiT3io/hq1.jpg)](https://youtu.be/7JletmiT3io) ## Documentation and Resources * Application Links: [UI](https://cloudtesting.contosotraders.com/) | [Carts API](https://contoso-traders-cartsctprd.bluestone-748d2276.eastus.azurecontainerapps.io/swagger/index.html) | [Products API](https://contoso-traders-productsctprd.eastus.cloudapp.azure.com/swagger/index.html) * [Deployment Instructions](./docs/deployment-instructions.md) | [Running Locally](./docs/running-locally.md) ## Continuous Integration | Pipeline | Status | Details | | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | | [GitHub Workflow](./.github/workflows/contoso-traders-cloud-testing.yml) | [![contoso-traders-cloud-testing](https://github.com/microsoft/contosotraders-cloudtesting/actions/workflows/contoso-traders-cloud-testing.yml/badge.svg?branch=main)](https://github.com/microsoft/contosotraders-cloudtesting/actions/workflows/contoso-traders-cloud-testing.yml) | Deploys to [production](https://production.contosotraders.com) | | [Azure DevOps Pipeline](./.azurepipelines/contoso-traders-cloud-testing.yml) | [![Build Status](https://dev.azure.com/MicrosoftTestDemos/ContosoTraders_Testing/_apis/build/status%2Fmicrosoft.contosotraders-cloudtesting?branchName=main)](https://dev.azure.com/MicrosoftTestDemos/ContosoTraders_Testing/_build/latest?definitionId=1&branchName=main) | Deploys to [staging](https://staging.contosotraders.com) | ## Demo Scripts * [Developer Workflow](./demo-scripts/dev-workflow/walkthrough.md) * Azure Load Testing - Generate high-scale load and identify performance bottlenecks. * [Create a load test for the shopping cart API.](./demo-scripts/azure-load-testing/walkthrough.md) * [Use GitHub Actions for regression testing.](./demo-scripts/azure-load-testing/walkthrough.md#walkthrough-regression-testing-with-github-workflows) * [Create a load test for a private endpoint that’s behind a VNet.](./demo-scripts/azure-load-testing/private-endpoints.md) * [Right-size your AKS cluster using load tests.](./demo-scripts/azure-load-testing/aks-cost-optimization.md) * Azure Chaos Studio - Improve application resilience by introducing faults and simulating outages. * [Create an experiment using Key Vault Deny Access fault to test the products API (AKS).](./demo-scripts/azure-chaos-studio/walkthrough.md) * [Run experiment in GitHub Actions to inject faults (pod failures) into the AKS cluster.](./demo-scripts/azure-chaos-studio/walkthrough.md#walkthrough-running-chaos-experiments-via-github-workflows) * Playwright - Reliable end-to-end testing for modern web apps. * [Use the VS Code extension to explore and run web tests](./demo-scripts/testing-with-playwright/walkthrough.md) for [API testing](src/ContosoTraders.Ui.Website/tests/api), [Authentication](src/ContosoTraders.Ui.Website/tests/auth.setup.ts), [Shopping cart](src/ContosoTraders.Ui.Website/tests/cart.spec.ts), [Uploading files](src/ContosoTraders.Ui.Website/tests/fileupload.spec.ts), [Visual Comparisons](src/ContosoTraders.Ui.Website/tests/pages.spec.ts#L63), [Emulation](src/ContosoTraders.Ui.Website/tests/map.spec.ts), [Mocking](src/ContosoTraders.Ui.Website/tests/mocks.spec.ts), and [using a CSV for data](src/ContosoTraders.Ui.Website/tests/account.ts) ## Architecture ![Architecture](./docs/architecture/contoso-traders-enhancements.drawio.png) ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit . When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. ================================================ FILE: SECURITY.md ================================================ # Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). ================================================ FILE: SUPPORT.md ================================================ # Support ## How to file issues and get help This project uses GitHub Issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new Issue. ## Microsoft Support Policy Support for this project is limited to the resources listed above. ================================================ FILE: demo-scripts/azure-chaos-studio/walkthrough.md ================================================ # Azure Chaos Studio: Overview Azure Chaos Studio is an Azure service that allows you to create & run chaos experiments on your application's infrastructure. By deliberately introducing faults that simulate real-world outages, you can test your application's resiliency and identify potential issues before they impact your customers. ## Key Takeaways In this demo, you'll get an overview of Azure's Chaos Studio service; a managed service that can be used to simulate faults on your application's infrastructure. - Running Chaos Experiments to introduce faults in Azure Key Vault (deny access) and seeing its impact on the application. - Running Chaos Experiments via GitHub Workflows. ## Before You Begin Please execute the steps outlined in the [deployment instructions](../../docs/deployment-instructions.md) to provision the infrastructure in your own Azure subscription. ## Walkthrough: Identify the Chaos Studio target (Key Vault) 1. In the Azure portal, you can navigate to the Azure Chaos Studio service from the search bar as follows. ![chaos studio](./media/chaos1.png) 2. Next, click on the `Target` tab and filter down to the `contoso-traders-rg{SUFFIX}` resource group. ![chaos studio](./media/chaos2.png) 3. Next, go to the `contosotraderskv${SUFFIX}` key vault resource, and click on the `Manage actions` button. ![chaos studio](./media/chaos3.png) 4. You'll notice that the "Key Vault Deny Access" fault will be being injected in the key vault when the chaos experiment is run. This fault will prevent the application from accessing the key vault, leading the application to fail. ![chaos studio](./media/chaos4.png) ## Walkthrough: Review the Chaos Experiment 1. In the Chaos Studio, click on the `Experiment` tab and click on the `contoso-traders-chaos-kv-experiment{SUFFIX}` experiment. ![chaos studio](./media/chaos5.png) 2. Click on the experiment's `Edit` button to review the experiment's configuration. ![chaos studio](./media/chaos6.png) 3. Click on the action's `Edit` button to review the action's configuration. ![chaos studio](./media/chaos7.png) 4. You'll notice that the experiment is configured to run on the the `contosotraderskv${SUFFIX}` key vault resource for 5 minutes. For the duration of the experiment, the `Key Vault Deny Access` fault will be injected into the key vault (i.e. the key vault will not be accessible even to principals mentioned in its access policies). ![chaos studio](./media/chaos8.png) ![chaos studio](./media/chaos9.png) ## Walkthrough: Run the Chaos Experiment 1. Before starting the experiment, you can verify that the application is working as expected by navigating to the application's URL and clicking on any product category (e.g. `laptops`). The application should load the product category page successfully by fetching data from the API. ![chaos studio](./media/chaos10.png) 2. Next, navigate to the Chaos Studio and click on the `Experiment` tab. Click on the `contoso-traders-chaos-kv-experiment{SUFFIX}` experiment and click on the `Start` button. ![chaos studio](./media/chaos11.png) ![chaos studio](./media/chaos12.png) 3. The experiment is now underway and during the course of the experiment, the key vault will not be accessible. ![chaos studio](./media/chaos13.png) ![chaos studio](./media/chaos14.png) ## Walkthrough: Exposing resiliency issues in application 1. The application's Products API follows the externalized configuration pattern, wherein upon startup, the API fetches DB connection strings, passwords etc from Azure key vault. The API then uses the connection string to connect to its product catalog db. If the key vault is not accessible, the API will fail to fetch the connection string and will fail to start. ![chaos studio](./media/kv-config-provider.png) 2. Let us force the API to restart (note: upon restarting, it'll attempt to connect to the key vault to fetch the connection string). We can do by simply deleting the API's pod. The AKS deployment will then recreate the pod and the API will restart. ![chaos studio](./media/chaos15.png) ![chaos studio](./media/chaos16.png) 3. As soon as the new pod is created, the API will attempt to connect to the key vault to fetch the connection string. Since the key vault is not accessible, the API will fail to start. ![chaos studio](./media/chaos17.png) 4. You can verify that the application is not working as expected by navigating to the application's URL and clicking on any product category (e.g. `laptops`). The application should fail to load the product category page. ![chaos studio](./media/chaos18.png) ## Walkthrough: After the Chaos Experiment ends 1. After the 5 minutes are up, the experiment will end and the key vault will be accessible again. ![chaos studio](./media/chaos19.png) 2. AKS's deployment ensures that the API will automatically restarted on crashes (with exponential back-off applied). Once the chaos experiment ends, the key vault will be accessible again. When AKS restarts the pod after this, the API will be able to connect to the key vault and will start successfully. ![chaos studio](./media/chaos20.png) ![chaos studio](./media/chaos21.png) ## Walkthrough: Running Chaos Experiments via GitHub Workflows 1. We have a Chaos Experiment `contoso-traders-chaos-aks-experiment{SUFFIX}` that injects faults (pod failures) into the AKS cluster: `contoso-traders-aks{SUFFIX}` for a duration of 5 mins. ![chaos studio](./media/chaos22.png) 2. Internally, this experiment leverages [Chaos Mesh](https://chaos-mesh.org/), a CNCF project that orchestrates fault injection on Kubernetes environments (e.g. network latency, pod failures, and even node failures). ![chaos studio](./media/chaos23.png) 3. The github workflow `contoso-traders-cloud-testing.yml` triggers the AKS chaos experiment (pod failures), while simultaneously running a load test against the same AKS cluster. ![chaos studio](./media/chaos24.png) ## More Information - [Azure Chaos Studio](https://learn.microsoft.com/azure/chaos-studio/) - [Pricing](https://azure.microsoft.com/pricing/details/chaos-studio/) ================================================ FILE: demo-scripts/azure-load-testing/aks-cost-optimization.md ================================================ # Azure Load Testing: AKS Cost Optimization ## Key Takeaways In this demo, we'll see how Azure Load Testing can be used to help "right-size" an AKS cluster. The demo will start with a representative load test against a large AKS cluster. We'll iteratively scale down the cluster until we find the smallest cluster size that can handle the load. We'll also use the load test's integrated server-side metrics to analyze the cluster's resource utilization. ## Before you begin Please execute the steps outlined in the [deployment instructions](../../docs/deployment-instructions.md) to provision the infrastructure in your own Azure subscription. ## Walkthrough 1. We have a GitHub workflow (and a Azure DevOps pipeline too) that executes load tests on a AKS cluster using a matrix of cluster sizes. The workflow is defined in the [aks-cost-optimization.yml](../../.github/workflows/aks-cost-optimization.yml) file. The workflow can be triggered manually on-demand. ![aks cost optimization](./media/aks-cost-optimization-0.png) 2. The load test, defined in the [load-test-aks.yml](../../.github/workflows/load-test-aks.yml) file, targets the `Products API`. 3. [Optional Step] From the Azure Portal, we'll tweak the existing load test `contoso-traders-products` to incorporate server side AKS metrics using steps below: ![aks cost optimization](./media/aks-cost-optimization-1.png) ![aks cost optimization](./media/aks-cost-optimization-2.png) ![aks cost optimization](./media/aks-cost-optimization-3.png) ![aks cost optimization](./media/aks-cost-optimization-4.png) 4. We manually invoke the GitHub workflow. In the beginning, the AKS cluster's size is automatically increased (CPU Limit: 250m, Mem Limit: 256 Mi) and a load test is run against it with 25 virtual concurrent users (threads) for a short duration of 30 seconds. 5. In the next matrix step, the cluster is automatically rescaled a smaller size (CPU Limit: 250m, Mem Limit: 128 Mi) and the load test is run again. 6. In next iterations, the scale down continues until the cluster is unable handle the load any longer. The last passing iteration is the smallest cluster size that can handle the load. In below example, the "right-sized" cluster will have CPU Limit: 250m, Mem Limit: 128 Mi. ![aks cost optimization](./media/aks-cost-optimization-5.png) ## Cost Considerations A quick note on costs considerations when you run the GitHub workflow (or Azure DevOps pipeline): 1. Github Actions ([pricing details](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#included-storage-and-minutes)): Each run will consume approximately 25 billable minutes on linux runners. 2. Azure Load Testing ([pricing details](https://azure.microsoft.com/pricing/details/load-testing/)): The number of virtual users and duration of the test are the key factors that determine the cost of the test. In this demo, the load tests are configured to use 25 virtual users and the test matrix sets up 4 test runs, each of 30 seconds duration. ## Summary In this demo, we saw how Azure Load Testing was used to help "right-size" an AKS cluster to optimize costs. The demo started with a representative load test against a large AKS cluster. We iteratively scaled down the cluster until we found the smallest cluster size that can handle the load. ================================================ FILE: demo-scripts/azure-load-testing/private-endpoints.md ================================================ # Azure Load Testing: Private Endpoints ## Key Takeaways In this demo, we'll attempt to load test an private API endpoint using the Azure Load Testing Preview. The private API endpoint in question is only accessible from within an Azure virtual network (VNET). We'll demonstrate Azure Load Testing service's capability to generate load from within a virtual network (using VNET resource injection). ## Before You Begin Please execute the steps outlined in the [deployment instructions](../../docs/deployment-instructions.md) to provision the infrastructure in your own Azure subscription. > **Warning** To deploy the additional resources for this walkthrough ensure you have set the GitHub Action's variable `DEPLOYPRIVATEENDPOINTS` to `true`. If you're using Azure DevOps Pipeline, then please ensure that the variable `DEPLOYPRIVATEENDPOINTS` is set to `true` in the `contosotraders-cloudtesting-variable-group` variable group. Specifically, here's what happens behind the scenes: * An Azure virtual network (VNET) is created with three subnets: * A subnet for Azure Container Apps to deploy its infrastructure as well as the application's API private endpoints. * A subnet for Azure Load Testing to inject its resources. * A subnet for Azure VMs (jumpboxes) to access the application's private endpoints (for visual verification purposes). * An Azure Container Apps instance is deployed to host the application's API endpoints. The API endpoints are configured to be private endpoints, and the ingress controller only allows internal VNET traffic (i.e. endpoint is only accessible from within the VNET). * A private DNS zone (e.g. `eastus.azurecontainerapps.io`) is created to resolve the private endpoints' DNS names. This private DNS zone is associated with the VNET's subnet ACA. We add the necessary A records to link the endpoint FQDN to the ACA's private IP address. * A jumpbox VM is deployed to the VNET's VM subnet. This VM will be used in this demo to access the application's private endpoints. ![VNET configuration](./media/vnet-configuration.png) ## Walkthrough: Identify the Load Test Target 1. In the Azure portal, you can navigate to the `contoso-traders-intcarts{SUFFIX}` Azure Container App in the `contoso-traders-rg` resource group. This is the application that hosts the `Carts API`. Note that this application is only accessible from within the VNET. ![ACA](./media/private-endpoint-1.png) 2. You can get the URL of the `Carts API` by as shown below. ![ACA](./media/private-endpoint-2.png) 3. In a separate browser tab, enter the following url in the address bar to load the API's swagger page: `/swagger/index.html`. You'll notice that the API's endpoints are not reachable via the public internet. ![ACA](./media/private-endpoint-3.png) 4. To access the API's endpoints, you'll need to access the API from within the VNET. You can RDP into the `jumpboxvm` VM. This is a jumpbox VM located in the same resource group `contoso-traders-rg`. From this RDP session, you can access the API's swagger page: `/swagger/index.html` ![ACA](./media/private-endpoint-4.png) 5. The specific API that we'll be using for is the `Carts API`'s `GET /v1/ShoppingCart/loadtest` endpoint. Please note down this endpoint for later use. ![ACA](./media/private-endpoint-5.png) ## Walkthrough: Modify the previously created Load Test 1. In the Azure portal, navigate to the Azure Load Testing instance (in the `contoso-traders-rg` resource group) that we created in the [previous demo](./walkthrough.md). 2. Now click on `Configure` > `Tests`. ![Load Test Private Endpoint](./media/load-test-private-endpoint-1.png) 3. Navigate to the `Parameters` tab in the Edit Test blade. Modify the `domain` value to point to the `Carts API`'s private endpoint. You can use the `` you noted down earlier (note: please remove the `https://` prefix). ![Load Test Private Endpoint](./media/load-test-private-endpoint-2.png) 4. Navigate to the `Load` tab. Change `Configure test traffic mode` to `private`. Also, specify the VNET and subnet details: `contoso-traders-vnet{SUFFIX}` and `subnet-loadtest` respectively. ![Load Test Private Endpoint](./media/load-test-private-endpoint-3.png) 5. Click apply to save the changes. 6. Now, click `Run` to start the load test against the private endpoint. ![Load Test Private Endpoint](./media/load-test-private-endpoint-4.png) 7. Behind the scenes, the Azure load testing service will inject the testing infrastructure into the specified VNET. The load test will successfully run to completion after that. ![Load Test Private Endpoint](./media/load-test-private-endpoint-5.png) ## Summary In this demo, we saw how Azure Load Testing can be used to generate load from within a virtual network to test private/restricted API endpoints. ## More Information * [Troubleshooting private endpoints](https://docs.microsoft.com/azure/container-apps/troubleshoot-private-endpoints) * [Test private endpoints by deploying Azure Load Testing in an Azure virtual network](https://learn.microsoft.com/azure/load-testing/how-to-test-private-endpoint) * [Scenarios for deploying Azure Load Testing in a virtual network](https://learn.microsoft.com/azure/load-testing/concept-azure-load-testing-vnet-injection) * [Blog Post: Load test endpoints with access restrictions using Azure Load Testing](https://techcommunity.microsoft.com/t5/apps-on-azure-blog/load-test-endpoints-with-access-restrictions-using-azure-load/ba-p/3610412) * [Blog Post: Load test private endpoints deployed in another Azure region or subscription](https://techcommunity.microsoft.com/t5/apps-on-azure-blog/load-test-private-endpoints-deployed-in-another-azure-region-or/ba-p/3693277) ================================================ FILE: demo-scripts/azure-load-testing/walkthrough.md ================================================ # Azure Load Testing: Overview ## Key Takeaways In this demo, you'll get an overview of Azure's Load Testing service; a managed service that can be used to simulate load on your application's UI and APIs endpoints. You'll also get an insight into: - how to identify an application's breaking point under incrementally increasing load. - how to leverage server-side metrics and Azure AppInsights to identify the performance bottleneck. - how to guard your application against performance regressions leveraging load testing in CI/CD pipelines. All these are especially crucial for an e-commerce application like Contoso Traders, which is expected to instantly handle a large, sudden spike in number of users, with low latency and no downtime. ## Before You Begin Please execute the steps outlined in the [deployment instructions](../../docs/deployment-instructions.md) to provision the infrastructure in your own Azure subscription. ## Walkthrough: Identify the Load Test Target 1. In the Azure portal, you can navigate to the Azure Container App in the `contoso-traders-rg{SUFFIX}` resource group. This is the application that hosts the `Carts API`. ![ACA](./media/aca-2.png) 2. You can get the URL of the `Carts API` by as shown below. ![ACA](./media/aca-endpoint.png) 3. In a separate browser tab, enter the following url in the address bar to load the API's swagger page: `/swagger/index.html` ![ACA](./media/aca-swagger.png) 4. You can now identify the API that you want to load test. In this case, we'll be load testing the `Carts API`'s `GET /v1/ShoppingCart/loadtest` endpoint. Please note down this endpoint for later use. ![ACA](./media/aca-swagger-2.png) ## Walkthrough: Creating a Load Test 1. In the Azure portal, you can navigate to the Azure Load Testing service in the `contoso-traders-rg{SUFFIX}` resource group. ![load testing](./media/load-test-browse.png) 2. You can create a new load test as follows: Navigate to the `Tests` section, and then click on `Create` > `Create a URL-based Test` button. ![load testing](./media/load-test-create-1.png) 3. In the `Basic` blade, you can specify the target URL. You can also specify the number of concurrent users, and the duration of the test. See example below: ![load testing](./media/load-test-create-2.png) > **Note**: The target URL is the URL from the `Carts API` that you identified in the previous section. ## Walkthrough: Running the Load Test 1. Once you've entered the load test specifications above, you can run it by clicking on the `Run` button. ![load testing](./media/load-test-run.png) 2. The load test will take about 2 minutes to complete. Once done, it'll display the summary and client-side metrics. ![load testing](./media/load-test-in-progress.png) ![load testing](./media/load-test-completed.png) ## Walkthrough: Incorporate Server Side Metrics 1. Click on the `App Components` button. Then from the flyout, select the `contoso-traders-carts{SUFFIX}` CosmosDB component. This will add relevant metrics from the CosmosDB to the load test dashboard. ![load testing](./media/load-test-server-side-metrics.png) 2. Re-run the load test, and you'll see the impact of the synthetic load on the DB (in real-time). ![load testing](./media/load-test-run-2.png) > **Note**: Unfortunately, ACA metrics are not yet supported in Azure Load Testing's server side metrics. This feature will be coming soon. ## Walkthrough: Review ACA Metrics & Dashboards 1. In the Azure portal, you can navigate to the Azure Container App in the `contoso-traders-rg{SUFFIX}` resource group. ![ACA](./media/aca.png) 2. For demo purposes, we have configured a `HTTP Scaling` rule that horizontally scales out additional replicas when the number of concurrent requests exceeds a threshold (`3` in this case). ACA also supports automatic scale-in to zero when traffic dips below threshold. ![ACA Scaling Rules](./media/aca-scaling-rules.png) 3. In the metrics tab, you can see the various metrics measured & published by the ACA infrastructure. You can create a metric chart that combines two metrics: `replica count` vs `requests`. It'll now have updated with the latest data after the load test. Of particular interest is the replica count chart of `Carts API`, which shows the instances auto-scaled out under increasing load. After load subsided, the instances auto-scaled back in to zero. ![load testing ACA](./media/aca-metrics2.png) ## Walkthrough: Export the JMX File, Results 1. Navigate back to the Load Testing service, and click on the recently concluded test run. From there you can click on `Download` > `Input File`. This will download the JMX file in a zip archive. ![load testing](./media/load-test-export-jmx.png) 2. You can review the JMX file by simply loading it up in notepad or VSCode. ![load testing](./media/load-test-jmx-view.png) 3. The load test results can also be downloaded via in the `Download` > `Results` button. This will download a CSV file (inside a zip archive). ![load testing](./media/load-test-export-results.png) ![load testing](./media/load-test-results-view.png) ## Walkthrough: Identify application's breakpoints 1. Let us now modify the existing load test. We'll use it to put the application under increasing load, ultimately leading to failure. The goal is to identify the application's breakpoints (performance bottlenecks). ![app breakpoint](./media/app-breakpoint-1.png) 2. Modify the existing test configuration as follows: 1. Increase the number of concurrent users to `250` (from original `5`). 2. Change the test duration to `300` seconds (from original `120` seconds) 3. Change the ramp-up time to `300` seconds (from original `120` seconds). ![app breakpoint](./media/app-breakpoint-2.png) 3. Increase the number of engine instances to `2` (from original `1`). ![app breakpoint](./media/app-breakpoint-3.png) 4. Run the modified load test. You'll notice that the application starts to eventually fail under the increased load. ![app breakpoint](./media/app-breakpoint-4.png) 5. If you add the server-side metrics for the `contoso-traders-cartsct{SUFFIX}` CosmosDB, you'll notice that the DB's normalized RU consumption eventually starts to peg at 100% under load. ![app breakpoint](./media/app-breakpoint-4-2.png) 6. App Insights can help us narrow down the root cause of the error. Navigate to the `contoso-traders-rg{SUFFIX}` resource group, and click on the `contoso-traders-aict{SUFFIX}` resource. ![app breakpoint](./media/app-breakpoint-5.png) 7. In the App Insights blade, click on the `Failures` tab. Narrow down the time range to (say) the last 30 minutes. You'll see the listed failures (sampled by App Insights) that occurred during the load test. ![app breakpoint](./media/app-breakpoint-6.png) 8. Clicking on any one sample will give you a detailed view of the error (including stack trace in case of an exception). In this case, the error is a `500` error, caused by a `TaskCanceledException` (due to a gateway timeout in CosmosDB). This is a good indication that the application is failing due to a performance bottleneck. ![app breakpoint](./media/app-breakpoint-7.png) ## Walkthrough: Regression Testing with Github Workflows 1. We have a GitHub workflow that executes load tests on the application's APIs. This workflow is automatically triggered on every checkin to the `main` branch. Specifically the load tests are run on the `Product API` and `Carts API` immediately after they're deployed to the AKS cluster and ACA respectively. This will help identify if any code (or infra) change causes the application performance to degrade under (simulated) load. ![github workflow](./media/github-workflow-2.png) 2. The workflow uses a github action to invoke the [Azure Load Testing](https://learn.microsoft.com/en-us/azure/load-testing/) service and simulate load on the application's `Product API` and `Carts API`, which are hosted on AKS and ACA respectively. ![github action for load testing](./media/github-action.png) 3. The workflow file references a load test configuration file (yml), which specifies the following: 1. The load test parameters. 2. The JMX/JMeter script to be used. 3. The pass/fail criteria for the test. See an example of a load test configuration file below. ```yaml testName: contoso-traders-carts testPlan: contoso-traders-carts.jmx engineInstances: 1 failureCriteria: - avg(response_time_ms) > 5000 - percentage(error) > 20 ``` 4. The load test takes about 3 minutes to execute. In this specific example, you can see that the load test failed since the average response time exceeded the specified threshold of 5000ms. ![workflow for load testing](./media/github-workflow-3.png) 5. Once done, you can navigate to the Azure Portal to get more in-depth details about the test. ![load testing portal](./media/portal-load-test.png) ## Summary In this demo, you got an overview of Azure's Load Testing service; including how to create a load test, run it, and review the results. You also saw how to incorporate server-side metrics from Azure Services, and how to export the JMX file and results. Finally, you saw how to create a new load test from the JMX file, and how to use a GitHub workflow to execute load tests on the application's APIs. ## More Information - [Azure Load Testing](https://learn.microsoft.com/azure/load-testing/) - [Blogs on Azure Load Testing](https://techcommunity.microsoft.com/t5/apps-on-azure-blog/bg-p/AppsonAzureBlog/label-name/Azure%20Load%20Testing) - [Pricing](https://azure.microsoft.com/pricing/details/load-testing/) ================================================ FILE: demo-scripts/dev-workflow/walkthrough.md ================================================ # DevOps with GitHub & Azure: Technical Walkthrough ## Key Takeaways The key takeaways from this demo are: - GitHub Actions is used to automate the deployment of the application and infrastructure. Using GitHub Action workflows, the application and infrastructure can be deployed to Azure cloud with a single click, allowing you to implement continuous integration and continuous deployment process. - GitHub Actions integrates with Azure services to enable you to build, test, and deploy to Azure directly from your GitHub repository, along with tons of other integrations. - Using GitHub Actions, you will be able to incorporate your application testing, performance testing to validate your application for production readiness as part of your DevOps process itself. - As part of this demo script, you will be adding a new feature to enable **dark mode** capability on Contoso Traders Website, along with adding playwright based testing in your DevOps cycle. ## Before you Begin You must have Contoso Traders deployed in your environment and setup with GitHub Actions. Please refer to the deployment instructions [here](../../docs/deployment-instructions.md) ## Walkthrough – GitHub Actions for CI/CD GitHub Actions is a way to automate processes and workflows in your GitHub repository. Some of the benefits of using GitHub Actions include the ability to automate your software development lifecycle, integrate with other tools and services, including Azure services. Let us take a look at the GitHub Actions used by Contoso Traders for CI/CD. ## Review Workflows used in Contoso Traders 1. Navigate to [ContosoTraders CloudTesting repository.](https://github.com/microsoft/ContosoTraders-CloudTesting) 2. Go to the **github/workflows** folder; inside, you'll find the workflow **YAML file** that are used to deploy and set up the resources. ![image](media/actionlist.png) 3. Here is a quick overview of the workflow. If you are interested, you can review the workflow code to get into more details. a. **contoso-traders-cloud-testing:** This workflow provisions Azure resources used for hosting the application, deploys the application to the provisioned resources. It also runs load testing & playwright tests as part of CI/CD cycle ensure the app is production ready with each change. It includes everything needed to get the application up and running in an Azure Environment. ![image](media/provision.png) ## Monitor GitHub Actions Workflow GitHub Actions workflows can be monitored from the Actions tab on a repository. This tab shows a list of all the active and past workflows, along with their status and any associated logs. Users can see at a glance whether their workflows are running successfully and can troubleshoot any issues that may arise. Additionally, users can set up notifications to be alerted when a workflow starts or completes, or if it encounters an error. This can help users stay on top of their workflows and ensure that their projects are running smoothly. Let us take a look at the workflows status for Contoso Traders in this public repository. 1. Navigate to [ContosoTraders/Actions](https://github.com/microsoft/ContosoTraders-CloudTesting/actions). Select the workflow **contoso-traders-cloud-testing**. This will the history of workflows execution. ![image](media/actions1.png) 2. Select the latest run from the list. In Summary, you will see 5 jobs listed. - `provision`: Used for provisioning Azure resources, configure access policies and permissions, seeding initial database. - `playwright-tests-ui`: Used to execute playwright tests to validate UI functionalities before the UI service is deployed. - `load-tests-carts-api`: Used to execute Azure Load testing to validate performance for Carts API. - `load-tests-carts-internal-api`: Same as above, but executes Load testing against an internal/private API endpoint. - `load-tests-with-chaos-products-api`: Used to execute Azure Load testing to validate performance for Products API with simultaneous chaos experiments (fault injection) enabled. ![image](media/actionmonitor.png) 3. Click on **provision** job. You can now see the detailed task of this job and expand to see the logs and steps. ![image](media/actions3.png) Similarly, you can review other jobs and workflows. Workflow are set to run on push to the main branch so that any new code change to the main branch is automatically built and deployed. ## Demo – Experience GitHub Actions in Action Now that we have reviewed the GitHub Actions workflows, let us take a step-by-step approach to test the end-to-end CI/CD process. As part of enhancement to the Contoso Traders website, you are asked to add **dark mode** functionality to the website. Let us add the dark mode functionality to the application by modifying the source code of our website. ![image](media/L300-1.png) Your marketing team requires changing this to following Contoso Traders is one stop shop for electronics items including smartphones, laptops, and other popular gadgets. Contoso Traders delivery premium quality electronics at affordable rates to resellers across the globe. Let us make the changes and experience magic of GitHub Actions. 1. Login to your fork of Contoso Traders repository and navigate to Contoso Traders repository `https://github.com/**YOURGITHUBUSERNAME**/ContosoTraders-CloudTesting`. 2. Create a new branch **add-dark-mode**. ![image](media/addbranch.png) 3. Navigate to appbar.js file, located in **src/ContosoTraders.Ui.Website/src/shared/header/appbar.js** 4. To make it easier for you to complete this change, source code already include code blocks for adding dark mode functionality. You will need to uncomment the code block, let's start by clicking on edit **symbol (2)**. 5. Uncomment line number 8, 414,415 & 416. ![image](media/uncommentcode1.png) ![image](media/uncommentcode2.png) 6. Commit the change to add-dark-mode branch. Click on Commit changes after updating commit message. ![image](media/commit1.png) 7. Now, let's update playwright tests to include testing for dark mode functionality. Edit **darkmode.spec.ts** located in **src/ContosoTraders.Ui.Website/tests** and remove the `skip` annotation from line 7. 8. Now, let us raise a pull request to merge this change to **main branch**. Click on **Pull Requests** and notice that changes are detected already. Click on **Review and raise Pull Request**. ![image](media/L300-5.png) 9. Change the base from microsoft/contosotraders to **YOURUSERNAME/contosotraders** and click **Create Pull Request**. ![image](media/addpr.png) 10. Merge the pull request to main branch. ![image](media/L300-7.png) 11. Merging the pull request should trigger your GitHub Action workflow, it will take few minutes for workflow to complete. You can navigate inside the workflow to review progress, as documented in previous step. ![image](media/workflowrunning.png) 12. Once the workflow successfully completes, you can navigate to your Contoso Traders URL and see that the dark mode functionality is now added to the website. ![image](media/darkmode.png) ## Summary In this scenario, we looked at how GitHub Actions can incorporate entire lifecycle of application deployment, performance testing & testing with playwright as part of CI/CD cycle. ## Additional Reading Reference Links - [DevSecops in GitHub](https://learn.microsoft.com/azure/architecture/solution-ideas/articles/devsecops-in-github) - [GitHub features and Actions](https://github.com/features/actions) - [GitHub Advnced Security](https://docs.github.com/get-started/learning-about-github/about-github-advanced-security) - [Microsoft Defender for DevOps - the benefits and features | Microsoft Learn](https://learn.microsoft.com/azure/defender-for-cloud/defender-for-devops-introduction) ================================================ FILE: demo-scripts/testing-with-playwright/walkthrough.md ================================================ # Testing with Playwright: Overview ## Key Takeaways ## Before You Begin 1. Please execute the steps outlined in the [deployment instructions](../../docs/deployment-instructions.md) to provision the infrastructure in your own Azure subscription. 2. Once done, please execute the steps mentioned in the [running locally](../../docs/running-locally.md) document. Basically this will ensure that Playwright is installed on your machine. ## Installing VSCode Extension You can install the Playwright extension for VSCode from the marketplace ([LINK](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright)). This extension will help you get started with Playwright quickly. It will also help you run and debug your tests from within VSCode. ![Playwright](./media/playwright-1.png) The Playwright docs have more details here: [Using the Playwright VSCode Extension](https://playwright.dev/docs/getting-started-vscode) ## Running tests in VSCode 1. Restore project dependencies: * Open a cmd window and navigate to the `src/ContosoTraders.Ui.Website` folder. * Run `npm install`. 1. Set environment variables for the remote testing endpoints. The values can be grabbed from the GitHub Action logs that set them in the pipeline. 1. Navigate to [ContosoTraders/Actions](https://github.com/microsoft/ContosoTraders-CloudTesting/actions) 1. Drill into the latest run by clicking on the `playwright-tests-ui` job, then clicking on the step `Set env variables for testing endpoints`. ![image](media/actions1.png) 1. This project uses a `.env.playwright.local` file to easily manage these variables. Open `src\ContosoTraders.Ui.Website\.env.playwright.local` and set the following environment variables using the values from the logs above (these will be unique to your environment). NOTE: Do NOT check in these changes to source control!: ```bash REACT_APP_APIURLSHOPPINGCART = '' REACT_APP_APIURL = '' REACT_APP_BASEURLFORPLAYWRIGHTTESTING = '' ``` 1. Click on the `Testing` tab in VSCode's activity bar. This will show you all the tests in your project (in a tree structure). There are tests that demonstrate [API testing](../../src/ContosoTraders.Ui.Website/tests/api.cart.spec.ts), [Authentication](../../src/ContosoTraders.Ui.Website/tests/auth.setup.ts), [Visual Comparisons](../../src/ContosoTraders.Ui.Website/tests/pages.spec.ts:63), [Emulation](../../src/ContosoTraders.Ui.Website/tests/map.spec.ts), [Mocking](../../src/ContosoTraders.Ui.Website/tests/mocks.spec.ts), and [using a CSV for data](../../src/ContosoTraders.Ui.Website/tests/account.ts). ![Playwright](./media/playwright-2.png) 1. You can run a single test (or a group of tests) by clicking the triangle symbol next to it. When Playwright finishes executing the test(s), you will see a green tick next to your test block as well as the time it took to run the test. ![Playwright](./media/playwright-3.png) 1. You can navigate to the test code by right-clicking on the test name in the tree structure, selecting `Go To Test`. ![Playwright](./media/playwright-4.png) ## Debugging the tests 1. You can set breakpoints in your test code by clicking on the left "gutter" (the left-most column in the code editor). Right-clicking in the gutter will show you more options (like setting a conditional breakpoint). ![Playwright](./media/playwright-5.png) 1. Then you can run a test in debug mode by clicking on the `Debug` button next to the test name in the tree structure. ![Playwright](./media/playwright-6.png) 1. Once the breakpoint is hit, you can use the single-step through the code and inspect variables (Note: These debugging features are already built into VSCode, and aren't playwright specific. [More details](https://code.visualstudio.com/docs/editor/debugging)). ![Playwright](./media/playwright-7.png) ## Testing with Azure AD In order to test authentication, we can configure AAD, then run tests to log in to a Contoso Traders account. The specific steps are: 1. Identify the Service Principal details created in the [deployment instructions](../../docs/deployment-instructions.md). 1. Add the above Service Principal into the the [Application Administrator](https://learn.microsoft.com/azure/active-directory/roles/permissions-reference#application-administrator) active directory role. 1. Go to the Azure portal, and navigate to the Azure Active Directory blade. Then click on the `Roles and Administrators` tab on the left. 1. Select the `Application Administrator` role, and click on the `Add assignments` button. 1. Select the service principal that you created in the previous step. Click on the `Add` button. ![Application Administrator](../../docs/images/ad-application-administrator.png) > > Notes: > > * Unfortunately, there is no AZ CLI, AZ PowerShell or Bicep template support to add a service principal to the `Application Administrator` role. You'll have to do this manually through the Azure portal. > > * Note: In order for you to add the service principal to the `Application Administrator` role, you must yourself be a member of the `Global Administrator` role in Azure Active Directory. > 1. Create a test account (MFA disabled). 1. To run the [example test](../../src/ContosoTraders.Ui.Website/tests/account.ts) in GitHub Actions, add the test account credentials as github environment-level variables. | Variable Name | Variable Value | | ------------- | ---------------------------- | | `AADUSERNAME` | username of the test account | | `AADPASSWORD` | password of the test account | > If you're using Azure Pipelines instead of GitHub Actions, you can set the above variables in the `contosotraders-cloudtesting-variable-group` variable group. See [this document](../../docs/deployment-instructions-azure-pipelines.md#prepare-your-azure-pipeline-for-deployment) for details. > > If you wish to run the [example test](../../src/ContosoTraders.Ui.Website/tests/account.ts) locally, set the credentials as 2 environment variables: REACT_APP_AADUSERNAME and REACT_APP_AADPASSWORD 1. Re-run the github workflow `contoso-traders-cloud-testing`. This will configure the Azure AD to enable login functionality in the app. This test has a beforeAll hook that will log in to the app, then the test case uses the logged in state to fill out the personal info form. 1. Read the [Playwright Authentication Documentation](https://playwright.dev/docs/auth) > Tests written with Playwright execute in isolated clean-slate environments called browser contexts. This isolation model improves reproducibility and prevents cascading test failures. New browser contexts can load existing authentication state. This eliminates the need to login in every context and speeds up test execution. ## More Information * [Playwright Documentation](https://playwright.dev/) * [Using the Playwright VSCode Extension](https://playwright.dev/docs/getting-started-vscode) * [VSCode Debugging](https://code.visualstudio.com/docs/editor/debugging) ================================================ FILE: docs/architecture/.$contoso-traders-enhancements.drawio.bkp ================================================ 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 ================================================ FILE: docs/architecture/.$contoso-traders-enhancements.drawio.dtmp ================================================ 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 ================================================ FILE: docs/architecture/contoso-traders-enhancements.drawio ================================================ 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 ================================================ FILE: docs/deployment-instructions-azure-pipelines.md ================================================ # Contoso Traders - Deployment instructions using Azure Pipelines This document will help you deploy the Contoso Traders application in your Azure environment using Azure Pipelines. ## Prerequisites 1. You'll have to follow the steps in the [Deployment Instructions](./deployment-instructions.md) document to set up your Azure subscription and github repository fork. 2. You'll need to have an Azure DevOps organization and project set up. If you don't have one, you can follow the instructions [here](https://docs.microsoft.com/en-us/azure/devops/organizations/projects/create-project?view=azure-devops&tabs=preview-page). 3. Please ensure that the following Azure DevOps marketplace extensions are installed for your organization: - [Azure Load Testing](https://marketplace.visualstudio.com/items?itemName=AzloadTest.AzloadTesting) - [Replace Tokens](https://marketplace.visualstudio.com/items?itemName=qetza.replacetokens) ## Prepare your Azure Pipeline for Deployment 1. Ensure that you [provide access](https://github.com/settings/connections/applications/0d4949be3b947c3ce4a5) to the Azure Devops application in your GitHub account. Specifically ensure that the application has access to your forked GitHub repository. 2. Create a new Azure Pipeline: In your Azure DevOps project, click on `Pipelines` > `New pipeline` > `GitHub` > Select your GitHub fork > `Existing Azure Pipelines YAML file` > `contoso-traders-cloud-testing/azure-pipelines.yml` (ensure branch is `main`, path is `/.azurepipelines/azure-pipelines.yml`) > `Continue`. 3. Set up a service connection to Azure: In your Azure DevOps project, click on `Project settings` (bottom left of page) > `Service Connections` > `New service connection` > `Azure Resource Manager` > `Service principal (manual)` > `Next`. At this point, you'll need the JSON output from the `az ad sp create-for-rbac` command [executed earlier](./deployment-instructions.md#prepare-your-azure-subscription). ```json { "clientId": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", "clientSecret": "your-client-secret", "tenantId": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", "subscriptionId": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz" } ``` On the `New Azure Service Connection` page, enter the following values: - **Environment**: `Azure Cloud` - **Scope Level**: `Subscription` - **Subscription Id**: `subscriptionId` property from the JSON output - **Subscription Name**: The name of your Azure subscription - **Service Principal Id**: `clientId` property from the JSON output - **Credential**: Choose the `Service principal key` option - **Service Principal Key**: `clientSecret` property from the JSON output - **Tenant ID**: `tenantId` property from the JSON output - **Service Connection Name**: `SERVICEPRINCIPAL` (please use this exact name) - **Description**: `Service connection to Azure subscription using service principal` - **Grant access permission to all pipelines**: Ensure this option is checked Click the `Verify and save` button. 4. Set up the variables for your Azure Pipeline. Click on `Pipelines` > `Library` > `+ Variable Group`. Create a variable group called `contosotraders-cloudtesting-variable-group` with the following variables: | Variable Name | Variable Value | Is Secret? | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `SQLPASSWORD` | 8 to 15 characters long, must contain uppercase, lowercase, and numeric characters | YES | | `SUFFIX` | A unique environment suffix (max 6 characters, alphanumeric, lower case only, no whitespace, no special chars). E.g. 'test51' or '1stg' | NO | | `DEPLOYMENTREGION` | The Azure region to deploy the application in. Must be one of: `australiaeast`,`centralus`,`eastus`,`eastus2`,`japaneast`,`northcentralus`,`uksouth`,`westcentralus`,`westeurope` | NO | After saving the variable group, click on `Pipeline permissions` and ensure that the `azure-pipelines.yml` pipeline has access to the variable group. ### Deploy the Application 1. Go to your Azure Project's `Pipelines` tab, select the `contoso-traders-cloud-testing` pipeline, and click on the `Run Pipeline` button. 2. The Azure pipeline will provision the necessary infrastructure to your Azure subscription as well as deploy the applications (APIs, UI) to the infrastructure. Note that the pipeline might take about 15 mins to complete. ![workflow-logs](./images/github-workflow.png) >You may get an error if you do not have free parallel jobs available in your Azure DevOps organization. If you get this error, you'll have to [request for more parallel jobs](https://docs.microsoft.com/en-us/azure/devops/pipelines/licensing/concurrent-jobs?view=azure-devops&tabs=yaml) in your Azure DevOps organization. > >For any other deployment errors, please check the [Troubleshooting Deployment Errors section](./deployment-instructions.md#troubleshooting-deployment-errors) in the Deployment Instruction document. ## Next Steps You can now proceed to [exploring demo scenarios](./deployment-instructions.md#explore-demo-scenarios). ================================================ FILE: docs/deployment-instructions.md ================================================ # Contoso Traders - Deployment instructions This document will help you deploy the Contoso Traders application in your Azure environment. You'll be using both GitHub Actions and Azure CLI for this. Once deployed, you'll be able to walk through various demo scenarios for Microsoft Playwright, Azure Load Testing, and Azure Chaos Studio. ## Prerequisites You will need following to get started: 1. **GitHub account**: Create a free account [here](https://github.com/). 2. **Azure subscription**: Create a free account [here](https://azure.microsoft.com/free/). 3. **Azure CLI**: Instructions to download and install [here](https://learn.microsoft.com/cli/azure/install-azure-cli). 4. **VS Code**: Download and install [here](https://code.visualstudio.com/download). ## Prepare your Azure Subscription 1. Log into Azure CLI with your Azure credentials: `az login` 2. Ensure that the correct Azure subscription is selected: `az account show` * If not, select the correct subscription: `az account set -s `. Replace `` with your Azure subscription ID. 3. Register some required resource providers in your Azure subscription: * `az provider register -n Microsoft.OperationsManagement -c` * `az provider register -n Microsoft.Cdn -c` * `az provider register -n Microsoft.Chaos -c` 4. Create an Azure Service Principal and add it to the `Owner` role in your Azure subscription: * `az ad sp create-for-rbac -n contosotraders-sp --role Owner --scopes /subscriptions/ --sdk-auth`. Replace `` with your Azure subscription ID. * Make a note of the JSON output from above step (especially the `clientId`, `clientSecret`, `subscriptionId` and `tenantId` properties). These will be required later. * You'll notice a warning in the output: `Option '--sdk-auth' has been deprecated and will be removed in a future release`. This is [a known issue, without workarounds, but can be safely ignored](https://github.com/Azure/azure-cli/issues/20743). 5. If for some reason, you do not have permissions to add the service principal in the `Owner` role on the subscription, then you can create a custom role and assign it to the service principal as follows (remember to replace `` in snippets below with your Azure subscription ID). 1. If using bash: ```bash az role definition create --role-definition '{ "Name": "ContosoTraders Write Role Assignments", "Description": "Perform Role Assignments", "Actions": ["Microsoft.Authorization/roleAssignments/write"], "AssignableScopes": ["/subscriptions/"] }' ``` 2. If using PowerShell or cmd shell, you can run `az role definition create --role-definition ./custom-role.json`. Note that you need to first create a file called `custom-role.json` containing the following snippet. ```json { "Name": "ContosoTraders Write Role Assignments", "Description": "Perform Role Assignments", "Actions": ["Microsoft.Authorization/roleAssignments/write"], "AssignableScopes": ["/subscriptions/"] } ``` 3. Finally create the service principal and assign it to the custom role: ```bash `az ad sp create-for-rbac -n contosotraders-sp --role "ContosoTraders Write Role Assignments" --scopes /subscriptions/ --sdk-auth` ``` 6. If you haven't used Azure Cognitive Services with your subscription, you'll need to accept the responsible AI terms. Manually create an Azure Cognitive Service resource in your subscription temporarily, and accept the Responsible AI terms. You can then delete the resource. * The Responsible AI terms are shown only once per subscription (during first Cognitive Service resource creation in subscription), and once accepted, they are not shown again. * Currently, there exists no mechanism to accept the Responsible AI terms programmatically. It can only be done manually through the Azure portal. * You can read more about Responsible AI [here](https://learn.microsoft.com/azure/machine-learning/concept-responsible-ai). ![agree-cognitive-service-screenshot](./images/agree-cognitive-service-screenshot.png) ## Prepare your GitHub Repository 1. Fork the [contosotraders-cloudtesting repo](https://github.com/microsoft/contosotraders-cloudtesting) in your account. ## Prepare your GitHub Workflow for Deployment > >If you wish to deploy using Azure Pipelines instead of GitHub Workflows, you can follow the instructions [here](./deployment-instructions-azure-pipelines.md) and skip this section entirely. > 1. Set up the repository secrets in your forked repo. On your fork of the github repository, go to the `Settings` tab > `Secrets and variables` > `Actions` > `Secrets` tab and create these necessary repository secrets: | Secret Name | Secret Value | | ------------------ | ---------------------------------------------------------------------------------- | | `SQLPASSWORD` | 8 to 15 characters long, must contain uppercase, lowercase, and numeric characters | | `SERVICEPRINCIPAL` | See details below | The value of the `SERVICEPRINCIPAL` secret above needs to have the below format. ```json { "clientId": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", "clientSecret": "your-client-secret", "tenantId": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", "subscriptionId": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz" } ``` The values of the properties needed can be found in the JSON output of the `az ad sp create-for-rbac` command in the previous section. 2. Set up the repository variables in your forked repo. On your fork of the github repository, go to the `Settings` tab > `Secrets and variables` > `Actions` > `Variables` tab and create these necessary repository variables: | Variable Name | Variable Value | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SUFFIX` | A unique environment suffix (max 6 characters, alphanumeric, lower case only, no whitespace, no special chars). E.g. 'test51' or '1stg' | | `DEPLOYMENTREGION` | The Azure region to deploy the application in. Must be one of: `australiaeast`,`centralus`,`eastus`,`eastus2`,`japaneast`,`northcentralus`,`uksouth`,`westcentralus`,`westeurope` | 3. (optional) if you would like to deploy the additional resources to test private endpoints, set the following variable:' | Variable Name | Variable Value | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DEPLOYPRIVATEENDPOINTS` | `true` ### Deploy the Application 1. Go to your forked repo's `Actions` tab, selecting the `contoso-traders-cloud-testing` workflow, and click on the `Run workflow` button. 2. This github workflow will provision the necessary infrastructure to your Azure subscription as well as deploy the applications (APIs, UI) to the infrastructure. Note that the workflow might take about 15 mins to complete. ![workflow-logs](./images/github-workflow.png) ### Verify the Deployment 1. Once the workflow completes, the UI's accessible CDN endpoint URL will be displayed in the github workflow run. ![Endpoints in workflow logs](./images/ui-endpoint-github-workflow.png) 2. Clicking on the URL above, will load the application in a new browser tab. You can then verify that the application is indeed up and running. ### Troubleshooting Deployment Errors Here are some common problems that you may encounter during deployment: 1. Intermittent errors: Should you encounter any of [these intermittent errors](https://github.com/microsoft/ContosoTraders/issues?q=is%3Aissue+is%3Aopen+label%3Adevops) in the github workflow, please re-run the failed jobs (it'll will pass on retry). We're working to fix these soon. 2. There is a [known issue](https://github.com/Azure/login/issues/249) where the Azure login github action fails if the service principal's `clientSecret` begins with `-` (hyphen). If you encounter this, please regenerate a new secret, update the repository secret in your github fork, and restart the workflow. ## Explore Demo Scenarios For further learning, you can run through some of the demo scripts listed below: * [Developer workflow](../demo-scripts/dev-workflow/walkthrough.md) * [Azure Load Testing](../demo-scripts/azure-load-testing/walkthrough.md) * [Azure Chaos Studio](../demo-scripts/azure-chaos-studio/walkthrough.md) * [UI Testing with Playwright](../demo-scripts/testing-with-playwright/walkthrough.md) ## Cleanup Once you are done deploying, testing, exploring, you should delete the `contoso-traders-rg{SUFFIX}` resource group to prevent incurring additional costs. ![resource group deletion](./images/resource-group-deletion.png) The `contoso-traders-aks-nodes-rg{SUFFIX}` will be automatically deleted as part of the AKS cluster deletion. ## Cost Considerations A quick note on costs considerations when you deploy the application to your Azure subscription: 1. Azure Load Testing ([pricing details](https://azure.microsoft.com/pricing/details/load-testing/)): The number of virtual users and duration of the test are the key factors that determine the cost of the test. In this demo, the load tests are configured to use 5 virtual users and the test is set to run for 3 mins. 2. Azure Kubernetes Service ([pricing details](https://azure.microsoft.com/pricing/details/kubernetes-service/)): The number of nodes and the number of hours that the cluster is running are the key factors that determine the cost of the cluster. In this demo, the cluster is configured to use 1 node (powered by vm scale sets) and the cluster is set to run 24x7 (you can manually stop the cluster when not in use). Because of a [limitation in the AKS bicep schema](https://github.com/Azure/bicep/issues/6974), the AKS cluster has to use premium SSD storage disks. 3. Azure Container Apps ([pricing details](https://azure.microsoft.com/pricing/details/container-apps/)): Each instance has 0.5 vCPU and 1.0 GiB of memory. In this demo, the container app is configured to use 1 instance, but can autoscale out to max 3 instances under load. 4. Azure Virtual Machines ([pricing details](https://azure.microsoft.com/pricing/details/virtual-machines/windows/)): The jumpbox VM uses the `Standard_D2s_v3` VM size, which has 2 vCPU and 8 GiB of memory. The jumpbox VMs are schedule to auto-shutdown at 1900 UTC daily. You can also manually stop & deallocate the VM when not in use. 5. Github Actions / storage quota ([pricing details](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#included-storage-and-minutes)): We've set the playwright test to enable recordings only on failures/retries. This brings the playwright report to ~55 MB when tests fail. > > The above costs are based on the default configuration of the demo. You can modify the configuration to reduce the costs. For example, you can reduce the number of instances in the container app, reduce the number of virtual users in the load test, etc. > ================================================ FILE: docs/running-locally.md ================================================ # ContosoTraders: Running Locally 1. Follow the [deployment instructions](./deployment-instructions.md) to provision the infrastructure in your own Azure subscription. 2. Ensure that you have the following installed on your local machine: * [Git](https://git-scm.com/downloads) * [Node LTS v18.15.0](https://nodejs.org/dist/v18.15.0/) * [DOTNET 7 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) * [AZ CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) 3. Login to AZ CLI using the details of the [service principal previously created](./deployment-instructions.md): * `az login --service-principal -u -p --tenant ` 4. Git clone your forked repository to your local machine. * `git clone ` 5. Run the Products API locally: * Open a cmd window and navigate to the `src/ContosoTraders.Api.Products` folder. * Run `dotnet user-secrets set "KeyVaultEndpoint" "https://contosotraderskv.vault.azure.net/"`. Replace `` with the [value used in the github repository variable](./deployment-instructions.md#prepare-your-github-account). * Run `dotnet build && dotnet run`. This will start the web API at `https://localhost:62300/swagger`. * Note that your browser may show you a warning about insecure connection which you can safely ignore. ![self signed cert warning](./images/self-signed-cert.png) 6. Run the Carts API locally: * Open a cmd window and navigate to the `src/ContosoTraders.Api.Carts` folder. * Run `dotnet user-secrets set "KeyVaultEndpoint" "https://contosotraderskv.vault.azure.net/"`. Replace `` with the [value used in the github repository variable](./deployment-instructions.md#prepare-your-github-account). * Run `dotnet build && dotnet run`. This will start the web API at `https://localhost:62400/swagger`. * Note that your browser may show you a warning about insecure connection which you can safely ignore. 7. Run the UI locally: * Open a cmd window and navigate to the `src/ContosoTraders.Ui.Website` folder. * Run `npm install`. * Set the following environment variables: * `set REACT_APP_APIURL=https://localhost:62300/v1` * `set REACT_APP_APIURLSHOPPINGCART=https://localhost:62400/v1` * `set REACT_APP_BASEURLFORPLAYWRIGHTTESTING=http://localhost:3000` (note: This endpoint is `http://` and NOT `https://`) * `set REACT_APP_B2CCLIENTID=` (note: This step is optional. Replace `` with the output of the command: `az ad app list --display-name contoso-traders-cloud-testing-app --query "[].appId" -o tsv`) * Run `npm run start`. This will start the UI on `http://localhost:3000`. * Note that your browser may show you a warning about insecure connection which you can safely ignore. 8. Run the Playwright UI tests locally: * Ensure that you have executed step #7 above and launched the UI locally. * Open a cmd window and navigate to the `src/ContosoTraders.Ui.Website` folder. * Run `npx playwright install --with-deps`. * Run `npx playwright test`. * The Playwright UI tests will run for a few minutes and a HTML report will be displayed at the end. ================================================ FILE: iac/createPrivateDnsZone.bicep ================================================ // Note: There is a very specific reason the creation of the private DNS zone happens in this separate bicep module (as opposed // to the main bicep file). A few of resource 'names' are derived from the FQDN on the ACA app; which only becomes known // "AFTER" the ACA app is created. // If you attempt to use the FQDN in the 'name' properties (e.g. name of A record, name of private dns zone, etc.) then you'll // encounter an error like this: // // This expression is being used in an assignment to the "name" property of the "Microsoft.XXXX/YYYY" type, which requires a value // that can be calculated at the start of the deployment. Properties of XXXYYY which can be calculated at the start include "name" // // More details: https://stackoverflow.com/q/73232751 param privateDnsZoneName string param privateDnsZoneVnetId string param privateDnsZoneVnetLinkName string param privateDnsZoneARecordName string param privateDnsZoneARecordIp string param resourceTags object // private dns zone resource privdnszone 'Microsoft.Network/privateDnsZones@2020-06-01' = { name: privateDnsZoneName location: 'global' tags: resourceTags } // vnet link resource privdnszone_vnetlink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { name: privateDnsZoneVnetLinkName location: 'global' tags: resourceTags parent: privdnszone properties: { registrationEnabled: true virtualNetwork: { id: privateDnsZoneVnetId } } } // private dns zone: 'A' record resource symbolicname 'Microsoft.Network/privateDnsZones/A@2020-06-01' = { name: privateDnsZoneARecordName parent: privdnszone properties: { aRecords: [ { ipv4Address: privateDnsZoneARecordIp } ] ttl: 3600 } } ================================================ FILE: iac/createResourceGroup.bicep ================================================ // common targetScope = 'subscription' // parameters //////////////////////////////////////////////////////////////////////////////// // common @description('Rg for storage account, service bus, cosmos db & function app. Value is passed from GHA variable.') param rgName string @minLength(3) @maxLength(6) @description('A unique environment suffix (max 6 characters, alphanumeric only).') param suffix string @description('Set rg location') @allowed([ 'australiaeast' 'centralus' 'eastus' 'eastus2' 'japaneast' 'northcentralus' 'uksouth' 'westcentralus' 'westeurope' ]) param rgLocation string // variables //////////////////////////////////////////////////////////////////////////////// // tags var rgTags = { Product: '${rgName}${suffix}' Environment: suffix } // resource groups //////////////////////////////////////////////////////////////////////////////// resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { name: '${rgName}${suffix}' location: rgLocation tags: rgTags } // outputs //////////////////////////////////////////////////////////////////////////////// output outputRgName string = rg.name ================================================ FILE: iac/createResources.bicep ================================================ // common targetScope = 'resourceGroup' // parameters //////////////////////////////////////////////////////////////////////////////// // common @minLength(3) @maxLength(6) @description('A unique environment suffix (max 6 characters, alphanumeric only).') param suffix string @secure() @description('A password which will be set on all SQL Azure DBs.') param sqlPassword string // @TODO: Obviously, we need to fix this! param resourceLocation string = resourceGroup().location // tenant param tenantId string = subscription().tenantId // aks param aksLinuxAdminUsername string // value supplied via parameters file param prefix string = 'contosotraders' param prefixHyphenated string = 'contoso-traders' // sql param sqlServerHostName string = environment().suffixes.sqlServerHostname // use param to conditionally deploy private endpoint resources param deployPrivateEndpoints bool = false // variables //////////////////////////////////////////////////////////////////////////////// // key vault var kvName = '${prefix}kv${suffix}' var kvSecretNameProductsApiEndpoint = 'productsApiEndpoint' var kvSecretNameProductsDbConnStr = 'productsDbConnectionString' var kvSecretNameProfilesDbConnStr = 'profilesDbConnectionString' var kvSecretNameStocksDbConnStr = 'stocksDbConnectionString' var kvSecretNameCartsApiEndpoint = 'cartsApiEndpoint' var kvSecretNameCartsInternalApiEndpoint = 'cartsInternalApiEndpoint' var kvSecretNameCartsDbConnStr = 'cartsDbConnectionString' var kvSecretNameImagesEndpoint = 'imagesEndpoint' var kvSecretNameAppInsightsConnStr = 'appInsightsConnectionString' var kvSecretNameUiCdnEndpoint = 'uiCdnEndpoint' var kvSecretNameVnetAcaSubnetId = 'vnetAcaSubnetId' // user-assigned managed identity (for key vault access) var userAssignedMIForKVAccessName = '${prefixHyphenated}-mi-kv-access${suffix}' // cosmos db (stocks db) var stocksDbAcctName = '${prefixHyphenated}-stocks${suffix}' var stocksDbName = 'stocksdb' var stocksDbStocksContainerName = 'stocks' // cosmos db (carts db) var cartsDbAcctName = '${prefixHyphenated}-carts${suffix}' var cartsDbName = 'cartsdb' var cartsDbStocksContainerName = 'carts' // app service plan (products api) var productsApiAppSvcPlanName = '${prefixHyphenated}-products${suffix}' var productsApiAppSvcName = '${prefixHyphenated}-products${suffix}' var productsApiSettingNameKeyVaultEndpoint = 'KeyVaultEndpoint' var productsApiSettingNameManagedIdentityClientId = 'ManagedIdentityClientId' // sql azure (products db) var productsDbServerName = '${prefixHyphenated}-products${suffix}' var productsDbName = 'productsdb' var productsDbServerAdminLogin = 'localadmin' var productsDbServerAdminPassword = sqlPassword // sql azure (profiles db) var profilesDbServerName = '${prefixHyphenated}-profiles${suffix}' var profilesDbName = 'profilesdb' var profilesDbServerAdminLogin = 'localadmin' var profilesDbServerAdminPassword = sqlPassword // azure container app (carts api) var cartsApiAcaName = '${prefixHyphenated}-carts${suffix}' var cartsApiAcaEnvName = '${prefix}acaenv${suffix}' var cartsApiAcaSecretAcrPassword = 'acr-password' var cartsApiAcaContainerDetailsName = '${prefixHyphenated}-carts${suffix}' var cartsApiSettingNameKeyVaultEndpoint = 'KeyVaultEndpoint' var cartsApiSettingNameManagedIdentityClientId = 'ManagedIdentityClientId' // azure container app (carts api - internal only) var cartsInternalApiAcaName = '${prefixHyphenated}-intcarts${suffix}' var cartsInternalApiAcaEnvName = '${prefix}intacaenv${suffix}' var cartsInternalApiAcaSecretAcrPassword = 'acr-password' var cartsInternalApiAcaContainerDetailsName = '${prefixHyphenated}-intcarts${suffix}' var cartsInternalApiSettingNameKeyVaultEndpoint = 'KeyVaultEndpoint' var cartsInternalApiSettingNameManagedIdentityClientId = 'ManagedIdentityClientId' // storage account (product images) var productImagesStgAccName = '${prefix}img${suffix}' var productImagesProductDetailsContainerName = 'product-details' var productImagesProductListContainerName = 'product-list' // storage account (old website) var uiStgAccName = '${prefix}ui${suffix}' // storage account (new website) var ui2StgAccName = '${prefix}ui2${suffix}' // storage account (image classifier) var imageClassifierStgAccName = '${prefix}ic${suffix}' var imageClassifierWebsiteUploadsContainerName = 'website-uploads' // cdn var cdnProfileName = '${prefixHyphenated}-cdn${suffix}' var cdnImagesEndpointName = '${prefixHyphenated}-images${suffix}' var cdnUiEndpointName = '${prefixHyphenated}-ui${suffix}' var cdnUi2EndpointName = '${prefixHyphenated}-ui2${suffix}' // azure container registry var acrName = '${prefix}acr${suffix}' // load testing service var loadTestSvcName = '${prefixHyphenated}-loadtest${suffix}' // application insights var logAnalyticsWorkspaceName = '${prefixHyphenated}-loganalytics${suffix}' var appInsightsName = '${prefixHyphenated}-ai${suffix}' // portal dashboard var portalDashboardName = '${prefixHyphenated}-dashboard${suffix}' // aks cluster var aksClusterName = '${prefixHyphenated}-aks${suffix}' var aksClusterDnsPrefix = '${prefixHyphenated}-aks${suffix}' var aksClusterNodeResourceGroup = '${prefixHyphenated}-aks-nodes-rg${suffix}' // virtual network var vnetName = '${prefixHyphenated}-vnet${suffix}' var vnetAddressSpace = '10.0.0.0/16' var vnetAcaSubnetName = 'subnet-aca' var vnetAcaSubnetAddressPrefix = '10.0.0.0/23' var vnetVmSubnetName = 'subnet-vm' var vnetVmSubnetAddressPrefix = '10.0.2.0/23' var vnetLoadTestSubnetName = 'subnet-loadtest' var vnetLoadTestSubnetAddressPrefix = '10.0.4.0/23' // jumpbox vm var jumpboxPublicIpName = '${prefixHyphenated}-jumpbox${suffix}' var jumpboxNsgName = '${prefixHyphenated}-jumpbox${suffix}' var jumpboxNicName = '${prefixHyphenated}-jumpbox${suffix}' var jumpboxVmName = 'jumpboxvm' var jumpboxVmAdminLogin = 'localadmin' var jumpboxVmAdminPassword = sqlPassword var jumpboxVmShutdownSchduleName = 'shutdown-computevm-jumpboxvm' var jumpboxVmShutdownScheduleTimezoneId = 'UTC' // private dns zone var privateDnsZoneVnetLinkName = '${prefixHyphenated}-privatednszone-vnet-link${suffix}' // chaos studio var chaosKvExperimentName = '${prefixHyphenated}-chaos-kv-experiment${suffix}' var chaosKvSelectorId = guid('${prefixHyphenated}-chaos-kv-selector-id${suffix}') var chaosAksExperimentName = '${prefixHyphenated}-chaos-aks-experiment${suffix}' var chaosAksSelectorId = guid('${prefixHyphenated}-chaos-aks-selector-id${suffix}') // tags var resourceTags = { Product: prefixHyphenated Environment: suffix } // resources //////////////////////////////////////////////////////////////////////////////// // // key vault // resource kv 'Microsoft.KeyVault/vaults@2022-07-01' = { name: kvName location: resourceLocation tags: resourceTags properties: { // @TODO: Hack to enable temporary access to devs during local development/debugging. accessPolicies: [ { objectId: '31de563b-fc1a-43a2-9031-c47630038328' tenantId: tenantId permissions: { secrets: [ 'get' 'list' 'delete' 'set' 'recover' 'backup' 'restore' ] } } ] sku: { family: 'A' name: 'standard' } softDeleteRetentionInDays: 7 tenantId: tenantId } // secret resource kv_secretProductsApiEndpoint 'secrets' = { name: kvSecretNameProductsApiEndpoint tags: resourceTags properties: { contentType: 'endpoint url (fqdn) of the products api' value: 'placeholder' // Note: This will be set via github worfklow } } // secret resource kv_secretProductsDbConnStr 'secrets' = { name: kvSecretNameProductsDbConnStr tags: resourceTags properties: { contentType: 'connection string to the products db' value: 'Server=tcp:${productsDbServerName}${sqlServerHostName},1433;Initial Catalog=${productsDbName};Persist Security Info=False;User ID=${productsDbServerAdminLogin};Password=${productsDbServerAdminPassword};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;' } } // secret resource kv_secretProfilesDbConnStr 'secrets' = { name: kvSecretNameProfilesDbConnStr tags: resourceTags properties: { contentType: 'connection string to the profiles db' value: 'Server=tcp:${profilesDbServerName}${sqlServerHostName},1433;Initial Catalog=${profilesDbName};Persist Security Info=False;User ID=${profilesDbServerAdminLogin};Password=${profilesDbServerAdminPassword};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;' } } // secret resource kv_secretStocksDbConnStr 'secrets' = { name: kvSecretNameStocksDbConnStr tags: resourceTags properties: { contentType: 'connection string to the stocks db' value: stocksdba.listConnectionStrings().connectionStrings[0].connectionString } } // secret resource kv_secretCartsApiEndpoint 'secrets' = { name: kvSecretNameCartsApiEndpoint tags: resourceTags properties: { contentType: 'endpoint url (fqdn) of the carts api' value: cartsapiaca.properties.configuration.ingress.fqdn } } // secret resource kv_secretCartsInternalApiEndpoint 'secrets' = if (deployPrivateEndpoints) { name: kvSecretNameCartsInternalApiEndpoint tags: resourceTags properties: { contentType: 'endpoint url (fqdn) of the (internal) carts api' value: deployPrivateEndpoints ? cartsinternalapiaca.properties.configuration.ingress.fqdn : '' } } // secret resource kv_secretCartsDbConnStr 'secrets' = { name: kvSecretNameCartsDbConnStr tags: resourceTags properties: { contentType: 'connection string to the carts db' value: cartsdba.listConnectionStrings().connectionStrings[0].connectionString } } // secret resource kv_secretImagesEndpoint 'secrets' = { name: kvSecretNameImagesEndpoint tags: resourceTags properties: { contentType: 'endpoint url of the images cdn' value: 'https://${cdnprofile_imagesendpoint.properties.hostName}' } } // secret resource kv_secretAppInsightsConnStr 'secrets' = { name: kvSecretNameAppInsightsConnStr tags: resourceTags properties: { contentType: 'connection string to the app insights instance' value: appinsights.properties.ConnectionString } } // secret resource kv_secretUiCdnEndpoint 'secrets' = { name: kvSecretNameUiCdnEndpoint tags: resourceTags properties: { contentType: 'endpoint url (cdn endpoint) of the ui' value: cdnprofile_ui2endpoint.properties.hostName } } // secret resource kv_secretVnetAcaSubnetId 'secrets' = if (deployPrivateEndpoints) { name: kvSecretNameVnetAcaSubnetId tags: resourceTags properties: { contentType: 'subnet id of the aca subnet' value: deployPrivateEndpoints ? vnet.properties.subnets[0].id : '' } } // access policies resource kv_accesspolicies 'accessPolicies' = { name: 'replace' properties: { // @TODO: I was unable to figure out how to assign an access policy to the AKS cluster's agent pool's managed identity. // Hence, that specific access policy will be assigned from a github workflow (using AZ CLI). accessPolicies: [ { tenantId: tenantId objectId: userassignedmiforkvaccess.properties.principalId permissions: { secrets: ['get', 'list'] } } { tenantId: tenantId objectId: aks.properties.identityProfile.kubeletidentity.objectId permissions: { secrets: ['get', 'list'] } } ] } } } resource kv_roledefinitionforchaosexp 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { scope: kv // This is the Key Vault Contributor role // See https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#key-vault-contributor name: 'f25e0fa2-a7c8-4377-a976-54943a77a395' } resource kv_roleassignmentforchaosexp 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: kv name: guid(kv.id, chaoskvexperiment.id, kv_roledefinitionforchaosexp.id) properties: { roleDefinitionId: kv_roledefinitionforchaosexp.id principalId: chaoskvexperiment.identity.principalId principalType: 'ServicePrincipal' } } resource userassignedmiforkvaccess 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' = { name: userAssignedMIForKVAccessName location: resourceLocation tags: resourceTags } // // stocks db // // cosmos db account resource stocksdba 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' = { name: stocksDbAcctName location: resourceLocation tags: resourceTags properties: { databaseAccountOfferType: 'Standard' enableFreeTier: false capabilities: [ { name: 'EnableServerless' } ] locations: [ { locationName: resourceLocation } ] } // cosmos db database resource stocksdba_db 'sqlDatabases' = { name: stocksDbName location: resourceLocation tags: resourceTags properties: { resource: { id: stocksDbName } } // cosmos db collection resource stocksdba_db_c1 'containers' = { name: stocksDbStocksContainerName location: resourceLocation tags: resourceTags properties: { resource: { id: stocksDbStocksContainerName partitionKey: { paths: [ '/id' ] } } } } } } // // carts db // // cosmos db account resource cartsdba 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' = { name: cartsDbAcctName location: resourceLocation tags: resourceTags properties: { databaseAccountOfferType: 'Standard' enableFreeTier: false capabilities: [ { name: 'EnableServerless' } ] locations: [ { locationName: resourceLocation } ] } // cosmos db database resource cartsdba_db 'sqlDatabases' = { name: cartsDbName location: resourceLocation tags: resourceTags properties: { resource: { id: cartsDbName } } // cosmos db collection resource cartsdba_db_c1 'containers' = { name: cartsDbStocksContainerName location: resourceLocation tags: resourceTags properties: { resource: { id: cartsDbStocksContainerName partitionKey: { paths: [ '/Email' ] } } } } } } // // products api // // app service plan (linux) resource productsapiappsvcplan 'Microsoft.Web/serverfarms@2022-03-01' = { name: productsApiAppSvcPlanName location: resourceLocation tags: resourceTags sku: { name: 'B1' } properties: { reserved: true } kind: 'linux' } // app service resource productsapiappsvc 'Microsoft.Web/sites@2022-03-01' = { name: productsApiAppSvcName location: resourceLocation tags: resourceTags identity: { type: 'UserAssigned' userAssignedIdentities: { '${userassignedmiforkvaccess.id}': {} } } properties: { clientAffinityEnabled: false httpsOnly: true serverFarmId: productsapiappsvcplan.id siteConfig: { linuxFxVersion: 'DOTNETCORE|7.0' alwaysOn: true appSettings: [ { name: productsApiSettingNameKeyVaultEndpoint value: kv.properties.vaultUri } { name: productsApiSettingNameManagedIdentityClientId value: userassignedmiforkvaccess.properties.clientId } ] } } } // // products db // // sql azure server resource productsdbsrv 'Microsoft.Sql/servers@2022-05-01-preview' = { name: productsDbServerName location: resourceLocation tags: resourceTags properties: { administratorLogin: productsDbServerAdminLogin administratorLoginPassword: productsDbServerAdminPassword publicNetworkAccess: 'Enabled' } // sql azure database resource productsdbsrv_db 'databases' = { name: productsDbName location: resourceLocation tags: resourceTags sku: { capacity: 5 tier: 'Basic' name: 'Basic' } } // sql azure firewall rule (allow access from all azure resources/services) resource productsdbsrv_db_fwlallowazureresources 'firewallRules' = { name: 'AllowAllWindowsAzureIps' properties: { endIpAddress: '0.0.0.0' startIpAddress: '0.0.0.0' } } // @TODO: Hack to enable temporary access to devs during local development/debugging. resource productsdbsrv_db_fwllocaldev 'firewallRules' = { name: 'AllowLocalDevelopment' properties: { endIpAddress: '255.255.255.255' startIpAddress: '0.0.0.0' } } } // // profiles db // // sql azure server resource profilesdbsrv 'Microsoft.Sql/servers@2022-05-01-preview' = { name: profilesDbServerName location: resourceLocation tags: resourceTags properties: { administratorLogin: profilesDbServerAdminLogin administratorLoginPassword: profilesDbServerAdminPassword publicNetworkAccess: 'Enabled' } // sql azure database resource profilesdbsrv_db 'databases' = { name: profilesDbName location: resourceLocation tags: resourceTags sku: { capacity: 5 tier: 'Basic' name: 'Basic' } } // sql azure firewall rule (allow access from all azure resources/services) resource profilesdbsrv_db_fwl 'firewallRules' = { name: 'AllowAllWindowsAzureIps' properties: { endIpAddress: '0.0.0.0' startIpAddress: '0.0.0.0' } } } // // carts api // // aca environment resource cartsapiacaenv 'Microsoft.App/managedEnvironments@2022-06-01-preview' = { name: cartsApiAcaEnvName location: resourceLocation tags: resourceTags sku: { name: 'Consumption' } properties: { zoneRedundant: false } } // aca resource cartsapiaca 'Microsoft.App/containerApps@2022-06-01-preview' = { name: cartsApiAcaName location: resourceLocation tags: resourceTags identity: { type: 'UserAssigned' userAssignedIdentities: { '${userassignedmiforkvaccess.id}': {} } } properties: { configuration: { activeRevisionsMode: 'Single' ingress: { external: true allowInsecure: false targetPort: 80 traffic: [ { latestRevision: true weight: 100 } ] } registries: [ { passwordSecretRef: cartsApiAcaSecretAcrPassword server: acr.properties.loginServer username: acr.name } ] secrets: [ { name: cartsApiAcaSecretAcrPassword value: acr.listCredentials().passwords[0].value } ] } environmentId: cartsapiacaenv.id template: { scale: { minReplicas: 1 maxReplicas: 10 rules: [ { name: 'http-scaling-rule' http: { metadata: { concurrentRequests: '3' } } } ] } containers: [ { env: [ { name: cartsApiSettingNameKeyVaultEndpoint value: kv.properties.vaultUri } { name: cartsApiSettingNameManagedIdentityClientId value: userassignedmiforkvaccess.properties.clientId } ] // using a public image initially because no images have been pushed to our private ACR yet // at this point. At a later point, our github workflow will update the ACA app to use the // images from our private ACR. image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' name: cartsApiAcaContainerDetailsName resources: { cpu: json('0.5') memory: '1.0Gi' } } ] } } } // // product images // // storage account (product images) resource productimagesstgacc 'Microsoft.Storage/storageAccounts@2023-01-01' = { name: productImagesStgAccName location: resourceLocation tags: resourceTags sku: { name: 'Standard_LRS' } kind: 'StorageV2' properties: { allowBlobPublicAccess: true } // blob service resource productimagesstgacc_blobsvc 'blobServices' = { name: 'default' // container resource productimagesstgacc_blobsvc_productdetailscontainer 'containers' = { name: productImagesProductDetailsContainerName properties: { publicAccess: 'Container' } } // container resource productimagesstgacc_blobsvc_productlistcontainer 'containers' = { name: productImagesProductListContainerName properties: { publicAccess: 'Container' } } } } // // main website / ui // new website / ui // // storage account (main website) resource uistgacc 'Microsoft.Storage/storageAccounts@2023-01-01' = { name: uiStgAccName location: resourceLocation tags: resourceTags sku: { name: 'Standard_LRS' } kind: 'StorageV2' properties: { allowBlobPublicAccess: true } // blob service resource uistgacc_blobsvc 'blobServices' = { name: 'default' } } resource uistgacc_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' = { name: 'DeploymentScript' location: resourceLocation tags: resourceTags } resource uistgacc_roledefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { scope: subscription() // This is the Storage Account Contributor role, which is the minimum role permission we can give. // See https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#:~:text=17d1049b-9a84-46fb-8f53-869881c3d3ab name: '17d1049b-9a84-46fb-8f53-869881c3d3ab' } // This requires the service principal to be in 'owner' role or a custom role with 'Microsoft.Authorization/roleAssignments/write' permissions. // Details: https://learn.microsoft.com/en-us/answers/questions/287573/authorization-failed-when-when-writing-a-roleassig.html resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: uistgacc name: guid(resourceGroup().id, uistgacc_mi.id, uistgacc_roledefinition.id) properties: { roleDefinitionId: uistgacc_roledefinition.id principalId: uistgacc_mi.properties.principalId principalType: 'ServicePrincipal' } } resource deploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { name: 'DeploymentScript' location: resourceLocation kind: 'AzurePowerShell' identity: { type: 'UserAssigned' userAssignedIdentities: { '${uistgacc_mi.id}': {} } } dependsOn: [ // we need to ensure we wait for the role assignment to be deployed before trying to access the storage account roleAssignment ] properties: { azPowerShellVersion: '3.0' scriptContent: loadTextContent('./scripts/enable-static-website.ps1') retentionInterval: 'PT4H' environmentVariables: [ { name: 'ResourceGroupName' value: resourceGroup().name } { name: 'StorageAccountName' value: uistgacc.name } ] } } // storage account (new website) resource ui2stgacc 'Microsoft.Storage/storageAccounts@2023-01-01' = { name: ui2StgAccName location: resourceLocation tags: resourceTags sku: { name: 'Standard_LRS' } kind: 'StorageV2' properties: { allowBlobPublicAccess: true } // blob service resource ui2stgacc_blobsvc 'blobServices' = { name: 'default' } } resource ui2stgacc_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview' = { name: 'DeploymentScript2' location: resourceLocation tags: resourceTags } resource ui2stgacc_roledefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { scope: subscription() // This is the Storage Account Contributor role, which is the minimum role permission we can give. // See https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#:~:text=17d1049b-9a84-46fb-8f53-869881c3d3ab name: '17d1049b-9a84-46fb-8f53-869881c3d3ab' } // This requires the service principal to be in 'owner' role or a custom role with 'Microsoft.Authorization/roleAssignments/write' permissions. // Details: https://learn.microsoft.com/en-us/answers/questions/287573/authorization-failed-when-when-writing-a-roleassig.html resource roleAssignment2 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: ui2stgacc name: guid(resourceGroup().id, ui2stgacc_mi.id, ui2stgacc_roledefinition.id) properties: { roleDefinitionId: ui2stgacc_roledefinition.id principalId: ui2stgacc_mi.properties.principalId principalType: 'ServicePrincipal' } } resource deploymentScript2 'Microsoft.Resources/deploymentScripts@2020-10-01' = { name: 'DeploymentScript2' location: resourceLocation kind: 'AzurePowerShell' identity: { type: 'UserAssigned' userAssignedIdentities: { '${ui2stgacc_mi.id}': {} } } dependsOn: [ // we need to ensure we wait for the role assignment to be deployed before trying to access the storage account roleAssignment ] properties: { azPowerShellVersion: '3.0' scriptContent: loadTextContent('./scripts/enable-static-website.ps1') retentionInterval: 'PT4H' environmentVariables: [ { name: 'ResourceGroupName' value: resourceGroup().name } { name: 'StorageAccountName' value: ui2stgacc.name } ] } } // // image classifier // // storage account (main website) resource imageclassifierstgacc 'Microsoft.Storage/storageAccounts@2023-01-01' = { name: imageClassifierStgAccName location: resourceLocation tags: resourceTags sku: { name: 'Standard_LRS' } kind: 'StorageV2' properties: { allowBlobPublicAccess: true } // blob service resource imageclassifierstgacc_blobsvc 'blobServices' = { name: 'default' // container resource uistgacc_blobsvc_websiteuploadscontainer 'containers' = { name: imageClassifierWebsiteUploadsContainerName properties: { publicAccess: 'Container' } } } } // // cdn // resource cdnprofile 'Microsoft.Cdn/profiles@2022-11-01-preview' = { name: cdnProfileName location: 'global' tags: resourceTags sku: { name: 'Standard_Microsoft' } } // endpoint (product images) resource cdnprofile_imagesendpoint 'Microsoft.Cdn/profiles/endpoints@2022-11-01-preview' = { name: cdnImagesEndpointName location: 'global' tags: resourceTags parent: cdnprofile properties: { isCompressionEnabled: true contentTypesToCompress: [ 'image/svg+xml' ] deliveryPolicy: { rules: [ { name: 'Global' order: 0 actions: [ { name: 'CacheExpiration' parameters: { typeName: 'DeliveryRuleCacheExpirationActionParameters' cacheBehavior: 'SetIfMissing' cacheType: 'All' cacheDuration: '10:00:00' } } ] } ] } originHostHeader: replace(replace(productimagesstgacc.properties.primaryEndpoints.blob, 'https://', ''), '/', '') origins: [ { name: replace( replace(replace(productimagesstgacc.properties.primaryEndpoints.blob, 'https://', ''), '/', ''), '.', '-' ) properties: { hostName: replace(replace(productimagesstgacc.properties.primaryEndpoints.blob, 'https://', ''), '/', '') originHostHeader: replace( replace(productimagesstgacc.properties.primaryEndpoints.blob, 'https://', ''), '/', '' ) } } ] } } // endpoint (ui / old website) resource cdnprofile_uiendpoint 'Microsoft.Cdn/profiles/endpoints@2022-11-01-preview' = { name: cdnUiEndpointName location: 'global' tags: resourceTags parent: cdnprofile properties: { isCompressionEnabled: true contentTypesToCompress: [ 'application/eot' 'application/font' 'application/font-sfnt' 'application/javascript' 'application/json' 'application/opentype' 'application/otf' 'application/pkcs7-mime' 'application/truetype' 'application/ttf' 'application/vnd.ms-fontobject' 'application/xhtml+xml' 'application/xml' 'application/xml+rss' 'application/x-font-opentype' 'application/x-font-truetype' 'application/x-font-ttf' 'application/x-httpd-cgi' 'application/x-javascript' 'application/x-mpegurl' 'application/x-opentype' 'application/x-otf' 'application/x-perl' 'application/x-ttf' 'font/eot' 'font/ttf' 'font/otf' 'font/opentype' 'image/svg+xml' 'text/css' 'text/csv' 'text/html' 'text/javascript' 'text/js' 'text/plain' 'text/richtext' 'text/tab-separated-values' 'text/xml' 'text/x-script' 'text/x-component' 'text/x-java-source' ] deliveryPolicy: { rules: [ { name: 'Global' order: 0 actions: [ { name: 'CacheExpiration' parameters: { typeName: 'DeliveryRuleCacheExpirationActionParameters' cacheBehavior: 'SetIfMissing' cacheType: 'All' cacheDuration: '10:00:00' } } ] } ] } originHostHeader: replace(replace(uistgacc.properties.primaryEndpoints.web, 'https://', ''), '/', '') origins: [ { name: replace(replace(replace(uistgacc.properties.primaryEndpoints.web, 'https://', ''), '/', ''), '.', '-') properties: { hostName: replace(replace(uistgacc.properties.primaryEndpoints.web, 'https://', ''), '/', '') originHostHeader: replace(replace(uistgacc.properties.primaryEndpoints.web, 'https://', ''), '/', '') } } ] } } // endpoint (ui / new website) resource cdnprofile_ui2endpoint 'Microsoft.Cdn/profiles/endpoints@2022-11-01-preview' = { name: cdnUi2EndpointName location: 'global' tags: resourceTags parent: cdnprofile properties: { isCompressionEnabled: true contentTypesToCompress: [ 'application/eot' 'application/font' 'application/font-sfnt' 'application/javascript' 'application/json' 'application/opentype' 'application/otf' 'application/pkcs7-mime' 'application/truetype' 'application/ttf' 'application/vnd.ms-fontobject' 'application/xhtml+xml' 'application/xml' 'application/xml+rss' 'application/x-font-opentype' 'application/x-font-truetype' 'application/x-font-ttf' 'application/x-httpd-cgi' 'application/x-javascript' 'application/x-mpegurl' 'application/x-opentype' 'application/x-otf' 'application/x-perl' 'application/x-ttf' 'font/eot' 'font/ttf' 'font/otf' 'font/opentype' 'image/svg+xml' 'text/css' 'text/csv' 'text/html' 'text/javascript' 'text/js' 'text/plain' 'text/richtext' 'text/tab-separated-values' 'text/xml' 'text/x-script' 'text/x-component' 'text/x-java-source' ] deliveryPolicy: { rules: [ { name: 'Global' order: 0 actions: [ { name: 'CacheExpiration' parameters: { typeName: 'DeliveryRuleCacheExpirationActionParameters' cacheBehavior: 'SetIfMissing' cacheType: 'All' cacheDuration: '02:00:00' } } ] } { name: 'EnforceHttps' order: 1 conditions: [ { name: 'RequestScheme' parameters: { typeName: 'DeliveryRuleRequestSchemeConditionParameters' matchValues: [ 'HTTP' ] operator: 'Equal' negateCondition: false transforms: [] } } ] actions: [ { name: 'UrlRedirect' parameters: { typeName: 'DeliveryRuleUrlRedirectActionParameters' redirectType: 'Found' destinationProtocol: 'Https' } } ] } ] } originHostHeader: replace(replace(ui2stgacc.properties.primaryEndpoints.web, 'https://', ''), '/', '') origins: [ { name: replace(replace(replace(ui2stgacc.properties.primaryEndpoints.web, 'https://', ''), '/', ''), '.', '-') properties: { hostName: replace(replace(ui2stgacc.properties.primaryEndpoints.web, 'https://', ''), '/', '') originHostHeader: replace(replace(ui2stgacc.properties.primaryEndpoints.web, 'https://', ''), '/', '') } } ] } } // // container registry // resource acr 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' = { name: acrName location: resourceLocation tags: resourceTags sku: { name: 'Basic' } properties: { adminUserEnabled: true publicNetworkAccess: 'Enabled' } } // // load testing service // resource loadtestsvc 'Microsoft.LoadTestService/loadTests@2022-12-01' = { name: loadTestSvcName location: resourceLocation tags: resourceTags identity: { type: 'UserAssigned' userAssignedIdentities: { '${userassignedmiforkvaccess.id}': {} } } } // // application insights // // log analytics workspace resource loganalyticsworkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { name: logAnalyticsWorkspaceName location: resourceLocation tags: resourceTags properties: { publicNetworkAccessForIngestion: 'Enabled' publicNetworkAccessForQuery: 'Enabled' sku: { name: 'PerGB2018' // pay-as-you-go } } } // app insights instance resource appinsights 'Microsoft.Insights/components@2020-02-02' = { name: appInsightsName location: resourceLocation tags: resourceTags kind: 'web' properties: { Application_Type: 'web' WorkspaceResourceId: loganalyticsworkspace.id } } // // portal dashboard // resource dashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { name: portalDashboardName location: resourceLocation tags: resourceTags properties: { lenses: [ { order: 0 parts: [ { position: { x: 0 y: 0 rowSpan: 4 colSpan: 2 } } ] } ] } } // // aks cluster // resource aks 'Microsoft.ContainerService/managedClusters@2022-10-02-preview' = { name: aksClusterName location: resourceLocation tags: resourceTags identity: { type: 'SystemAssigned' } properties: { dnsPrefix: aksClusterDnsPrefix nodeResourceGroup: aksClusterNodeResourceGroup agentPoolProfiles: [ { name: 'agentpool' osDiskSizeGB: 0 // Specifying 0 will apply the default disk size for that agentVMSize. count: 1 vmSize: 'standard_b2s' osType: 'Linux' mode: 'System' } ] linuxProfile: { adminUsername: aksLinuxAdminUsername ssh: { publicKeys: [ { keyData: loadTextContent('rsa.pub') // @TODO: temporary hack, until we autogen the keys } ] } } } } resource aks_roledefinitionforchaosexp 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { scope: aks // This is the Azure Kubernetes Service Cluster Admin Role // See https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-kubernetes-service-cluster-admin-role name: '0ab0b1a8-8aac-4efd-b8c2-3ee1fb270be8' } resource aks_roleassignmentforchaosexp 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: aks name: guid(aks.id, chaosaksexperiment.id, aks_roledefinitionforchaosexp.id) properties: { roleDefinitionId: aks_roledefinitionforchaosexp.id principalId: chaosaksexperiment.identity.principalId principalType: 'ServicePrincipal' } } // // virtual network // resource vnet 'Microsoft.Network/virtualNetworks@2022-07-01' = if (deployPrivateEndpoints) { name: vnetName location: resourceLocation tags: resourceTags properties: { addressSpace: { addressPrefixes: [ vnetAddressSpace ] } subnets: [ { name: vnetAcaSubnetName properties: { addressPrefix: vnetAcaSubnetAddressPrefix } } { name: vnetVmSubnetName properties: { addressPrefix: vnetVmSubnetAddressPrefix } } { name: vnetLoadTestSubnetName properties: { addressPrefix: vnetLoadTestSubnetAddressPrefix } } ] } } // // jumpbox vm // // public ip address resource jumpboxpublicip 'Microsoft.Network/publicIPAddresses@2022-07-01' = if (deployPrivateEndpoints) { name: jumpboxPublicIpName location: resourceLocation tags: resourceTags sku: { name: 'Standard' tier: 'Regional' } properties: { deleteOption: 'Delete' publicIPAllocationMethod: 'Static' } } // network security group resource jumpboxnsg 'Microsoft.Network/networkSecurityGroups@2022-07-01' = if (deployPrivateEndpoints) { name: jumpboxNsgName location: resourceLocation tags: resourceTags properties: { securityRules: [ { name: 'allow-rdp-port-3389' properties: { access: 'Allow' destinationAddressPrefix: 'VirtualNetwork' destinationPortRange: '3389' direction: 'Inbound' priority: 300 protocol: '*' sourceAddressPrefix: '*' sourcePortRange: '*' } } ] } } // network interface controller resource jumpboxnic 'Microsoft.Network/networkInterfaces@2022-07-01' = if (deployPrivateEndpoints) { name: jumpboxNicName location: resourceLocation tags: resourceTags properties: { ipConfigurations: [ { name: 'nic-ip-config' properties: { primary: true privateIPAllocationMethod: 'Dynamic' subnet: { id: deployPrivateEndpoints ? vnet.properties.subnets[1].id : '' } publicIPAddress: { id: deployPrivateEndpoints ? jumpboxpublicip.id : '' } } } ] networkSecurityGroup: { id: deployPrivateEndpoints ? jumpboxnsg.id : '' } nicType: 'Standard' } } // virtual machine resource jumpboxvm 'Microsoft.Compute/virtualMachines@2022-08-01' = if (deployPrivateEndpoints) { name: jumpboxVmName location: resourceLocation tags: resourceTags properties: { hardwareProfile: { vmSize: 'standard_b2s' } storageProfile: { osDisk: { createOption: 'FromImage' managedDisk: { storageAccountType: 'StandardSSD_LRS' } } imageReference: { offer: 'WindowsServer' publisher: 'MicrosoftWindowsServer' sku: '2019-datacenter-gensecond' version: 'latest' } } networkProfile: { networkInterfaces: [ { id: deployPrivateEndpoints ? jumpboxnic.id : '' properties: { deleteOption: 'Delete' } } ] } osProfile: { adminPassword: jumpboxVmAdminPassword #disable-next-line adminusername-should-not-be-literal // @TODO: This is a temporary hack, until we can generate the password adminUsername: jumpboxVmAdminLogin computerName: jumpboxVmName } } } // auto-shutdown schedule resource jumpboxvmschedule 'Microsoft.DevTestLab/schedules@2018-09-15' = if (deployPrivateEndpoints) { name: jumpboxVmShutdownSchduleName location: resourceLocation tags: resourceTags properties: { targetResourceId: deployPrivateEndpoints ? jumpboxvm.id : '' dailyRecurrence: { time: '2100' } notificationSettings: { status: 'Disabled' } status: 'Enabled' taskType: 'ComputeVmShutdownTask' timeZoneId: jumpboxVmShutdownScheduleTimezoneId } } // // private dns zone // module privateDnsZone './createPrivateDnsZone.bicep' = if (deployPrivateEndpoints) { name: 'createPrivateDnsZone' params: { privateDnsZoneName: deployPrivateEndpoints ? join(skip(split(cartsinternalapiaca.properties.configuration.ingress.fqdn, '.'), 2), '.') : '' privateDnsZoneVnetId: deployPrivateEndpoints ? vnet.id : '' privateDnsZoneVnetLinkName: privateDnsZoneVnetLinkName privateDnsZoneARecordName: deployPrivateEndpoints ? join(take(split(cartsinternalapiaca.properties.configuration.ingress.fqdn, '.'), 2), '.') : '' privateDnsZoneARecordIp: deployPrivateEndpoints ? cartsinternalapiacaenv.properties.staticIp : '' resourceTags: resourceTags } } // aca environment (internal) resource cartsinternalapiacaenv 'Microsoft.App/managedEnvironments@2022-06-01-preview' = if (deployPrivateEndpoints) { name: cartsInternalApiAcaEnvName location: resourceLocation tags: resourceTags sku: { name: 'Consumption' } properties: { zoneRedundant: false vnetConfiguration: { infrastructureSubnetId: deployPrivateEndpoints ? vnet.properties.subnets[0].id : '' internal: true } } } // aca (internal) resource cartsinternalapiaca 'Microsoft.App/containerApps@2022-06-01-preview' = if (deployPrivateEndpoints) { name: cartsInternalApiAcaName location: resourceLocation tags: resourceTags identity: { type: 'UserAssigned' userAssignedIdentities: { '${userassignedmiforkvaccess.id}': {} } } properties: { configuration: { activeRevisionsMode: 'Single' ingress: { external: true allowInsecure: false targetPort: 80 traffic: [ { latestRevision: true weight: 100 } ] } registries: [ { passwordSecretRef: cartsInternalApiAcaSecretAcrPassword server: acr.properties.loginServer username: acr.name } ] secrets: [ { name: cartsInternalApiAcaSecretAcrPassword value: acr.listCredentials().passwords[0].value } ] } environmentId: cartsinternalapiacaenv.id template: { scale: { minReplicas: 1 maxReplicas: 3 rules: [ { name: 'http-scaling-rule' http: { metadata: { concurrentRequests: '3' } } } ] } containers: [ { env: [ { name: cartsInternalApiSettingNameKeyVaultEndpoint value: kv.properties.vaultUri } { name: cartsInternalApiSettingNameManagedIdentityClientId value: userassignedmiforkvaccess.properties.clientId } ] // using a public image initially because no images have been pushed to our private ACR yet // at this point. At a later point, our github workflow will update the ACA app to use the // images from our private ACR. image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' name: cartsInternalApiAcaContainerDetailsName resources: { cpu: json('0.5') memory: '1.0Gi' } } ] } } } // // chaos studio // // target: kv resource chaoskvtarget 'Microsoft.Chaos/targets@2022-10-01-preview' = { name: 'Microsoft-KeyVault' location: resourceLocation scope: kv properties: {} // capability: kv (deny access) resource chaoskvcapability 'capabilities' = { name: 'DenyAccess-1.0' } } // chaos experiment: kv resource chaoskvexperiment 'Microsoft.Chaos/experiments@2022-10-01-preview' = { name: chaosKvExperimentName location: resourceLocation tags: resourceTags identity: { type: 'SystemAssigned' } properties: { selectors: [ { type: 'List' id: chaosKvSelectorId targets: [ { id: chaoskvtarget.id type: 'ChaosTarget' } ] } ] startOnCreation: false steps: [ { name: 'step1' branches: [ { name: 'branch1' actions: [ { name: 'urn:csci:microsoft:keyVault:denyAccess/1.0' type: 'continuous' selectorId: chaosKvSelectorId duration: 'PT5M' parameters: [] } ] } ] } ] } } // target: aks resource chaosakstarget 'Microsoft.Chaos/targets@2022-10-01-preview' = { name: 'Microsoft-AzureKubernetesServiceChaosMesh' location: resourceLocation scope: aks properties: {} // capability: aks (pod failures) resource chaosakscapability 'capabilities' = { name: 'PodChaos-2.1' } } // chaos experiment: aks (chaos mesh) resource chaosaksexperiment 'Microsoft.Chaos/experiments@2022-10-01-preview' = { name: chaosAksExperimentName location: resourceLocation tags: resourceTags identity: { type: 'SystemAssigned' } properties: { selectors: [ { type: 'List' id: chaosAksSelectorId targets: [ { id: chaosakstarget.id type: 'ChaosTarget' } ] } ] startOnCreation: false steps: [ { name: 'step1' branches: [ { name: 'branch1' actions: [ { name: 'urn:csci:microsoft:azureKubernetesServiceChaosMesh:podChaos/2.1' type: 'continuous' selectorId: chaosAksSelectorId duration: 'PT5M' parameters: [ { key: 'jsonSpec' value: '{\'action\':\'pod-failure\',\'mode\':\'all\',\'duration\':\'3s\',\'selector\':{\'namespaces\':[\'default\'],\'labelSelectors\':{\'app\':\'contoso-traders-products\'}}}' } ] } ] } ] } ] } } // outputs //////////////////////////////////////////////////////////////////////////////// output cartsApiEndpoint string = 'https://${cartsapiaca.properties.configuration.ingress.fqdn}' output uiCdnEndpoint string = 'https://${cdnprofile_ui2endpoint.properties.hostName}' ================================================ FILE: iac/createResources.parameters.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { "aksLinuxAdminUsername": { "value": "localadmin" } } } ================================================ FILE: iac/dashboard.json ================================================ { "properties": { "lenses": { "0": { "order": 0, "parts": { "0": { "position": { "x": 0, "y": 0, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Web/serverFarms/contoso-traders-productstest" }, "name": "CpuPercentage", "aggregationType": 3, "namespace": "microsoft.web/serverfarms", "metricVisualization": { "displayName": "CPU Percentage" } }, { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Web/serverFarms/contoso-traders-productstest" }, "name": "MemoryPercentage", "aggregationType": 3, "namespace": "microsoft.web/serverfarms", "metricVisualization": { "displayName": "Memory Percentage" } } ], "title": "Products API : AppSvcPlan : Max CPU% and Max Memory%", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Web/serverFarms/contoso-traders-productstest" }, "name": "CpuPercentage", "aggregationType": 3, "namespace": "microsoft.web/serverfarms", "metricVisualization": { "displayName": "CPU Percentage" } }, { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Web/serverFarms/contoso-traders-productstest" }, "name": "MemoryPercentage", "aggregationType": 3, "namespace": "microsoft.web/serverfarms", "metricVisualization": { "displayName": "Memory Percentage" } } ], "title": "Products API : AppSvcPlan : Max CPU%, Max Mem%", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } } } }, "1": { "position": { "x": 0, "y": 2, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Web/sites/contoso-traders-productstest" }, "name": "Http5xx", "aggregationType": 1, "namespace": "microsoft.web/sites", "metricVisualization": { "displayName": "Http Server Errors", "resourceDisplayName": "contoso-traders-productstest" } } ], "title": "Http 5xx", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 2 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Web/sites/contoso-traders-productstest" }, "name": "Http5xx", "aggregationType": 1, "namespace": "microsoft.web/sites", "metricVisualization": { "displayName": "Http Server Errors", "resourceDisplayName": "contoso-traders-productstest" } } ], "title": "Products API : AppSvc : Http 5xx Errors", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } } } }, "2": { "position": { "x": 4, "y": 2, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Web/sites/contoso-traders-productstest" }, "name": "Requests", "aggregationType": 1, "namespace": "microsoft.web/sites", "metricVisualization": { "displayName": "Requests" } } ], "title": "Products API : AppSvc : Sum Requests", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Web/sites/contoso-traders-productstest" }, "name": "Requests", "aggregationType": 1, "namespace": "microsoft.web/sites", "metricVisualization": { "displayName": "Requests" } } ], "title": "Products API : AppSvc : Requests", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } } } }, "3": { "position": { "x": 8, "y": 2, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Web/sites/contoso-traders-productstest" }, "name": "HttpResponseTime", "aggregationType": 3, "namespace": "microsoft.web/sites", "metricVisualization": { "displayName": "Response Time" } } ], "title": "Products API : AppSvc : Response Time", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Web/sites/contoso-traders-productstest" }, "name": "HttpResponseTime", "aggregationType": 3, "namespace": "microsoft.web/sites", "metricVisualization": { "displayName": "Response Time" } } ], "title": "Products API : AppSvc : Response Time", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "4": { "position": { "x": 0, "y": 4, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Sql/servers/contoso-traders-productstest/databases/productsdb" }, "name": "dtu_limit", "aggregationType": 4, "namespace": "microsoft.sql/servers/databases", "metricVisualization": { "displayName": "DTU Limit" } }, { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Sql/servers/contoso-traders-productstest/databases/productsdb" }, "name": "dtu_used", "aggregationType": 3, "namespace": "microsoft.sql/servers/databases", "metricVisualization": { "displayName": "DTU used" } } ], "title": "Products DB : Sql : DTU Utilization", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Sql/servers/contoso-traders-productstest/databases/productsdb" }, "name": "dtu_limit", "aggregationType": 4, "namespace": "microsoft.sql/servers/databases", "metricVisualization": { "displayName": "DTU Limit" } }, { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Sql/servers/contoso-traders-productstest/databases/productsdb" }, "name": "dtu_used", "aggregationType": 3, "namespace": "microsoft.sql/servers/databases", "metricVisualization": { "displayName": "DTU used" } } ], "title": "Products DB : Sql : DTU Utilization", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "5": { "position": { "x": 4, "y": 4, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Sql/servers/contoso-traders-productstest/databases/productsdb" }, "name": "dtu_consumption_percent", "aggregationType": 3, "namespace": "microsoft.sql/servers/databases", "metricVisualization": { "displayName": "DTU percentage" } } ], "title": "Products DB : Sql : DTU% ", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.Sql/servers/contoso-traders-productstest/databases/productsdb" }, "name": "dtu_consumption_percent", "aggregationType": 3, "namespace": "microsoft.sql/servers/databases", "metricVisualization": { "displayName": "DTU percentage" } } ], "title": "Products DB : Sql : DTU% ", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "6": { "position": { "x": 0, "y": 6, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "TotalRequests", "aggregationType": 7, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Total Requests", "color": null } } ], "title": "Stocks DB : CosmosDB : Requests", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "TotalRequests", "aggregationType": 7, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Total Requests", "color": null } } ], "title": "Stocks DB : CosmosDB : Requests", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "7": { "position": { "x": 4, "y": 6, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "TotalRequestUnits", "aggregationType": 1, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Total Request Units", "color": null } } ], "title": "Stocks DB : CosmosDB : RUs", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "TotalRequestUnits", "aggregationType": 1, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Total Request Units", "color": null } } ], "title": "Stocks DB : CosmosDB : RUs", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "8": { "position": { "x": 8, "y": 6, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "TotalRequests", "aggregationType": 7, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Throttled Requests (429s)", "color": null } } ], "title": "Stocks DB : CosmosDB : Throttled Requests (429s)", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "filterCollection": { "filters": [ { "key": "StatusCode", "operator": 0, "values": [ "429" ] } ] }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "TotalRequests", "aggregationType": 7, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Throttled Requests (429s)", "color": null } } ], "title": "Stocks DB : CosmosDB : Throttled Requests (429s)", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "StatusCode": { "model": { "operator": "equals", "values": [ "429" ] } }, "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "9": { "position": { "x": 12, "y": 6, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "NormalizedRUConsumption", "aggregationType": 3, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Normalized RU Consumption", "color": null } } ], "title": "StocksDB : CosmosDB : Normalized RU Consumption", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "NormalizedRUConsumption", "aggregationType": 3, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Normalized RU Consumption", "color": null } } ], "title": "StocksDB : CosmosDB : Normalized RU Consumption", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "10": { "position": { "x": 0, "y": 8, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.App/containerApps/contoso-traders-cartstest" }, "name": "UsageNanoCores", "aggregationType": 3, "namespace": "microsoft.app/containerapps", "metricVisualization": { "displayName": "CPU Usage" } } ], "title": "Carts API : ACA : CPU Usage", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.App/containerApps/contoso-traders-cartstest" }, "name": "UsageNanoCores", "aggregationType": 3, "namespace": "microsoft.app/containerapps", "metricVisualization": { "displayName": "CPU Usage" } } ], "title": "Carts API : ACA : CPU Usage", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "11": { "position": { "x": 4, "y": 8, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.App/containerApps/contoso-traders-cartstest" }, "name": "Requests", "aggregationType": 1, "namespace": "microsoft.app/containerapps", "metricVisualization": { "displayName": "Requests" } } ], "title": "Carts API : ACA : Requests", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.App/containerApps/contoso-traders-cartstest" }, "name": "Requests", "aggregationType": 1, "namespace": "microsoft.app/containerapps", "metricVisualization": { "displayName": "Requests" } } ], "title": "Carts API : ACA : Requests", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "12": { "position": { "x": 8, "y": 8, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.App/containerApps/contoso-traders-cartstest" }, "name": "Replicas", "aggregationType": 3, "namespace": "microsoft.app/containerapps", "metricVisualization": { "displayName": "Replica Count" } } ], "title": "Carts API : ACA : Replica Count", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.App/containerApps/contoso-traders-cartstest" }, "name": "Replicas", "aggregationType": 3, "namespace": "microsoft.app/containerapps", "metricVisualization": { "displayName": "Replica Count" } } ], "title": "Carts API : ACA : Replica Count", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "13": { "position": { "x": 0, "y": 10, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "TotalRequests", "aggregationType": 7, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Total Requests", "color": null } } ], "title": "Carts DB : CosmosDB : Requests", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "TotalRequests", "aggregationType": 7, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Total Requests", "color": null } } ], "title": "Carts DB : CosmosDB : Requests", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "14": { "position": { "x": 4, "y": 10, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "TotalRequestUnits", "aggregationType": 1, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Total Request Units", "color": null } } ], "title": "Carts DB : CosmosDB : RUs", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "TotalRequestUnits", "aggregationType": 1, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Total Request Units", "color": null } } ], "title": "Carts DB : CosmosDB : RUs", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "15": { "position": { "x": 8, "y": 10, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "TotalRequests", "aggregationType": 7, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Total Requests", "color": null } } ], "title": "Carts DB : CosmosDB : Throttled Requests (429s)", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "filterCollection": { "filters": [ { "key": "StatusCode", "operator": 0, "values": [ "429" ] } ] }, "grouping": { "dimension": "StatusCode", "top": 50 }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "TotalRequests", "aggregationType": 7, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Total Requests", "color": null } } ], "title": "Carts DB : CosmosDB : Throttled Requests (429s)", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true }, "grouping": { "dimension": "StatusCode", "top": 50 } } } } }, "filters": { "StatusCode": { "model": { "operator": "equals", "values": [ "429" ] } }, "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } }, "16": { "position": { "x": 12, "y": 10, "colSpan": 4, "rowSpan": 2 }, "metadata": { "inputs": [ { "name": "options", "value": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "NormalizedRUConsumption", "aggregationType": 3, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Normalized RU Consumption", "color": null } } ], "title": "Carts DB : CosmosDB : Normalized RU Consumption", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } } }, "timespan": { "relative": { "duration": 1800000 }, "showUTCTime": false, "grain": 1 } } }, "isOptional": true }, { "name": "sharedTimeRange", "isOptional": true } ], "type": "Extension/HubsExtension/PartType/MonitorChartPart", "settings": { "content": { "options": { "chart": { "metrics": [ { "resourceMetadata": { "id": "/subscriptions/d0c33f09-f408-4bb7-935e-c53c50358a38/resourceGroups/contoso-traders-rg/providers/Microsoft.DocumentDB/databaseAccounts/contoso-traders-cartstest" }, "name": "NormalizedRUConsumption", "aggregationType": 3, "namespace": "microsoft.documentdb/databaseaccounts", "metricVisualization": { "displayName": "Normalized RU Consumption", "color": null } } ], "title": "Carts DB : CosmosDB : Normalized RU Consumption", "titleKind": 2, "visualization": { "chartType": 2, "legendVisualization": { "isVisible": true, "position": 2, "hideSubtitle": false }, "axisVisualization": { "x": { "isVisible": true, "axisType": 2 }, "y": { "isVisible": true, "axisType": 1 } }, "disablePinning": true } } } } }, "filters": { "MsPortalFx_TimeRange": { "model": { "format": "local", "granularity": "auto", "relative": "30m" } } } } } } } }, "metadata": { "model": { "timeRange": { "value": { "relative": { "duration": 24, "timeUnit": 1 } }, "type": "MsPortalFx.Composition.Configuration.ValueTypes.TimeRange" }, "filterLocale": { "value": "en-us" }, "filters": { "value": { "MsPortalFx_TimeRange": { "model": { "format": "utc", "granularity": "auto", "relative": "24h" }, "displayCache": { "name": "UTC Time", "value": "Past 24 hours" }, "filteredPartIds": [ "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d3e", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d40", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d42", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d44", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d46", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d48", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d4a", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d4c", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d4e", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d50", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d52", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d54", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d56", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d58", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d5a", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d5c", "StartboardPart-MonitorChartPart-0d25c87b-86d2-4f4c-b66c-ada9ac7b3d5e" ] } } } } } }, "name": "contoso-traders-dashboardtest", "type": "Microsoft.Portal/dashboards", "location": "INSERT LOCATION", "tags": { "hidden-title": "contoso-traders-dashboardtest" }, "apiVersion": "2015-08-01-preview" } ================================================ FILE: iac/rsa ================================================ -----BEGIN OPENSSH PRIVATE KEY----- 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 -----END OPENSSH PRIVATE KEY----- ================================================ FILE: iac/rsa.pub ================================================ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDmGzcTpLPWs7LSA8xCVDXgAa/9dLR+7OWAkcxjHcfZ++vpBhww2MbN3WErdTv/3ItheqRIxwhs1sZ2D2NKHdg69NAU1gZrVDUTQosRyt7qa2xQxm5vO3vsPr9rHWKscwbev/KtUt2rQMWE4yDoozn9yUqYtWvW9Nj+/PMCyWJ48WNweEMh4MMbvfZozoSV1qFIVT+8R4xHDP1FtjQOD+P88cpB81xgUtQGXkKQgRGPxhMwS/S8CcEIPGJufh7mVjUrcXZNnv+Uf5NSSBdVvUKhwUiW7JqgVI38vqL6OOGnEO34GfCMTLQoPPFt3cTCyJFqvYY2w7XGYMNV3xYxortiaxKfdflrd4BOqejm/dxFMuRt96WIHiUN7u8G7lTZ2DoAJviOXxodDrgm33nFHQjhO0XndEkZ44DbGd/aOLk36jyMOW2Z25wVILGggB6lMsPcE6VrTQsOT0RtzdA59v3gdq7Phs/H0Oqhi9uA1rsgsd7sqzj4vpdaq9rrAK1gQ9ODQi8kN5pvwD5wWgnzT78/3kMQq2oLtrZrbPFNQS5TZJ9Hu/kMnCxRpf5rAIvG6u5wP4Up64Goa8y7ZOBHs3I0WGw55HL0NRkJRqd9PIvPo/C7cYoy5U1tskzzJkZoTPsU+4AQtPojuRX0OQlSH+VvXbEzyVJTnrL9FsWbfWm6KQ== localadmin@highwaystar ================================================ FILE: iac/scripts/enable-static-website.ps1 ================================================ $ErrorActionPreference = 'Stop' $storageAccount = Get-AzStorageAccount -ResourceGroupName $env:ResourceGroupName -AccountName $env:StorageAccountName Enable-AzStorageStaticWebsite -Context $storageAccount.Context -IndexDocument 'index.html' -ErrorDocument404Path 'index.html' ================================================ FILE: loadtests/contoso-traders-carts-internal.yaml ================================================ testName: contoso-traders-carts-internal testPlan: contoso-traders-carts.jmx description: engineInstances: 1 subnetId: {{LOAD_TEST_SUBNET_ID}} failureCriteria: - avg(response_time_ms) > 5000 - percentage(error) > 20 ================================================ FILE: loadtests/contoso-traders-carts.jmx ================================================ false true false threads_per_engine ${__BeanShell( System.getenv("threads_per_engine") )} = ramp_up_time ${__BeanShell( System.getenv("ramp_up_time") )} = duration_in_sec ${__BeanShell( System.getenv("duration_in_sec") )} = domain ${__BeanShell( System.getenv("domain") )} = protocol ${__BeanShell( System.getenv("protocol") )} = path ${__BeanShell( System.getenv("path") )} = continue false -1 ${threads_per_engine} ${ramp_up_time} true ${duration_in_sec} 5 true ${domain} ${protocol} ${path} GET true false true false ================================================ FILE: loadtests/contoso-traders-carts.yaml ================================================ testName: contoso-traders-carts testPlan: contoso-traders-carts.jmx description: engineInstances: 1 failureCriteria: - avg(response_time_ms) > 5000 - percentage(error) > 20 ================================================ FILE: loadtests/contoso-traders-products.jmx ================================================ false true false threads_per_engine ${__BeanShell( System.getenv("threads_per_engine") )} = ramp_up_time ${__BeanShell( System.getenv("ramp_up_time") )} = duration_in_sec ${__BeanShell( System.getenv("duration_in_sec") )} = domain ${__BeanShell( System.getenv("domain") )} = protocol ${__BeanShell( System.getenv("protocol") )} = path ${__BeanShell( System.getenv("path") )} = continue false -1 ${threads_per_engine} ${ramp_up_time} true ${duration_in_sec} 5 true ${domain} ${protocol} ${path} GET true false true false ================================================ FILE: loadtests/contoso-traders-products.yaml ================================================ testName: contoso-traders-products testPlan: contoso-traders-products.jmx description: engineInstances: 1 failureCriteria: - avg(response_time_ms) > 5000 - percentage(error) > 20 ================================================ FILE: src/.dockerignore ================================================ **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/azds.yaml **/bin **/charts **/docker-compose* **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md ================================================ FILE: src/ContosoTraders.Api.Carts/ContosoTraders.Api.Carts.csproj ================================================  net7.0 enable enable 5da7ea86-4b97-4e79-aec6-1fed11e373de Linux ================================================ FILE: src/ContosoTraders.Api.Carts/Controllers/ProfilesController.cs ================================================ namespace ContosoTraders.Api.Carts.Controllers; [Route("v1/[controller]")] public class ProfilesController : ContosoTradersControllerBase { public ProfilesController(IMediator mediator) : base(mediator) { } [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetProfiles() { var request = new GetProfilesRequest(); return await ProcessHttpRequestAsync(request); } [HttpGet("me")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetProfile([FromHeader(Name = RequestHeaderConstants.HeaderNameUserEmail)] string userEmail) { var request = new GetProfileRequest { Email = userEmail }; return await ProcessHttpRequestAsync(request); } } ================================================ FILE: src/ContosoTraders.Api.Carts/Controllers/ShoppingCartController.cs ================================================ namespace ContosoTraders.Api.Carts.Controllers; [Route("v1/[controller]")] public class ShoppingCartController : ContosoTradersControllerBase { public ShoppingCartController(IMediator mediator) : base(mediator) { } /// /// /// [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetCart([FromHeader(Name = RequestHeaderConstants.HeaderNameUserEmail)] string userEmail) { var request = new GetCartRequest { Email = userEmail?.ToLowerInvariant() }; return await ProcessHttpRequestAsync(request); } [HttpPost] [ProducesResponseType(StatusCodes.Status201Created)] public async Task AddItemToCart([FromBody] CartDto cartDto) { var request = new AddItemToCartRequest { CartItem = cartDto }; return await ProcessHttpRequestAsync(request); } [HttpPut("product")] [ProducesResponseType(StatusCodes.Status201Created)] // 201 to preserve compatibility with the original API. public async Task UpdateCartItemQuantity([FromBody] CartDto cartDto) { var request = new UpdateCartItemQuantityRequest { CartItem = cartDto }; return await ProcessHttpRequestAsync(request); } [HttpDelete("product")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task RemoveItemFromCart([FromBody] CartDto cartDto) { var request = new RemoveItemFromCartRequest { CartItem = cartDto }; return await ProcessHttpRequestAsync(request); } #region Load testing // @TODO: Remove this later and replace with JMeter/JMX tests [HttpGet("loadtest")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task LoadTest() { var request = new LoadTestRequest(); return await ProcessHttpRequestAsync(request); } #endregion } ================================================ FILE: src/ContosoTraders.Api.Carts/Dockerfile ================================================ FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build WORKDIR /src COPY ["ContosoTraders.Api.Carts/ContosoTraders.Api.Carts.csproj", "ContosoTraders.Api.Carts/"] COPY ["ContosoTraders.Api.Core/ContosoTraders.Api.Core.csproj", "ContosoTraders.Api.Core/"] RUN dotnet restore "ContosoTraders.Api.Carts/ContosoTraders.Api.Carts.csproj" COPY . . WORKDIR "/src/ContosoTraders.Api.Carts" RUN dotnet build "ContosoTraders.Api.Carts.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "ContosoTraders.Api.Carts.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "ContosoTraders.Api.Carts.dll"] ================================================ FILE: src/ContosoTraders.Api.Carts/Program.cs ================================================ DependencyInjection.ConfigureApp(); ================================================ FILE: src/ContosoTraders.Api.Carts/Properties/ServiceDependencies/tailwind-traders-cart - Zip Deploy/profile.arm.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", "contentVersion": "1.0.0.0", "metadata": { "_dependencyType": "compute.function.linux.appService" }, "parameters": { "resourceGroupName": { "type": "string", "defaultValue": "contoso-traders-sandbox-rg", "metadata": { "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." } }, "resourceGroupLocation": { "type": "string", "defaultValue": "eastus", "metadata": { "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." } }, "resourceName": { "type": "string", "defaultValue": "contoso-traders-cart", "metadata": { "description": "Name of the main resource to be created by this template." } }, "resourceLocation": { "type": "string", "defaultValue": "[parameters('resourceGroupLocation')]", "metadata": { "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." } } }, "resources": [ { "type": "Microsoft.Resources/resourceGroups", "name": "[parameters('resourceGroupName')]", "location": "[parameters('resourceGroupLocation')]", "apiVersion": "2019-10-01" }, { "type": "Microsoft.Resources/deployments", "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", "resourceGroup": "[parameters('resourceGroupName')]", "apiVersion": "2019-10-01", "dependsOn": [ "[parameters('resourceGroupName')]" ], "properties": { "mode": "Incremental", "expressionEvaluationOptions": { "scope": "inner" }, "parameters": { "resourceGroupName": { "value": "[parameters('resourceGroupName')]" }, "resourceGroupLocation": { "value": "[parameters('resourceGroupLocation')]" }, "resourceName": { "value": "[parameters('resourceName')]" }, "resourceLocation": { "value": "[parameters('resourceLocation')]" } }, "template": { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "resourceGroupName": { "type": "string" }, "resourceGroupLocation": { "type": "string" }, "resourceName": { "type": "string" }, "resourceLocation": { "type": "string" } }, "variables": { "storage_name": "[toLower(concat('storage', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId))))]", "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", "storage_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Storage/storageAccounts/', variables('storage_name'))]", "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]", "function_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/sites/', parameters('resourceName'))]" }, "resources": [ { "location": "[parameters('resourceLocation')]", "name": "[parameters('resourceName')]", "type": "Microsoft.Web/sites", "apiVersion": "2015-08-01", "tags": { "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" }, "dependsOn": [ "[variables('appServicePlan_ResourceId')]", "[variables('storage_ResourceId')]" ], "kind": "functionapp", "properties": { "name": "[parameters('resourceName')]", "kind": "functionapp", "httpsOnly": true, "reserved": false, "serverFarmId": "[variables('appServicePlan_ResourceId')]", "siteConfig": { "alwaysOn": true, "linuxFxVersion": "dotnet|3.1" } }, "identity": { "type": "SystemAssigned" }, "resources": [ { "name": "appsettings", "type": "config", "apiVersion": "2015-08-01", "dependsOn": [ "[variables('function_ResourceId')]" ], "properties": { "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storage_name'), ';AccountKey=', listKeys(variables('storage_ResourceId'), '2017-10-01').keys[0].value, ';EndpointSuffix=', 'core.windows.net')]", "FUNCTIONS_EXTENSION_VERSION": "~3", "FUNCTIONS_WORKER_RUNTIME": "dotnet" } } ] }, { "location": "[parameters('resourceGroupLocation')]", "name": "[variables('storage_name')]", "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2017-10-01", "tags": { "[concat('hidden-related:', concat('/providers/Microsoft.Web/sites/', parameters('resourceName')))]": "empty" }, "properties": { "supportsHttpsTrafficOnly": true }, "sku": { "name": "Standard_LRS" }, "kind": "Storage" }, { "location": "[parameters('resourceGroupLocation')]", "name": "[variables('appServicePlan_name')]", "type": "Microsoft.Web/serverFarms", "apiVersion": "2015-02-01", "kind": "linux", "properties": { "name": "[variables('appServicePlan_name')]", "sku": "Standard", "workerSizeId": "0", "reserved": true } } ] } } } ] } ================================================ FILE: src/ContosoTraders.Api.Carts/Properties/launchSettings.json ================================================ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "ContosoTraders.Api.Carts": { "commandName": "Project", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:62400;http://localhost:62401", "dotnetRunMessages": true }, "Docker": { "commandName": "Docker", "launchBrowser": true, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "publishAllPorts": true, "useSSL": true } } } ================================================ FILE: src/ContosoTraders.Api.Carts/Usings.cs ================================================ global using MediatR; global using Microsoft.AspNetCore.Mvc; global using ContosoTraders.Api.Core; global using ContosoTraders.Api.Core.Constants; global using ContosoTraders.Api.Core.Controllers; global using ContosoTraders.Api.Core.Models.Implementations.Dto; global using ContosoTraders.Api.Core.Requests.Definitions; ================================================ FILE: src/ContosoTraders.Api.Core/Constants/AuthConstants.cs ================================================ namespace ContosoTraders.Api.Core.Constants; public class AuthConstants { public static readonly string DefaultJwtSigningKey = "Ta!lwindTraderssssssss"; } ================================================ FILE: src/ContosoTraders.Api.Core/Constants/CosmosConstants.cs ================================================ namespace ContosoTraders.Api.Core.Constants; public class CosmosConstants { public const string DatabaseNameCarts = "cartsdb"; public const string DatabaseNameStocks = "stocksdb"; // keep the rest of the properties in alphabetical order public const string ContainerNameCarts = "carts"; public const string ContainerNameStocks = "stocks"; } ================================================ FILE: src/ContosoTraders.Api.Core/Constants/KeyVaultConstants.cs ================================================ namespace ContosoTraders.Api.Core.Constants; internal class KeyVaultConstants { #region secrets public static readonly string SecretNameJwtAudience = "jwtAudience"; public static readonly string SecretNameJwtAuthority = "jwtAuthority"; public static readonly string SecretNameAppInsightsConnectionString = "appInsightsConnectionString"; public static readonly string SecretNameImagesEndpoint = "imagesEndpoint"; public static readonly string SecretNameCartsDbConnectionString = "cartsDbConnectionString"; public static readonly string SecretNameProductsDbConnectionString = "productsDbConnectionString"; public static readonly string SecretNameProfilesDbConnectionString = "profilesDbConnectionString"; public static readonly string SecretNameStocksDbConnectionString = "stocksDbConnectionString"; public static readonly string SecretNameCognitiveServicesEndpoint = "cognitiveServicesEndpoint"; public static readonly string SecretNameCognitiveServicesAccountKey = "cognitiveServicesAccountKey"; #endregion } ================================================ FILE: src/ContosoTraders.Api.Core/Constants/RequestHeaderConstants.cs ================================================ namespace ContosoTraders.Api.Core.Constants; public class RequestHeaderConstants { public const string HeaderNameUserEmail = "x-tt-email"; } ================================================ FILE: src/ContosoTraders.Api.Core/ContosoTraders.Api.Core.csproj ================================================  net7.0 enable disable all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: src/ContosoTraders.Api.Core/Controllers/ContosoTradersControllerBase.cs ================================================ namespace ContosoTraders.Api.Core.Controllers; [ApiController] public class ContosoTradersControllerBase : ControllerBase { private readonly IMediator _mediator; protected ContosoTradersControllerBase(IMediator mediator) { _mediator = mediator; } protected async Task ProcessHttpRequestAsync(IRequest request) { try { return await _mediator.Send(request); } catch (ContosoTradersBaseException contosoTradersBaseException) { return contosoTradersBaseException.ToActionResult(); } catch (ValidationException validationException) { return new BadRequestObjectResult(validationException.Message); } } } ================================================ FILE: src/ContosoTraders.Api.Core/DependencyInjection.cs ================================================ using System.Reflection; using Azure.Identity; using Microsoft.AspNetCore.Builder; using Microsoft.Azure.Cosmos; using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Logging; [assembly: FunctionsStartup(typeof(DependencyInjection))] namespace ContosoTraders.Api.Core; public class DependencyInjection : FunctionsStartup { private const string _allowSpecificOrigins = "allowSpecificOrigins"; public static void ConfigureServices(HostBuilderContext context, IServiceCollection services) { ConfigureServicesInternal(services, context.Configuration); } /// /// This is implicitly called by the Azure Functions runtime. /// public override void Configure(IFunctionsHostBuilder builder) { ConfigureServicesInternal(builder.Services, builder.GetContext().Configuration); } /// /// To be explicitly called from an asp.net core application only /// public static void ConfigureApp() { var builder = WebApplication.CreateBuilder(); if (builder.Environment.IsDevelopment()) builder.Configuration.AddAzureKeyVault( new Uri(builder.Configuration["KeyVaultEndpoint"]), new DefaultAzureCredential()); else builder.Configuration.AddAzureKeyVault( new Uri(builder.Configuration["KeyVaultEndpoint"]), new DefaultAzureCredential( new DefaultAzureCredentialOptions { ManagedIdentityClientId = builder.Configuration["ManagedIdentityClientId"] })); ConfigureServicesInternal(builder.Services, builder.Configuration); ConfigureAspNetCoreServices(builder.Services, builder.Configuration); var app = builder.Build(); ConfigureAspNetCoreMiddleware(app); app.Run(); } /// /// @TODO: Currently, this method is very 'aspnetcore' specific. Make it more generic (for azure functions) later. /// private static void ConfigureServicesInternal(IServiceCollection services, IConfiguration configuration) { // injecting auto-mapper services.AddAutoMapper(typeof(AutoMapperProfile)); // inject mediatr services.AddMediatR(Assembly.GetExecutingAssembly()); // inject ef-core dbcontexts (after fetching connection string from azure keyvault). var productsDbConnectionString = configuration[KeyVaultConstants.SecretNameProductsDbConnectionString]; services.AddDbContext(options => options.UseSqlServer(productsDbConnectionString)); var profilesDbConnectionString = configuration[KeyVaultConstants.SecretNameProfilesDbConnectionString]; services.AddDbContext(options => options.UseSqlServer(profilesDbConnectionString)); // injecting the cosmosdb clients var stocksDbConnectionString = configuration[KeyVaultConstants.SecretNameStocksDbConnectionString]; services.AddSingleton(_ => new CosmosClient(stocksDbConnectionString).GetDatabase(CosmosConstants.DatabaseNameStocks)); var cartsDbConnectionString = configuration[KeyVaultConstants.SecretNameCartsDbConnectionString]; services.AddSingleton(_ => new CosmosClient(cartsDbConnectionString).GetDatabase(CosmosConstants.DatabaseNameCarts)); // inject services services .AddScoped() .AddScoped() .AddScoped() .AddScoped() .AddScoped() .AddScoped(); // inject repositories services .AddScoped() .AddScoped(); } private static void ConfigureAspNetCoreServices(IServiceCollection services, IConfiguration configuration) { services.AddControllers(); services.AddEndpointsApiExplorer(); services.AddSwaggerGen(); var appInsightsConnectionString = configuration[KeyVaultConstants.SecretNameAppInsightsConnectionString]; services.AddApplicationInsightsTelemetry(options => options.ConnectionString = appInsightsConnectionString); // @TODO: Temporary. Fix later. services.AddCors(options => options.AddPolicy(_allowSpecificOrigins, policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod())); IdentityModelEventSource.ShowPII = true; } /// /// Add middleware to the asp.net core request pipeline. /// Note: COR pattern applies, ordering matters. /// private static void ConfigureAspNetCoreMiddleware(WebApplication app) { app.UseSwagger(); app.UseSwaggerUI(); app.UseHttpsRedirection(); app.UseCors(_allowSpecificOrigins); #if TODO_INVESTIGATE_LATER app.UseAuthorization(); // very important, else [Authorize] will not work in controllers. app.UseAuthentication(); #endif app.MapControllers(); } } ================================================ FILE: src/ContosoTraders.Api.Core/Exceptions/CartNotFoundException.cs ================================================ namespace ContosoTraders.Api.Core.Exceptions; public class CartNotFoundException : ContosoTradersBaseException { public CartNotFoundException(string email) : base($"Shopping Cart for '{email}' could not be found.") { } public override IActionResult ToActionResult() { return new NotFoundObjectResult(Message); } } ================================================ FILE: src/ContosoTraders.Api.Core/Exceptions/ContosoTradersBaseException.cs ================================================ namespace ContosoTraders.Api.Core.Exceptions; public abstract class ContosoTradersBaseException : Exception { protected ContosoTradersBaseException(string message) : base(message) { } public abstract IActionResult ToActionResult(); } ================================================ FILE: src/ContosoTraders.Api.Core/Exceptions/MatchingProductsNotFoundException.cs ================================================ namespace ContosoTraders.Api.Core.Exceptions; public class MatchingProductsNotFoundException : ContosoTradersBaseException { public MatchingProductsNotFoundException(string tags) : base($"No matching products found for tags : {tags}") { } public override IActionResult ToActionResult() { return new NotFoundObjectResult(Message); } } ================================================ FILE: src/ContosoTraders.Api.Core/Exceptions/ProductNotFoundException.cs ================================================ namespace ContosoTraders.Api.Core.Exceptions; public class ProductNotFoundException : ContosoTradersBaseException { public ProductNotFoundException(int productId) : base($"Product with id '{productId}' could not be found.") { } public override IActionResult ToActionResult() { return new NotFoundObjectResult(Message); } } ================================================ FILE: src/ContosoTraders.Api.Core/Exceptions/ProfileNotFoundException.cs ================================================ namespace ContosoTraders.Api.Core.Exceptions; public class ProfileNotFoundException : ContosoTradersBaseException { public ProfileNotFoundException(string email) : base($"Profile for email '{email}' could not be found.") { } public override IActionResult ToActionResult() { return new NotFoundObjectResult(Message); } } ================================================ FILE: src/ContosoTraders.Api.Core/Exceptions/StockNotFoundException.cs ================================================ namespace ContosoTraders.Api.Core.Exceptions; public class StockNotFoundException : ContosoTradersBaseException { public StockNotFoundException(int productId) : base($"Stock not found for product id '{productId}'") { } public override IActionResult ToActionResult() { return new NotFoundObjectResult(Message); } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/AutoMapperProfile.cs ================================================ using Profile = AutoMapper.Profile; using ProfileDao = ContosoTraders.Api.Core.Models.Implementations.Dao.Profile; namespace ContosoTraders.Api.Core.Models; public class AutoMapperProfile : Profile { public AutoMapperProfile() { #region DAO (storage model) to DTO (API/REST model) conversion CreateMap() .ForMember(dest => dest.ProductId, opt => opt.MapFrom(src => Convert.ToInt32(src.id))); CreateMap() .ForMember(dest => dest.CartItemId, opt => opt.MapFrom(src => src.id)); // @TODO: Fix this later //CreateMap<(Product, IEnumerable, IEnumerable), ProductDto>() // .ForPath(dest => dest, opt => opt.MapFrom(src => MappingHelper.CustomJoin(src.Item1, src.Item2, src.Item3))); #endregion #region DTO (API/REST model) to DAO (storage model) conversion CreateMap() .ForMember(dest => dest.id, opt => opt.MapFrom(src => src.ProductId.ToString())); CreateMap() .ForMember(dest => dest.id, opt => opt.MapFrom(src => src.CartItemId)); #endregion } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dao/Brand.cs ================================================ namespace ContosoTraders.Api.Core.Models.Implementations.Dao; public class Brand { public int Id { get; set; } public string Name { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dao/CartDao.cs ================================================ namespace ContosoTraders.Api.Core.Models.Implementations.Dao; public class CartDao : ICosmosDao { public string Email { get; set; } // partition key public int ProductId { get; set; } public string Name { get; set; } public int Price { get; set; } public string ImageUrl { get; set; } public int Quantity { get; set; } public string id { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dao/Feature.cs ================================================ namespace ContosoTraders.Api.Core.Models.Implementations.Dao; public class Feature { public int Id { get; set; } public string Title { get; set; } public string Description { get; set; } public int? ProductItemId { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dao/Product.cs ================================================ namespace ContosoTraders.Api.Core.Models.Implementations.Dao; public class Product { public int Id { get; set; } public string Name { get; set; } public decimal? Price { get; set; } public string ImageName { get; set; } public int? BrandId { get; set; } public int? TypeId { get; set; } public int? TagId { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dao/Profile.cs ================================================ namespace ContosoTraders.Api.Core.Models.Implementations.Dao; public class Profile { public int Id { get; set; } public string Name { get; set; } public string Address { get; set; } public string PhoneNumber { get; set; } public string Email { get; set; } public string ImageNameSmall { get; set; } public string ImageNameLarge { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dao/StockDao.cs ================================================ namespace ContosoTraders.Api.Core.Models.Implementations.Dao; public class StockDao : ICosmosDao { public int StockCount { get; set; } public string id { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dao/Tag.cs ================================================ namespace ContosoTraders.Api.Core.Models.Implementations.Dao; public class Tag { public int Id { get; set; } public string Value { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dao/Type.cs ================================================ namespace ContosoTraders.Api.Core.Models.Implementations.Dao; public class Type { public int Id { get; set; } public string Code { get; set; } public string Name { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dto/AccessToken.cs ================================================ using Newtonsoft.Json; namespace ContosoTraders.Api.Core.Models.Implementations.Dto; public class AccessToken { [JsonProperty(PropertyName = "token")] public string Token { get; set; } [JsonProperty(PropertyName = "token_type")] public string TokenType { get; set; } [JsonProperty(PropertyName = "expires_in")] public int ExpiresIn { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dto/CartDto.cs ================================================ namespace ContosoTraders.Api.Core.Models.Implementations.Dto; public class CartDto { public string CartItemId { get; set; } public string Email { get; set; } public int ProductId { get; set; } public string Name { get; set; } public int Price { get; set; } public string ImageUrl { get; set; } public int Quantity { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dto/ProductDto.cs ================================================ using Type = ContosoTraders.Api.Core.Models.Implementations.Dao.Type; namespace ContosoTraders.Api.Core.Models.Implementations.Dto; public class ProductDto { public int Id { get; set; } public string Name { get; set; } public decimal? Price { get; set; } public string ImageUrl { get; set; } public Brand Brand { get; set; } public Type Type { get; set; } public IEnumerable Features { get; set; } public int StockUnits { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dto/StockDto.cs ================================================ namespace ContosoTraders.Api.Core.Models.Implementations.Dto; public class StockDto { public int ProductId { get; set; } public int StockCount { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Implementations/Dto/TokenRequest.cs ================================================ using Newtonsoft.Json; namespace ContosoTraders.Api.Core.Models.Implementations.Dto; public class TokenRequest { public string Username { get; set; } public string Password { get; set; } [JsonProperty(PropertyName = "grant_type")] public string GrantType { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Models/Interfaces/ICosmosDao.cs ================================================ namespace ContosoTraders.Api.Core.Models.Interfaces; public interface ICosmosDao { // ReSharper disable once InconsistentNaming #pragma warning disable IDE1006 // Naming Styles public T id { get; set; } #pragma warning restore IDE1006 // Naming Styles } ================================================ FILE: src/ContosoTraders.Api.Core/Repositories/Implementations/CartRepository.cs ================================================ using Microsoft.Azure.Cosmos; namespace ContosoTraders.Api.Core.Repositories.Implementations; public class CartRepository : CosmosGenericRepositoryBase, ICartRepository { public CartRepository(IEnumerable cosmosDatabases) : base(cosmosDatabases.Single(db => db.Id == CosmosConstants.DatabaseNameCarts), CosmosConstants.ContainerNameCarts) { } } ================================================ FILE: src/ContosoTraders.Api.Core/Repositories/Implementations/CosmosGenericRepositoryBase.cs ================================================ using System.Net; using Microsoft.Azure.Cosmos; namespace ContosoTraders.Api.Core.Repositories.Implementations; public abstract class CosmosGenericRepositoryBase : ICosmosGenericRepository where TEntity : class { protected readonly string ContainerName; protected readonly Database CosmosDatabase; protected CosmosGenericRepositoryBase(Database cosmosDatabase, string containerName) { ContainerName = containerName; CosmosDatabase = cosmosDatabase; } public async Task> QueryAsync(string querySpec, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); return await ExecuteQueryAsync(querySpec, cancellationToken); } public async Task> ListAsync(string filterClause = default, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var querySpec = "select * from c"; if (!string.IsNullOrWhiteSpace(filterClause)) querySpec = $"{querySpec} where {filterClause}"; return await ExecuteQueryAsync(querySpec, cancellationToken); } public async Task GetAsync(string partitionKey, string id, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); try { var response = await CosmosDatabase .GetContainer(ContainerName) .ReadItemAsync(id, new PartitionKey(partitionKey), cancellationToken: cancellationToken); return response.Resource; } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return default; // i.e. null } } public async Task AddAsync(string partitionKey, TEntity entity, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); await CosmosDatabase .GetContainer(ContainerName) .CreateItemAsync(entity, new PartitionKey(partitionKey), cancellationToken: cancellationToken); } public async Task UpsertAsync(string partitionKey, TEntity entity, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); await CosmosDatabase .GetContainer(ContainerName) .UpsertItemAsync(entity, new PartitionKey(partitionKey), cancellationToken: cancellationToken); } public async Task DeleteAsync(string partitionKey, string id, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); await CosmosDatabase .GetContainer(ContainerName) .DeleteItemAsync(id, new PartitionKey(partitionKey), cancellationToken: cancellationToken); } private async Task> ExecuteQueryAsync(string querySpec, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); using var queryIterator = CosmosDatabase .GetContainer(ContainerName) .GetItemQueryIterator(new QueryDefinition(querySpec)); var results = new List(); while (queryIterator.HasMoreResults) { cancellationToken.ThrowIfCancellationRequested(); var response = await queryIterator.ReadNextAsync(cancellationToken); results.AddRange(response.ToList()); } return results; } } ================================================ FILE: src/ContosoTraders.Api.Core/Repositories/Implementations/StockRepository.cs ================================================ using Microsoft.Azure.Cosmos; namespace ContosoTraders.Api.Core.Repositories.Implementations; public class StockRepository : CosmosGenericRepositoryBase, IStockRepository { public StockRepository(IEnumerable cosmosDatabases) : base(cosmosDatabases.Single(db => db.Id == CosmosConstants.DatabaseNameStocks), CosmosConstants.ContainerNameStocks) { } } ================================================ FILE: src/ContosoTraders.Api.Core/Repositories/Interfaces/ICartRepository.cs ================================================ namespace ContosoTraders.Api.Core.Repositories.Interfaces; public interface ICartRepository : ICosmosGenericRepository { } ================================================ FILE: src/ContosoTraders.Api.Core/Repositories/Interfaces/ICosmosGenericRepository.cs ================================================ namespace ContosoTraders.Api.Core.Repositories.Interfaces; /// /// The type parameter constraint of 'class' is only required because of this open github issue: /// - https://github.com/dotnet/runtime/issues/41749 /// Else we could have just used interface types for the type parameter. /// public interface ICosmosGenericRepository where TEntity : class { Task> QueryAsync(string querySpec, CancellationToken cancellationToken = default); Task> ListAsync(string filterClause = default, CancellationToken cancellationToken = default); Task GetAsync(string partitionKey, string id, CancellationToken cancellationToken = default); Task AddAsync(string partitionKey, TEntity entity, CancellationToken cancellationToken = default); Task UpsertAsync(string partitionKey, TEntity entity, CancellationToken cancellationToken = default); Task DeleteAsync(string partitionKey, string id, CancellationToken cancellationToken = default); } ================================================ FILE: src/ContosoTraders.Api.Core/Repositories/Interfaces/IStockRepository.cs ================================================ namespace ContosoTraders.Api.Core.Repositories.Interfaces; public interface IStockRepository : ICosmosGenericRepository { } ================================================ FILE: src/ContosoTraders.Api.Core/Repositories/ProductsDbContext.cs ================================================ using Microsoft.EntityFrameworkCore; using Type = ContosoTraders.Api.Core.Models.Implementations.Dao.Type; namespace ContosoTraders.Api.Core.Repositories; public class ProductsDbContext : DbContext { public ProductsDbContext() { } public ProductsDbContext(DbContextOptions options) : base(options) { } public virtual DbSet Brands { get; set; } public virtual DbSet Features { get; set; } public virtual DbSet Products { get; set; } public virtual DbSet Tags { get; set; } public virtual DbSet Types { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.Property(e => e.Id).ValueGeneratedNever(); entity.Property(e => e.Name).HasMaxLength(255); }); modelBuilder.Entity(entity => { entity.Property(e => e.Id).ValueGeneratedNever(); entity.Property(e => e.Title).HasMaxLength(255); }); modelBuilder.Entity(entity => { entity.Property(e => e.Id).ValueGeneratedNever(); entity.Property(e => e.ImageName).HasMaxLength(255); entity.Property(e => e.Name).HasMaxLength(255); entity.Property(e => e.Price).HasColumnType("decimal(9, 2)"); }); modelBuilder.Entity(entity => { entity.Property(e => e.Id).ValueGeneratedNever(); entity.Property(e => e.Value).HasMaxLength(255); }); modelBuilder.Entity(entity => { entity.Property(e => e.Id).ValueGeneratedNever(); entity.Property(e => e.Code).HasMaxLength(255); entity.Property(e => e.Name).HasMaxLength(255); }); } } ================================================ FILE: src/ContosoTraders.Api.Core/Repositories/ProfilesDbContext.cs ================================================ using Microsoft.EntityFrameworkCore; using Profile = ContosoTraders.Api.Core.Models.Implementations.Dao.Profile; namespace ContosoTraders.Api.Core.Repositories; public class ProfilesDbContext : DbContext { public ProfilesDbContext() { } public ProfilesDbContext(DbContextOptions options) : base(options) { } public virtual DbSet Profiles { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.HasKey(e => e.Email); entity.Property(e => e.Email).HasMaxLength(50); entity.Property(e => e.Address).HasMaxLength(256); entity.Property(e => e.ImageNameLarge) .HasMaxLength(256) .IsFixedLength(); entity.Property(e => e.ImageNameSmall).HasMaxLength(256); entity.Property(e => e.Name).HasMaxLength(50); entity.Property(e => e.PhoneNumber).HasMaxLength(100); }); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/AddItemToCartRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class AddItemToCartRequest : IRequest { public CartDto CartItem { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/DecrementStockCountRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class DecrementStockCountRequest : IRequest { public int ProductId { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/GetCartRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class GetCartRequest : IRequest { public string Email { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/GetPopularProductsRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class GetPopularProductsRequest : IRequest { } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/GetProductRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class GetProductRequest : IRequest { public int ProductId { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/GetProductsRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class GetProductsRequest : IRequest { public int[] Brands { get; set; } public string[] Types { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/GetProfileRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class GetProfileRequest : IRequest { public string Email { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/GetProfilesRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class GetProfilesRequest : IRequest { } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/GetStockRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class GetStockRequest : IRequest { public int ProductId { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/LoadTestRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class LoadTestRequest : IRequest { } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/PostImageRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class PostImageRequest : IRequest { public IFormFile File { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/RemoveItemFromCartRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class RemoveItemFromCartRequest : IRequest { public CartDto CartItem { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/SearchTextRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class SearchTextRequest : IRequest { public string Text { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Definitions/UpdateCartItemQuantityRequest.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Definitions; public class UpdateCartItemQuantityRequest : IRequest { public CartDto CartItem { get; set; } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/AddItemToCartRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class AddItemToCartRequestHandler : IRequestPreProcessor, IRequestHandler { private readonly ICartService _cartService; public AddItemToCartRequestHandler(ICartService cartService) { _cartService = cartService; } public async Task Handle(AddItemToCartRequest request, CancellationToken cancellationToken) { await _cartService.AddItemToCartAsync(request.CartItem, cancellationToken); var responseMessage = $"{request.CartItem.Name} added to shopping cart, id: {request.CartItem.ProductId}"; return new ObjectResult(responseMessage) { StatusCode = 201 }; } public async Task Process(AddItemToCartRequest request, CancellationToken cancellationToken) { var validator = new AddItemToCartRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/DecrementStockCountRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class DecrementStockCountRequestHandler : IRequestPreProcessor, IRequestHandler { private readonly IStockService _stockService; public DecrementStockCountRequestHandler(IStockService stockService) { _stockService = stockService; } public async Task Handle(DecrementStockCountRequest request, CancellationToken cancellationToken) { var stockCountDto = await _stockService.DecrementStockCountAsync(request.ProductId, cancellationToken); return new OkObjectResult(stockCountDto); } public async Task Process(DecrementStockCountRequest request, CancellationToken cancellationToken) { var validator = new DecrementStockCountRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/GetCartRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class GetCartRequestHandler : IRequestPreProcessor, IRequestHandler { private readonly ICartService _cartService; public GetCartRequestHandler(ICartService cartService) { _cartService = cartService; } public async Task Handle(GetCartRequest request, CancellationToken cancellationToken) { var cartItemsDto = await _cartService.GetCartAsync(request.Email, cancellationToken); return new OkObjectResult(cartItemsDto); } public async Task Process(GetCartRequest request, CancellationToken cancellationToken) { var validator = new GetCartRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/GetPopularProductsRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class GetPopularProductsRequestHandler : IRequestPreProcessor, IRequestHandler { /// /// @TODO: To be implemented later. /// public async Task Handle(GetPopularProductsRequest request, CancellationToken cancellationToken) { var result = new OkResult(); return await Task.FromResult(result); } public async Task Process(GetPopularProductsRequest request, CancellationToken cancellationToken) { var validator = new GetPopularProductsRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/GetProductRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class GetProductRequestHandler : IRequestPreProcessor, IRequestHandler { private readonly IProductService _productService; private readonly IStockService _stockService; public GetProductRequestHandler(IProductService productService, IStockService stockService) { _productService = productService; _stockService = stockService; } public async Task Handle(GetProductRequest request, CancellationToken cancellationToken) { var productDto = _productService.GetProduct(request.ProductId); try { var stockDto = await _stockService.GetStockAsync(request.ProductId, cancellationToken); productDto.StockUnits = stockDto.StockCount; } catch (StockNotFoundException) { productDto.StockUnits = 0; } return new OkObjectResult(productDto); } public async Task Process(GetProductRequest request, CancellationToken cancellationToken) { var validator = new GetProductRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/GetProductsRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class GetProductsRequestHandler : RequestHandler, IRequestPreProcessor { private readonly IProductService _productService; public GetProductsRequestHandler(IProductService productService) { _productService = productService; } public async Task Process(GetProductsRequest request, CancellationToken cancellationToken) { var validator = new GetProductsRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } protected override IActionResult Handle(GetProductsRequest request) { var brands = _productService.GetBrands(); var types = _productService.GetTypes(); var typeIds = types .Where(t => request.Types.Contains(t.Code)) .Select(t => t.Id) .ToArray(); var productDtos = _productService.GetProducts(request.Brands, typeIds); if (!productDtos.Any()) return new NoContentResult(); var aggregateResponse = new { Products = productDtos, Brands = brands, Types = types }; return new OkObjectResult(aggregateResponse); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/GetProfileRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class GetProfileRequestHandler : RequestHandler, IRequestPreProcessor { private readonly IProfileService _profileService; public GetProfileRequestHandler(IProfileService profileService) { _profileService = profileService; } public async Task Process(GetProfileRequest request, CancellationToken cancellationToken) { var validator = new GetProfileRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } protected override IActionResult Handle(GetProfileRequest request) { var profileDto = _profileService.GetProfile(request.Email); return new OkObjectResult(profileDto); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/GetProfilesRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class GetProfilesRequestHandler : RequestHandler, IRequestPreProcessor { private readonly IProfileService _profileService; public GetProfilesRequestHandler(IProfileService profileService) { _profileService = profileService; } public async Task Process(GetProfilesRequest request, CancellationToken cancellationToken) { var validator = new GetProfilesRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } protected override IActionResult Handle(GetProfilesRequest request) { var profileDtos = _profileService.GetAllProfiles(); return new OkObjectResult(profileDtos); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/GetStockRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class GetStockRequestHandler : IRequestPreProcessor, IRequestHandler { private readonly IStockService _stockService; public GetStockRequestHandler(IStockService stockService) { _stockService = stockService; } public async Task Handle(GetStockRequest request, CancellationToken cancellationToken) { var stockDto = await _stockService.GetStockAsync(request.ProductId, cancellationToken); return new OkObjectResult(stockDto); } public async Task Process(GetStockRequest request, CancellationToken cancellationToken) { var validator = new GetStockRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/LoadTestRequestHandler.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Handlers; internal class LoadTestRequestHandler : IRequestHandler { private readonly ICartService _cartService; public LoadTestRequestHandler(ICartService cartService) { _cartService = cartService; } public async Task Handle(LoadTestRequest request, CancellationToken cancellationToken) { const string email = "testuser@contosotraders.com"; IEnumerable cartDtos; try { cartDtos = await _cartService.GetCartAsync(email, cancellationToken); } catch (CartNotFoundException) { var newCartDto = new CartDto { Email = email, Quantity = 1, ProductId = 17, Name = "Dell Optiplex 380 17 inch (43.18 cms) Desktop", Price = 1399, ImageUrl = "https://contoso-traders-imagesctprod.azureedge.net/product-details/PID17-1.jpg", CartItemId = Guid.NewGuid().ToString() }; await _cartService.UpdateCartItemQuantityAsync(newCartDto, cancellationToken); // upsert cartDtos = await _cartService.GetCartAsync(email, cancellationToken); } return new OkObjectResult(cartDtos); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/PostImageRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class PostImageRequestHandler : IRequestPreProcessor, IRequestHandler { private readonly IImageSearchService _imageSearchService; public PostImageRequestHandler(IImageSearchService imageSearchService) { _imageSearchService = imageSearchService; } public async Task Handle(PostImageRequest request, CancellationToken cancellationToken = default) { if (!request.File.ContentType.Contains("image")) return new BadRequestObjectResult("Invalid file type."); var products = await _imageSearchService.GetSimilarProductsAsync(request.File.OpenReadStream(), cancellationToken); return new OkObjectResult(products); } public async Task Process(PostImageRequest request, CancellationToken cancellationToken) { var validator = new PostImageRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/RemoveItemFromCartRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class RemoveItemFromCartRequestHandler : IRequestPreProcessor, IRequestHandler { private readonly ICartService _cartService; public RemoveItemFromCartRequestHandler(ICartService cartService) { _cartService = cartService; } public async Task Handle(RemoveItemFromCartRequest request, CancellationToken cancellationToken) { await _cartService.RemoveItemFromCartAsync(request.CartItem, cancellationToken); return new OkObjectResult($"Product removed from cart, id: {request.CartItem.ProductId}"); } public async Task Process(RemoveItemFromCartRequest request, CancellationToken cancellationToken) { var validator = new RemoveItemFromCartRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/SearchTextRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class SearchTextRequestHandler : IRequestPreProcessor, IRequestHandler { private readonly IProductService _productService; public SearchTextRequestHandler(IProductService productService) { _productService = productService; } public async Task Handle(SearchTextRequest request, CancellationToken cancellationToken = default) { var products = await Task.FromResult(_productService.GetProducts(request.Text)); return new OkObjectResult(products); } public async Task Process(SearchTextRequest request, CancellationToken cancellationToken) { var validator = new SearchTextRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Handlers/UpdateCartItemQuantityRequestHandler.cs ================================================ using MediatR.Pipeline; namespace ContosoTraders.Api.Core.Requests.Handlers; internal class UpdateCartItemQuantityRequestHandler : IRequestPreProcessor, IRequestHandler { private readonly ICartService _cartService; public UpdateCartItemQuantityRequestHandler(ICartService cartService) { _cartService = cartService; } public async Task Handle(UpdateCartItemQuantityRequest request, CancellationToken cancellationToken) { await _cartService.UpdateCartItemQuantityAsync(request.CartItem, cancellationToken); var responseMessage = $"Product quantity updated, id: {request.CartItem.ProductId}"; return new ObjectResult(responseMessage) { StatusCode = 201 }; } public async Task Process(UpdateCartItemQuantityRequest request, CancellationToken cancellationToken) { var validator = new UpdateCartItemQuantityRequestValidator(); await validator.ValidateAndThrowAsync(request, cancellationToken); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/AddItemToCartRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class AddItemToCartRequestValidator : AbstractValidator { public AddItemToCartRequestValidator() { RuleFor(request => request) .NotNull(); RuleFor(request => request.CartItem) .NotNull(); RuleFor(request => request.CartItem.CartItemId) .NotEmpty() .WithMessage("CartItemId cannot be empty/null"); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/DecrementStockCountRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class DecrementStockCountRequestValidator : AbstractValidator { public DecrementStockCountRequestValidator() { RuleFor(request => request) .NotNull(); RuleFor(request => request.ProductId) .GreaterThan(0); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/GetCartRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class GetCartRequestValidator : AbstractValidator { public GetCartRequestValidator() { RuleFor(request => request) .NotNull(); RuleFor(request => request.Email) .NotEmpty() .WithMessage("Email cannot be null/empty."); RuleFor(request => request.Email) .EmailAddress() .WithMessage("Incorrect format for Email."); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/GetPopularProductsRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class GetPopularProductsRequestValidator : AbstractValidator { } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/GetProductRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class GetProductRequestValidator : AbstractValidator { public GetProductRequestValidator() { RuleFor(request => request) .NotNull(); RuleFor(request => request.ProductId) .GreaterThan(0); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/GetProductsRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class GetProductsRequestValidator : AbstractValidator { } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/GetProfileRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class GetProfileRequestValidator : AbstractValidator { } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/GetProfilesRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class GetProfilesRequestValidator : AbstractValidator { } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/GetStockRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class GetStockRequestValidator : AbstractValidator { public GetStockRequestValidator() { RuleFor(request => request) .NotNull(); RuleFor(request => request.ProductId) .GreaterThan(0); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/PostImageRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class PostImageRequestValidator : AbstractValidator { public PostImageRequestValidator() { RuleFor(request => request) .NotNull(); RuleFor(request => request.File) .NotNull(); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/RemoveItemFromCartRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class RemoveItemFromCartRequestValidator : AbstractValidator { public RemoveItemFromCartRequestValidator() { RuleFor(request => request) .NotNull(); RuleFor(request => request.CartItem) .NotNull(); RuleFor(request => request.CartItem.CartItemId) .NotEmpty() .WithMessage("CartItemId cannot be null/empty."); RuleFor(request => request.CartItem.Email) .NotEmpty() .WithMessage("Email cannot be null/empty."); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/SearchTextRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class SearchTextRequestValidator : AbstractValidator { public SearchTextRequestValidator() { RuleFor(request => request) .NotNull(); RuleFor(request => request.Text) .NotNull(); } } ================================================ FILE: src/ContosoTraders.Api.Core/Requests/Validators/UpdateCartItemQuantityRequestValidator.cs ================================================ namespace ContosoTraders.Api.Core.Requests.Validators; public class UpdateCartItemQuantityRequestValidator : AbstractValidator { public UpdateCartItemQuantityRequestValidator() { RuleFor(request => request) .NotNull(); RuleFor(request => request.CartItem) .NotNull(); RuleFor(request => request.CartItem.CartItemId) .NotNull() .WithMessage("CartItemId cannot be null/empty."); RuleFor(request => request.CartItem.Quantity) .NotNull() .WithMessage("Quantity cannot be null/empty."); } } ================================================ FILE: src/ContosoTraders.Api.Core/Services/ContosoTradersServiceBase.cs ================================================ namespace ContosoTraders.Api.Core.Services; internal abstract class ContosoTradersServiceBase { protected readonly IConfiguration Configuration; protected readonly ILogger Logger; protected readonly IMapper Mapper; protected ContosoTradersServiceBase(IMapper mapper, IConfiguration configuration, ILogger logger) { Mapper = mapper; Configuration = configuration; Logger = logger; } } ================================================ FILE: src/ContosoTraders.Api.Core/Services/Implementations/CartService.cs ================================================ namespace ContosoTraders.Api.Core.Services.Implementations; internal class CartService : ContosoTradersServiceBase, ICartService { private readonly ICartRepository _cartRepository; public CartService(ICartRepository cartRepository, IMapper mapper, IConfiguration configuration, ILogger logger) : base(mapper, configuration, logger) { _cartRepository = cartRepository; } public async Task> GetCartAsync(string email, CancellationToken cancellationToken = default) { var filterClause = $"LOWER(c.Email) = '{email.ToLower()}'"; var cartItemDaos = await _cartRepository.ListAsync(filterClause, cancellationToken); if (!cartItemDaos.Any()) throw new CartNotFoundException(email); var cartItemDtos = cartItemDaos.Select(item => Mapper.Map(item)).ToList(); return cartItemDtos; } public async Task AddItemToCartAsync(CartDto cartItemDto, CancellationToken cancellationToken = default) { var cartItemDao = Mapper.Map(cartItemDto); cartItemDao.id = Guid.NewGuid().ToString(); await _cartRepository.AddAsync(cartItemDao.Email, cartItemDao, cancellationToken); } public async Task UpdateCartItemQuantityAsync(CartDto cartItemDto, CancellationToken cancellationToken = default) { var cartItemDao = Mapper.Map(cartItemDto); await _cartRepository.UpsertAsync(cartItemDao.Email, cartItemDao, cancellationToken); } public async Task RemoveItemFromCartAsync(CartDto cartItemDto, CancellationToken cancellationToken = default) { var cartItemDao = Mapper.Map(cartItemDto); await _cartRepository.DeleteAsync(cartItemDao.Email, cartItemDao.id, cancellationToken); } } ================================================ FILE: src/ContosoTraders.Api.Core/Services/Implementations/ImageAnalysisService.cs ================================================ namespace ContosoTraders.Api.Core.Services.Implementations; internal class ImageAnalysisService : ContosoTradersServiceBase, IImageAnalysisService { public ImageAnalysisService(IMapper mapper, IConfiguration configuration, ILogger logger) : base(mapper, configuration, logger) { } public async Task> AnalyzeImageAsync(Stream imageStream, CancellationToken cancellationToken = default) { var client = GetComputerVisionClient(); var features = new List { VisualFeatureTypes.Tags }; var results = await client.AnalyzeImageInStreamAsync(imageStream, features, cancellationToken: cancellationToken); var searchTerms = results.Tags .Select(tag => tag.Name) .ToList(); return searchTerms; } private ComputerVisionClient GetComputerVisionClient() { var accountKey = Configuration[KeyVaultConstants.SecretNameCognitiveServicesAccountKey]; var endpoint = Configuration[KeyVaultConstants.SecretNameCognitiveServicesEndpoint]; var credentials = new ApiKeyServiceClientCredentials(accountKey); var client = new ComputerVisionClient(credentials) { Endpoint = endpoint }; return client; } } ================================================ FILE: src/ContosoTraders.Api.Core/Services/Implementations/ImageSearchService.cs ================================================ namespace ContosoTraders.Api.Core.Services.Implementations; internal class ImageSearchService : ContosoTradersServiceBase, IImageSearchService { private readonly IImageAnalysisService _imageAnalysisService; private readonly IProductService _productService; public ImageSearchService( IProductService productService, IImageAnalysisService imageAnalysisService, IMapper mapper, IConfiguration configuration, ILogger logger) : base(mapper, configuration, logger) { _productService = productService; _imageAnalysisService = imageAnalysisService; } public async Task> GetSimilarProductsAsync(Stream imageStream, CancellationToken cancellationToken = default) { var searchTerms = await _imageAnalysisService.AnalyzeImageAsync(imageStream, cancellationToken); var products = new List(); foreach (var term in searchTerms) { var matchingProducts = _productService.GetProducts(term); if (matchingProducts.Any()) products.AddRange(matchingProducts); } if (!products.Any()) { var searchTags = string.Empty; searchTerms.ToList().ForEach(tag => { searchTags += $"{tag}, "; }); searchTags = searchTags.Remove(searchTags.Length - 2, 2) + '.'; throw new MatchingProductsNotFoundException(searchTags); } var productList = products .GroupBy(p => p.Id) .Select(p => p.First()); return productList; } } ================================================ FILE: src/ContosoTraders.Api.Core/Services/Implementations/ProductService.cs ================================================ using Type = ContosoTraders.Api.Core.Models.Implementations.Dao.Type; namespace ContosoTraders.Api.Core.Services.Implementations; internal class ProductService : ContosoTradersServiceBase, IProductService { private readonly ProductsDbContext _productRepository; public ProductService(ProductsDbContext productDbContext, IMapper mapper, IConfiguration configuration, ILogger logger) : base(mapper, configuration, logger) { _productRepository = productDbContext; } /// /// @TODO: Just a placeholder implementation for now. Fix this later. /// public ProductDto GetProduct(int id) { var productDao = _productRepository.Products.SingleOrDefault(product => product.Id == id); if (productDao is null) throw new ProductNotFoundException(id); var productDto = CustomMapping(productDao, _productRepository.Brands.ToArray(), _productRepository.Types.ToArray(), _productRepository.Features.ToArray(), false); return productDto; } /// /// @TODO: Just a placeholder implementation for now. Fix this later. /// public IEnumerable GetProducts(int[] brands, int[] typeIds) { var responseDaos = brands.Any() || typeIds.Any() ? GetProductsByFilter(brands, typeIds) : GetAllProducts(); var allBrands = _productRepository.Brands.ToArray(); var allTypes = _productRepository.Types.ToArray(); var allFeatures = _productRepository.Features.ToArray(); var responseDtos = responseDaos.ToArray() .Select(dao => CustomMapping(dao, allBrands, allTypes, allFeatures)); return responseDtos; } public IEnumerable GetProducts(string searchTerm) { searchTerm = searchTerm.ToLower(); var allTypes = _productRepository.Types.ToArray(); var allFeatures = _productRepository.Features.ToArray(); var responseDaos = _productRepository.Products.AsEnumerable() .Where(product => searchTerm.Split(' ', StringSplitOptions.RemoveEmptyEntries) .Any(term => product.Name.ToLower().Contains(term) || allTypes.FirstOrDefault(type => type.Id == product.TypeId).Name.ToLower().Contains(term) || allFeatures.Where(feature => feature.ProductItemId == product.Id) .Any(item => item.Description.Contains(term)))); var responseDtos = responseDaos.ToArray() .Select(dao => CustomMapping(dao, null, allTypes, null)); return responseDtos; } public IEnumerable GetBrands() { return _productRepository.Brands.ToList(); } public IEnumerable GetTypes() { return _productRepository.Types.ToList(); } #region Private Helper Methods private IEnumerable GetAllProducts() { return _productRepository.Products.ToList(); } private IEnumerable GetProductsByFilter(int[] brands, int[] typeIds) { var filteredProducts = _productRepository.Products .ToList() .Where(p => (brands.Any() ? brands.Contains(p.BrandId.GetValueOrDefault()) : true) && (typeIds.Any() ? typeIds.Contains(p.TypeId.GetValueOrDefault()) : true)); return filteredProducts; } private ProductDto CustomMapping(Product productDao, IEnumerable brands, IEnumerable types, IEnumerable features, bool thumbnailImages = true) { var imagesEndpoint = Configuration[KeyVaultConstants.SecretNameImagesEndpoint]; var imagesType = thumbnailImages ? "product-list" : "product-details"; var productDto = new ProductDto { Id = productDao.Id, Name = productDao.Name, Price = productDao.Price, ImageUrl = $"{imagesEndpoint}/{imagesType}/{productDao.ImageName}", Brand = brands?.FirstOrDefault(brand => brand.Id == productDao.BrandId), Type = types?.FirstOrDefault(type => type.Id == productDao.TypeId), Features = features?.Where(feature => feature.ProductItemId == productDao.Id).ToList() }; return productDto; } #endregion } ================================================ FILE: src/ContosoTraders.Api.Core/Services/Implementations/ProfileService.cs ================================================ using Profile = ContosoTraders.Api.Core.Models.Implementations.Dao.Profile; namespace ContosoTraders.Api.Core.Services.Implementations; internal class ProfileService : ContosoTradersServiceBase, IProfileService { private readonly ProfilesDbContext _profileRepository; public ProfileService(ProfilesDbContext profileRepository, IMapper mapper, IConfiguration configuration, ILogger logger) : base(mapper, configuration, logger) { _profileRepository = profileRepository; } public IEnumerable GetAllProfiles() { return _profileRepository.Profiles.ToList(); } public Profile GetProfile(string email) { var profile = _profileRepository.Profiles.SingleOrDefault(profile => profile.Email == email); if (profile is null) throw new ProfileNotFoundException(email); return profile; } } ================================================ FILE: src/ContosoTraders.Api.Core/Services/Implementations/StockService.cs ================================================ namespace ContosoTraders.Api.Core.Services.Implementations; internal class StockService : ContosoTradersServiceBase, IStockService { private readonly IStockRepository _stockRepository; public StockService(IStockRepository stockRepository, IMapper mapper, IConfiguration configuration, ILogger logger) : base(mapper, configuration, logger) { _stockRepository = stockRepository; } public async Task GetStockAsync(int productId, CancellationToken cancellationToken = default) { var requestDto = new StockDto { ProductId = productId, StockCount = 0 }; var requestDao = Mapper.Map(requestDto); var responseDao = await _stockRepository.GetAsync(requestDao.id, requestDao.id, cancellationToken); if (responseDao is null) throw new StockNotFoundException(productId); var responseDto = Mapper.Map(responseDao); return responseDto; } public async Task DecrementStockCountAsync(int productId, CancellationToken cancellationToken) { var requestDto = new StockDto { ProductId = productId, StockCount = 0 }; var requestDao = Mapper.Map(requestDto); var responseDao = await _stockRepository.GetAsync(requestDao.id, requestDao.id, cancellationToken); if (responseDao is null) throw new StockNotFoundException(productId); responseDao.StockCount -= 1; await _stockRepository.UpsertAsync(responseDao.id, responseDao, cancellationToken); var responseDto = Mapper.Map(responseDao); return responseDto; } } ================================================ FILE: src/ContosoTraders.Api.Core/Services/Interfaces/ICartService.cs ================================================ namespace ContosoTraders.Api.Core.Services.Interfaces; internal interface ICartService { /// /// /// /// /// /// Task> GetCartAsync(string email, CancellationToken cancellationToken = default); /// /// /// /// /// Task AddItemToCartAsync(CartDto cartItemDto, CancellationToken cancellationToken = default); /// /// /// /// /// Task UpdateCartItemQuantityAsync(CartDto cartItemDto, CancellationToken cancellationToken = default); /// /// /// /// /// Task RemoveItemFromCartAsync(CartDto cartItemDto, CancellationToken cancellationToken = default); } ================================================ FILE: src/ContosoTraders.Api.Core/Services/Interfaces/IImageAnalysisService.cs ================================================ namespace ContosoTraders.Api.Core.Services.Interfaces; internal interface IImageAnalysisService { /// /// /// /// /// Task> AnalyzeImageAsync(Stream imageStream, CancellationToken cancellationToken = default); } ================================================ FILE: src/ContosoTraders.Api.Core/Services/Interfaces/IImageSearchService.cs ================================================ namespace ContosoTraders.Api.Core.Services.Interfaces; internal interface IImageSearchService { /// /// /// /// /// Task> GetSimilarProductsAsync(Stream imageStream, CancellationToken cancellationToken = default); } ================================================ FILE: src/ContosoTraders.Api.Core/Services/Interfaces/IProductService.cs ================================================ using Type = ContosoTraders.Api.Core.Models.Implementations.Dao.Type; namespace ContosoTraders.Api.Core.Services.Interfaces; public interface IProductService { /// /// /// /// /// ProductDto GetProduct(int id); /// /// /// /// /// IEnumerable GetProducts(int[] brands, int[] typeIds); /// /// /// /// IEnumerable GetProducts(string searchTerm); /// /// /// IEnumerable GetBrands(); /// /// /// IEnumerable GetTypes(); } ================================================ FILE: src/ContosoTraders.Api.Core/Services/Interfaces/IProfileService.cs ================================================ using Profile = ContosoTraders.Api.Core.Models.Implementations.Dao.Profile; namespace ContosoTraders.Api.Core.Services.Interfaces; internal interface IProfileService { IEnumerable GetAllProfiles(); Profile GetProfile(string email); } ================================================ FILE: src/ContosoTraders.Api.Core/Services/Interfaces/IStockService.cs ================================================ namespace ContosoTraders.Api.Core.Services.Interfaces; internal interface IStockService { /// /// /// /// /// /// Task GetStockAsync(int productId, CancellationToken cancellationToken = default); /// /// /// /// /// /// Task DecrementStockCountAsync(int productId, CancellationToken cancellationToken = default); } ================================================ FILE: src/ContosoTraders.Api.Core/Usings.cs ================================================ global using AutoMapper; global using ContosoTraders.Api.Core; global using ContosoTraders.Api.Core.Constants; global using ContosoTraders.Api.Core.Exceptions; global using ContosoTraders.Api.Core.Models; global using ContosoTraders.Api.Core.Models.Implementations.Dao; global using ContosoTraders.Api.Core.Models.Implementations.Dto; global using ContosoTraders.Api.Core.Models.Interfaces; global using ContosoTraders.Api.Core.Repositories; global using ContosoTraders.Api.Core.Repositories.Implementations; global using ContosoTraders.Api.Core.Repositories.Interfaces; global using ContosoTraders.Api.Core.Requests.Definitions; global using ContosoTraders.Api.Core.Requests.Validators; global using ContosoTraders.Api.Core.Services.Implementations; global using ContosoTraders.Api.Core.Services.Interfaces; global using FluentValidation; global using MediatR; global using Microsoft.AspNetCore.Http; global using Microsoft.AspNetCore.Mvc; global using Microsoft.Azure.CognitiveServices.Vision.ComputerVision; global using Microsoft.Azure.CognitiveServices.Vision.ComputerVision.Models; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.Logging; ================================================ FILE: src/ContosoTraders.Api.Products/ContosoTraders.Api.Products.csproj ================================================  net7.0 disable enable 23a3ea2a-a58a-475e-92b3-30d4960d8125 Linux Always ================================================ FILE: src/ContosoTraders.Api.Products/Controllers/LoginController.cs ================================================ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using ContosoTraders.Api.Core.Constants; using Microsoft.IdentityModel.Tokens; namespace ContosoTraders.Api.Products.Controllers; [Route("v1/[controller]")] [Produces("application/json")] public class LoginController : ContosoTradersControllerBase { private readonly IConfiguration config; public LoginController(IConfiguration config, IMediator mediator) : base(mediator) { this.config = config; } [HttpPost] public IActionResult Login([FromBody] TokenRequest request) { if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password)) return BadRequest("Could not verify username and password"); return Ok(new { access_token = CreateAccessToken(request.Username), refresh_token = "" }); } private AccessToken CreateAccessToken(string username) { var claims = new[] { new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.Sid, Guid.NewGuid().ToString()) }; // demo only, do not do this in real life! var securityKey = config["SecurityKey"] ?? AuthConstants.DefaultJwtSigningKey; var encoding = Encoding.UTF8.GetBytes(securityKey); var key = new SymmetricSecurityKey(encoding); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var expiresInDays = 365; var token = new JwtSecurityToken( claims: claims, issuer: config["Issuer"] ?? "ContosoWebsite", expires: DateTime.Now.AddDays(expiresInDays), signingCredentials: creds); return new AccessToken { Token = new JwtSecurityTokenHandler().WriteToken(token), ExpiresIn = expiresInDays * 24 * 60 * 60, TokenType = "bearer" }; } } ================================================ FILE: src/ContosoTraders.Api.Products/Controllers/ProductsController.cs ================================================ namespace ContosoTraders.Api.Products.Controllers; [Route("v1/[controller]")] [Produces("application/json")] public class ProductsController : ContosoTradersControllerBase { public ProductsController(IMediator mediator) : base(mediator) { } [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetProducts( [FromQuery(Name = "brand")] int[] brands, [FromQuery(Name = "type")] string[] types) { var request = new GetProductsRequest { Brands = brands, Types = types }; return await ProcessHttpRequestAsync(request); } [HttpGet("{id:int}")] [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetProduct(int id) { var request = new GetProductRequest { ProductId = id }; return await ProcessHttpRequestAsync(request); } [HttpGet("landing")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetPopularProducts() { var request = new GetPopularProductsRequest(); return await ProcessHttpRequestAsync(request); } [HttpPost("imageclassifier")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task PostImage(IFormFile file) { var request = new PostImageRequest { File = file }; return await ProcessHttpRequestAsync(request); } [HttpGet("search/{text}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task Search(string text) { var request = new SearchTextRequest { Text = text }; return await ProcessHttpRequestAsync(request); } } ================================================ FILE: src/ContosoTraders.Api.Products/Controllers/ProfilesController.cs ================================================ namespace ContosoTraders.Api.Products.Controllers; [Route("v1/[controller]")] [Produces("application/json")] public class ProfilesController : ContosoTradersControllerBase { public ProfilesController(IMediator mediator) : base(mediator) { } [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetProfiles() { var request = new GetProfilesRequest(); return await ProcessHttpRequestAsync(request); } [HttpGet("me")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetProfile() { var email = "admin@contosotraders.com"; var request = new GetProfileRequest { Email = email }; return await ProcessHttpRequestAsync(request); } } ================================================ FILE: src/ContosoTraders.Api.Products/Controllers/StocksController.cs ================================================ namespace ContosoTraders.Api.Products.Controllers; [Route("v1/[controller]")] [Produces("application/json")] public class StocksController : ContosoTradersControllerBase { public StocksController(IMediator mediator) : base(mediator) { } [HttpGet("{id:int}")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetStock(int id) { var request = new GetStockRequest { ProductId = id }; return await ProcessHttpRequestAsync(request); } [HttpPost("{id:int}/consume")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task DecrementStockCount(int id) { var request = new DecrementStockCountRequest { ProductId = id }; return await ProcessHttpRequestAsync(request); } } ================================================ FILE: src/ContosoTraders.Api.Products/Dockerfile ================================================ FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build WORKDIR /src COPY ["ContosoTraders.Api.Products/ContosoTraders.Api.Products.csproj", "ContosoTraders.Api.Products/"] COPY ["ContosoTraders.Api.Core/ContosoTraders.Api.Core.csproj", "ContosoTraders.Api.Core/"] RUN dotnet restore "ContosoTraders.Api.Products/ContosoTraders.Api.Products.csproj" COPY . . WORKDIR "/src/ContosoTraders.Api.Products" RUN dotnet build "ContosoTraders.Api.Products.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "ContosoTraders.Api.Products.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "ContosoTraders.Api.Products.dll"] ================================================ FILE: src/ContosoTraders.Api.Products/Manifests/Certificate.yaml ================================================ apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: tls-secret spec: secretName: tls-secret dnsNames: #Note: The '{AKS_FQDN}' token will be substituted with the actual DNS label of the ingress controller's public IP by github workflow. - {AKS_FQDN} issuerRef: name: letsencrypt kind: ClusterIssuer ================================================ FILE: src/ContosoTraders.Api.Products/Manifests/ClusterIssuer.yaml ================================================ apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt spec: acme: # The ACME server URL server: https://acme-v02.api.letsencrypt.org/directory # Email address used for ACME registration email: mithun.shanbhag@spektrasystems.com # Name of a secret used to store the ACME account private key privateKeySecretRef: name: letsencrypt # Enable HTTP01 validations solvers: - http01: ingress: class: nginx ================================================ FILE: src/ContosoTraders.Api.Products/Manifests/ClusterRole.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: containerHealth-log-reader rules: - apiGroups: ["", "metrics.k8s.io", "extensions", "apps"] resources: - "pods/log" - "events" - "nodes" - "pods" - "deployments" - "replicasets" verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: containerHealth-read-logs-global roleRef: kind: ClusterRole name: containerHealth-log-reader apiGroup: rbac.authorization.k8s.io subjects: - kind: User name: clusterUser apiGroup: rbac.authorization.k8s.io ================================================ FILE: src/ContosoTraders.Api.Products/Manifests/Deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: contoso-traders-products spec: #Note: The '{AKS_REPLICAS}' token will be substituted by the github workflow. replicas: {AKS_REPLICAS} selector: matchLabels: app: contoso-traders-products template: metadata: labels: app: contoso-traders-products spec: nodeSelector: "kubernetes.io/os": linux containers: - name: contoso-traders-products #Note: The '{SUFFIX}' token will be substituted with the value of the SUFFIX github variable by github workflow. image: contosotradersacr{SUFFIX}.azurecr.io/contosotradersapiproducts:latest env: - name: KeyVaultEndpoint valueFrom: secretKeyRef: name: contoso-traders-kv-endpoint key: contoso-traders-kv-endpoint - name: ManagedIdentityClientId valueFrom: secretKeyRef: name: contoso-traders-mi-clientid key: contoso-traders-mi-clientid resources: requests: cpu: 100m memory: 128Mi limits: #Note: The '{AKS_CPU_LIMIT}', '${AKS_MEMORY_LIMIT}' tokens will be substituted by the github workflow. cpu: {AKS_CPU_LIMIT} memory: {AKS_MEMORY_LIMIT} ================================================ FILE: src/ContosoTraders.Api.Products/Manifests/Ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: contoso-traders-products annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: /$1 nginx.ingress.kubernetes.io/use-regex: "true" nginx.ingress.kubernetes.io/ssl-redirect: "false" cert-manager.io/cluster-issuer: letsencrypt spec: tls: - hosts: #Note: The '{AKS_FQDN}' token will be substituted with the actual DNS label of the ingress controller's public IP by github workflow. - {AKS_FQDN} secretName: tls-secret rules: #Note: The '{AKS_FQDN}' token will be substituted with the actual DNS label of the ingress controller's public IP by github workflow. - host: {AKS_FQDN} http: paths: - path: /(.*) pathType: ImplementationSpecific backend: service: name: contoso-traders-products port: number: 80 ================================================ FILE: src/ContosoTraders.Api.Products/Manifests/NamespaceCertManager.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: cert-manager labels: cert-manager.io/disable-validation: "true" ================================================ FILE: src/ContosoTraders.Api.Products/Manifests/NamespaceChaosTesting.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: chaos-testing ================================================ FILE: src/ContosoTraders.Api.Products/Manifests/Service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: contoso-traders-products spec: type: ClusterIP ports: - port: 80 selector: app: contoso-traders-products ================================================ FILE: src/ContosoTraders.Api.Products/Migration/productsdb.sql ================================================ use productsdb DROP TABLE IF EXISTS Brands CREATE TABLE Brands ( Id int NOT NULL, Name nvarchar(255) NULL, CONSTRAINT PK_Brands PRIMARY KEY(Id) ) INSERT INTO Brands VALUES (1, 'Microsoft'), (2, 'ASUS'), (3, 'Dell'), (4, 'Acer'), (5, 'OnePlus'), (6, 'HP'), (7, 'Lenovo'), (8, 'Samsung'), (9, 'ViewSonic'), (10,'Zebronics') DROP TABLE IF EXISTS Features CREATE TABLE Features ( Id int NOT NULL, Title nvarchar(255) NULL, Description nvarchar(max) NULL, ProductItemId int NULL, CONSTRAINT PK_Features PRIMARY KEY(Id) ) INSERT INTO Features Values (1,'Model Number','Model Number: Xbox Series X',1), (2,'Additional Content','Additional Content: Series X console, One Wireless Controller, A high-speed HDMI Cable and Power Cable.',1), (3,'Model Number','Model Number: Xbox Series X',2), (4,'Additional Content','Additional Content: Series X console, One Wireless Controller, A high-speed HDMI Cable and Power Cable.',2), (5,'Model Number','Model Number: Xbox Series X',3), (6,'Additional Content','Additional Content: Series X console, One Wireless Controller, A high-speed HDMI Cable and Power Cable.',3), (7,'Model Number','Model Number: Xbox Series X',4), (8,'Additional Content','Additional Content: Series X console, One Wireless Controller, A high-speed HDMI Cable and Power Cable.',4), (9,'Model Number','Model Number: Xbox Series X',5), (10,'Additional Content','Additional Content: Series X console, One Wireless Controller, A high-speed HDMI Cable and Power Cable.',5), (11,'Series','Series : Surface Pro 7',6), (12,'Specifications','Specifications : Touchscreen 2-in-1 Laptop (10th Gen Intel Core i5/8GB/256GB SSD/Windows 10 Home/Intel Iris Plus Graphics, 25% Off on Microsoft 365), Black',6), (13,'Series','Series : Surface Pro 7',7), (14,'Specifications','Specifications : Touchscreen Laptop (8GB/256GB SSD/Windows 11 Home /Radeon Graphics/Platinum/1.265 kg) - 5PB-00023',7), (15,'Series','Series : Surface Pro X ',8), (16,'Specifications','Specifications : (Qualcomm Microsoft Sq1/8Gb/128Gb Ssd/Windows 10 Home/Microsoft Sq1 Adreno 685 Gpu Graphics, Wi-Fi), Matte Black',8), (17,'Series','Series : Surface GO 2 ',9), (18,'Specifications','Specifications : (Gold Processor 4425Y/8GB/128GB SSD/Windows 10 Home in S Mode/Intel UHD 615 Graphics), Platinum',9), (19,'Series','Series : Series : Surface Laptop 3',10), (20,'Specifications','Specifications : (8GB/128GB SSD/Windows 10 Home/Integrated Graphics/Platinum/1.265kg, 25% Off on Microsoft 365), VGY-00021',10), (21,'Series','Series : Vivobook 15',11), (22,'Specifications','Specifications :(4GB RAM/256GB SSD/Integrated Graphics/Windows 11 Home/Transparent Silver/1.8 Kg), X515MA-BR011W',11), (23,'Series','Series : Vivobook S15',12), (24,'Specifications','Specifications :Intel Core Evo i5-12500H 12th Gen, Thin and Light Laptop (16GB/512GB SSD/Iris Xe Graphics/Windows 11/Office 2021/Black/1.8 kg) K3502ZA-L502WS',12), (25,'Series','Series : TUF Gaming F15',13), (26,'Specifications','Specifications : Intel Core i5-10300H 10th Gen, GTX 1650 4GB Graphics, Gaming Laptop (8GB RAM/512GB NVMe SSD/Windows 11/Black/2.30 Kg), FX506LH-HN258W',13), (27,'Series','Series : TUF Gaming A15',14), (28,'Specifications','Specifications :AMD Ryzen 7 4800H, 4GB GeForce RTX 3050 Graphics, Gaming Laptop (16GB/512GB SSD/90WHrs Battery/Windows 11/with Office/Black/2.3 kg), FA506ICB-HN075W',14), (29,'Series','Series :Vivobook 14',15), (30,'Specifications','Specifications :14" (35.56 cms) 2.8K OLED 16:10 90Hz, Thin & Laptop (8GB/512GB SSD/Wins 11/Office 2021/Alexa/Backlit KB/Fingerprint/Black/1.6 kg), X1405ZA-KM311WS',15), (31,'Series','Series : Dell 5410',16), (32,'Specifications','Specifications : 16 GB DDR4, 1 TB + 256GB SSD, Windows 11 Home, Office 2021, Dark Shadow Grey Molded (Black), 3 Years, 23.5" FHD',16), (33,'Series','Series : Dell 380',17), (34,'Specifications','Specifications (Core2Duo /8 GB RAM / 500 GB HDD/ Windows7 Pro, MS Office/Intel Hd Graphics 17 inches Monitor(1398 x 780)) Silver',17), (35,'Series','Series : Dell Optiplex',18), (36,'Specifications','Specifications :(Intel i5 3470, 8GB, 500GB HDD, 19 inches HD Monitor, Keyboard, Mouse, HD Webcam, Mic, Speakers, Wifi, Display Port), Windows 10 Pro',18), (37,'Series','Series : Acer EK220Q',19), (38,'Specifications','Specifications :250 Nits HDMI and VGA Ports Eye Care Features Like Bluelight Shield, Flickerless and Comfy View (1920 x 1080 Pixels, Black)',19), (39,'Series','Series : Acer Ha220Q',20), (40,'Specifications','Specifications :1920 X 1080 Pixels Full Hd IPS Ultra Slim (6.6Mm Thick) Monitor I Frameless Design I AMD Free Sync I Eye Care Features I Stereo Speakers (White)',20), (41,'Series','Series : Acer Aspire C24',21), (42,'Specifications','Specifications : Intel Core i5-1035G1 I 8GB DDR4 I 512GB SSD I Windows 11 Home I MS Office Home & Student 2021 I Full HD Camera I Wireless Keyboard & Mouse',21), (43,'Model','Model : ROG Phone 2 ZS660KL',22), (44,'Specifications','Specifications :8GB RAM, 128GB ROM Snapdragon 855 Plus Processor, 6000 mAh Battery',22), (45,'Model','Model : Zenfone 5Z ZS621KL-2A012IN',23), (46,'Specifications','Specifications :(Midnight Blue, 6GB RAM, 128GB Storage), Dual SIM, Proximity, Fingerprint sensor, E-compass, Ambient light sensor, Hall sensor, GPS, RGB sensor, Gyro, Video Player, Music Player, Accelerator',23), (47,'Model','Model : ZS630KL-2A037WW-cr',24), (48,'Specifications','Specifications : (Black, 64 GB) (6 GB RAM),Dual Front Camera, Camera,Flip, Smartphone, 5000 Milliamp Hours',24), (49,'Model','Model : Nord CE 2 Lite',25), (50,'Specifications','Specifications : 5G (Blue Tide, 6GB RAM, 128GB Storage), 64MP Main Camera with EIS; 2MP Depth Lens and 2MP Macro Lens; Front (Selfie) Camera: 16MP Sony IMX471, 6.59 Inches; 120 Hz Refresh Rate, Qualcomm Snapdragon 695 5G Processor, 5000 mAh Battery with 33W SuperVOOC Charge support',25), (51,'Model','Model : Nord 2T ',26), (52,'Specifications','Specifications :5G (Gray Shadow, 8GB RAM, 128GB Storage), 50MP Main Camera with Sony IMX766 and OIS, 8MP Ultrawide Camera with 120 degree FOV and 2MP mono lens with Dual LED Flash; 32MP Front (Selfie) Camera with Sony IMX615, 6.43 Inches; 90 Hz AMOLED Display with Corning Gorilla Glass 5, Mediatek Dimensity 1300 Processor, Battery 4500 mAh with 80W SuperVOOC',26), (53,'Model','Model : 10R',27), (54,'Specifications','Specifications : 5G (Sierra Black, 8GB RAM, 128GB Storage, 80W SuperVOOC), 0MP Main Camera with Sony IMX766 and 2MP Macro Camera with Dual LED Flash; 16MP Front (Selfie) Camera with Sony IMX471, 6.7 Inches; 120 Hz IRIS Display; Resolution: 2400 X 1080 pixels 394 ppi; Aspect Ratio: 20:9, OxygenOS based on Android 12, MTK D8100 Max Processor, In-Display Fingerprint Sensor',27), (55,'Model','Model : 10T',28), (56,'Specifications','Specifications : 5G (Moonstone Black, 16GB RAM, 256GB Storage)',28), (57,'Model Number','Model Number: Xbox Series X ',29), (58,'Additional Content','Additional Content: Series X console, One Wireless Controller, A high-speed HDMI Cable and Power Cable.',29), (59,'Model Number','Model Number: Xbox Series X ',30), (60,'Additional Content','Additional Content: Series X console, One Wireless Controller, A high-speed HDMI Cable and Power Cable.',30), (61,'Model Number','Model Number: Xbox Series X ',31), (62,'Additional Content','Additional Content: Series X console, One Wireless Controller, A high-speed HDMI Cable and Power Cable.',31), (63,'Model Number','Model Number: Xbox Series X ',32), (64,'Additional Content','Additional Content: Series X console, One Wireless Controller, A high-speed HDMI Cable and Power Cable.',32), (65,'Model Number','Model Number: Xbox Series X ',33), (66,'Additional Content','Additional Content: Series X console, One Wireless Controller, A high-speed HDMI Cable and Power Cable.',33), (67,'Series','Series : Dell IdeaCentre AIO 3 ',34), (68,'Specifications','Specifications : (AMD Ryzen 3/8GB/1TB HDD/Windows 11/MS Office 2021/AMD Radeon Graphics/HD 720p Camera/Wired Keyboard, Mouse), Black (F0G6008AIN)',34), (69,'Series','Series : HP AIO ',35), (70,'Specifications',': Specifications: 21.5 inch(54.6 cm) FHD Anti Glare Display (8GB/1TB HDD+256GB SSD/Win 11/Intel UHD Graphics/Wireless Keyboard & Mouse Combo/MS Office/Jet Black,22-dd2456in',35), (71,'Series','Series : HP AIO ',36), (72,'Specifications',' :Specifications : 27inches/68.6 cm 8GB RAM/512GB SSD/FHD, Micro-Edge, Anti-Glare Display/Wireless Keyboard & Mouse/Intel UHD Graphics/Windows 11/MSO 2021, 27-cb1345in',36), (73,'Series','Series : Dell Optiplex ',37), (74,'Specifications',' :Specifications : Core i5 8 GB 500GB HDD Windows MS Office Intel HD Graphics), Black, RAM Size 8 GB, Memory Technology - DDR3, Computer Memory Type GDDR3, Maximum Memory Supported 8 GB, Hard Drive Size 500 GB, Hard Disk Description HDD',37), (75,'Series','Series : Dell Optiplex 19 ',38), (76,'Specifications',' :Specifications : (Intel i5 3470/8 GB/1TB HDD /19" HD Monitor+Keyboard+Mouse+ HD Webcam+Mic+Speakers+WiFi+Display Port/Windows 10 Pro/MS Office)',38), (77,'Series','Series : HP AIO',39), (78,'Specifications',' : VA, 3-Sided Micro-Edge, Anti-Glare/4GB RAM/1TB HDD/Wired Keyboard & Mouse/Dual Speakers/Win 11/Intel UHD Graphics/MSO 2021/5.7Kg, 22-dd2342in, White',39), (79,'Model','Model : M13',40), (80,'Specifications',' :Specifications : Aqua Green, 6GB, 128GB Storage | 6000mAh Battery | Upto 12GB RAM with RAM Plus, 6000mAh lithium-ion battery, 1 year manufacturer warranty for device and 6 months manufacturer warranty for in-box accessories including batteries from the date of purchase, Upto 12GB RAM with RAM Plus | 128GB internal memory expandable up to 1TB| Dual Sim (Nano), 50MP+5MP+2MP Triple camera setup- True 50MP (F1.8) main camera +5MP(F2.2)+ 2MP (F2.4) | 8MP (F2.2) front cam, Android 12,One UI Core 4 with a powerful Octa Core Processor, 16.72 centimeters (6.6-inch) FHD+ LCD - infinity O Display, FHD+ resolution with 1080 x 2408 pixels resolution, 401 PPI with 16M color',40), (81,'Model','Model : F13',41), (82,'Specifications',' :Specifications : (Nightsky Green, 4GB RAM 64GB Storage), 4 GB RAM | 64 GB ROM | Expandable Upto 1 TB, 16.76 cm (6.6 inch) Full HD+ Display, 50MP + 5MP + 2MP | 8MP Front Camera, 6000 mAh Lithium Ion Battery, Exynos 850 Processor',41), (83,'Model','Model : S20',42), (84,'Specifications',' :Specifications : Cloud Lavender, 8GB RAM, 128GB Storage, 5G Ready powered by Qualcomm Snapdragon 865 Octa-Core processor, 8GB RAM, 128GB internal memory expandable up to 1TB, Android 11.0 operating system and dual SIM, Triple Rear Camera Setup - 12MP (Dual Pixel) OIS F1.8 Wide Rear Camera + 8MP OIS Tele Camera + 12MP Ultra Wide | 30X Space Zoom, Single Take & Night Mode | 32MP F2.2 Front Punch Hole Camera 6.5-inch(16.40 centimeters) Infinity-O Super AMOLED Display with 120Hz Refresh rate, 1080 x 2400 (FHD+) Resolution " 4500 mAh battery (Non -removable) with Super Fast Charging, FAst Wireless Charging & Finger Print sensor',42), (85,'Series','Series : D-Series',43), (86,'Specifications',' :Specifications : |16.7 Mn Colors, 3000:1 Contrast Ratio, 75Hz, 4ms, AMD FreeSync, HDMI, VESA Wall Mount, TUV Eye Comfort & Low Blue Light -D22e-20',43), (87,'Series','Series : VA2432-MH ',44), (88,'Specifications',' :Specifications : IPS Panel, 1920 x 1080 Full HD Pixels Display, Super Clear IPS Technology, 75 Hz Refresh Rate, 3 Side Borderless Design, Dual Integrated Speakers, HDMI | VGA Enabled',44), (89,'Series','Series : LF24T350FHWXXL ',45), (90,'Specifications',' :Specifications : IPS, 75 Hz, Bezel Less Design, AMD FreeSync, Flicker Free, HDMI, D-sub, Dark Blue Gray), 24 inch Samsung Monitor - 1,920 x 1,080 Resolution IPS Panel Monitor 3-sided borderless display for All-expansive view, Fluid pictures with 75hz refresh rate | 5 ms response time | 250cd/m2 Brightness (Typical)',45), (91,'Series','Series : EK220Q ',46), (92,'Specifications',' :Specifications : 250 Nits HDMI and VGA Ports Eye Care Features Like Bluelight Shield, Flickerless and Comfy View (1920 x 1080 Pixels, Black), 21.5 Inch VA Panel Full HD 1920 X 1080 Resolution Monitor I 250 Nits Brightness I 178 / 178 View Angle, Connectivity Options : HDMI and VGA Ports with Inbox HDMI Cable, 5MS Response Time 75Hz Fast Refresh Rate, Eye Care Featues Includes Bluelight Shield I Flickerless I Comfyview, Wall Mount Ready I -5 to -20 Tilt Adjustment',46), (93,'Series','Series : Ha220Q',47), (94,'Specifications',' :Specifications : 1920 X 1080 Pixels Full Hd IPS Ultra Slim (6.6Mm Thick) Monitor I Frameless Design I AMD Free Sync I Eye Care Features I Stereo Speakers (White), Exceptional Full HD IPS 21.5 Inch Ultra Thin Display : Enjoy immaculate image quality with 1920x1080 resolution and 178 degree wide viewing angles, Connectivity Ports : 1 VGA Port, 1 HDMI, 1 Audio-In Port, Inbox HDMI and VGA Cable, Eye Care Features Includes Blue Light Shield, Flickerless, Comfyview helps to protect the eyes and gives comfortable viewing experience, 250 Nits Brightness, 4MS Response Time 75Hz Refresh Rate with AMD Free Sync Technology',47), (95,'Series','Series : HA270',48), (96,'Specifications',' :Specifications : 6.6mm Thick Frameless Design AMD Free Sync LCD Monitor with Eye Care Features and Stereo Speakers (White), Exceptional Full HD IPS 27 Inch Ultra Thin Display : Enjoy immaculate image quality with 1920x1080 resolution and 178 degree wide viewing angles, Connectivity Ports : 1 VGA Port, 1 HDMI, 1 Audio-In Port, Inbox HDMI and VGA Cable, Eye Care Features Includes Blue Light Shield, Flickerless, Comfyview helps to protect the eyes and gives comfortable viewing experience, 250 Nits Brightness, 4MS Response Time 75Hz Refresh Rate with AMD Free Sync Technology',48), (97,'Series','Series : Zeb-V16HD',49), (98,'Specifications',' :Specifications : 15.4 with Supporting HDMI, has VGA Input, HD 1280 x 800, Glossy Panel, Slim Feature and Wall mountable, VGA / HDMI input.Aspect ratio 16:10. Viewing angle H90/V50, Slim design, Anti glare, Built-in power supply',49), (99,'Series','Series : Zeb-V19HD',50), (100,'Specifications',' :Specifications : Supporting Hdmi, Vga Input, Hd 1366 X 768 Pixels, 16.7M Colors, Glossy Panel, Slim Design & Wall Mountable, Black, HD 1366 x 768 Native resolution with 16:9 Aspect Ratio, Supports 16.7 Million Colors.High quality 220cd/m² Maximum Brightness Dynamic Contrast Ratio 500000:1, Viewing Angle H170°/ V160°, 8ms Response Time & 0.3 x 0.3 mm Pixel Pitch Green',50) DROP TABLE IF EXISTS Tags CREATE TABLE Tags ( Id int NOT NULL, Value nvarchar(255) NULL, CONSTRAINT PK_Tags PRIMARY KEY(Id) ) INSERT INTO Tags VALUES (1, 'RechargeableScrewdriver'), (2, 'Multitool'), (3, 'HardHat') DROP TABLE IF EXISTS Types CREATE TABLE Types ( Id int NOT NULL, Code nvarchar(255) NULL, Name nvarchar(255) NULL, CONSTRAINT PK_Types PRIMARY KEY(Id) ) INSERT INTO Types VALUES (1, 'controllers' , 'Controllers'), (2, 'laptops' , 'Laptops'), (3, 'desktops' , 'Desktops'), (4, 'mobiles' , 'Mobiles'), (5, 'monitors' , 'Monitors') DROP TABLE IF EXISTS Products CREATE TABLE Products ( Id int NOT NULL, Name nvarchar(255) NULL, Price decimal(9, 2) NULL, ImageName nvarchar(255) NULL, BrandId int NULL, TypeId int NULL, TagId int NULL, CONSTRAINT PK_Products PRIMARY KEY(Id) ) INSERT INTO Products (Id,Name,Price,ImageName,BrandId,TypeId,TagId) VALUES (1,'Xbox Wireless Controller Lunar Shift Special Edition',99,'PID1-1.jpg',1,1,NULL), (2,'Xbox Wireless Controller Mineral Camo Special Edition',112,'PID2-1.jpg',1,1,NULL), (3,'Xbox Elite Wireless Controller Series 2 Core (White)',139,'PID3-1.jpg',1,1,NULL), (4,'Xbox Wireless Controller 20th Anniversary Special Edition',89,'PID4-1.jpg',1,1,NULL), (5,'Xbox Elite Wireless Controller Series 2 Halo Infinite Limited Edition',139,'PID5-1.jpg',1,1,NULL), (6,'Microsoft Surface Pro 7 PUV-00028 12.3" Black',249,'PID6-1.jpg',1,2,NULL), (7,'Microsoft Surface Laptop 4 AMD Ryzen 5 4680U 13.5 inches',299,'PID7-1.jpg',1,2,NULL), (8,'Microsoft Surface Pro X 1876 13 Inches Laptop',699,'PID8-1.jpg',1,2,NULL), (9,'Microsoft Surface GO 2 STQ-00013 10.1" (26.54 cms) Laptop',549,'PID9-1.jpg',1,2,NULL), (10,'Microsoft Surface Laptop 3 Intel Core i5 10th Gen 13.5" (34.29 cms) Touchscreen Laptop',349,'PID10-1.jpg',1,2,NULL), (11,'ASUS VivoBook 15 (2021), 15.6-inch (39.62 cm) HD, Dual Core Intel Celeron N4020, Thin and Light Laptop',429,'PID11-1.jpg',2,2,NULL), (12,'ASUS Vivobook S15 OLED 2022, 15.6" 39.62 cm FHD OLED Laptop',899,'PID12-1.jpg',2,2,NULL), (13,'ASUS TUF Gaming F15 (2021), 15.6" (39.62 cms) FHD 144Hz Laptop',1299,'PID13-1.jpg',2,2,NULL), (14,'ASUS TUF Gaming A15, 15.6" (39.62 cms) FHD 144Hz Laptop',1199,'PID14-1.jpg',2,2,NULL), (15,'ASUS Vivobook 14 OLED, 12th Gen Intel Core i3-1215U Laptop',1099,'PID15-1.jpg',2,2,NULL), (16,'Dell 24" All-in-One 5410 Core I3 12th Gen desktop',1199,'PID16-1.jpg',3,3,NULL), (17,'Dell Optiplex 380 17 inch (43.18 cms) Desktop',1399,'PID17-1.jpg',3,3,NULL), (18,'Dell Optiplex 19 inch, All in One Desktop Set',899,'PID18-1.jpg',3,3,NULL), (19,'Acer EK220Q 54.61 cm Full HD VA Panel Backlit LED Monitor',899,'PID19-1.jpg',4,5,NULL), (20,'Acer Ha220Q 21.5 Inch (54.61 cm) LCD Monitor',899,'PID20-1.jpg',4,5,NULL), (21,'Acer Aspire C24 23.8 inch Full HD IPS All in One Desktop',1399,'PID21-1.jpg',4,3,NULL), (22,'ASUS ROG Phone 2',699,'PID22-1.jpg',2,4,NULL), (23,'Asus Zenfone 5Z ',699,'PID23-1.jpg',2,4,NULL), (24,'ASUS 6Z',859,'PID24-1.jpg',2,4,NULL), (25,'OnePlus Nord CE 2 Lite',679,'PID25-1.jpg',5,4,NULL), (26,'OnePlus Nord 2T ',578,'PID26-1.jpg',5,4,NULL), (27,'OnePlus 10R',999,'PID27-1.jpg',5,4,NULL), (28,'OnePlus 10T',999,'PID28-1.jpg',5,4,NULL), (29,'Xbox Wireless Controller Forza Horizon 5 Limited Edition',299,'PID29-1.jpg',1,1,NULL), (30,'Xbox Wireless Controller Aqua Shift Special Edition',299,'PID30-1.jpg',1,1,NULL), (31,'Xbox Wireless Controller Daystrike Camo Special Edition',299,'PID31-1.jpg',1,1,NULL), (32,'Xbox Wireless Controller Black',299,'PID32-1.jpg',1,1,NULL), (33,'Xbox Wireless Controller White',299,'PID33-1.jpg',1,1,NULL), (34,'Lenovo IdeaCentre AIO 3 21.5 inches Full HD IPS All-in-One Desktop',899,'PID34-1.jpg',7,3,NULL), (35,'HP AIO PC 12th Gen Intel Core i3-1215U desktop',899,'PID35-1.jpg',6,3,NULL), (36,'HP All-in-One 12th Gen Intel Core i3 desktop',899,'PID36-1.jpg',6,3,NULL), (37,'Dell Optiplex Desktop',899,'PID37-1.jpg',3,3,NULL), (38,'Dell Optiplex 19" All-in-One Desktop Computer Set',899,'PID38-1.jpg',3,3,NULL), (39,'HP AIO Intel Celeron J4025-54.6cm(21.5 inch) FHD desktop',899,'PID39-1.jpg',6,3,NULL), (40,'Samsung Galaxy M13',999,'PID40-1.jpg',8,4,NULL), (41,'SAMSUNG Galaxy F13',999,'PID41-1.jpg',8,4,NULL), (42,'Samsung Galaxy S20 FE 5G',1899,'PID42-1.jpg',8,4,NULL), (43,'Lenovo D-Series 54.61 cm (22 inch) FHD VA 3-Side NearEdgeless Monitor',799,'PID43-1.jpg',7,5,NULL), (44,'ViewSonic VA2432-MH 23.8 inches Monitor',799,'PID44-1.jpg',9,5,NULL), (45,'Samsung 24-inch(60.46cm) FHD Monitor',799,'PID45-1.jpg',8,5,NULL), (46,'Acer EK220Q 54.61 cm Full HD VA Panel Backlit LED Monitor',799,'PID46-1.jpg',4,5,NULL), (47,'Acer Ha220Q 21.5 Inch (54.61 Cm) LCD Monitor',799,'PID47-1.jpg',4,5,NULL), (48,'Acer HA270 27 Inch (68.58 cm) Full HD 1920 x 1080 IPS Ultra Slim Monitor',799,'PID48-1.jpg',4,5,NULL), (49,'Zebronics Zeb-V16HD LED Monitor',799,'PID49-1.jpg',10,5,NULL), (50,'ZEBRONICS Zeb-V19Hd 18.5 Inch (46.99 Cm) Led Monitor',799,'PID50-1.jpg',10,5,NULL) ================================================ FILE: src/ContosoTraders.Api.Products/Program.cs ================================================ DependencyInjection.ConfigureApp(); ================================================ FILE: src/ContosoTraders.Api.Products/Properties/ServiceDependencies/tailwind-traders-product - Web Deploy/profile.arm.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", "contentVersion": "1.0.0.0", "metadata": { "_dependencyType": "compute.appService.windows" }, "parameters": { "resourceGroupName": { "type": "string", "defaultValue": "contoso-traders-sandbox-rg", "metadata": { "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." } }, "resourceGroupLocation": { "type": "string", "defaultValue": "eastus", "metadata": { "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." } }, "resourceName": { "type": "string", "defaultValue": "contoso-traders-product", "metadata": { "description": "Name of the main resource to be created by this template." } }, "resourceLocation": { "type": "string", "defaultValue": "[parameters('resourceGroupLocation')]", "metadata": { "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." } } }, "variables": { "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]" }, "resources": [ { "type": "Microsoft.Resources/resourceGroups", "name": "[parameters('resourceGroupName')]", "location": "[parameters('resourceGroupLocation')]", "apiVersion": "2019-10-01" }, { "type": "Microsoft.Resources/deployments", "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", "resourceGroup": "[parameters('resourceGroupName')]", "apiVersion": "2019-10-01", "dependsOn": [ "[parameters('resourceGroupName')]" ], "properties": { "mode": "Incremental", "template": { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "resources": [ { "location": "[parameters('resourceLocation')]", "name": "[parameters('resourceName')]", "type": "Microsoft.Web/sites", "apiVersion": "2015-08-01", "tags": { "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" }, "dependsOn": [ "[variables('appServicePlan_ResourceId')]" ], "kind": "app", "properties": { "name": "[parameters('resourceName')]", "kind": "app", "httpsOnly": true, "reserved": false, "serverFarmId": "[variables('appServicePlan_ResourceId')]", "siteConfig": { "metadata": [ { "name": "CURRENT_STACK", "value": "dotnetcore" } ] } }, "identity": { "type": "SystemAssigned" } }, { "location": "[parameters('resourceLocation')]", "name": "[variables('appServicePlan_name')]", "type": "Microsoft.Web/serverFarms", "apiVersion": "2015-08-01", "sku": { "name": "S1", "tier": "Standard", "family": "S", "size": "S1" }, "properties": { "name": "[variables('appServicePlan_name')]" } } ] } } } ] } ================================================ FILE: src/ContosoTraders.Api.Products/Properties/ServiceDependencies/tailwind-traders-product - Web Deploy1/profile.arm.json ================================================ { "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", "contentVersion": "1.0.0.0", "metadata": { "_dependencyType": "compute.appService.windows" }, "parameters": { "resourceGroupName": { "type": "string", "defaultValue": "contoso-traders-sandbox-rg", "metadata": { "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." } }, "resourceGroupLocation": { "type": "string", "defaultValue": "eastus", "metadata": { "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support." } }, "resourceName": { "type": "string", "defaultValue": "contoso-traders-product", "metadata": { "description": "Name of the main resource to be created by this template." } }, "resourceLocation": { "type": "string", "defaultValue": "[parameters('resourceGroupLocation')]", "metadata": { "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." } } }, "variables": { "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]" }, "resources": [ { "type": "Microsoft.Resources/resourceGroups", "name": "[parameters('resourceGroupName')]", "location": "[parameters('resourceGroupLocation')]", "apiVersion": "2019-10-01" }, { "type": "Microsoft.Resources/deployments", "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]", "resourceGroup": "[parameters('resourceGroupName')]", "apiVersion": "2019-10-01", "dependsOn": [ "[parameters('resourceGroupName')]" ], "properties": { "mode": "Incremental", "template": { "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "resources": [ { "location": "[parameters('resourceLocation')]", "name": "[parameters('resourceName')]", "type": "Microsoft.Web/sites", "apiVersion": "2015-08-01", "tags": { "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty" }, "dependsOn": [ "[variables('appServicePlan_ResourceId')]" ], "kind": "app", "properties": { "name": "[parameters('resourceName')]", "kind": "app", "httpsOnly": true, "reserved": false, "serverFarmId": "[variables('appServicePlan_ResourceId')]", "siteConfig": { "metadata": [ { "name": "CURRENT_STACK", "value": "dotnetcore" } ] } }, "identity": { "type": "SystemAssigned" } }, { "location": "[parameters('resourceLocation')]", "name": "[variables('appServicePlan_name')]", "type": "Microsoft.Web/serverFarms", "apiVersion": "2015-08-01", "sku": { "name": "S1", "tier": "Standard", "family": "S", "size": "S1" }, "properties": { "name": "[variables('appServicePlan_name')]" } } ] } } } ] } ================================================ FILE: src/ContosoTraders.Api.Products/Properties/launchSettings.json ================================================ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "ContosoTraders.Api.Products": { "commandName": "Project", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:62300;http://localhost:62301", "dotnetRunMessages": true }, "Docker": { "commandName": "Docker", "launchBrowser": true, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "publishAllPorts": true, "useSSL": true } } } ================================================ FILE: src/ContosoTraders.Api.Products/Usings.cs ================================================ global using MediatR; global using Microsoft.AspNetCore.Mvc; global using ContosoTraders.Api.Core; global using ContosoTraders.Api.Core.Services.Interfaces; global using ContosoTraders.Api.Core.Controllers; global using ContosoTraders.Api.Core.Models.Implementations.Dto; global using ContosoTraders.Api.Core.Requests.Definitions; ================================================ FILE: src/ContosoTraders.Api.Profiles/.gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # Azure Functions localsettings file local.settings.json # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json project.fragment.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted #*.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings node_modules/ orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc ================================================ FILE: src/ContosoTraders.Api.Profiles/ContosoTraders.Api.Profiles.csproj ================================================ net7.0 v4 PreserveNewest PreserveNewest Never ================================================ FILE: src/ContosoTraders.Api.Profiles/Controllers/ProfilesController.cs ================================================ using System.IO; using System.Net; using System.Threading.Tasks; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; using Newtonsoft.Json; namespace ContosoTraders.Api.Profiles.Controllers; public class ProfilesController : ContosoTradersControllerBase { private readonly ILogger _logger; public ProfilesController(IMediator mediator, ILogger log) : base(mediator) { _logger = log; } [FunctionName("Function1")] [OpenApiOperation("Run", "name")] [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)] [OpenApiParameter("name", In = ParameterLocation.Query, Required = true, Type = typeof(string), Description = "The **Name** parameter")] [OpenApiResponseWithBody(HttpStatusCode.OK, "text/plain", typeof(string), Description = "The OK response")] public async Task Run( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req) { _logger.LogInformation("C# HTTP trigger function processed a request."); string name = req.Query["name"]; var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); dynamic data = JsonConvert.DeserializeObject(requestBody); name = name ?? data?.name; var responseMessage = string.IsNullOrEmpty(name) ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response." : $"Hello, {name}. This HTTP triggered function executed successfully."; return new OkObjectResult(responseMessage); } } ================================================ FILE: src/ContosoTraders.Api.Profiles/Properties/serviceDependencies.json ================================================ { "dependencies": { "appInsights1": { "type": "appInsights" }, "storage1": { "type": "storage", "connectionId": "AzureWebJobsStorage" } } } ================================================ FILE: src/ContosoTraders.Api.Profiles/Properties/serviceDependencies.local.json ================================================ { "dependencies": { "appInsights1": { "type": "appInsights.sdk" }, "storage1": { "type": "storage.emulator", "connectionId": "AzureWebJobsStorage" } } } ================================================ FILE: src/ContosoTraders.Api.Profiles/Usings.cs ================================================ global using ContosoTraders.Api.Core.Controllers; ================================================ FILE: src/ContosoTraders.Api.Profiles/host.json ================================================ { "version": "2.0", "logging": { "applicationInsights": { "samplingSettings": { "isEnabled": true, "excludedTypes": "Request" } } } } ================================================ FILE: src/ContosoTraders.Ui.Website/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local .env.playwright.local npm-debug.log* yarn-debug.log* yarn-error.log* appsettings.Development.json # Playwright tests /test-results/ /playwright-report/ /playwright/.cache/ /test-results/ /playwright-report/ /playwright/.cache/ /.auth/ /playwright-report-junit ================================================ FILE: src/ContosoTraders.Ui.Website/README.md ================================================ # Getting Started with Create React App This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). ## Available Scripts In the project directory, you can run: ### `npm start` Runs the app in the development mode.\ Open [http://localhost:3000](http://localhost:3000) to view it in your browser. The page will reload when you make changes.\ You may also see any lint errors in the console. ### `npm test` Launches the test runner in the interactive watch mode.\ See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. ### `npm run build` Builds the app for production to the `build` folder.\ It correctly bundles React in production mode and optimizes the build for the best performance. The build is minified and the filenames include the hashes.\ Your app is ready to be deployed! See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. ### `npm run eject` **Note: this is a one-way operation. Once you `eject`, you can't go back!** If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. ## Learn More You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). To learn React, check out the [React documentation](https://reactjs.org/). ### Code Splitting This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) ### Analyzing the Bundle Size This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) ### Making a Progressive Web App This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) ### Advanced Configuration This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) ### Deployment This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) ### `npm run build` fails to minify This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) ================================================ FILE: src/ContosoTraders.Ui.Website/package.json ================================================ { "name": "website-ui", "version": "0.1.0", "private": true, "dependencies": { "@mui/icons-material": "^5.13.7", "@mui/material": "^5.14.0", "axios": "^1.6.0", "formik": "^2.4.4", "history": "^5.3.0", "msal": "^1.4.18", "mui-file-dropzone": "^4.0.2", "qs": "^6.11.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-material-ui-carousel": "^3.4.2", "react-redux": "^8.1.1", "react-router-dom": "^6.14.1", "react-scripts": "5.0.1", "redux": "^4.2.1", "web-vitals": "^3.3.2", "yup": "^1.2.0" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "devDependencies": { "@playwright/test": "^1.43.1", "@types/node": "^18.11.18", "babel-plugin-macros": "^3.1.0", "csv-parse": "^5.5.0", "sass": "^1.63.6", "typescript": "^5.1.6" }, "overrides": { "typescript": "^5.1.6" } } ================================================ FILE: src/ContosoTraders.Ui.Website/playwright.config.ts ================================================ import { defineConfig, devices } from '@playwright/test'; /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ require('dotenv').config({ path: '.env.playwright.local' }) export default defineConfig({ testDir: './tests', /* Maximum time one test can run for. */ timeout: 80 * 1000, expect: { // Account for pixel difference between login being enabled/disabled toHaveScreenshot: { maxDiffPixels: 100 } }, /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ['list'], ['html'], ["junit", { outputFile: "playwright-report-junit/e2e-junit-results.xml" }], ...(process.env.CI ? [['github'] as ['github']] : []), ], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* https://github.com/microsoft/playwright/issues/14440 - TODO - Investigate later */ ignoreHTTPSErrors: true, /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: process.env.REACT_APP_BASEURLFORPLAYWRIGHTTESTING || 'https://production.contosotraders.com/', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'on-first-retry', }, projects: [ // Setup project { name: 'setup', testMatch: /.*\.setup\.ts/ }, // Test project that requires authentication { name: 'authenticated', testMatch: /.account\.ts/, use: { ...devices['Desktop Chrome'], // Use prepared auth state. storageState: '.auth/user.json', }, dependencies: ['setup'], }, // Test projects that don't require authentication { name: 'chromium', use: { ...devices['Desktop Chrome'] }, testIgnore: /api/, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, testIgnore: /api/, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, testIgnore: /api/, }, { name: 'api', testMatch: 'tests/api/**/*.spec.ts', } ], }); ================================================ FILE: src/ContosoTraders.Ui.Website/public/browserconfig.xml ================================================ #ffffff ================================================ FILE: src/ContosoTraders.Ui.Website/public/index.html ================================================  Contoso Traders
================================================ FILE: src/ContosoTraders.Ui.Website/public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: src/ContosoTraders.Ui.Website/public/site.webmanifest ================================================ { "name": "Contoso Traders", "short_name": "Contoso Traders", "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/android-chrome-256x256.png", "sizes": "256x256", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" } ================================================ FILE: src/ContosoTraders.Ui.Website/src/App.css ================================================ .App { text-align: center; } .App-logo { height: 40vmin; pointer-events: none; } @media (prefers-reduced-motion: no-preference) { .App-logo { animation: App-logo-spin infinite 20s linear; } } .App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; } .App-link { color: #61dafb; } @keyframes App-logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/App.js ================================================ import React, { Fragment, useCallback, useEffect, useState } from "react"; import { Route, Routes } from "react-router-dom"; import { connect } from "react-redux"; // import { CartService } from "./services"; // import Meeting from './pages/home/components/videoCall/Meeting'; import Header from "./components/header/header"; import HeaderMessage from "./components/header/headerMessage"; import Appbar from "./components/header/appbar"; import Footer from "./components/footer/footer"; import { Home, List, // MyCoupons, Detail, SuggestedProductsList, Profile, // ShoppingCart, Arrivals, RefundPolicy, TermsOfService, AboutUs, ErrorPage, Cart, } from "./pages"; // import "./i18n"; import "./main.scss"; import warningIcon from './assets/images/original/Contoso_Assets/Icons/information_icon.svg' import { useLocation } from "react-router-dom"; import { CartService } from "./services"; import { getCartQuantity } from "./actions/actions"; function App(props) { const location = useLocation() // const [shoppingCart, setShoppingCart] = useState([]) const [quantity, setQuantity] = useState(0) const getQuantity = useCallback(async() => { let quantity = 0; //Show cart using API if (props.userInfo.token) { const shoppingcart = await CartService.getShoppingCart( props.userInfo.token ); // if (shoppingcart) { // setShoppingCart({ shoppingcart }); // } quantity = shoppingcart.length; }else{ let cart = localStorage.getItem('cart_items') ? JSON.parse(localStorage.getItem('cart_items')) : []; quantity = cart.length; } setQuantity(quantity); },[props]) useEffect(() => { props.getCartQuantity(quantity) }, [quantity, props]); React.useEffect(() => { getQuantity() }, [getQuantity]); React.useEffect(() => { window.scrollTo(0, 0); }, [location.pathname]); return (
{location.pathname === '/' || location.pathname === '/new-arrivals' ?
:
}
} /> } /> {/* */} } /> } /> } /> } /> } /> } /> } /> {/* */} {props.userInfo.loggedIn === true ? <> } /> :null} }/> {/* */} } />
); } // } const mapStateToProps = (state) => { return { userInfo : state.login.userInfo, theme : state.login.theme } }; const mapDispatchToProps = (dispatch) => ({ getCartQuantity: (value) => dispatch(getCartQuantity(value)), }) export default connect(mapStateToProps, mapDispatchToProps)(App); ================================================ FILE: src/ContosoTraders.Ui.Website/src/App.test.js ================================================ import { render, screen } from '@testing-library/react'; import App from './App'; test('renders learn react link', () => { render(); const linkElement = screen.getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); }); ================================================ FILE: src/ContosoTraders.Ui.Website/src/actions/actions.js ================================================ import { FORM_EMAIL, SAVE_USER, REMOVE_USER, THEME_CHANGE, GET_QUANTITY } from '../types/types'; export const textAction = email => ({ type: FORM_EMAIL, email, }); export const submitAction = userInfo => ({ type: SAVE_USER, userInfo }); export const clickAction = () => ({ type: REMOVE_USER, }); export const handleThemeChange = (value) => ({ type : THEME_CHANGE, value : value, field : 'theme' }); export const getCartQuantity = (quantity) => ({ type : GET_QUANTITY, quantity : quantity }); ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/Input/checkbox.js ================================================ import React from "react"; import PropTypes from "prop-types"; const Checkbox = ({ type = "checkbox", name, checked = false, onChange, code }) => ( ); Checkbox.propTypes = { type: PropTypes.string, name: PropTypes.string.isRequired, checked: PropTypes.bool, onChange: PropTypes.func.isRequired, id: PropTypes.string, }; export default Checkbox; ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/accordion/accordion.js ================================================ import React from "react"; import MuiAccordion from "@mui/material/Accordion"; import MuiAccordionSummary from "@mui/material/AccordionSummary"; import MuiAccordionDetails from "@mui/material/AccordionDetails"; import Typography from "@mui/material/Typography"; import description_off from "../../assets/images/original/Contoso_Assets/product_page_assets/description_off_icon.svg"; import description_on from "../../assets/images/original/Contoso_Assets/product_page_assets/description_on_icon.svg"; import './accordion.scss' const Accordion = (MuiAccordion); const AccordionSummary = (MuiAccordionSummary); const AccordionDetails = (MuiAccordionDetails); export default function CustomizedAccordions(props) { const { accordionItems } = props const [expanded, setExpanded] = React.useState(null); const handleChange = (panel) => (event, newExpanded) => { setExpanded(newExpanded ? panel : false); }; return (
{accordionItems.map((item, key) => { return( ) : ( ) } > {item.title} {item.body} ) })}
); } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/accordion/accordion.scss ================================================ .MuiAccordion-root{ .panelSummary { flex-direction: row-reverse; // padding-left: 10px; border: 1px solid rgba(112, 112, 112, 0.1); border-right: none; border-left: none; .MuiAccordionSummary-content{ font-family: 'Heebo'; font-size: 16px; margin: 23.5px 0; margin-left: 10px; } .MuiIconButton-root{ padding-left: 0; } } &:last-child{ .panelSummary { border-bottom: none; } } } .AccordionSection { margin-top: 30px; font-family: "Heebo"; font-size: 14px; font-weight: 400; .detail_feature p{ font-family: 'Heebo'; font-size: 14px; } } .AccordionSection .descpAttributes { letter-spacing: -0.25px; color: #878787; opacity: 0.92; padding: 4px 16px 4px 8px; } .AccordionSection .descpDetails { letter-spacing: -0.25px; color: #000000; opacity: 0.92; padding: 4px 0; } .AccordionSection .descpicon { margin-right: 16px; } .AccordionSection .Offerslist { margin-bottom: 11px; } .AccordionSection .discount_icon { width: 16px; height: 16px; margin-right: 12px; } .AccordionSection .TClink { text-decoration: none; color: #2874f0; } .MuiAccordionSummary-expandIcon.Mui-expanded { transform: none !important; } //Responsive @media screen and (min-width : 320px) and (max-width : 767px) { .AccordionSection{ margin-top: 0; } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/accordion/sidebarAccordion.js ================================================ import React, { useRef, createRef } from "react"; import MuiAccordion from "@mui/material/Accordion"; import MuiAccordionSummary from "@mui/material/AccordionSummary"; import MuiAccordionDetails from "@mui/material/AccordionDetails"; import Typography from "@mui/material/Typography"; import { Grid } from "@mui/material"; import description_off from '../../assets/images/original/Contoso_Assets/Icons/plus.png' import description_on from '../../assets/images/original/Contoso_Assets/Icons/minus.png' import { useParams } from "react-router-dom"; const Accordion = (MuiAccordion); const AccordionSummary = (MuiAccordionSummary); const AccordionDetails = (MuiAccordionDetails); export default function SidebarAccordion(props) { const [expanded, setExpanded] = React.useState("panel1"); const { code } = useParams(); const dataType = props.id; const checkRef = useRef([]) checkRef.current = props.data && props.data.map((_, i) => checkRef.current[i] ?? createRef()); // const [color, setColorState] = React.useState({ blue : true }); const [checkedItems, setCheckedItems] = React.useState(new Map()); const handleChange = (panel) => (event, newExpanded) => { setExpanded(newExpanded ? panel : false); }; // const handleColorChange = (event) => { // setColorState({ ...color, [event.target.name]: event.target.checked }); // }; const handleChangeBrands = (e, dataType) => { const item = e.target.name; const isChecked = e.target.checked; setCheckedItems(checkedItems.set(item, isChecked)); props.onFilterChecked(e, dataType); }; React.useEffect(() => { if(code === 'all-products'){ props.data && props.data.forEach((item)=>{ setCheckedItems(new Map().set(`brand${item.id}`, false)); }); checkRef.current && checkRef.current.forEach((item)=>{ if(item.current.checked && item.current.checked === true){ item.current.checked = false; let ele = item.current; ele.target = item.current; ele.target.name = item.current.name; props.onFilterChecked(ele, dataType); } }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [code]); return (
) : ( ) } > Brand {props.data && props.data.map((item,key) => ( { handleChangeBrands(e, dataType) }} /> {/* { handleChangeBrands(e, dataType) }} name={`brand${item.id}`} color="primary" code={`${item.code || item.id}`} id={item.id} /> } label={item.name} /> */} ))}
); } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/breadcrumb/breadcrumb.js ================================================ import React from "react"; import { Link } from "react-router-dom"; import './breadcrumb.scss' const Breadcrumb = ({parentPath, parentUrl, currentPath}) => { return(

Home / {parentPath ? {parentPath} / : null} {currentPath ? {currentPath} : null}

); } export default Breadcrumb; ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/breadcrumb/breadcrumb.scss ================================================ .breadcrump { padding: 24px 70px; p { margin: 0; text-transform: capitalize; span { font-family: 'Heebo'; font-size: 14px; font-weight: 400; } b { font-family: 'Heebo'; font-size: 14px; font-weight: 500; a { color: #000000; &:hover { color: #2974f0 !important; text-decoration: none; } } } } } //Responsive @media screen and (min-width : 320px) and (max-width : 767px) { .breadcrump { padding: 12px 35px; } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/corousel/corousel.js ================================================ import React from 'react'; import Carousel from 'react-material-ui-carousel' import { Button, Grid } from '@mui/material' import './corousel.scss' export default function Corousel(props) { const { items } = props; return ( { items.map( (item, i) => ) } ) } function Item(props) { return (
{props.item.name}
{props.item.description}
{props.item.buttons && props.item.buttons.map((btn, key) => { return ( ) })}
) } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/corousel/corousel.scss ================================================ .courousel-style { padding: 70px; /* background-image: url('./assets/images/original/Contoso_Assets/Slider_section/hero_banner.jpg') !important; */ background-repeat: no-repeat !important; background-size: cover !important; background-position: right !important; min-height: 594px; } .courousel-style { background: #000000; cursor: pointer; max-height: 594px; } .courousel-style .BannerGrid { padding: 100px 100px 0px 70px !important; } .courousel-style .BannerHeading { text-align: left; font-family: var(--fontInter) !important; font-size: 34px !important; font-weight: 600 !important; color: #ffffff; opacity: 1; padding: 0px 0px 0px 0px; text-transform: capitalize; letter-spacing: -1.59px; } .courousel-style .BannerContent { text-align: left; font-family: var(--fontHeebo) !important; font-size: 16px !important; font-weight: 400 !important; color: #ffffff; opacity: 1; padding: 16px 0px 0px 0px; } .courousel-style .BannerButtondiv { padding: 50px 0px 0px 0px; } .courousel-style .BannerButton1 { max-width: 142px; opacity: 1; width: 224px; height: 49px; background: #2874F0 !important; border-radius: 5px; cursor: pointer; text-align: center; font-family: var(--fontHeebo) !important; font-size: 16px !important; font-weight: 500 !important; color: #ffffff; box-shadow: none; margin-right: 24px; } .courousel-style .BannerButton2 { max-width: 142px; opacity: 1; width: 224px; height: 49px; background: #666666 !important; border-radius: 5px; cursor: pointer; text-align: center; font-family: var(--fontHeebo) !important; font-size: 16px !important; font-weight: 500 !important; letter-spacing: -0.36px; color: #ffffff; box-shadow: none; margin-right: 24px; } /* Button test */ button[aria-label='Previous'] { margin-left: 70px; } button[aria-label='Next'] { margin-right: 70px; } @media screen and (min-width : 320px) and (max-width : 767px) { button[aria-label='Previous'] { margin-left: 10px; } button[aria-label='Next'] { margin-right: 10px; } .courousel-style { padding: 70px 50px 0 80px; background-position: unset !important; .BannerGrid { padding: 0 !important; } .BannerButton1, .BannerButton2 { margin: 10px 0 10px 0 !important; width: 100% !important; max-width: unset !important; } } } @media screen and (min-width : 768px) and (max-width : 899px) { .courousel-style { background-position: unset !important; } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/dropdowns/categories.js ================================================ import React from 'react'; import Button from '@mui/material/Button'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import MenuIcon from '@mui/icons-material/Menu'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; // import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; import { useNavigate } from 'react-router-dom'; import './categories.scss' const StyledMenu = ((props) => ( )); const StyledMenuItem = (MenuItem); export default function Categories(props) { const { categories } = props const history = useNavigate(); const [anchorEl, setAnchorEl] = React.useState(null); const handleClick = (event) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; const redirectUrl = (url) => { history(url) } return (
{categories.categorylist.map((item, key) => { return ( redirectUrl(item.url)} key={key}> ) })}
); } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/dropdowns/categories.scss ================================================ .mainHeader .categories-btn { min-width: 198px; background-color: #F8F8F8 !important; border: 1px solid #EFEFEF !important; } .dark .mainHeader .categories-btn { color: #000000 !important; } #customized-menu .MuiPopover-paper { left: 70px !important; } #customized-menu .MuiMenu-paper { box-shadow: 0px 3px 26px #00000029; border: 1px solid #E5E5E5; border-radius: 8px; } #customized-menu .MuiPopover-paper .MuiTypography-root { font-family: 'Poppins' !important; font-size: 16px; } #customized-menu .MuiPopover-paper .MuiListItemIcon-root { min-width: 40px; } #customized-menu .MuiPopover-paper ul li { min-width: 341px; padding: 12px 19px; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/footer/footer.js ================================================ import React, { useCallback } from 'react' import { Link } from 'react-router-dom'; import { Grid } from '@mui/material'; import phoneLogo from '../../assets/images/original/Contoso_Assets/Icons/telephone_icon.svg' import emailLogo from '../../assets/images/original/Contoso_Assets/Icons/email_icon.svg' import { ReactComponent as Logo } from '../../assets/images/logo-horizontal.svg'; import './footer.scss' const Footer = () => { const getLocation = useCallback(() => { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(showPosition); } else { setStatus("Geolocation is not supported by this browser."); } }, []); React.useEffect(() => { getLocation(); }, [getLocation]); const [lat, setLat] = React.useState(null); const [lng, setLng] = React.useState(null); const [status, setStatus] = React.useState(null); const [location, setLocation] = React.useState(null); function showPosition(position) { setLat(position.coords.latitude); setLng(position.coords.longitude); let point = position.coords.latitude + ',' + position.coords.longitude const geoApiUrlForAddress = `${process.env.REACT_APP_GEOAPIBASEURL}/Locations/${point}?key=${process.env.REACT_APP_BINGMAPSKEY}`; fetch(geoApiUrlForAddress) .then(res => res.json()) .then(dat => { setStatus(null); let address = dat.resourceSets[0].resources[0].address.locality + ', ' + dat.resourceSets[0].resources[0].address.countryRegion; setLocation(address) }) } return (

Contoso Traders is an e-commerce platform that specializes in electronic items. Our website offers a wide range of electronics, including smartphones, laptops, and other popular gadgets. We pride ourselves on providing high-quality products at competitive prices, and our dedicated customer service team is always on hand to assist with any queries or concerns. With fast and secure shipping, convenient payment options, and a user-friendly interface, Contoso Traders is the perfect place to shop for all your electronic needs.

  • Catalog
  • All Products
  • Controllers
  • Laptops
  • Monitors
  • Legals
  • Terms of Service
  • Refund Policy
  • About Us
  • Contact us
  • Feel free to get in touch with us via phone or send us a message
  • phone-logo
    {' '}+123456768910
  • email-logo
    {' '}support@contosotraders.com
  • Your Current location
  • {status &&
  • {status}
  • } {lat && } {lng && } {location &&
    {location}
    } {lat && lng ?
    : null}
) } export default Footer ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/footer/footer.scss ================================================ // ----------------------------------------------------------------------------- // This file contains all styles related to the footer of the site/application. // ----------------------------------------------------------------------------- @import "../../main.scss"; .foo { background-color: $color-background-medium; display: flex; flex-wrap: wrap; padding: $padding-l; &__content { align-items: center; display: flex; flex-direction: column; margin-left: auto; margin-right: auto; width: 100%; @media (min-width: $mq-s) { width: $grid-max-width-s; } @media (min-width: $mq-m) { width: $grid-max-width-m; } @media (min-width: $mq-l) { width: $grid-max-width-l; } @media (min-width: $mq-xl) { width: $grid-max-width-xl; } } &__disclaimer { margin-top: $margin-s; } &-text { @include text-xs(); color: $color-text-primary; margin-top: $margin-m; text-align: center; margin-bottom: 0; } svg { fill: $color-primary; display: block; margin-bottom: $margin-m; } &__links { margin-top: $margin-m; display: flex; flex-direction: column; flex-wrap: wrap; text-align: center; @media (min-width: $mq-m) { margin-top: $margin-s; flex-direction: row; } } &__link { @include text-s(); color: $color-text-primary; font-weight: $font-weight-medium; white-space: nowrap; &:not(:last-of-type) { @media (min-width: $mq-m) { margin-right: $margin-l; } } } } /* sha********************* */ .footer-container { padding: 60px 70px; font-size: smaller; } .footer-container .MuiGrid-container { justify-content: space-around; } .footer-container .footer-grid-container ul { list-style: none; } .footer-container .footer-grid-container li { padding-bottom: 20px; } .footer-container .footer-grid-container li a { color: #302F2F; text-decoration: none !important; } .footer-container .footer-grid-container p { font-family: 'Roboto' !important; font-size: 16px !important; color: #302F2F !important; } .footer-container .section-2 { padding-left: 20px; } .footer-container .section-3 { padding-left: 60px; } .footer-container .section-4 { padding-left: 100px; float: right; } .footer-container .contact-div { display: inline; } .footer-container .logo-div { float: left; padding-right: 5px; } .footer-container .list-element { font-family: 'Roboto' !important; font-size: 14px !important; color: #302F2F !important; } .footer-container .main-element { font-family: 'Poppins' !important; font-size: 14px !important; color: #302F2F !important; font-weight: 600; } //Responsive @media screen and (min-width : 320px) and (max-width : 767px) { .footer-container { padding: 30px 35px; .section-2, .section-3, .section-4 { padding: 0; ul { padding: 0; } } #mapviewer { iframe { width: 100%; } } } } @media screen and (min-width : 900px) and (max-width : 1350px) { #mapviewer { iframe { width: 100%; } } } @media screen and (min-width : 768px) and (max-width : 899px) { .footer-container { padding: 30px 35px; .section-2, .section-3, .section-4 { padding: 0; ul { padding: 0; } } } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/header/appbar.js ================================================ import React, { useCallback, useEffect, useRef } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { connect } from 'react-redux'; import { AppBar, InputAdornment, TextField, Button } from '@mui/material'; //#region Uncomment below lines to run dark mode tests // import {FormGroup, FormControlLabel, Switch } from '@mui/material'; //#endregion import Toolbar from '@mui/material/Toolbar'; import IconButton from '@mui/material/IconButton'; import Badge from '@mui/material/Badge'; import MenuItem from '@mui/material/MenuItem'; import Menu from '@mui/material/Menu'; import AccountCircle from '@mui/icons-material/AccountCircle'; // import MenuIcon from '@mui/icons-material/Menu'; import MailIcon from '@mui/icons-material/Mail'; import NotificationsIcon from '@mui/icons-material/Notifications'; import Logo from '../../assets/images/logo-horizontal.svg'; import SearchIconNew from '../../assets/images/original/Contoso_Assets/Icons/image_search_icon.svg' import ProfileIcon from '../../assets/images/original/Contoso_Assets/Icons/profile_icon.svg' import BagIcon from '../../assets/images/original/Contoso_Assets/Icons/cart_icon.svg' import UploadFile from '../uploadFile/uploadFile'; import { clickAction, submitAction, handleThemeChange, getCartQuantity } from '../../actions/actions'; import AuthB2CService from '../../services/authB2CService'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; // import Alert from "react-s-alert"; import personal_information_icon from "../../assets/images/original/Contoso_Assets/profile_page_assets/personal_information_icon.svg"; import logout_icon from "../../assets/images/original/Contoso_Assets/profile_page_assets/logout_icon.svg"; // import delete_icon from "../../assets/images/original/Contoso_Assets/profile_page_assets/delete_icon.svg"; import { CartService, ProductService } from '../../services'; import './header.scss' const StyledMenu = ((props) => ( )); const StyledMenuItem = (MenuItem); function TopAppBar(props) { const history = useNavigate(); const searchRef = useRef(); const [anchorEl, setAnchorEl] = React.useState(null); const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = React.useState(null); const [searchUpload, setSearchUpload] = React.useState(false) const [mobileSearch, setMobileSearch] = React.useState(false) const authService = new AuthB2CService(); const isMenuOpen = Boolean(anchorEl); const isMobileMenuOpen = Boolean(mobileMoreAnchorEl); React.useEffect(() => { if (searchUpload === true) { window.addEventListener('click', function (e) { if (!document.getElementById('searchbox').contains(e.target)) { setSearchUpload(false) } }); } }, [searchUpload]); // React.useEffect(() => { // setSearchUpload(false) // }, [history.location.pathname]); const redirectUrl = (url) => { history(url); } const handleProfileMenuOpen = (event) => { setAnchorEl(event.currentTarget); }; const handleMobileMenuClose = () => { setMobileMoreAnchorEl(null); }; const handleMenuClose = () => { setAnchorEl(null); handleMobileMenuClose(); }; const handleMobileMenuOpen = (event) => { // setMobileMoreAnchorEl(event.currentTarget); setMobileSearch(!mobileSearch) }; const { loggedIn } = props.userInfo; const getQuantity = useCallback(async () => { if (props.userInfo.token) { const shoppingcart = await CartService.getShoppingCart( props.userInfo.token ); if (shoppingcart) { let quantity = shoppingcart.length; props.getCartQuantity(quantity) } if (localStorage.getItem('cart_items')) { const email = localStorage.getItem('state') ? JSON.parse(localStorage.getItem('state')).userName : null var tempProps = JSON.parse(localStorage.getItem('cart_items')); tempProps.map(async (temp) => { temp.email = email; temp.id = temp.productId; Object.preventExtensions(temp); const productToCart = await CartService.addProduct(props.userInfo.token, temp) if (productToCart.errMessage) { // adding product to cart failed } else { getQuantity() } }) localStorage.removeItem('cart_items') } } else { let cartItem = localStorage.getItem('cart_items') ? JSON.parse(localStorage.getItem('cart_items')) : [] let quantity = cartItem.length; props.getCartQuantity(quantity) } }, [props]) useEffect(() => { getQuantity() // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onClickLogIn = async () => { let user = await authService.login(); if (user) { user['loggedIn'] = true; user['isB2c'] = true; user['token'] = sessionStorage.getItem('msal.idtoken'); localStorage.setItem('state', JSON.stringify(user)) props.submitAction(user); window.location.reload() } } const onClickLogout = () => { localStorage.clear(); if (props.userInfo.isB2c) { authService.logout(); } props.clickAction(); history('/'); } const setTextSearch = () => { if (searchRef.current.value.length > 0) { let searchData = searchRef.current.value; ProductService.getSearchResults(searchData) .then((relatedProducts) => { searchRef.current.value = ''; if (relatedProducts.length > 1) { history("/suggested-products-list", { state: { relatedProducts }, }); } else if (relatedProducts.length === 1) { history(`/product/detail/${relatedProducts[0].id}`); } else { props.history.push("/suggested-products-list", { state: { relatedProducts }, }); } }) .catch(() => { searchRef.current.value = ''; // Alert.error("There was an error, please try again", { // position: "top", // effect: "scale", // beep: true, // timeout: 6000, // }); });//search function } } React.useEffect(() => { const listener = event => { if (searchRef.current.value.length > 0 && (event.code === "Enter" || event.code === "NumpadEnter")) { event.preventDefault(); setTextSearch(); } }; document.addEventListener("keydown", listener); return () => { document.removeEventListener("keydown", listener); }; }); const menuId = 'primary-search-account-menu'; const renderMenu = ( redirectUrl('/profile/personal')}> {/* redirectUrl('/profile/orders')}> */} {/* redirectUrl('/profile/wishlist')}> redirectUrl('/profile/address')}> */} ); const mobileMenuId = 'primary-search-account-menu-mobile'; const renderMobileMenu = (

Messages

Notifications

Profile

); useEffect(() => { if (window.innerWidth >= '768') { setMobileSearch(true) } }, []); return (
{mobileSearch && }
{loggedIn && loggedIn ?
{/* redirectUrl('/wishlist')}> iconimage */} iconimage {/* redirectUrl('/cart')} > iconimage */}
: // null process.env.REACT_APP_B2CCLIENTID && <> } redirectUrl('/cart')} > iconimage {/* #region Uncomment below lines to run dark mode tests */} {/* props.handleThemeChange(e.target.checked)}/>} label="Dark Mode" /> */} {/* #endregion */}
iconimage
{renderMobileMenu} {renderMenu}
); } const mapStateToProps = (state) => { return { userInfo: state.login.userInfo, theme: state.login.theme, quantity: state.login.quantity } }; const mapDispatchToProps = (dispatch) => ({ handleThemeChange: (value) => dispatch(handleThemeChange(value)), submitAction: (user) => dispatch(submitAction(user)), clickAction: () => dispatch(clickAction()), getCartQuantity: (value) => dispatch(getCartQuantity(value)), }) export default (connect(mapStateToProps, mapDispatchToProps)(TopAppBar)); ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/header/header.js ================================================ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; // import { NamespacesConsumer } from 'react-i18next'; import { ConfigService } from '../../services'; import AuthB2CService from '../../services/authB2CService'; // import { withRouter } from "react-router-dom"; // import LoginContainer from './components/loginContainer'; // import LoginComponent from './components/loginComponent'; // import UserPortrait from './components/userPortrait'; import { ReactComponent as Close } from '../../assets/images/icon-close.svg'; // import { ReactComponent as Hamburger } from '../../assets/images/icon-menu.svg'; // import { ReactComponent as Cart } from '../../assets/images/icon-cart.svg'; import { clickAction, submitAction } from "../../actions/actions"; import Categories from '../dropdowns/categories'; // const Login = LoginContainer(LoginComponent); import laptopsImg from '../../assets/images/original/Contoso_Assets/Mega_menu_dropdown_assets/laptop_icon.svg'; import controllersImg from '../../assets/images/original/Contoso_Assets/Mega_menu_dropdown_assets/controllers_icon.svg'; import desktopsImg from '../../assets/images/original/Contoso_Assets/Mega_menu_dropdown_assets/desktops_icon.svg'; import mobilesImg from '../../assets/images/original/Contoso_Assets/Mega_menu_dropdown_assets/mobiles_icon.svg'; import monitorImg from '../../assets/images/original/Contoso_Assets/Mega_menu_dropdown_assets/monitor_icon.svg'; import ProfileIcon from '../../assets/images/original/Contoso_Assets/Icons/profile_icon.svg' import BagIcon from '../../assets/images/original/Contoso_Assets/Icons/cart_icon.svg' import logout_icon from "../../assets/images/original/Contoso_Assets/profile_page_assets/logout_icon.svg"; import { Badge, IconButton } from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import LoginIcon from '@mui/icons-material/Login'; class Header extends Component { constructor() { super(); this.authService = new AuthB2CService(); this.state = { isopened: false, ismodalopened: false, profile: {}, UseB2C: null }; this.loginModalRef = React.createRef(); } async componentDidMount() { this.loadSettings(); if (this.props.userInfo.token) { // const profileData = await UserService.getProfileData(this.props.userInfo.token); // this.setState({ ...profileData }); } const setComponentVisibility = this.setComponentVisibility.bind(this); setComponentVisibility(document.documentElement.clientWidth); window.addEventListener('resize', function () { setComponentVisibility(document.documentElement.clientWidth); }); } loadSettings = async () => { await ConfigService.loadSettings(); const UseB2C = ConfigService._UseB2C; this.setState({ UseB2C }) } setComponentVisibility(width) { if (width > 1280) { this.setState({ isopened: false }); } } toggleClass = () => { this.setState(prevState => ({ isopened: !prevState.isopened, })); }; toggleModalClass = () => { if (!document.body.classList.contains("is-blocked")) { document.body.classList.add("is-blocked"); } else { document.body.classList.remove("is-blocked"); } this.setState(prevState => ({ ismodalopened: !prevState.ismodalopened })); }; onClickClose = () => { this.toggleModalClass(); } onClickLogout = () => { localStorage.clear(); if (this.props.userInfo.isB2c) { this.authService.logout(); } this.props.clickAction(); this.props.history.push('/'); } onClickLogIn = async () => { let user = await this.authService.login(); if (user) { user['loggedIn'] = true; user['isB2c'] = true; user['token'] = sessionStorage.getItem('msal.idtoken'); localStorage.setItem('state', JSON.stringify(user)) this.props.submitAction(user); } } render() { const categories = { title: 'All Categories', categorylist: [ { name: 'Laptops', url: '/list/laptops', img: laptopsImg }, { name: 'Controllers', url: '/list/controllers', img: controllersImg }, { name: 'Desktops', url: '/list/desktops', img: desktopsImg }, { name: 'Mobiles', url: '/list/mobiles', img: mobilesImg }, { name: 'Monitors', url: '/list/monitors', img: monitorImg }, ] } // const { profile } = this.state; const { loggedIn } = this.props.userInfo; return (
{/* {this.state.ismodalopened ? : null} */}
); } } const mapStateToProps = (state) => { return { userInfo : state.login.userInfo, theme : state.login.theme, quantity : state.login.quantity } }; export default (connect(mapStateToProps, { clickAction, submitAction })(Header)); ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/header/header.scss ================================================ // ----------------------------------------------------------------------------- // This file contains all styles related to the header of the site/application. // ----------------------------------------------------------------------------- @import "../../main.scss"; .header { align-items: center; display: flex; height: auto; // justify-content: space-between; padding-top: 37px; padding-left: 70px; padding-right: 70px; overflow: hidden; svg { fill: $color-primary; height: $icon-l; &:hover { cursor: pointer; } } >svg { // Logo height: 2.35625rem; width: 10.875rem; min-width: 10.875rem; } } .main-nav { align-items: center; background-color: #eaf2f9; //$color-background-primary; display: flex; height: 100vh; order: 2; // overflow: hidden; // padding: $space-m; position: fixed; right: -100vw; text-align: center; text-transform: uppercase; top: 0; transition: right $animation-speed-regular $animation-type-regular; width: 60%; will-change: right; z-index: $z-index-l; @media (min-width: $mq-sl) { background-color: transparent; height: auto; // justify-content: space-around; order: 0; position: relative; right: auto; top: auto; &__actions { display: none; } button:last-child { display: none; } } &:not(.is-opened) .main-nav__item { color: $color-text-primary; } &.is-opened { flex-direction: column; padding-top: $padding-xl; overflow: auto; right: 0; .main-nav__item { display: block; color: $color-text-tertiary; font-weight: $font-weight-medium; margin-bottom: $margin-m; &:nth-child(7) { margin-bottom: $margin-xl; } } button.main-nav__item { background-color: transparent; border: 0; } .btn-close { display: block; position: absolute; right: 20px; top: 20px; svg { fill: $color-svg-tertiary; } } } &__item { @include text-xxs(); color: $color-link; display: block; text-decoration: none; text-transform: uppercase; white-space: nowrap; &:hover { cursor: pointer; text-decoration: underline; } } } .secondary-nav { display: flex; justify-content: space-around; min-width: 13rem; width: 10rem; svg:nth-child(2) { display: none; } @media (min-width: $mq-sl) { svg:nth-child(2) { display: inline; } button:last-child { display: none; } } &__cart { position: relative; &-number { display: flex; align-items: center; justify-content: center; position: absolute; width: 1.25rem; height: 1.25rem; font-size: 0.5rem; border-radius: 50%; background-color: $color-background-light; border: 1px solid currentColor; top: -.5rem; right: -.5rem; color: $color-text-primary; } } &__login { color: $color-text-primary; cursor: pointer; } .portrait { width: 1.875rem; height: 1.875rem; border-radius: 50%; } } //Profile dropdown popup in header #profile-dropdown .MuiPopover-paper { right: 122px !important; left: unset !important; } #profile-dropdown .MuiMenu-paper { box-shadow: 0px 3px 26px #00000029; border: 1px solid #E5E5E5; border-radius: 8px; } #profile-dropdown .MuiPopover-paper .MuiTypography-root { font-family: 'Poppins' !important; font-size: 16px; } #profile-dropdown .MuiPopover-paper .MuiListItemIcon-root { min-width: 40px; } #profile-dropdown .MuiPopover-paper ul li { min-width: 336px; padding: 12px 19px; } /* Header Message */ .headerMessageDiv { height: 55px; font-family: 'Poppins'; letter-spacing: 0.42px; font-weight: 500; display: flex; align-items: center; justify-content: center; } .headerMessageDiv.warning { background-color: #F0A428; color: #fff; } .headerMessageDiv.error { background-color: #E81D1D; color: #fff; } //Top bar of application : Appbar .appbar { box-shadow: none !important; padding-left: 70px; padding-right: 70px; padding-top: 31px; } .appbar .searchBar { width: 41%; position: relative; margin-left: 50px; margin-right: 16px; border-radius: 4px; background-color: rgba(255, 255, 255, 0.15); } .appbar .sectionDesktop { display: flex; } .appbar .sectionMobile { display: flex; } .appbar .searchBar input { font-family: 'Roboto' !important; font-size: 14px !important; font-weight: 300 !important; } .appbar .searchBar .MuiOutlinedInput-adornedEnd { height: 48px; padding-right: 8px; } .appbar .searchBar input::placeholder { font-family: 'Roboto' !important; font-size: 14px !important; font-weight: 300 !important; color: #686868 !important; } .appbar .iconButton { padding: 12px 18px; } .appbar .iconButton:focus { outline: none !important; } .appbar .headerLogo { height: 71px; width: 195px; } .appbar svg { fill: #2f4b66; overflow: unset; } .appbar .searchBtn { border-radius: 5px; padding: 5px; } .appbar .searchBtn:focus { outline: none; } .appbar .upload__subtitle { font-family: 'Roboto' !important; } //Main menu .main-nav { margin-left: 50px; } .main-nav__item { text-decoration: none !important; text-transform: capitalize !important; text-align: left; font-family: 'Roboto' !important; letter-spacing: 0px !important; color: #302F2F !important; opacity: 0.92 !important; font-size: 14px !important; font-weight: 400 !important; padding: 14px 24px 20px 24px; text-align: center; } .main-nav__item:hover { text-decoration: none !important; text-transform: capitalize !important; text-align: left; font-family: 'Roboto', sans-serif !important; letter-spacing: 0px !important; color: #2F4B66 !important; opacity: 0.92 !important; font-size: 14px; font-weight: 500; padding: 14px 24px 14px 24px; border-bottom: 5px solid #2874F0 !important; background: rgba(40, 116, 240, 0.1) !important; text-align: center; } .main-nav__item_active { text-decoration: none !important; text-transform: capitalize !important; text-align: left; font-family: 'Roboto', sans-serif !important; letter-spacing: 0px !important; color: #2F4B66 !important; opacity: 0.92 !important; font-size: 14px; font-weight: 500; padding: 14px 24px 20px 24px; border-bottom: 5px solid #2874F0 !important; background: rgba(40, 116, 240, 0.1) !important; text-align: center; } //dark mode .theme-class>label { border: 1px solid lightgray; background-color: #fff; border-radius: 20px; padding: 3px 12px; margin: 0; color: #000; } //Responsive @media (max-width: 65em) { .sectionDesktop { display: none !important; } .mainHeader .categories-btn>.MuiButton-startIcon { display: none !important; } .mainHeader .categories-btn { min-width: 135px !important; padding: 0; } .appbar .iconButton { display: none; } .secondary-nav { justify-content: end; align-items: center; } .header { justify-content: space-between; } .mainHeader { position: relative; } .appbar .searchbar-upload { padding: 15px; } .appbar .searchbar-upload .upload p { font-size: 12px; } .appbar .searchbar-upload .upload>div { min-height: 160px; } .main-nav__item { font-weight: bold !important; } .home, .list, .ProductContainerSectionMain, .profileMain, .CartMain, .refund-policy-section { margin-top: 0 !important; } } @media screen and (min-width : 320px) and (max-width : 340px) { .secondary-nav { justify-content: start; align-items: center; } } @media screen and (min-width : 320px) and (max-width : 767px) { .appbar{ padding: 0 15px !important; flex-direction: column !important; } .appbar > div { flex-direction: column !important; } .appbar .searchBar{ margin: 0; width: 100%; } .appbar .sectionMobile{ position: absolute; z-index: 1; right: 0; top: 15px; } .header{ padding: 0 15px !important; justify-content: space-between; } .theme-class{ padding-top: 10px; position: relative; width: 100%; } .theme-class > label{ position: absolute; top: 60px; min-width: 140px; right: 0; } } @media screen and (min-width : 320px) and (max-width : 1039px) { .theme-class > label > span { font-size: 11px; } .dark .main-nav{ background-color: #302F2F !important; } .dark .main-nav__item{ color: #fff !important; } .dark .header img { background-color: #fff; border-radius: 50%; padding: 3px; } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/header/headerMessage.js ================================================ import React from 'react'; function HeaderMessage(props) { const { type, icon, message } = props return (

{message}

); } export default HeaderMessage; ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/imageSlider/imageSlider.js ================================================ import React from "react"; import productdetailimg1 from "../../assets/images/original/Contoso_Assets/product_page_assets/product_image_main.jpg"; import productdetailimg2 from "../../assets/images/original/Contoso_Assets/product_page_assets/image_2.jpg"; import productdetailimg3 from "../../assets/images/original/Contoso_Assets/product_page_assets/image_3.jpg"; import productdetailimg4 from "../../assets/images/original/Contoso_Assets/product_page_assets/image_4.jpg"; import chevron_up from "../../assets/images/original/Contoso_Assets/product_page_assets/chevron_up.svg"; import chevron_down from "../../assets/images/original/Contoso_Assets/product_page_assets/chevron_down.svg"; import { Button } from "@mui/material"; import './imageSlider.scss' function ImageSlider(props) { const [min, setMin] = React.useState(0) const [max, setMax] = React.useState(4) const sliderImages = [ { id : 1, img : productdetailimg1 }, { id : 2, img : props.imageUrl }, { id : 3, img : productdetailimg3 }, { id : 4, img : productdetailimg4 }, { id : 2, img : productdetailimg2 }, { id : 1, img : productdetailimg1 }, { id : 4, img : productdetailimg4 }, { id : 3, img : productdetailimg3 }, ]; const decrementMinMax = () => { setMin(min-4) setMax(max-4) } const incrementMinMax = () => { setMin(min+4) setMax(max+4) } return (
{sliderImages.slice(min,max).map((item, key)=>(
image1props.setSliderImg(item.img)} />
))}
); } export default ImageSlider; ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/imageSlider/imageSlider.scss ================================================ .slidesection { margin-right: 16px; display: flex; justify-content: center; } .slideimagelist { flex-direction: row-reverse; .imgdiv.active { border: 2px solid #2874F0; } .imgdiv { width: 100px; height: 100px; margin: 4px 0; } } .slideimage { width: 100%; } .chevron_prevbtn { margin-bottom: 8px; } .chevron_nextbtn { margin-top: 10px; } .chevron_btn { width: 100px; height: 38px; background: #ffffff 0% 0% no-repeat padding-box; border: 1px solid #2F4B66 !important; border-radius: 2px; opacity: 1; } .chevron_btn.disabled { border: 1px solid #e5e5e5 !important; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/loadingSpinner/loadingSpinner.js ================================================ import React from "react"; import './loadingSpinner.scss' const LoadingSpinner = () => (
); export default LoadingSpinner; ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/loadingSpinner/loadingSpinner.scss ================================================ @import '../../styles/abstracts/mixins/loader'; .loader-wrapper { align-items: center; display: flex; height: 100vh; justify-content: center; } .loader { @include loader(); } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/minimalSelect/minimalSelect.js ================================================ import React, { useState } from 'react'; import minimalSelectStyles from './minimalSelect.styles'; import Select from '@material-ui/core/Select'; import MenuItem from '@material-ui/core/MenuItem'; import FormControl from '@material-ui/core/FormControl'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; const MinimalSelect = () => { const [val,setVal] = useState(1); const handleChange = (event) => { setVal(event.target.value); }; const minimalSelectClasses = minimalSelectStyles; const iconComponent = (props) => { return ( )}; return ( ); }; export default MinimalSelect; ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/minimalSelect/minimalSelect.scss ================================================ .minimalSelect { background-color: #0000000A; min-width: 238px; padding: 18px 0 18px 32px; border-radius: 114px; height: 56px; } .minimalSelect svg { margin-right: 32px; } .minimalSelect .MuiSelect-select { background-color: transparent !important; font-family: var(--fontInter); font-weight: 500; color: #626262; font-size: 16px; } .minimalSelect .MuiSelect-select:focus { background-color: transparent !important; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/minimalSelect/minimalSelect.styles.js ================================================ import { deepPurple } from '@material-ui/core/colors'; export default () => ({ select: { minWidth: 200, background: 'white', color: deepPurple[500], fontWeight:200, borderStyle:'none', borderWidth: 2, borderRadius: 12, paddingLeft: 24, paddingTop: 14, paddingBottom: 15, boxShadow: '0px 5px 8px -3px rgba(0,0,0,0.14)', "&:focus":{ borderRadius: 12, background: 'white', borderColor: deepPurple[100] }, }, icon:{ color: deepPurple[300], right: 12, position: 'absolute', userSelect: 'none', pointerEvents: 'none' }, paper: { borderRadius: 12, marginTop: 8 }, list: { paddingTop:0, paddingBottom:0, background:'white', "& li":{ fontWeight:200, paddingTop:12, paddingBottom:12, }, "& li:hover":{ background: deepPurple[100] }, "& li.Mui-selected":{ color:'white', background: deepPurple[400] }, "& li.Mui-selected:hover":{ background: deepPurple[500] } } }); ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/productCard/product.js ================================================ import React from 'react'; import Card from '@mui/material/Card'; import CardMedia from '@mui/material/CardMedia'; import CardContent from '@mui/material/CardContent'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; // import WishlistIcon from '../../../../assets/images/original/Contoso_Assets/Icons/wishlist_icon.svg' import { useNavigate } from 'react-router-dom'; import './product.scss' export default function Product(props) { const navigate = useNavigate() const { prodImg, imageUrl, name, price, id, type } = props; const productDetailPage = (id = 1) => { navigate('/product/detail/'+id) } const discountOffer = (price) => { let dicsount = price - ((price/100)*15) return( ${parseInt(dicsount).toFixed(2)} ) } return ( productDetailPage(id)}>
{name?name:'Lunar Shift Special Edition'} {/* like */}
{type?type.name:'Controller'}
{discountOffer(price)} ${price?price.toFixed(2):'00.00'} 15% OFF
); } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/productCard/product.scss ================================================ .productCard { box-shadow: none !important; } .productCard:hover { cursor: pointer; box-shadow: 0px 3px 6px #00000029 !important; border: 1px solid #E5E5E5; border-radius: 4px; } .productCard .MuiCardMedia-root { width: auto; height: 306px; margin: auto; background-size: contain; } .productCard .MuiCardContent-root { padding: 8px 8px 0 8px !important; } .productCard .wishlist_icon { padding: 0; } .productName { font-family: var(--fontInter) !important; font-weight: 600 !important; font-size: 16px !important; color: #171520 !important; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .productType { font-family: var(--fontInter) !important; font-weight: 400 !important; font-size: 14px !important; color: #626262 !important; } .productOrgPrice { font-family: var(--fontInter) !important; font-weight: 500 !important; font-size: 20px !important; color: #171520 !important; } .productOldPrice { font-family: var(--fontInter) !important; font-weight: 400 !important; font-size: 14px !important; color: #626262 !important; text-decoration: line-through; } .productOffer { font-family: var(--fontInter) !important; font-weight: 400 !important; font-size: 14px !important; color: #E21D1D !important; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/quantityCounter/productCounter.js ================================================ import React, { Component } from 'react'; import { CartService } from '../../services'; import './productCounter.scss' export default class QuantityPicker extends Component { constructor(props) { super(props); this.state = { value: this.props.qty || this.props.min, disableDec: true, disableInc: false } this.increment = this.increment.bind(this); this.decrement = this.decrement.bind(this); } //cartItemId componentDidMount() { if(this.state.value > this.props.min){ this.setState({ disableDec: false }); } if(this.state.value >= this.props.max){ this.setState({ disableInc: true }); } } componentDidUpdate(prevProps, prevState) { if (prevState.value !== this.state.value && this.props?.page === 'cart') { this.updateProductQty() } if (this.props.setQty) { this.props.setQty(this.state.value) } } async updateProductQty() { if(this.props.loggedIn){ let response = await CartService.updateQuantity(this.props.detailProduct, this.state.value, this.props.token) if (response) { this.props.getCartItems() } }else{ let cart_items = JSON.parse(localStorage.getItem('cart_items')); let objIndex = cart_items.findIndex((obj => obj.productId === this.props.detailProduct.productId)); cart_items[objIndex].quantity = this.state.value localStorage.setItem('cart_items',JSON.stringify(cart_items)) this.props.getCartItems() } } increment() { const plusState = this.state.value + 1; if (this.state.value < this.props.max) { this.setState({ value: plusState }); this.setState({ disable: false }); } if (this.state.value === (this.props.max - 1)) { this.setState({ disableInc: true }); } if (this.state.value === this.props.min) { this.setState({ disableDec: false }); } } decrement() { const minusState = this.state.value - 1; if (this.state.value > this.props.min) { this.setState({ value: minusState }); if (this.state.value === this.props.min + 1) { this.setState({ disableDec: true }); } } else { this.setState({ value: this.props.min }); } if (this.state.value === this.props.max) { this.setState({ disableInc: false }); } } render() { const { disableDec, disableInc } = this.state; return ( ); } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/quantityCounter/productCounter.scss ================================================ /*---------------------------------quantitypicker----------------------------*/ .quantity-picker { display: inline-flex; width: 89px; height: 40px; align-items: center; justify-content: space-around; vertical-align: middle; border: 1px solid #707070; border-radius: 8px; } .quantity-input:focus { background: red; } .quantity-modifier, .quantity-display { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; outline: none; } .quantity-modifier { font-size: 1.5rem; background: none !important; border: 0 solid #dbdbdb; text-align: center; cursor: pointer; width: 24px; color: #171520 !important; } .quantity-modifier:hover { background: #dadada; color: #555555; } .quantity-modifier:focus { outline: 0; } .left-modifier { border-radius: 3px 0 0 3px; } .mod-disable { color: #d9d9d9 !important; } .mod-disable:hover { color: #d9d9d9; } .right-modifier { border-radius: 0 3px 3px 0; } .quantity-display { font-family: "Inter"; font-weight: 600; font-size: 14px; width: 23px; height: 28px; border: 0; text-align: center; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/shared/index.js ================================================ import Header from '../header/header'; import Appbar from '../header/appbar'; import Footer from '../footer/footer'; import HeaderMessage from '../header/headerMessage'; export { Header, Appbar, Footer, HeaderMessage, }; ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/slider/slider.js ================================================ import React from 'react'; import Carousel from 'react-material-ui-carousel' import { Grid } from '@mui/material' import Product from '../productCard/product'; import productImg1 from '../../assets/images/original/Contoso_Assets/Caurosal/product_1.jpg' import productImg2 from '../../assets/images/original/Contoso_Assets/Caurosal/product_2.jpg' import productImg3 from '../../assets/images/original/Contoso_Assets/Caurosal/product_3.jpg' import productImg4 from '../../assets/images/original/Contoso_Assets/Caurosal/product_4.jpg' import productImg5 from '../../assets/images/original/Contoso_Assets/Caurosal/product_5.jpg' export default function Slider(props) { var items = [ { name: "The Fastest, Most Powerful Xbox Ever.", description: "Elevate your game with the all-new Xbox Wireless Controller - Lunar Shift Special Edition", page: 1 }, { name: "The Fastest, Most Powerful Xbox Ever.", description: "Elevate your game with the all-new Xbox Wireless Controller - Lunar Shift Special Edition", page: 2 }, ] return (
{props.firstHeading}

{props.secondHeading}

{ items.map((item, i) => ) }
) } function Item(props) { return (
{/*
{props.firstHeading}

{props.secondHeading}

*/}
{props.item.page === 1 ? : null} {props.item.page === 2 ? : null}
) } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/slider/slider.scss ================================================ .slider-style { padding: 0 70px; } .slider-style .productCard { box-shadow: none !important; } .slider-style .productCard .MuiCardMedia-root { width: auto !important; height: 270px !important; margin: auto; } .slider-style .productCard:hover { box-shadow: 0px 1px 6px rgba(0, 0, 0, 0.16) !important; border: 1px solid #E5E5E5; border-radius: 4px; cursor: pointer; } /* product slider */ .product-slider-corousel>div { min-height: 454px; } .slider-section button[aria-label='Previous'] { margin-left: 86px; } .slider-section button[aria-label='Next'] { margin-right: 86px; } .slider-section .topSection { display: flex; margin-bottom: 30px; align-items: center; padding: 70px 70px 0 70px; } .slider-section .LapHeadSection { margin-right: auto; } .slider-section .LapButtonSection { display: flex; } .slider-section .MuiGrid-root { overflow: hidden; } .slider-section .LapSectionContent>div { width: calc(112% + 40px) !important; flex-wrap: nowrap !important; } .slider-section .LapSectionContent .MuiCard-root { margin: auto !important; } .slider-section .LapHead { font: normal normal normal 14px/21px "Poppins"; letter-spacing: -0.31px; color: #302f2f; text-transform: capitalize; opacity: 1; } .slider-section .LapHeadmain { line-height: unset !important; font: normal normal 600 25px/56px "Poppins"; color: #111111; text-transform: uppercase; opacity: 1; margin-bottom: 0; } @media screen and (max-width : 1250px) { .slider-style .LapSectionContent>div { width: calc(100% + 40px) !important; } .slider-style .LapSectionContent>div>div:last-child { display: none; } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/uploadFile/uploadFile.js ================================================ import React from "react"; import { connect } from 'react-redux'; // import { withRouter } from "react-router-dom"; // import Alert from "react-s-alert"; import { ProductService } from '../../services'; import SearchIconNew from '../../assets/images/original/Contoso_Assets/product_page_assets/upload_icon.svg' import { DropzoneArea } from 'mui-file-dropzone' import { useNavigate } from "react-router-dom"; import './uploadFile.scss' // class UploadFile extends Component { // constructor(props) { // super(props); // this.uploadFile = this.uploadFile.bind(this); // } // componentDidMount() { // // const html = ''; // // document.getElementsByClassName('MuiDropzoneArea-root')[0].innerHTML += html; // } function UploadFile(props) { const navigate = useNavigate(); const uploadFile = (e) => { const file = e[0]; if (file) { const formData = new FormData(); formData.append("file", file); ProductService.getRelatedProducts(formData, props.userInfo.token) .then((relatedProducts) => { if (relatedProducts.length > 1) { navigate("/suggested-products-list",{ state: { relatedProducts }, }); } else { navigate({ pathname: `/product/detail/${relatedProducts[0].id}`, }); } }) .catch(() => { // Alert.error("There was an error uploading the image, please try again", { // position: "top", // effect: "scale", // beep: true, // timeout: 6000, // }); }); } } // const resetFileValue = (e) => { // e.target.value = null; // } const { title, subtitle } = props; return (
{/* */} {/* */} ); } // } const mapStateToProps = state => state.login; export default connect(mapStateToProps)(UploadFile); ================================================ FILE: src/ContosoTraders.Ui.Website/src/components/uploadFile/uploadFile.scss ================================================ .appbar .searchbar-upload{ position: absolute; z-index: 110; width: 100%; text-align: center; padding: 30px; background-color: #fff; border: 1px solid #DFDFDF; } .appbar .searchbar-upload .upload{ margin: auto; max-width: 100%; margin-top: 10px; background: transparent !important; display: flex; align-items: center; justify-content: center; } .appbar .searchbar-upload .upload .MuiDropzoneArea-textContainer, .appbar .searchbar-upload .upload .MuiDropzoneArea-textContainer .MuiDropzonePreviewList-root{ display: none; } .appbar .searchbar-upload .upload .MuiDropzoneArea-root{ display: flex; align-items: center; justify-content: center; /* background: #FAFAFA !important; */ background: transparent; } .appbar .searchbar-upload .upload .upload__label{ box-shadow: none; /* background: #FAFAFA !important; */ position: absolute; z-index: -1; } .appbar .searchbar-upload .upload .upload__label img{ width: 50px; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/helpers/errorsHandler.js ================================================ import axios from 'axios' // import toast from './toast' import { handleUnathenticatedRequest } from './refreshJWTHelper' function errorResponseHandler(error) { // if it's token expired error, solve it silently if (isTokenExpiredError(error)) { return handleUnathenticatedRequest(error); } // otherwise, if has response show the error // if (error.response) { // toast.error(error.response.data || error.response.statusText); // } // check for errorHandle config if (error.config.hasOwnProperty('errorHandle') && error.config.errorHandle === false) { return Promise.reject(error); } } function isTokenExpiredError(error) { if (error.response && error.response.status === 401) { return true; } if (error.response && error.response.data) { const errorResponseData = error.response.data; if (typeof errorResponseData === 'string') { return !!errorResponseData.match(/401|[Uu]nauthorized/); } } return false; } // apply interceptor on response axios.interceptors.response.use( response => response, errorResponseHandler ); export default errorResponseHandler; ================================================ FILE: src/ContosoTraders.Ui.Website/src/helpers/localStorage.js ================================================ export const loadState = () => { try { const serializedState = localStorage.getItem('state'); if (serializedState === null) { return undefined; } return JSON.parse(serializedState); } catch (err) { return undefined; } }; export const getItemValue = (key) => { const state = loadState(); return state[key]; } export const setItemValue = (key, value) => { const state = loadState(); const newState = { ...state, [key]: value }; saveState(newState); } export const saveState = (state) => { try { const serializedState = JSON.stringify(state); localStorage.setItem('state', serializedState); } catch { // ignore write errors } }; ================================================ FILE: src/ContosoTraders.Ui.Website/src/helpers/refreshJWTHelper.js ================================================ import axios from 'axios'; import { getRefreshToken, setAccessToken, setRefreshToken } from './tokensHelper'; import { ConfigService } from '../services' import AuthB2CService from '../services/authB2CService'; let failedRequestToRetry = []; let isAlreadyFetchingAccessToken = false; const addSubscriber = (callback) => failedRequestToRetry.push(callback); const fetchNewJWTokens = async (refreshToken) => { const dataToken = { token: refreshToken } const response = await axios.put(`${ConfigService._apiUrl}/auth/refresh`, dataToken); if (!response.data) { return null; } return { newAccessToken: response.data.access_token.token, newRefreshToken: response.data.refresh_token }; } const onAccessTokenFetched = (accessToken) => { failedRequestToRetry.forEach(callback => callback(accessToken)); failedRequestToRetry = []; } export const handleUnathenticatedRequest = async (authenticationError) => { try { const { response: errorResponse } = authenticationError; const useB2cFromEnv = process.env.REACT_APP_USE_B2C ? JSON.parse(process.env.REACT_APP_USE_B2C.toLowerCase()) : false; if (useB2cFromEnv) { return handleUnathenticatedRequestFromB2c(errorResponse, authenticationError); } return handleUnathenticatedRequestFromFake(errorResponse, authenticationError); } catch (err) { return Promise.reject(err); } } const handleUnathenticatedRequestFromFake = async (errorResponse, authenticationError) => { const refreshToken = getRefreshToken(); if (!refreshToken) { // We can't refresh, throw the error return Promise.reject(authenticationError); } if (!isAlreadyFetchingAccessToken) { isAlreadyFetchingAccessToken = true; const newJWTokens = await fetchNewJWTokens(refreshToken); if (!newJWTokens) { return Promise.reject(authenticationError); } setAccessToken(newJWTokens.newAccessToken); setRefreshToken(newJWTokens.newRefreshToken); isAlreadyFetchingAccessToken = false; onAccessTokenFetched(newJWTokens.newAccessToken); } return retryOriginalRequest(errorResponse); } const handleUnathenticatedRequestFromB2c = async (errorResponse, authenticationError) => { const authB2CService = new AuthB2CService(); let accessToken; try { accessToken = await authB2CService.getToken(); } catch (e) { await authB2CService.login(); accessToken = await authB2CService.getToken(); } if (!accessToken) { // We can't refresh, throw the error return Promise.reject(authenticationError); } setAccessToken(accessToken); return retryOriginalRequest(errorResponse); } const retryOriginalRequest = (errorResponse) => { return new Promise(resolve => { addSubscriber(accessToken => { errorResponse.config.headers.Authorization = `Bearer ${accessToken}`; resolve(axios(errorResponse.config)); }); }); } ================================================ FILE: src/ContosoTraders.Ui.Website/src/helpers/toast.js ================================================ import 'izitoast/dist/css/iziToast.min.css' import iZtoast from 'izitoast' const toast = { error: (message, title = 'Error') => { return iZtoast.error({ title: title, message: message, position: 'bottomCenter' }); }, success: (message, title = 'Success') => { return iZtoast.success({ title: title, message: message, position: 'bottomCenter' }); } }; export default toast; ================================================ FILE: src/ContosoTraders.Ui.Website/src/helpers/tokensHelper.js ================================================ import { getItemValue, setItemValue } from "./localStorage"; export const getRefreshToken = () => getItemValue('refreshToken'); export const getAccessToken = () => getItemValue('token'); export const setRefreshToken = (newRefreshToken) => setItemValue('refreshToken', newRefreshToken); export const setAccessToken = (newAccessToken) => setItemValue('token', newAccessToken); ================================================ FILE: src/ContosoTraders.Ui.Website/src/index.css ================================================ :root { --primaryblue: #2974f0; --fontInter: 'Inter'; --fontRoboto: 'Roboto'; --fontHeebo: 'Heebo'; --white: #ffffff; } body { margin: 0; padding: 0; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-family: var(--fontRoboto) !important; } .fs-12{ font-size: 12px!important; } .fs-14{ font-size: 14px!important; } .fs-16{ font-size: 16px!important; } .fs-18{ font-size: 18px!important; } .fs-20{ font-size: 20px!important; } .fs-22{ font-size: 22px!important; } .fs-24{ font-size: 24px!important; } .p-0{ padding: 0 !important; } .pt-10{ padding-top: 10px; } .m-0{ margin: 0 !important; } .mr-1{ margin-right: 10px !important; } .mb-1{ margin-bottom: 10px !important; } .mb-3{ margin-bottom: 30px !important; } .mt-1{ margin-top: 10px !important; } .mt-2{ margin-top: 20px !important; } .fw-regular{ font-weight: 400 !important; } .text-danger{ color: red !important; } .outlined-primary-btn{ text-align: center; font: normal normal medium 16px/21px var(--fontRoboto); color: #302f2f; text-transform: capitalize; opacity: 1; border: 1px solid #707070; cursor: pointer; min-width: 112px; height: 53px; background: none; border-radius: 5px; font-weight: 500; font-size: 16px; font-family: var(--fontRoboto); } .outlined-primary-btn:hover{ border-color: #2874F0 !important; color: #2874F0 !important; } .box-shadow-0{ box-shadow: none !important; } .justify-content-end{ justify-content: flex-end; } .text-transform-capitalize{ text-transform: capitalize !important; } #box { width: 100%; height: 35px; box-shadow: 0px 4px 6px #00000029; } #box:after { content:""; } /* mainHeader */ .mainHeader{ position: fixed; z-index: 100; width: 100%; } @media screen and (min-width : 320px) and (max-width : 767px) { .flex-xs-fill { -ms-flex: 1 1 auto!important; flex: 1 1 auto!important; } .flex-xs-column{ flex-direction: column !important; } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/index.js ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { Provider } from 'react-redux'; import store from './store'; import { BrowserRouter } from "react-router-dom"; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); ================================================ FILE: src/ContosoTraders.Ui.Website/src/main.scss ================================================ // 1. Configuration and helpers @import 'styles/abstracts/variables', 'styles/abstracts/mixins/ellipsis', 'styles/abstracts/mixins/fonts', 'styles/abstracts/mixins/font-scale', 'styles/abstracts/mixins/font-placeholders', 'styles/abstracts/mixins/loader'; // 2. Vendors @import 'styles/vendor/normalize'; // 3. Base stuff @import 'styles/base/base', 'styles/base/typography', 'styles/base/utilities'; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/arrivals/arrivals.js ================================================ import React from "react"; import Slider from "../../components/slider/slider"; import Banner from "../home/sections/banner"; import './arrivals.scss' const Arrivals = (props) => { return (



); }; export default Arrivals; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/arrivals/arrivals.scss ================================================ .arrivals{ margin-top: 190px; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/cart/cart.js ================================================ import { Grid, Button, TextField, InputAdornment, Chip } from "@mui/material"; import React, { useCallback, useEffect } from "react"; import QuantityPicker from "../../components/quantityCounter/productCounter"; import Breadcrumb from "../../components/breadcrumb/breadcrumb"; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { CartService } from "../../services"; import { connect } from "react-redux"; import LoadingSpinner from "../../components/loadingSpinner/loadingSpinner"; import { getCartQuantity } from "../../actions/actions"; import './cart.scss' function Cart(props) { const textInput = React.useRef(null); const validCoupons = ['discount10','discount15']; const [coupon, setCoupon] = React.useState('DISCOUNT15'); const [invalidCoupon, setInvalidCoupon] = React.useState(false); const [discountPrice, setDiscountPrice] = React.useState(0); const [discountPercentage, setDiscountPercentage] = React.useState(15); const [cartItems, setCartItems] = React.useState([]); const [loading, setLoading] = React.useState(false) const [total, setTotal] = React.useState(0); const [grandtotal, setgrandTotal] = React.useState(0); const [delivery, setDelivery] = React.useState(10); const navigate = useNavigate(); const getCartItems = useCallback(async () => { setLoading(true) let items; //After logging take up cart detail from API if (props.userInfo.loggedIn) { let res = await CartService.getShoppingCart(props.userInfo.token) items = res ? res : [] } else { items = localStorage.getItem('cart_items') ? JSON.parse(localStorage.getItem('cart_items')) : [] } let sum = 0; if (items.length > 0) { items.map((item) => { return ( sum += item.price * item.quantity ) }) setTotal(sum); let discount = (sum/100)*discountPercentage; setDiscountPrice(Math.ceil(discount)); let deliveryChrge = 10; setDelivery(deliveryChrge) let totalval = parseInt((sum - discount) + deliveryChrge); setgrandTotal(totalval); } setCartItems(items) setLoading(false) let quantity = items.length; props.getCartQuantity(quantity) // eslint-disable-next-line react-hooks/exhaustive-deps }, [props]) useEffect(() => { getCartItems() }, [getCartItems]); useEffect(() => { if(total > 0){ let discount = (total/100)*discountPercentage; setDiscountPrice(Math.ceil(discount)); let totalval = parseInt((total - discount) + delivery); setgrandTotal(totalval); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [discountPercentage]); const location = useLocation(); const currentCategory = location.pathname.split("/").pop().replaceAll('-', ' '); const checkDiscount = () => { if(validCoupons.includes(textInput.current.value.toLowerCase())){ switch (textInput.current.value.toLowerCase()) { case 'discount15': setDiscountPercentage(15); break; case 'discount10': setDiscountPercentage(10); break; default: break; } setCoupon(textInput.current.value); textInput.current.value = '' setInvalidCoupon(false); }else{ setInvalidCoupon(true) } } const removeFromCart = async (item) => { if (props.userInfo.loggedIn) { await CartService.deleteProduct(item, props.userInfo.token) }else{ let cartItem = localStorage.getItem('cart_items') ? JSON.parse(localStorage.getItem('cart_items')) : []; var filtered = cartItem.filter(function(el) { return el.name !== item.name; }); localStorage.setItem('cart_items',JSON.stringify(filtered)) } getCartItems() } return ( <> {loading ? :
My Cart
{cartItems.length > 0 && <>
Grand Total:
${grandtotal.toFixed(2)}
}

{cartItems.length === 0 &&

Your Cart is empty

} {cartItems.length > 0 &&
Product Name Price Qty Subtotal
} {cartItems.map((item, key) => (
navigate('/product/detail/'+item.productId)} role="button"> {item.name} Price / Unit : ${item.price.toFixed(2)} Qty   Price : ${item.price.toFixed(2)} Qty : {item.quantity} Subtotal : ${(item.price * item.quantity).toFixed(2)} {/* Move to wishlist */} removeFromCart(item)}> Remove
))} {cartItems.length > 0 &&

Coupons

), }} />
{coupon && {setCoupon('');setDiscountPercentage(0)}} className="CouponChip" />
}
Order Summary
Sub Total ${total.toFixed(2)} Discount -${discountPrice.toFixed(2)} Delivery Fee ${delivery.toFixed(2)} Grand Total ${grandtotal.toFixed(2)}
}
} ); } const mapStateToProps = state => state.login; const mapDispatchToProps = (dispatch) => ({ getCartQuantity: (value) => dispatch(getCartQuantity(value)), }) export default connect(mapStateToProps, mapDispatchToProps)(Cart); ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/cart/cart.scss ================================================ .CartMain{ margin-top: 192px; .CartSection { padding: 0 70px 0 70px; .innerCart{ padding: 0 30px; } .CartTopHeadPart{ display: flex; align-items: center; padding: 8px 0; } } .CartSection .MyCartHeading { font: normal normal 600 24px/44px "Inter"; letter-spacing: 0px; color: #000000; margin: 0; margin-right: auto; } .CartSection .imagesection { width: 100%; // height: 100px; } .CartSection .CartTopGrandTotal { font-size: 16px; letter-spacing: 0px; color: #171520; margin: 0 16px; font-weight: 600; } .CartSection .CartTopHeadTotal { font: normal normal normal 14px/20px "Inter"; letter-spacing: 0px; color: #171520; margin: 0; } /*===================search bar===========================*/ .CartSection { font-family: "Inter"; } .CartSection .pincodesearchbar { min-width: 100%; padding: 14px 0 16px 0; &:focus{ border: none } input::placeholder{ font-size: 14px; font-family: 'Inter'; font-weight: 400; color: rgba(98, 98, 98, 0.9); } } .CartSection .pincodebar { width: 100%; } .CartSection .pincodebar input::placeholder{ text-align: left; font: normal normal normal 14px/20px "Inter"; letter-spacing: 0px; } .CartSection .pinsearchbtn { border-left: 1px solid #f1f1f1; height: 56px; float: right; position: absolute; right: 1px; background: #fafafa; width: 82px; padding: 16px; &:focus{ outline: none !important; } } .CartSection .pinsearchbtnactive{ background: #2874f0; color: white; &:hover{ background: #2874f0; color: white; } } .CartSection .PlaceOrderButton { background: #2874f0 0% 0% no-repeat padding-box; border-radius: 8px; width: 180px; height: 40px; float: right; text-align: center; letter-spacing: 0px; color: #ffffff; text-transform: capitalize; font-weight: 400; font-size: 16px !important; box-shadow: none !important; } .CartSection .allProductlist { margin: 23px 0px 0px 0px; } .CartSection .CartHeadings { margin: 34px 0px 0px 0px; text-align: left; letter-spacing: 0px; color: #000000; font-size: 16px; font-weight: 500; } .CartSection .Productname { letter-spacing: 0px; color: #171520; text-align: left; font-size: 16px; } .CartSection .Producttype { text-align: left; font-size: 16px; letter-spacing: 0px; color: #626262; margin: 8px 0px 18px 0px; } .CartSection .Producttype { text-align: left; font-size: 16px; letter-spacing: 0px; color: #626262; margin-bottom: 10px; } .CartSection .Productprice { text-align: left; font-size: 14px; letter-spacing: 0px; color: #171520; } .CartSection .Productlinks { text-align: center; text-decoration: underline; font-size: 14px; font-weight: 600; letter-spacing: 0px; } .CartSection .wishlistlink { color: #000000; } .CartSection .removelink { color: #b00020; } .CartSection .CartProducts { padding-left: 16px; .Productqty{ color: #626262 } } .CartSection .CouponHeading { text-align: left; font-size: 20px; font-weight: 600; margin: 42px 0px 0px 0px; letter-spacing: 0px; color: #000000; } .CartSection .OrderSubHeading { text-align: left; font-size: 16px; letter-spacing: 0px; color: #626262; margin-top: 12px; font-weight: 500; } .CartSection .OrdertotalHeading { text-align: left; font-weight: 600; font-size: 16px; letter-spacing: 0px; color: #171520; margin: 12px 0px 14px 0px; } .CartSection .OrderSubPrice { text-align: right; font-size: 16px; letter-spacing: 0px; color: #171520; margin-top: 12px; font-weight: 500; } .CartSection .OrderTotalPrice { font-weight: 600; font-size: 16px; text-align: right; margin-top: 12px; letter-spacing: 0px; color: #171520; margin: 12px 0px 14px 0px; } .CartSection .CouponChip { background: #29ae49 0% 0% no-repeat padding-box; border: 1px solid #f1f1f1; border-radius: 74px; // margin: 16px 0px 8px 0px; } .CartSection .MuiChip-root { height: 40px; margin: 0; } .CartSection .MuiChip-label { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-align: left; font: normal normal normal 14px/20px "Inter"; letter-spacing: 0px; color: #ffffff; padding-left: 16px; padding-right: 8px; } .CartSection .MuiChip-deleteIcon { color: white !important; margin: 0 16px 0 0 !important; } .CartSection .nocouponheading { text-align: left; font: normal normal normal 14px/20px "Inter"; letter-spacing: 0px; color: #62626280; } .CartSection .OrderButtonsection{ margin-top: 34px; } } //Responsive @media screen and (min-width : 320px) and (max-width : 767px) { .CartMain .CartSection { padding: 0 35px; } .CartMain .CartSection .CartTopHeadPart{ flex-direction: column; align-items: end; } .CartMain .CartSection .CartTopGrandTotal{ margin: 10px 0; } .CartMain .CartSection .Productlinks{ margin: 20px 0; } .CartMain .CartSection .Productprice{ display: flex; align-items: center; } .CartMain .CartSection .Productprice > b{ margin-right: auto !important; } .CartMain .CartSection .Productname, .CartMain .CartSection .Producttype, .CartMain .CartSection .CartProducts .Productqty{ text-align: center; } } @media screen and (min-width : 768px) and (max-width : 899px) { .CartMain .CartSection .Productlinks{ margin: 20px 0; } .cart-header{ display: none !important; } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/detail/detail.scss ================================================ // ----------------------------------------------------------------------------- // This file contains styles that are specific to the Detail Products page. // ----------------------------------------------------------------------------- @import "../../main.scss"; .ProductContainerSectionMain{ margin-top:192px; .ProductContainerSection{ padding: 0px 70px; .ProductDetailsSection { font-family: "Inter"; padding-top: 16px; .MuiGrid-container{ align-items: center; justify-content: center; padding: 20px; .productdetailsimagediv{ background-size:contain; background-position-x:center; background-position-y:center; background-repeat:no-repeat; min-height: 600px; .productdetailsimage { margin: auto; display: block; mix-blend-mode: darken; opacity: 0; width: 100%; max-height: 600px; // height: 100%; } } } .CartButton { background: #2874f0 0% 0% no-repeat padding-box; border-radius: 8px; opacity: 1; width: 309px; height: 56px; text-transform: none; box-shadow: none!important; font-weight: 600; color: white; margin: 30px 0 0 0; text-align: center; font: normal normal 600 14px/24px Inter; letter-spacing: 0px; opacity: 1; margin-right: 16px; .MuiButton-startIcon{ margin-right: 10px; } } .WishListButton { background: #ffffff 0% 0% no-repeat padding-box; border: 1px solid #000000; border-radius: 8px; opacity: 1; width: 309px; height: 56px; // margin-left: 16px; text-transform: none; margin-top: 30px; text-align: center; font: normal normal 600 14px/24px "Inter"; letter-spacing: 0px; color: #000000; opacity: 1; font-weight: 600; .MuiButton-startIcon{ margin-right: 10px; } } .productdetailName { font-size: 18px; font-weight: 400; margin-bottom: 16px; } .ProductImagesSection { padding: 0px 16px 40px 0px; } .newprice { font-size: 28px; font-weight: 500; margin-right: 8px; } .oldprice { font-size: 18px; font-weight: 400; margin-right: 8px; text-decoration: line-through; letter-spacing: 0px; color: #00000080; opacity: 1; } .newoffer { font-size: 14px; font-weight: 500; color: #ff404b; opacity: 1; } .detailsection { padding-left: 16px; } .pincodebar { margin: 30px 0px; display: flex; align-items: center; } .prodattributes { font-size: 16px; font-weight: 400; margin-right: 70px; } .pincodesearchbar { min-width: 381px; input::placeholder{ font-size: 14px; font-family: 'Inter'; font-weight: 400; color: rgba(98, 98, 98, 0.9); } fieldset{ border: 1px solid #F1F1F1; } } .ProductButtonGrp { border: 1px solid #1b4b66; border-radius: 4px; opacity: 1; } .Buttngrpbtn { width: 21px; height: 30px; } .pinsearchbtn { font-family: 'Inter'; font-weight: 400; font-size: 14px; border-left: 1px solid #f1f1f1; height: 49px; float: right; position: absolute; right: 1px; background: #fafafa; width: 82px; padding: 16px; } } } .detail { &__wrapper { background-color: $color-background-light; margin-bottom: 3rem; margin-left: auto; margin-right: auto; padding-left: $padding-m; padding-right: $padding-m; width: 100%; @media (min-width: 36em) { max-width: 46rem; padding-left: 0; padding-right: 0; } @media (min-width: $mq-r) { display: flex; max-width: 76.75rem; padding-top: 2.5rem; } @media (min-width: $mq-l) { padding-top: 2.5rem; } @media (min-width: $mq-xl) { max-width: 87.5rem; } } &__image { background-position: center; background-size: cover; height: 14rem; width: 100%; @media (min-width: $mq-r) { flex: 0 0 50%; height: 45.5rem; margin-top: 3.75rem; } } &__info { background: $color-background-light; padding-left: $padding-s; padding-right: $padding-s; @media (min-width: $mq-r) { box-shadow: $box-shadow-l; flex: 0 0 50%; margin-left: -5rem; min-height: 53.125rem; padding-bottom: $padding-l; padding-left: $padding-l; padding-right: $padding-l; } } &__label { color: $color-text-primary; font-weight: $font-weight-medium; @include text-s; } &__data { margin-bottom: 2rem; } &__title { color: $color-text-primary; font-weight: $font-weight-medium; margin-right: $margin-m; @include text-l; } &__tag { background: #f2f2f2; border-radius: $border-radius; color: $color-text-primary; font-size: 1rem; font-weight: $font-weight-medium; padding-left: $padding-m; padding-right: $padding-m; white-space: nowrap; @media (min-width: $mq-m) { font-size: 1.25rem; } &:not(:last-of-type) { margin-right: 0.625rem; } &:nth-child(3), &:nth-child(4) { background-color: $color-background-primary; color: $color-text-light; font-weight: $font-weight-light; } &.nostock { background: #ff7777; } } &__data-price { @include text-l-price; } .detail__buttons { display: flex; } .btn--primary { margin-right: $margin-m; max-width: 20rem; width: 100%; &.btn--cart:active { background-color: $color-background-success-altert; } } .btn--secondary { border: 2px solid currentColor; padding: calc(#{$padding-m} - 4px); box-shadow: initial; } &__description { @media (max-width: $mq-r) { display: none; } } &__feature { color: $color-text-primary; font-size: 1rem; margin-bottom: $margin-m; &-title { font-weight: $font-weight-medium; margin-right: $margin-s; } &-description { font-weight: $font-weight-light; } @media (min-width: $mq-m) { font-size: 1.25rem; } } } } //Responsive @media screen and (min-width : 320px) and (max-width : 767px) { .ProductContainerSectionMain .ProductContainerSection{ padding: 0 35px; } .ProductContainerSectionMain .ProductContainerSection .ProductDetailsSection .MuiGrid-container, .ProductContainerSectionMain .ProductContainerSection .ProductDetailsSection .detailsection, .CartMain .CartSection .innerCart{ padding: 0; } .ProductContainerSectionMain .ProductContainerSection .ProductDetailsSection .pincodesearchbar{ min-width: unset; } .ProductContainerSectionMain .ProductContainerSection .ProductDetailsSection .prodattributes{ margin-right: 10px; } .ProductContainerSectionMain .ProductContainerSection .ProductDetailsSection .CartButton{ width: 100% !important; margin: 30px 0; } .ProductContainerSectionMain .ProductContainerSection .ProductDetailsSection .MuiGrid-container .productdetailsimagediv{ min-height: unset; } .ProductContainerSectionMain .ProductContainerSection .ProductDetailsSection .MuiGrid-container .productdetailsimagediv .productdetailsimage{ width: 100%; } } @media screen and (min-width : 768px) and (max-width : 899px) { .ProductContainerSectionMain .ProductContainerSection .ProductDetailsSection .MuiGrid-container .productdetailsimagediv{ min-height: unset; } .ProductContainerSectionMain .ProductContainerSection .ProductDetailsSection .MuiGrid-container .productdetailsimagediv .productdetailsimage{ width: 100%; } } @media screen and (max-width : 1439px) { .ProductContainerSection .ProductDetailsSection .CartButton, .ProductContainerSection .ProductDetailsSection .WishListButton { width: 250px !important; } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/detail/detailContainer.js ================================================ import React, { useCallback } from "react"; import { connect } from 'react-redux'; // import { animateScroll as scroll } from "react-scroll"; import LoadingSpinner from "../../components/loadingSpinner/loadingSpinner"; // import Alert from "react-s-alert"; // import Detail from "./detail"; // import CartService from "../../services"; import { ProductService, CartService } from '../../services'; import ProductDetails from "./productDetails"; import Breadcrump from "../../components/breadcrumb/breadcrumb"; import { useNavigate, useParams } from "react-router-dom"; import { Alert, Snackbar } from "@mui/material"; import { getCartQuantity } from "../../actions/actions"; // import Slider from "../home/components/slider/slider"; import './detail.scss' function DetailContainer(props) { const { productId } = useParams(); const navigate = useNavigate(); const [detailProduct, setDetailProduct] = React.useState({}) const [loadingRelated, setLoadingRelated] = React.useState(null) const [loading, setLoading] = React.useState(true) const [alert, setAlert] = React.useState({ open: false, type: 'error', message: '' }) const relatedDetailProducts = [] const [qty, setQty] = React.useState(1); const getDetailPageData = useCallback( async (productId) => { let detailProducts = await ProductService.getDetailProductData(productId); if(detailProducts){ setDetailProduct(detailProducts) }else{ navigate('/product-not-found'); } setLoading(false) },[navigate]); React.useEffect(() => { getDetailPageData(productId); }, [productId, getDetailPageData]); const getQuantity = async () => { if (props.userInfo.token) { const shoppingcart = await CartService.getShoppingCart( props.userInfo.token ); if (shoppingcart) { let quantity = shoppingcart.length; props.getCartQuantity(quantity) } }else{ let cartItem = localStorage.getItem('cart_items') ? JSON.parse(localStorage.getItem('cart_items')) : [] let quantity = cartItem.length; props.getCartQuantity(quantity) } } const addProductToCart = async () => { var tempProps = JSON.parse(JSON.stringify(detailProduct)); if(!loggedIn){ let cartItem = { imageUrl: detailProduct.imageUrl, name: detailProduct.name, price: detailProduct.price, productId: detailProduct.id, quantity: qty, type: detailProduct.type } let arr = localStorage.getItem('cart_items') ? JSON.parse(localStorage.getItem('cart_items')) : [] let objIndex = arr.findIndex((obj => obj.productId === detailProduct.id)); if(objIndex === -1){ arr.push(cartItem) localStorage.setItem('cart_items',JSON.stringify(arr)) showSuccesMessage(`Added ${detailProduct.name} to Cart`) }else{ showErrorMessage({errMessage : `Already Added to Cart`}) } setLoadingRelated(true) getQuantity() }else{ const email = localStorage.getItem('state') ? JSON.parse(localStorage.getItem('state')).userName : null tempProps.email = email; tempProps.quantity = qty; Object.preventExtensions(tempProps); setDetailProduct(tempProps) const productToCart = await CartService.addProduct(props.userInfo.token, tempProps) if (productToCart.errMessage) { return showErrorMessage(productToCart) } else { showSuccesMessage(productToCart) getQuantity() } } // setLoadingRelated(true) // setTimeout(async () => { // let relatedDetailProducts = await CartService // .getRelatedDetailProducts(this.props.userInfo.token, this.state.detailProduct.type.id); // if (relatedDetailProducts) { // relatedDetailProducts = relatedDetailProducts.recommendations.slice(0, 3); // } // this.setState({ relatedDetailProducts, loadingRelated: false }); // }, 2000); // props.sumProductInState(); } const showSuccesMessage = (data) => { setAlert({ open: true, type: 'success', message: data }) // Alert.success(data.message, { // position: "top", // effect: "scale", // beep: true, // timeout: 1500, // }); } const showErrorMessage = (data) => { setAlert({ open: true, type: 'error', message: data.errMessage }) // Alert.error(data.errMessage, { // position: "top", // effect: "scale", // beep: true, // timeout: 3000, // }); } const handleClose = () => { setAlert({ open: false, type: 'error', message: '' }) } const { loggedIn } = props.userInfo return (
{alert.message} {loading ? : }

{/* */} {/*
*/}
); } // } const mapStateToProps = state => state.login; const mapDispatchToProps = (dispatch) => ({ getCartQuantity: (value) => dispatch(getCartQuantity(value)), }) export default connect(mapStateToProps,mapDispatchToProps)(DetailContainer); ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/detail/productDetails.js ================================================ import React from "react"; import { Grid, Button } from "@mui/material"; import CustomizedAccordions from "../../components/accordion/accordion"; import QuantityPicker from "../../components/quantityCounter/productCounter"; import add_to_bag_icon from "../../assets/images/original/Contoso_Assets/product_page_assets/add_to_bag_icon.svg"; import { Link } from "react-router-dom"; import discount_icon from "../../assets/images/original/Contoso_Assets/product_page_assets/discount.png"; function ProductDetails(props) { const { name, price, imageUrl, loggedIn } = props.detailProductData; const { features } = props.detailProductData; const [loading, setLoading] = React.useState(false) const addToCart = async () => { setLoading(true); await props.addProductToCart(); setLoading(false); }; const discountOffer = (price) => { let dicsount = price - ((price / 100) * 15) return ( ${parseInt(dicsount).toFixed(2)} ) } const accordionItems = [ { name: 'panel1', title: 'Description', body: {features && features.map((feature, index) => { return ( <> {feature.title} {feature.description} ) })} }, { name : 'panel2', title : 'Offers', body :
10% off on SBI Credit Card, up to ₹1,750, on orders of ₹5000 and above T&C
10% off on SBI Credit Card EMI Transactions, up to ₹2,250, on orders of ₹5000 and above T&C
Additional ₹750 discount on SBI Credit Card and EMI txns on net cart value of INR 29,999 and above T&C
No cost EMI ₹8,815/month. Standard EMI also available View plans
} ] return (
{name ? name : 'Xbox Series S Fortnite & Rocket League Bundle 512 GB (White)'}
{discountOffer(price)} {'$' + price.toFixed(2)} 15%Off
Quantity
); } export default ProductDetails; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/error/errorPage.js ================================================ import { Button } from '@mui/material'; import React from 'react'; import errorpic from "../../assets/images/original/Contoso_Assets/404_page_assets/404_image.svg"; import { useNavigate } from 'react-router-dom'; import './errorPage.scss' const ErrorPage = () => { const history = useNavigate(); const redirectTo = (path) => { history(path) } return ( <>
error404

Something wrong here…

Sorry, We’re having some technical issues (as you can see)
Try to refresh the page, something work.

) } export default ErrorPage; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/error/errorPage.scss ================================================ .ErrorSection { margin-top: 137px; background-image: url("../../assets/images/original/Contoso_Assets/404_page_assets/background_image.jpg"); background-size: cover; padding-top: 130px; padding-bottom: 130px; } .ErrorSection .errorpic { display: block; margin-left: auto; margin-right: auto; } .ErrorSection .errorsecHeading { text-align: center; font-weight: 600; font-family: "Poppins"; font-size: 32px; letter-spacing: -1.53px; color: #111111; margin-bottom: 0 !important; } .ErrorSection .errorsecContent { text-align: center; font-family: "Poppins"; font-size: 18px; font-weight: 400; letter-spacing: -0.86px; color: #111111; margin: 8px auto 30px auto !important; } .ErrorSection .RefreshButton { border: 1px solid #2874f0; border-radius: 8px; font-family: "Inter"; font-size: 16px; font-weight: 500; letter-spacing: 0px; color: #2874f0; text-transform: capitalize; box-shadow: none !important; width: 134px; height: 36px; margin-right: 16px; } .ErrorSection .GoHomeButton { background: #2874f0 0% 0% no-repeat padding-box; border-radius: 8px; text-transform: capitalize; box-shadow: none !important; text-align: center; font-family: "Inter"; font-size: 16px; font-weight: 500; letter-spacing: 0px; color: #ffffff; width: 200px; height: 36px; } .ErrorSection .ButtonSection { text-align: center; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/home/home.js ================================================ import React, { useEffect } from "react"; import Hero from "./sections/hero"; import { ConfigService } from './../../services' // import Slider from "./components/slider/slider"; // import Banner from "./components/Banner/banner"; import Gridsection from "./sections/gridSection"; import Finalsection from "./sections/finalSection"; const Home = ({ recommendedProducts, popularProducts, loggedIn }) => { // const [customerSupportEnabled, setCustomerSupportEnabled] = useState(false); useEffect(() => { async function loadSettings() { await ConfigService.loadSettings(); // setCustomerSupportEnabled(ConfigService._customerSupportEnabled); } loadSettings(); },[]) return (
{/* */} {/* */} {/* */} {/* {loggedIn && } */}
); }; export default Home; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/home/home.scss ================================================ // ----------------------------------------------------------------------------- // This file contains styles that are specific to the Home page. // ----------------------------------------------------------------------------- @import "../../main.scss"; $dark: #000; $light: #fff; $darkgrey: #222222; .home { margin-top: 250px; .upload { margin-bottom: 1.5rem; } } .light { .mainHeader { background-color: $light; color: $dark !important; } } .dark { background-color: $darkgrey !important; color: $light !important; .appbar .iconButton { background: #fff; color: #000; width: 40px; height: 40px; margin: 0 10px; } a { color: $light !important; } .mainHeader { background-color: $dark !important; color: $light !important; header { background-color: $dark !important; } } .home { .LaptopSection { background-color: $dark !important; color: $light !important; .LapHeadmain { color: $light !important; } .LapHead { color: $light !important; } .LaptopButton { color: $light !important; border: 1px solid $light !important; } } .final-section-container { background-color: #e6e6e6; } } .footer-container { background-color: $dark !important; color: $light !important; p, li, .logo-div, .list-element { color: $light !important; } } } /* **************LAPTOPS DESIGNED BY MICROSOFT******************* */ .BannerSection { background-image: url("../../assets/images/original/Contoso_Assets/Static_Banner/banner_controller.jpg"); background-repeat: no-repeat; background-size: 100%; cursor: pointer; min-height: 540px; } .BannerSection .BannerHeading { font-family: 'Poppins' !important; font-weight: 600 !important; font-size: 34px !important; text-align: left; letter-spacing: -1.59px; color: #ffffff; opacity: 1; padding: 144px 0px 0px 150px; } .BannerSection .BannerContent { font: var(--unnamed-font-style-normal) normal var(--unnamed-font-weight-normal) 16px/24px "Poppins"; color: var(--unnamed-color-ffffff); text-align: left; font: normal normal normal 16px/24px "Poppins"; letter-spacing: -0.75px; color: #ffffff; opacity: 1; padding: 16px 0px 0px 150px; } .BannerSection .BannerButtondiv { padding: 50px 0px 0px 150px; } .BannerSection .BannerButton { border: 1px solid #ffffff; opacity: 1; width: 224px; height: 56px; background: none; border-radius: 5px; cursor: pointer; color: var(--unnamed-color-ffffff); text-align: center; font: normal normal medium 16px/21px var(--fontRoboto); letter-spacing: -0.36px; color: #ffffff; font-weight: 500; font-size: 16px; font-family: var(--fontRoboto); } /* laptopsection starts -------------------------------------------------------------------------------------------------------------*/ .LaptopSection { background-color: #fff; } .LaptopSection .LapHeadSection { padding-top: 70px; } .LaptopSection .LapHead { text-align: center; font: normal normal normal 14px/21px "Poppins"; letter-spacing: -0.31px; color: #302f2f; text-transform: uppercase; opacity: 1; } .LaptopSection .LapHeadmain { text-align: center; font: normal normal 600 30px/56px "Poppins"; letter-spacing: -1.44px; color: #111111; text-transform: uppercase; opacity: 1; } .LaptopSection .ProductSection { margin: 30px 38px 70px 70px; } .LaptopSection .LaptopButtondiv { text-align: center; padding: 50px 0px 50px 0px; } .LaptopSection .LaptopButton { text-align: center; font: normal normal medium 16px/21px var(--fontRoboto); letter-spacing: -0.36px; color: #302f2f; text-transform: capitalize; opacity: 1; border: 1px solid #707070; cursor: pointer; width: 224px; height: 56px; background: none; border-radius: 5px; font-weight: 500; font-size: 16px; font-family: var(--fontRoboto); } .LaptopSection .lapsecimage { border-radius: 5px; width: 100%; } .LaptopSection .laptopimage { width: 75%; margin: auto; display: block; margin-bottom: 23px !important; margin-top: 2px !important; } .LaptopSection .cardlap { min-height: 310px; background: #f5f5f580 0% 0% no-repeat padding-box; border-radius: 10px; opacity: 1; box-shadow: none; } .LaptopSection .Productname { font: var(--unnamed-font-style-normal) normal 600 18px / var(--unnamed-line-spacing-32) "Poppins"; text-align: center; font: normal normal 600 18px/32px "Poppins"; letter-spacing: -0.33px; color: #111111; opacity: 1; margin-right: auto; } .LaptopSection .price { text-align: center; letter-spacing: -0.36px; color: #2874f0; opacity: 1; background-color: white !important; border-radius: 10px; background: #ffffff 0% 0% no-repeat padding-box; border-radius: 17px; width: 63px; height: 34px; justify-content: center; display: flex; align-items: center; font-weight: 600; font-size: 16px; font-family: "Poppins"; } .LaptopSection .productdetails { display: flex; justify-content: space-around; align-items: center; } .LaptopSection .proddetails { padding: 16px 30px; box-shadow: none; } /* ************** */ .final-section-container { background-color: #80808030; height: 800px; background-image: url('../../assets/images/original/Contoso_Assets/5_Static_Banner/static_banner_2.jpg'); background-repeat: no-repeat; } .final-section-container .grid-container { padding-top: 30px; } .final-section-container .image-section img { min-height: 100%; min-width: 100%; max-width: 100%; max-height: 100%; } .final-section-container .content-section { padding-top: 170px; padding-right: 70px; } .final-section-container .content-section h1 { font-family: 'Poppins' !important; font-size: 34px !important; color: #000000 !important; font-weight: 600; margin: 16px 0; } .final-section-container .content-section p { font-family: 'Poppins' !important; font-size: 16px !important; color: #000000 !important; font-weight: 400; } .final-section-container .start-btn { text-align: center; color: #302f2f; text-transform: capitalize; opacity: 1; border: 1px solid #707070; cursor: pointer; width: 224px; height: 56px; background: none; border-radius: 5px; font-weight: 500; font-size: 16px; font-family: var(--fontRoboto) !important; margin-top: 34px; } // ----------------------------------------------------------------------------- // This file contains all styles related to the hero component of the site/application. // ----------------------------------------------------------------------------- .hero { position: relative; // svg { // fill: $color-svg-tertiary; // } &__banner { align-items: center; background-color: $color-background-primary; box-shadow: $box-shadow-m; color: $color-text-tertiary; display: flex; height: 3rem; justify-content: center; left: 50%; margin-top: -$margin-s; position: absolute; top: -$margin-s; transform: translateX(-50%) translateZ(0); width: 100%; z-index: $z-index-s; @media (min-width: $mq-s) { width: 22.5rem; } @media (min-width: $mq-m) { height: 4rem; width: 63.5rem; } @media (min-width: $mq-m) { width: 94%; } @media (min-width: $mq-l) { width: 80%; } } &__inner { align-items: center; display: flex; justify-content: space-between; width: 16rem; @media (min-width: $mq-s) { width: 17.8125rem; } @media (min-width: $mq-m) { width: 18rem; } @media (min-width: $mq-l) { width: 22rem; } & svg:nth-child(1) { display: none; @media (min-width: 23.625em) { display: block; } } } &__text { white-space: nowrap; &--strong { @include text-s(); font-weight: $font-weight-medium; } &--light { @include text-xs(); font-weight: $font-weight-light; } } &__image-wrapper { background-color: $color-background-primary; } &__image { height: 20rem; background-size: cover; opacity: 0; transition: opacity $animation-speed-regular $animation-type-regular; @media (min-width: $mq-m) { height: 38.5rem; } } .u-fade-in { opacity: 1; } } //Responsive @media screen and (min-width : 320px) and (max-width : 767px) { .final-section-container .content-section { padding: 170px 30px 0 30px; } .final-section-container { background-size: contain; height: 600px; } .LaptopSection { .img-class, .lapsecimage-top { padding-left: 0 !important; } } } @media screen and (min-width : 420px) and (max-width : 767px) { .final-section-container .content-section { padding: 350px 30px 0 30px; } .final-section-container { background-size: contain; height: 700px; } } @media screen and (min-width : 900px) and (max-width : 1350px) { .final-section-container { background-position: bottom; } } @media screen and (min-width : 768px) and (max-width : 899px) { .LaptopSection { .img-class, .lapsecimage-top { padding-left: 0 !important; } } .final-section-container .content-section { padding: 430px 30px 0 30px; } .final-section-container { background-size: contain; height: 800px; } } @media screen and (max-width : 1350px) { .LaptopSection .cardlap { min-height: 256px; } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/home/homeContainer.js ================================================ import React, { Component, Fragment } from "react"; // import { withRouter } from "react-router-dom"; import { connect } from 'react-redux'; import Electrical from "../../assets/images/home_electrical.jpg"; import Garden from "../../assets/images/home_gardencenter.jpg"; import Plumbing from "../../assets/images/home_plumbing.jpg"; import Powertools from "../../assets/images/home_powertools.jpg"; import { ProductService } from "../../services"; import Home from "./home"; import './home.scss' class HomeContainer extends Component { constructor() { super(); this.state = { recommendedProducts: [ ], defaultProducts: [ { title: "Power Tools", imageUrl: Powertools, cssClass: "grid__item-a", url: "/list/diytools" }, { title: "Plumbing", imageUrl: Plumbing, cssClass: "grid__item-b", url: "/list/kitchen" }, { title: "Electrical", imageUrl: Electrical, cssClass: "grid__item-c", url: "/list/home" }, { title: "Garden Center", imageUrl: Garden, cssClass: "grid__item-d", url: "/list/gardening" } ], popularProducts: [], loading: true, }; } async componentDidMount() { // if (this.props.userInfo.loggedIn) { // await this.renderPopularProducts() // } // this.getRank() } async shouldComponentUpdate(nextProps) { if ((this.props.userInfo.loggedIn !== nextProps.userInfo.loggedIn) && nextProps.userInfo.loggedIn) { await this.renderPopularProducts(nextProps.userInfo.token) } } async getRank() { var categories = { categories: this.state.defaultProducts.map((product) => { return product.title }) }; const response = await fetch("/api/personalizer/rank", { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(categories) }) if (!response.ok || response.statusText === "No Content") { if (response.error) { console.error(response.error); } this.setState({ recommendedProducts: this.state.defaultProducts }); return; } else { const data = await response.json(); console.log(`Rank request sent. EventId: ${data.eventId}`); this.setState({ recommendedProducts: this.getRerankedProducts(data) }); } } getRerankedProducts(data) { var cssEnum = ["grid__item-a", "grid__item-b", "grid__item-c", "grid__item-d"]; var recommendSource = this.state.defaultProducts; var newHeroIndex = recommendSource.findIndex(obj => obj.title === data.rewardActionId); var newRecommend = []; var counter; for (counter = 0; counter < recommendSource.length; counter++) { newRecommend.push(Object.assign({}, recommendSource[counter])); } newRecommend.unshift(newRecommend.splice(newHeroIndex, 1)[0]); newRecommend.map((category, index) => { category.cssClass = cssEnum[index]; category.eventId = data.eventId; return category; }) return newRecommend; } async renderPopularProducts(token) { token = token || this.props.userInfo.token; let popularProducts = await ProductService.getHomePageData(token); if (popularProducts && popularProducts.data.popularProducts) { popularProducts = popularProducts.data.popularProducts.slice(0, 3); this.setState({ popularProducts, loading: false }); } } render() { const { recommendedProducts, popularProducts } = this.state; const { loggedIn } = this.props.userInfo return ( ); } } const mapStateToProps = state => state.login; export default connect(mapStateToProps)(HomeContainer); ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/home/sections/banner.js ================================================ import React from "react"; import { Grid } from "@mui/material"; import { useNavigate } from "react-router-dom"; function Banner(props) { const history = useNavigate() const startShopping = () => { history('/list/controllers') } return (
{props.firstHeading}
{props.secondHeading}
); } export default Banner; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/home/sections/finalSection.js ================================================ import React from 'react' import { Grid } from '@mui/material'; import Button from '@mui/material/Button'; import { useNavigate } from 'react-router-dom'; const Finalsection = () => { const history = useNavigate() const startShopping = () => { history('/list/controllers') } return (

Play more, wait less

A streamlined dashboard designed to get you into the games and entertainment you love quickly.

) } export default Finalsection ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/home/sections/gridSection.js ================================================ import React from "react"; import { Card, CardContent, Grid, CardActionArea, } from "@mui/material"; import laptopgirl from "../../../assets/images/original/Contoso_Assets/Grid_Products_Collection/banner_1.jpg"; import laptoppic from "../../../assets/images/original/Contoso_Assets/Grid_Products_Collection/product_image.png"; import { useNavigate } from "react-router-dom"; function Gridsection() { const history = useNavigate() const startShopping = () => { history('/list/laptops') } return (
MICROSOFT LAPTOPS
LAPTOPS DESIGNED BY MICROSOFT
girl with laptop startShopping()}> girl with laptop
Surface Pro 8
$279
startShopping()}> girl with laptop
Surface Pro X
$499
startShopping()}> girl with laptop
Surface Go 3
$399
startShopping()}> girl with laptop
Surface Pro 8
$599
); } export default Gridsection; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/home/sections/hero.js ================================================ import React from "react"; import Corousel from "../../../components/corousel/corousel"; import heroBg from '../../../assets/images/original/Contoso_Assets/Slider_section/hero_banner.jpg'; import heroBg2 from '../../../assets/images/original/Contoso_Assets/Slider_section/hero_banner-2.jpg' import LocalMallIcon from '../../../assets/images/original/Contoso_Assets/Icons/cart-icon-copy.svg' import { useNavigate } from "react-router-dom"; function Hero() { const history = useNavigate() const buyNow = (id) => { history('/product/detail/'+id) } const moreDetails = () => { history('/list/controllers') } const items = [ { name: "The Fastest, Most Powerful Xbox Ever.", description: "Elevate your game with the all-new Xbox Wireless Controller - Lunar Shift Special Edition", bg: heroBg, buttons : [ { title : 'Buy Now', color : 'primary', class : 'BannerButton1', endIcon: , onClickFn : () => buyNow(1) }, { title : 'More Details', color : 'inherit', class : 'BannerButton2', endIcon : '', onClickFn : () => moreDetails() } ] }, { name: "Xbox Wireless Controller - Mineral Camo Special Edition", description: "Textured triggers and bumpers | Hybrid D-pad | Button mapping | Bluetooth® technology", bg: heroBg2, buttons : [ { title : 'Buy Now', color : 'primary', class : 'BannerButton1', endIcon: , onClickFn : () => buyNow(1) }, { title : 'More Details', color : 'inherit', class : 'BannerButton2', endIcon : '', onClickFn : () => moreDetails() } ] }, ] return (
); } export default Hero; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/index.js ================================================ import Home from './home/homeContainer'; // import MyCoupons from './mycoupons/myCouponsContainer'; import Detail from '../pages/detail/detailContainer'; import List from './list/listContainer'; import SuggestedProductsList from './suggestedProductsList/suggestedProductsList'; import Profile from './profile/profileForm'; // import ShoppingCart from './shoppingcart/shoppingCart' import Arrivals from './arrivals/arrivals'; import RefundPolicy from './legals/refundPolicy'; import TermsOfService from './legals/termsOfService'; import AboutUs from './legals/aboutUs'; import ErrorPage from './error/errorPage'; import Cart from './cart/cart'; export { Home, Arrivals, // MyCoupons, Detail, List, SuggestedProductsList, Profile, // ShoppingCart, RefundPolicy, TermsOfService, AboutUs, ErrorPage, Cart, }; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/legals/aboutUs.js ================================================ import React from 'react'; import Breadcrump from '../../components/breadcrumb/breadcrumb' import { useLocation } from 'react-router-dom'; import './legals.scss' const AboutUs = (props) => { const location = useLocation(); const currentCategory = location.pathname.split("/").pop().replaceAll('-',' '); return ( <>

About Us

{/*

Our Mission

*/}

Contoso Traders is an e-commerce platform that specializes in electronic items. Our website offers a wide range of electronics, including smartphones, laptops, and other popular gadgets.

We pride ourselves on providing high-quality products at competitive prices, and our dedicated customer service team is always on hand to assist with any queries or concerns. With fast and secure shipping, convenient payment options, and a user-friendly interface, Contoso Traders is the perfect place to shop for all your electronic needs.


); } export default AboutUs; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/legals/legals.scss ================================================ .refund-policy-section{ margin-top: 192px; font-family: var(--fontInter); } .refund-policy-section .refund-policy{ padding: 0 70px 36px 70px; } .refund-policy .mainHeading{ margin: 24px 0; font-size: 24px; font-weight: 600; } .refund-policy .subHeading{ margin-top: 35px; font-size: 18px; font-weight: 600; } .refund-policy .paragraph{ font-size: 14px; } .refund-policy .paragraph b{ font-family: var(--fontInter); font-weight: 600; font-size: 16px; line-height: 2; } //Responsive @media screen and (min-width : 320px) and (max-width : 767px) { .refund-policy-section .refund-policy{ padding: 0 35px; text-align: justify; } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/legals/refundPolicy.js ================================================ import React from 'react'; import Breadcrump from '../../components/breadcrumb/breadcrumb' import { useLocation } from 'react-router-dom'; const RefundPolicy = (props) => { const location = useLocation(); const currentCategory = location.pathname.split("/").pop().replaceAll('-',' '); return ( <>

Refund Policy

At Contoso Traders, we want our customers to be completely satisfied with their purchases.

1. If for any reason you are not satisfied with your purchase, you may return it within 30 days of the original purchase date for a full refund.

2. To be eligible for a refund, the item must be in its original condition, unused, and with all original packaging and tags.

3. To initiate a refund, please contact our customer service team at support@contosotraders.com.

4. Please note that original shipping charges are non-refundable, and the customer is responsible for the cost of return shipping.


); } export default RefundPolicy; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/legals/termsOfService.js ================================================ import React from 'react'; import Breadcrump from '../../components/breadcrumb/breadcrumb' import { useLocation } from 'react-router-dom'; const TermsOfService = (props) => { const location = useLocation(); const currentCategory = location.pathname.split("/").pop().replaceAll('-',' '); return ( <>

Terms Of Service

{/*

OVERVIEW

*/}

Introduction
These Website Standard Terms and Conditions written on this webpage shall manage your use of our website, Website Name accessible at Contoso traders.

By using our website, you accepted these terms and conditions in full. If you disagree with these terms and conditions or any part of these terms and conditions, you must not use our website.

Intellectual Property Rights
Unless otherwise stated, we or our licensors own the intellectual property rights on the website and material on the website. Subject to the license below, all these intellectual property rights are reserved.

License to use website
You may view, download for caching purposes only, and print pages from the website for your own personal use, subject to the restrictions set out below and elsewhere in these terms and conditions.

You must not:

  • republish material from this website (including republication on another website);
  • sell, rent or sub-license material from the website.
  • show any material from the website in public.
  • reproduce, duplicate, copy or otherwise exploit material on our website for a commercial purpose.
  • Edit or otherwise modify any material on the website; or
  • Redistribute material from this website except for content specifically and expressly made available for redistribution.
Where content is specifically made available for redistribution, it may only be redistributed within your organisation.

Acceptable use
- You must not use our website in any way that causes, or may cause, damage to the website or impairment of the availability or accessibility of the website; or in any way which is unlawful, illegal, fraudulent, or harmful, or in connection with any unlawful, illegal, fraudulent, or harmful purpose or activity.

- You must not use our website to copy, store, host, transmit, send, use, publish or distribute any material which consists of (or is linked to) any spyware, computer virus, Trojan horse, worm, keystroke logger, rootkit, or other malicious computer software.

- You must not conduct any systematic or automated data collection activities (including without limitation scraping, data mining, data extraction and data harvesting) on or in relation to our website without our express written consent.

- You must not use our website to transmit or send unsolicited commercial communications.

- You must not use our website for any purposes related to marketing without our express written consent.

Restricted access
We reserve the right to restrict access to areas of our website, or indeed our whole website, at our discretion and without notice.

User content
In these terms and conditions, “your user content” means material (including without limitation text, images, audio material, video material and audio-visual material) that you submit to our website, for whatever purpose.

You grant to us a worldwide, irrevocable, non-exclusive, royalty-free license to use, reproduce, adapt, publish, translate, and distribute your user content in any existing or future media. You also grant to us the right to sub-license these rights, and the right to bring an action for infringement of these rights.

Your user content must not be illegal or unlawful, must not infringe any third party's legal rights, and must not be capable of giving rise to legal action whether against you or us or a third party (in each case under any applicable law).


); } export default TermsOfService; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/list/list.js ================================================ import { Grid } from "@mui/material"; import React from "react"; import { OfferBanner, ListGrid, ListAside } from "./sections"; import Breadcrump from "../../components/breadcrumb/breadcrumb"; import { useLocation } from "react-router-dom"; const List = ({ typesList, brandsList, onFilterChecked, productsList, loggedIn }) => { const location = useLocation(); const currentCategory = location.pathname.split("/").pop().replaceAll('-',' '); return (
{currentCategory === 'all products'? : }
{currentCategory}

); }; export default (List); ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/list/list.scss ================================================ // ----------------------------------------------------------------------------- // This file contains styles that are specific to the Products List pages. // ----------------------------------------------------------------------------- @import "../../main.scss"; .list { margin-top: 192px; &__content { padding: 0 70px; .mainHeading{ font-family: 'Inter'; font-weight: 600; font-size: 24px; padding: 24px 0; margin: 0; text-transform: capitalize; } .list__aside{ padding-right: 32px; .AccordionSection{ .MuiAccordion-root{ border-bottom: 1px solid #ebe9ef !important; .panelSummary{ // padding-left: 0; font-family: 'Inter'; font-weight: 500; font-size: 16px; color: #13101E; border-top: none; border-bottom: 1px solid #ebe9ef !important; img{ width: 20px; height: 20px; } } .MuiAccordionDetails-root{ padding: 16px !important; .descpAttributes{ display: flex; align-items: center; label{ margin: 0 0 0 10px; color: #626262; font-family: 'Inter'; font-weight: 500; font-size: 16px; } .MuiCheckbox-root{ // padding: 0 9px; width: 18px; height: 18px; background: #2874F0 0% 0% no-repeat padding-box; border-radius: 2px; cursor: pointer; } .Mui-checked{ color: #2874F0 !important; } } } } } } } .offer_banner{ display: flex; background-color: #ebeaef;//#ecf3f3 background-image: linear-gradient(180deg, #ebeaef 80%, #ecf3f3 20%); align-items: center; margin: 0 70px; .banner__buttons{ grid-template-columns: repeat(2, 1fr) !important; img{ height: 134px; margin: 10px 0 10px 140px; } } .firstHeading{ font-family: 'Poppins'; font-size: 24px; color: #13101E; margin: 0; } .secondHeading{ font-family: 'Inter'; font-size: 45px; color:#13101E; margin: 0; } } // &__aside { // grid-column-end: span 3; // } // &__panel { // background-color: $color-background-light; // top: -100vh; // height: 100vh; // left: 0; // padding: $padding-xl; // position: absolute; // transition: top $animation-speed-regular $animation-type-regular; // width: 100%; // will-change: top; // @media (min-width: $mq-m) { // top: auto; // padding: 0; // position: relative; // } // &.is-opened { // top: 0; // padding: $padding-xl; // z-index: $z-index-l; // } // .btn--primary { // bottom: $space-l; // position: absolute; // left: 50%; // transform: translateX(-50%); // @media (min-width: $mq-m) { // display: none; // } // } // } // .btn__reset { // border: 0; // background-color: transparent; // } // @media (min-width: $mq-m) { // .btn--float { // display: none; // } // } .list-grid { .filter-section{ display: flex; align-items: center; padding: 16px 0; .page{ margin-right: auto; } } .pagination{ display: flex; padding: 44px 0 24px 0; justify-content: flex-end; .MuiPagination-ul{ background: #F1F1F1; padding: 4px; border-radius: 12px; margin-right: 8px; } .MuiPaginationItem-textPrimary{ background-color: #F1F1F1; width: 42px; height: 28px; border-radius: 8px; color: #626262; font-family: 'Inter'; font-weight: 500; font-size: 12px; } .MuiPaginationItem-textPrimary.Mui-selected{ background-color: #000000; color: #fff !important; } .nextBtn{ background-color: #000000; color: #fff; padding: 7px 20px; min-width: 67px; height: 36px; border-radius: 12px; border: none; font-family: 'Inter'; font-weight: 500; font-size: 12px; } } // display: grid; // grid-column-gap: $grid-gap-m; // grid-template-columns: repeat(12, 1fr); // grid-column-end: span 12; // @media (min-width: $mq-m) { // grid-column-end: span 9; // grid-row-gap: 5rem; // } .card__item { grid-column: span 12; @media (min-width: 30rem) { grid-column: span 6; } @media (min-width: 68.75rem) { grid-column: span 4; } } .card__photo { height: 12rem; margin-top: -2.125rem; width: 12rem; @media (min-width: 34.25em) { height: 14rem; width: 14rem; } } .btn { box-shadow: $box-shadow-l; margin-bottom: -$margin-m; margin-left: auto; margin-right: auto; } } } @media screen and (min-width : 320px) and (max-width : 767px) { .list{ margin-top: 0 !important; } .list .offer_banner{ margin: 0; } .list__content{ padding: 0 35px; } .list__content .list__aside{ padding-right: 0; } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/list/listContainer.js ================================================ import React, { Fragment } from 'react'; // import { withRouter } from 'react-router-dom'; import LoadingSpinner from '../../components/loadingSpinner/loadingSpinner'; import './list.scss' import List from './list'; import { ProductService } from '../../services'; import { useParams } from 'react-router-dom'; // class ListContainer extends Component { // constructor(props) { // super(props); // this.state = { // typesList: [], // brandsList: [], // productsList: [], // queryString: '', // loading: true, // }; // this.queryString = [ // { // brand: [], // type: [], // }, // ]; // this.type = []; // this.brand = []; // } const brand = [] function ListContainer() { const [typesList, setTypesList] = React.useState([]); const [brandsList, setBrandsList] = React.useState([]); const [productsList, setProductsList] = React.useState(''); const queryString = { brand: brand, type: '', } ; const [loading, setLoading] = React.useState(true); const [getType, setType] = React.useState([]); // const type = [] const { code } = useParams(); // React.useEffect(() => { // const filter = code || ''; // getProductData(code); // // setPageState(filteredProductsPageData); // }, []); React.useEffect(() => { getProductData(code); }, [code]);// eslint-disable-line react-hooks/exhaustive-deps const getProductData = async(type) => { setType(type) const filter = type === '' ? {} : (queryString.type = { type }); const filteredProductsPageData = await ProductService.getFilteredProducts(filter); setPageState(filteredProductsPageData.data) // return filteredProductsPageData.data; } const setPageState = (filteredProductsPageData) => { if (filteredProductsPageData === undefined) { return; } const typesList = filteredProductsPageData.types; const brandsList = filteredProductsPageData.brands; const productsList = filteredProductsPageData.products; // this.setState({ productsList, typesList, brandsList, loading: false }); setTypesList(typesList); setBrandsList(brandsList); setProductsList(productsList); setLoading(false); } const onFilterChecked = async (e, value) => { const isChecked = e.target.checked; const dataType = e.target.getAttribute('id'); setQueryStringState(isChecked, dataType, value); const apiCall = await ProductService.getFilteredProducts(queryString); // setState({ productsList: apiCall.data.products }); setProductsList(apiCall.data.products) }; const setQueryStringState = (isChecked, dataType, value) => { if (isChecked) { brand.push(dataType); queryString.brand = brand; queryString.type = getType ? getType : ''; queryString.type = queryString.type.type === undefined ? queryString.type : queryString.type.type; } else { let index = queryString[value].indexOf(dataType); if (index !== -1) { queryString[value].splice(index, 1); } queryString.type = getType ? getType : ''; } } return ( {loading ? ( ) : ( )} ); } // } export default (ListContainer); ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/list/sections/banner/offerBanner.js ================================================ import React from "react"; // import { withNamespaces } from "react-i18next"; import offerImg from '../../../../assets/images/original/Contoso_Assets/product_page_assets/collection_page_banner.jpg'; const OfferBanner = ({ t, loggedIn }) => { return (
); }; export default (OfferBanner); ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/list/sections/index.js ================================================ import ListAside from './listAside/listAside'; import ListGrid from './listGrid/listGrid'; import OfferBanner from './banner/offerBanner'; export { ListAside, ListGrid, OfferBanner, }; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/list/sections/listAside/listAside.js ================================================ import React, { Component } from "react"; // import { NamespacesConsumer } from "react-i18next"; import SidebarAccordion from "../../../../components/accordion/sidebarAccordion"; class ListAside extends Component { constructor() { super(); this.filterPanel = React.createRef(); this.state = { isopened: false, showComponent: false, }; this.openFilterPanel = this.openFilterPanel.bind(this); } componentDidMount() { const setComponentVisibility = this.setComponentVisibility.bind(this); setComponentVisibility(document.documentElement.clientWidth); window.addEventListener("resize", function () { setComponentVisibility(document.documentElement.clientWidth); }); } setComponentVisibility(width) { if (width > 768) { this.setState({ showComponent: true }); } else { this.setState({ showComponent: false, isopened: false }); } } toggleClass = () => { document.body.classList.toggle("u-overflow-hidden"); this.setState(prevState => ({ isopened: !prevState.isopened, showComponent: !prevState.showComponent, })); }; openFilterPanel() { if (this.state.showComponent === false) { this.setState( { showComponent: true, isopened: true, }, () => { let currentPosition = window.pageYOffset; this.filterPanel.current.style.top = `${currentPosition}px`; document.body.classList.toggle("u-overflow-hidden"); } ); } } render() { return ( ); } } export default ListAside; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/list/sections/listGrid/listGrid.js ================================================ import { Grid } from "@mui/material"; import React from "react"; import Product from "../../../../components/productCard/product"; // import MinimalSelect from "../../../../components/minimalselect"; // import Pagination from '@material-ui/lab/Pagination'; const ListGrid = ({ productsList }) => { return (
Showing 1 - {productsList ? productsList.length : 0} of {productsList ? productsList.length:0} items
{/*
Sort By
     */}
{productsList && productsList.length > 0 ? {productsList && productsList.map((productsListInfo, index) => { return ; })} :

No Products Found

} {/* */}
); }; export default ListGrid; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/profile/myAddressBook.js ================================================ import React from 'react'; function MyAddressBook(props) { return (
Address
); } export default MyAddressBook; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/profile/myOrders.js ================================================ import React from "react"; import { Button, Grid, IconButton, Typography, Divider, Avatar, FormLabel, InputAdornment, FormControl, TextField, } from "@mui/material"; import DeleteOutline from "@mui/icons-material/DeleteOutline"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; import Visibility from "@mui/icons-material/Visibility"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { useFormik } from "formik"; import * as yup from "yup"; const validationSchema = yup.object({ email: yup .string("Enter your email") .email("Enter a valid email") .required("Email is required"), newpassword: yup .string("Enter your password") .min(8, "Password should be of minimum 8 characters length") .required("Password is required"), currentpassword: yup .string("Enter your password") .min(8, "Password should be of minimum 8 characters length") .required("Password is required"), confirmpassword: yup .string("Enter your password") .min(8, "Password should be of minimum 8 characters length") .required("Password is required"), firstName: yup .string("Enter your first Name") .min(2, "Too Short!") .max(50, "Too Long!") .required("First name is required"), lastName: yup .string("Enter your last name") .min(2, "Too Short!") .max(50, "Too Long!") .required("Last name is required"), dob: yup .string("Enter your date of birth") .required("Date of Birth is required"), mobile: yup .number("Enter your mobile number") .positive("Invalid Number") .required("Mobile is required"), }); const MyOrders = () => { const [values, setValues] = React.useState({ password: "", showPassword: false, }); const handleClickShowPassword = () => { setValues({ ...values, showPassword: !values.showPassword, }); }; const [mydata, setData] = React.useState(""); const onImageChange = (event) => { if (event.target.files && event.target.files[0]) { let reader = new FileReader(); let file = event.target.files[0]; reader.onloadend = () => { setData({ ...mydata, imagePreview: reader.result, file: file, }); }; reader.readAsDataURL(file); } }; const formik = useFormik({ initialValues: { firstName: "", lastName: "", email: "", newpassword: "", currentpassword: "", confirmpassword: "", dob: "", mobile: "", }, validationSchema: validationSchema, onSubmit: (values) => { alert(JSON.stringify(values, null, 2)); }, }); return (
My Address Book
First Name Last Name Email Mobile Number Date of birth } style={{ height: "55px", }} >
Change Password
Current Password New Password {values.showPassword ? ( ) : ( )} ), }} /> Confirm Password
); }; export default MyOrders; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/profile/myWishlist.js ================================================ import React from 'react'; import { useLocation } from 'react-router-dom'; import Breadcrumb from '../../components/breadcrumb/breadcrumb'; function Wishlist(props) { const location = useLocation(); const currentCategory = location.pathname.split("/").pop().replaceAll('-',' '); return (
My Wishlist

); } export default Wishlist; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/profile/personalInformation.js ================================================ import React from "react"; import { Button, Grid, IconButton, Typography, Divider, Avatar, FormLabel, InputAdornment, TextField, } from "@mui/material"; import DeleteOutline from "@mui/icons-material/DeleteOutline"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; import Visibility from "@mui/icons-material/Visibility"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { useFormik } from "formik"; import * as yup from "yup"; const validationSchema = yup.object({ email: yup .string("Enter your email") .email("Enter a valid email") .required("Email is required"), newpassword: yup .string("Enter your password") .min(8, "Password should be of minimum 8 characters length") .required("Password is required"), currentpassword: yup .string("Enter your password") .min(8, "Password should be of minimum 8 characters length") .required("Password is required"), confirmpassword: yup .string("Enter your password") .min(8, "Password should be of minimum 8 characters length") .required("Password is required"), firstName: yup .string("Enter your first Name") .min(2, "Too Short!") .max(50, "Too Long!") .required("First name is required"), lastName: yup .string("Enter your last name") .min(2, "Too Short!") .max(50, "Too Long!") .required("Last name is required"), dob: yup .string("Enter your date of birth") .required("Date of Birth is required"), mobile: yup .number("Enter your mobile number") .positive("Invalid Number") .required("Mobile is required"), }); const MyOrders = () => { const [values, setValues] = React.useState({ password: "", showPassword: false, }); const handleClickShowPassword = () => { setValues({ ...values, showPassword: !values.showPassword, }); }; const [mydata, setData] = React.useState(""); const onImageChange = (event) => { if (event.target.files && event.target.files[0]) { let reader = new FileReader(); let file = event.target.files[0]; reader.onloadend = () => { setData({ ...mydata, imagePreview: reader.result, file: file, }); }; reader.readAsDataURL(file); } }; const formik = useFormik({ initialValues: { firstName: "", lastName: "", email: "", newpassword: "", currentpassword: "", confirmpassword: "", dob: "", mobile: "", }, validationSchema: validationSchema, onSubmit: (values) => { alert(JSON.stringify(values, null, 2)); }, }); return (
Personal Information
{/*
*/} {/* */}
First Name
{/* */}
Last Name
{/* */} {/* */} Email {/* */} {/* */} Mobile Number {/* */} {/* */} Date of birth } style={{ height: "55px", }} > {/* */} {/*
*/} {/*
*/}
Change Password
Current Password New Password {values.showPassword ? ( ) : ( )} ), }} /> Confirm Password
); }; export default MyOrders; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/profile/profile.scss ================================================ // ----------------------------------------------------------------------------- // This file contains styles that are specific to the User Profile page. // ----------------------------------------------------------------------------- @import "../../main.scss"; .profileMain { margin-top: 192px; .ProfileSection{ padding: 0 70px 0 70px; input[type="date"]::-webkit-calendar-picker-indicator { display: none; -webkit-appearance: none; appearance: none; } .MuiInputAdornment-positionStart{ position: absolute; right: 15px; } } .ProfileSection .topHeaderSection { display: flex; align-items: center; } .ProfileSection .myprofileHeader { margin: 0px auto 0px 0px; text-align: left; font: normal normal 600 24px/44px "Inter"; letter-spacing: 0px; color: #000000; } .ProfileSection .logout-btn { background: #ffffff 0% 0% no-repeat padding-box; border: 1px solid #2874f0; border-radius: 8px; width: 119px; height: 40px; text-align: center; font: normal normal 600 16px/24px "Inter"; letter-spacing: 0px; color: #2874f0; text-transform: capitalize; box-shadow: none; } .ProfileSection .sidebar-container { margin: 45px 0px 0px 0px; } .ProfileSection .sidebar-item { display: flex; align-items: center; background-color: #fff; height: 56px; margin-top: 20px; max-width: 320px; border-left: 4px solid white; cursor: pointer; } .ProfileSection .sidebar-item.active { border-radius: 2px 0px 0px 2px; border-left: 4px solid #2874F0; background-color: rgba(40, 116, 240, 0.08); } .ProfileSection .sidebarIcons{ margin: 0px 16px 0px 20px; width: 24px; height: 24px; } .ProfileSection .item-content{ text-align: left; font-family: "Inter"; font-size: 16px; font-weight: 500; letter-spacing: 0px; color: #000000; } .ProfileSection .item-arrow{ margin: 0px 31px 0px auto; } .ProfileSection .form-container { margin: 45px 0px 0px 32px; } .ProfileSection .section-heading { text-align: left; font: normal normal 600 20px/26px "Inter"; letter-spacing: 0px; color: #131; } .ProfileSection .img-section { display: flex; align-items: center; margin: 30px 0px 31px 0px; } .ProfileSection .btn-upload { background: #000000 0% 0% no-repeat padding-box; border-radius: 8px; color: #fff; box-shadow: none !important; width: 136px; height: 38px; text-align: center; font: normal normal medium 14px/18px "Inter"; letter-spacing: 0px; color: #ffffff; text-transform: capitalize; margin-right: 16px; } .ProfileSection .btn-delete { background: #ffffff 0% 0% no-repeat padding-box; border: 2px solid #b00020; border-radius: 8px; width: 136px; height: 44px; text-align: center; font: normal normal medium 14px/18px "Inter"; letter-spacing: 0px; color: #b00020; text-transform: capitalize; box-shadow: none !important; } .ProfileSection .img-div { margin-right: 16px; } .ProfileSection .form-labels { text-align: left; font-family: "Inter" !important; letter-spacing: 0px; color: #171520 !important; font-weight: 500 !important; font-size: 16px; margin-bottom: 7px; } .MuiInput-underline::before { border-bottom: none !important; } .ProfileSection .submit-div { float: right; } .ProfileSection .btn-submit { background: #2874f0 0% 0% no-repeat padding-box; border-radius: 8px; box-shadow: none !important; text-transform: capitalize; text-align: center; margin-top: 50px; font-size: 16px; font-family: "Inter"; font-weight: 500; letter-spacing: 0px; color: #ffffff; } .ProfileSection .formtextfields{ width: 100%; background: #F1F1F1; border-radius: 4px; border: 0; color: #626262; font-family: "Inter"; font-weight: 500; font-size: 16px; margin: 7px 0px 16px 0px; } .ProfileSection .MuiInputBase-input { padding: 17px 23px 14px 16px; // width: 550px; } .ProfileSection .newpassword-container .MuiInputBase-input { padding: 17px 23px 14px 16px; width: 100%; } .ProfileSection .MuiFormHelperText-root.Mui-error { color: red; background: white; margin: 0; } .ProfileSection .formsection{ width: 100%; } .ProfileSection .passworddiv{ margin-top: 50px; } .PersonalSection{ width: 100%; } .ProfileSection .deleteIconsvg{ width:24px; height: 24px; } } .profile { margin-bottom: 11.375rem; margin-left: auto; margin-right: auto; max-width: 87.5rem; .grid-container { grid-template-columns: 1fr; @media (min-width: $mq-m) { margin: 0 auto; display: grid; grid-template-columns: 1.4fr 1fr; grid-template-rows: 25rem; grid-auto-rows: auto; grid-column-gap: 1.5rem; grid-row-gap: 5rem; grid-template-areas: "profile-card aside" "history aside"; .profile-card { grid-area: profile-card; } .history { grid-area: history; } .aside { grid-area: aside; } } } &__data { &--empty { margin-right: $margin-m; margin-left: $margin-m; box-shadow: $box-shadow-l; padding-left: $grid-gap-m; padding-right: $grid-gap-m; padding-bottom: $grid-gap-m; padding-top: $grid-gap-m; @media (min-width: $mq-m) { margin-right: $margin-m; margin-left: 0; } } } &__heading { display: flex; justify-content: space-between; align-items: center; .btn { padding-left: $padding-m; padding-right: $padding-m; margin-right: $margin-m; @media (min-width: $mq-s) { padding-left: $padding-l; padding-right: $padding-l; } } &-title { @include text-xl; color: $color-text-primary; text-align: center; margin-top: 2.5rem; margin-bottom: 2.5rem; margin-left: $margin-l; text-align: left; } &-history { margin-left: -4rem; padding-left: $padding-m; } &-recommended { margin-bottom: 5rem; @media (min-width: $mq-m) { margin-bottom: 5.25rem; } } } &-wapper { margin: $margin-m; box-shadow: $box-shadow-l; padding-left: 1.5rem; padding-right: 1.5rem; padding-bottom: 1.5rem; @media (min-width: $mq-m) { display: flex; flex-direction: row; align-items: center; padding-top: 1.5rem; } } &__image { text-align: center; margin-left: auto; margin-right: auto; padding-top: $padding-l; padding-bottom: $padding-m; width: auto; text-align: center; margin: 0 auto; @media (min-width: $mq-s) { margin: 0 auto; width: 10rem; } @media (min-width: $mq-m) { margin-right: $margin-m; margin-left: 0; } img { max-width: 8.5rem; border-radius: 50%; } } &__info { color: $color-text-primary; font-size: $font-big; } &__title { margin-top: 0; margin-bottom: .5rem; font-weight: $font-weight-light; } &__subtitle { margin: 0; margin-bottom: $margin-m; } .mycoupons { &__grid { max-width: 100%; margin-left: $margin-m; margin-right: $margin-m; .coupon { grid-column: span 12; &__img { background-size: cover; background-position: center; } } } } .history { padding-left: 4rem; padding-right: $padding-m; @media (min-width: $mq-s) { padding-left: 6rem; } &__data { display: flex; align-items: center; margin-left: -2.875rem; @media (min-width: $mq-s) { margin-left: -4.875rem; } } &__item { background: #fff; box-shadow: $box-shadow-l; width: 100%; margin-bottom: $margin-m; } &__image { border-radius: 4.875rem; width: 6rem; height: 6rem; z-index: 1; @media (min-width: $mq-s) { width: 9.75rem; height: 9.75rem; } } &__info { @include text-m; font-weight: $font-weight-medium; color: $color-text-primary; min-height: 6.875rem; display: flex; flex-direction: column; justify-content: center; line-height: 2rem; margin-left: $margin-m; @media (min-width: $mq-s) { height: 9.5rem; margin-left: 0; padding-left: $padding-m; font-size: 1.5rem; } &-name { margin: 0; @media (min-width: $mq-s) { margin-bottom: .5rem; } } &-price { margin: 0; } } } .cards { margin-bottom: 0; @media (min-width: $mq-m) { margin-bottom: 5rem; } .btn { box-shadow: 10px 10px 52px 0 rgba(0, 0, 0, 0.2); display: block; margin-bottom: -$margin-m; margin-left: auto; margin-right: auto; } .card { &__description { max-width: 11.25rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @media (min-width: $mq-s) { max-width: 11.25rem; line-height: 2rem; } } } } .favorite { display: flex; flex-direction: column; @media (min-width: $mq-s) { flex-direction: row; } &__item { background-size: cover; background-repeat: no-repeat; align-items: center; display: flex; grid-column-end: span 4; justify-content: center; min-height: 14.5rem; // 232px background-position: center; margin-bottom: $margin-m; margin-left: $margin-m; margin-right: $margin-m; @media (min-width: $mq-s) { width: 100%; &:last-child { display: none; } } @media (min-width: $mq-m) { &:last-child { display: flex; } } } } } @media screen and (min-width : 320px) and (max-width : 767px) { .profileMain .ProfileSection { padding: 0 35px; } .profileMain .ProfileSection .form-container{ margin-left: 0 } .profile-name{ margin: 0 !important; } } @media screen and (min-width : 900px) and (max-width : 1350px) { .profile-div{ -ms-flex: 1 1 auto!important; flex: 1 1 auto!important; } } @media screen and (min-width : 768px) and (max-width : 899px) { .profile-div{ -ms-flex: 1 1 auto!important; flex: 1 1 auto!important; } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/profile/profileForm.js ================================================ import React from "react"; import { useParams } from "react-router-dom"; import { connect } from 'react-redux'; import { Button, Grid, Avatar, } from "@mui/material"; import NavigateNext from "@mui/icons-material/NavigateNext"; import PersonalInformation from "./personalInformation"; // import MyWishlist from "./MyWishlist"; // import MyOrders from "./MyOrders"; // import MyAddressBook from "./MyAddressBook"; import logout_icon from "../../assets/images/original/Contoso_Assets/profile_page_assets/logout_icon.svg"; import personal_information_icon from "../../assets/images/original/Contoso_Assets/profile_page_assets/personal_information_icon.svg"; // import my_wishlist_icon from "../../assets/images/original/Contoso_Assets/profile_page_assets/my_wishlist_icon.svg"; // import my_address_book_icons from "../../assets/images/original/Contoso_Assets/profile_page_assets/my_address_book_icons.svg"; // import my_orders_icon from "../../assets/images/original/Contoso_Assets/profile_page_assets/my_orders_icon.svg"; import Breadcrump from "../../components/breadcrumb/breadcrumb"; import AuthB2CService from "../../services/authB2CService"; import './profile.scss' const FormProfile = (props) => { const authService = new AuthB2CService(); const { page } = useParams() const [activeState, setActiveState] = React.useState(page); const onClickLogout = () => { localStorage.clear(); if (props.userInfo.isB2c) { authService.logout(); } props.clickAction(); props.history.push('/'); } return (

My Profile

{ setActiveState("personal"); }} >
Personal Information
{/*
{ setActiveState("orders"); }} >
My Orders
*/} {/*
{ setActiveState("wishlist"); }} >
My Wishlist
*/} {/*
{ setActiveState("address"); }} >
My Address Book
*/}
{activeState === "personal" ? : null} {/* {activeState === "orders" ? : null} {activeState === "wishlist" ? : null} {activeState === "address" ? : null} */}
); }; const mapStateToProps = (state) => { return { userInfo : state.login.userInfo, theme : state.login.theme } }; export default connect(mapStateToProps, null)(FormProfile); ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/suggestedProductsList/suggestedProductsList.js ================================================ import { Grid } from "@mui/material"; import React from "react"; import Breadcrump from "../../components/breadcrumb/breadcrumb"; import { ListGrid } from "../list/sections"; import { useLocation } from "react-router-dom"; const SuggestedProductsList = (props) => { const [suggestedProductsList, setSuggestedProductsList] = React.useState(null); const location = useLocation(); const currentCategory = location.pathname.split("/").pop().replaceAll('-',' '); React.useEffect(() => { const suggestedProducts = location.state; setSuggestedProductsList(suggestedProducts.relatedProducts); }, [location.state]); return (
{currentCategory}
{/* */}

); }; export default SuggestedProductsList; ================================================ FILE: src/ContosoTraders.Ui.Website/src/pages/suggestedProductsList/suggestedproductslist.scss ================================================ // ----------------------------------------------------------------------------- // This file contains all styles related to the Suggested Products List page // ----------------------------------------------------------------------------- .suggested-list-grid{ margin-top: 137px; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/reducers/login.reducer.js ================================================ import { FORM_EMAIL, SAVE_USER, REMOVE_USER, THEME_CHANGE, GET_QUANTITY } from '../types/types'; let userInfo = JSON.parse(localStorage.getItem('state')); const initialDefaultState = { userInfo: { loggedIn: false, token: '', user: { email: '', type: '' } }, theme : false, quantity : 0 } const defaultState = userInfo ? { userInfo } : { ...initialDefaultState }; const login = (state = defaultState, action) => { switch (action.type) { case FORM_EMAIL: return { ...state, ...action }; case SAVE_USER: return { userInfo: action.userInfo }; case GET_QUANTITY: return { ...state, quantity : action.quantity }; case THEME_CHANGE: return { ...state, [action.field] : action.value }; case REMOVE_USER: return { ...initialDefaultState }; default: return defaultState; } }; export default login; ================================================ FILE: src/ContosoTraders.Ui.Website/src/reducers/reducers.js ================================================ import { combineReducers } from 'redux'; import login from './login.reducer'; // import theme from './theme.reducer'; export default combineReducers({ login }); ================================================ FILE: src/ContosoTraders.Ui.Website/src/reportWebVitals.js ================================================ const reportWebVitals = onPerfEntry => { if (onPerfEntry && onPerfEntry instanceof Function) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); getLCP(onPerfEntry); getTTFB(onPerfEntry); }); } }; export default reportWebVitals; ================================================ FILE: src/ContosoTraders.Ui.Website/src/services/authB2CService.js ================================================ // import { config } from 'dotenv'; import * as Msal from 'msal'; import { ConfigService } from './' export default class AuthB2CService { constructor() { this.applicationConfig = { auth: { clientId: ConfigService._B2cClientId, authority: ConfigService._B2cAuthority, validateAuthority: false, redirectUri: `${window.location.origin}/authcallback` }, cache:{ cacheLocation: "sessionStorage", storeAuthStateInCookie: false } } this.msalAgent = new Msal.UserAgentApplication(this.applicationConfig); // this.msalAgent = new PublicClientApplication(this.applicationConfig); this.msalAgent.handleRedirectCallback(async (error, response) => { if(!response.accessToken) { await this.login(); } }); } login = async () => { let responseUser; await this.msalAgent.loginPopup( { scopes: ConfigService._B2cScopes, prompt: 'select_account' } ).then(response => { responseUser = response.account; }).catch((err) => { console.log('err',err) }); return (responseUser) ? responseUser : null; // this.msalAgent.loginRedirect({scopes:ConfigService._B2cScopes}); // this.msalAgent.handleRedirectPromise((response) => { // console.log(response) // }); } logout = () => this.msalAgent.logout(); getToken = async () => { return await this.msalAgent.acquireTokenSilent({ scopes: [ConfigService._B2cScopes], authority: ConfigService._B2cAuthority }) .then(accessToken => accessToken.accessToken); }; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/services/cartService.js ================================================ import axios from "axios"; import { ConfigService } from "./" require('../helpers/errorsHandler'); const CartService = { async getShoppingCart(token) { await ConfigService.loadSettings(); if (ConfigService._byPassShoppingCartApi) { const items = await ConfigService._shoppingCartDao.find(token); return items; } try { const response = await axios.get(`${ConfigService._apiUrlShoppingCart}/shoppingcart`, ConfigService.HeadersConfig(token)); return response.data; } catch(e) { return null; } }, async postProductToCart(token, detailProduct) { const products = await this.getShoppingCart(token); const product = products.find(product => product.id === detailProduct.id); if (product) { return this.updateQuantity(product._cdbid, product.qty + 1, token) .then(() => ({message: "Product added on shopping cart"})) .catch(() => ({ errMessage: "The product could not be added to the cart" })) } return this.addProduct(token, detailProduct); }, async addProduct(token, detailProduct) { await ConfigService.loadSettings(); const productInfo = { id: detailProduct.id, name: detailProduct.name, price: detailProduct.price, imageUrl: detailProduct.imageUrl, email: detailProduct.email, typeid: detailProduct.type.id }; const cartItems = { "cartItemId": Math.floor(Math.random() * 1000).toString(), "email": detailProduct.email.toLowerCase(), "productId": detailProduct.id, "name": detailProduct.name, "price": detailProduct.price, "imageUrl": detailProduct.imageUrl, "quantity": detailProduct.quantity, }; const dataToPost = { detailProduct: productInfo, qty: detailProduct.quantity }; if (ConfigService._byPassShoppingCartApi) { await ConfigService._shoppingCartDao.addItem(dataToPost); return { message: "Product added on shopping cart" }; } const addProduct = axios.post(`${ConfigService._apiUrlShoppingCart}/shoppingcart`, cartItems, ConfigService.HeadersConfig(token)) .then((response) => { return response.data; }) .catch(() => { return { errMessage: "The product could not be added to the cart" } }) return addProduct; }, async getRelatedDetailProducts(token, typeid = {}) { await ConfigService.loadSettings(); const response = await axios.get(`${ConfigService._apiUrlShoppingCart}/shoppingcart/relatedproducts/?type=${typeid}`, ConfigService.HeadersConfig(token)); return response.data[0]; }, async updateQuantity(detailProduct, qty, token) { await ConfigService.loadSettings(); const product = { "cartItemId": detailProduct.cartItemId, "email": detailProduct.email, "productId": detailProduct.productId, "name": detailProduct.name, "price": detailProduct.price, "imageUrl": detailProduct.imageUrl, "quantity": qty, }; const response = await axios.put(`${ConfigService._apiUrlShoppingCart}/shoppingcart/product`, product, ConfigService.HeadersConfig(token)); return response; }, async deleteProduct(detailProduct, token) { await ConfigService.loadSettings(); let config = ConfigService.HeadersConfig(token); config.data = { // id: id, "cartItemId": detailProduct.cartItemId, "email": detailProduct.email, "productId": detailProduct.productId, "name": detailProduct.name, "price": detailProduct.price, "imageUrl": detailProduct.imageUrl, "quantity": detailProduct.qty, } // const product = { // }; const response = await axios.delete(`${ConfigService._apiUrlShoppingCart}/shoppingcart/product`, config); return response; } } export default CartService; ================================================ FILE: src/ContosoTraders.Ui.Website/src/services/configService.js ================================================ import axios from "axios"; // require("dotenv").config(); const settingsUrl = "/api/settings"; // Note: The '{PRODUCTS_API_ENDPOINT}', '{CARTS_API_ENDPOINT}' tokens will be substituted by github workflow. const APIUrl = process.env.REACT_APP_APIURL; const APIUrlShoppingCart = process.env.REACT_APP_APIURLSHOPPINGCART; const UseB2C = process.env.REACT_APP_USEB2C; const B2cAuthority = process.env.REACT_APP_B2CAUTHORITY; const B2cClientId = process.env.REACT_APP_B2CCLIENTID; const B2cScopes = [process.env.REACT_APP_B2CSCOPES]; const userEmail = localStorage.getItem('state') ? JSON.parse(localStorage.getItem('state')).userName : null; const _HeadersConfig = (token, devspaces = undefined) => { const headers = token ? { Authorization: `Bearer ${token}` } : {}; if(userEmail){ headers['x-tt-email'] = userEmail } if (devspaces) { headers["azds-route-as"] = devspaces; } return { headers: headers }; }; const ConfigService = { _needLoadSettings: !APIUrl || !APIUrlShoppingCart, _apiUrl: APIUrl, _apiUrlShoppingCart: APIUrlShoppingCart, _UseB2C: UseB2C, _B2cAuthority: B2cAuthority, _B2cClientId: B2cClientId, _B2cScopes: B2cScopes, _applicationInsightsIntrumentationKey: "", _debugInformation: {}, _acsConnectionString: "", _acsResource: "", _logicAppUrl: "", _email: "", _customerSupportEnabled: false, async loadSettings() { if (this._needLoadSettings) { const settingsResponse = await axios.get(settingsUrl); // const settingsResponse = { // data: { // auth: null, // apiUrl: "https://f6b8ea119d5d435c846c.westus2.aksapp.io/webbff/v1", // apiUrlShoppingCart: "https://backend.contosotraders.com/cart-api", // useB2C: false, // b2CAuth: { clientId: "", authority: "", scopes: "" }, // cart: null, // applicationInsights: { instrumentationKey: null }, // debugInformation: { // sqlServerName: null, // mongoServerName: null, // customText: "", // showDebug: false, // }, // byPassShoppingCartApi: false, // devspacesName: "", // productImagesUrl: null, // personalizer: { apiKey: "", endpoint: "" }, // }, // }; this._needLoadSettings = false; this._apiUrl = settingsResponse.data.apiUrl; this._apiUrlShoppingCart = settingsResponse.data.apiUrlShoppingCart; this._UseB2C = settingsResponse.data.useB2C; if (this._UseB2C) { this._B2cAuthority = settingsResponse.data.b2CAuth.authority; this._B2cClientId = settingsResponse.data.b2CAuth.clientId; this._B2cScopes = settingsResponse.data.b2CAuth.scopes; } this._devspacesName = settingsResponse.data.devspacesName; this._applicationInsightsIntrumentationKey = settingsResponse.data.applicationInsights.instrumentationKey; this._debugInformation = settingsResponse.data.debugInformation; this._acsConnectionString = settingsResponse.data.acs.connectionString; this._acsResource = settingsResponse.data.acs.resource; this._logicAppUrl = settingsResponse.data.logicAppUrl; this._email = settingsResponse.data.email; this._customerSupportEnabled = settingsResponse.data.email && settingsResponse.data.acs.resource && settingsResponse.data.acs.connectionString && settingsResponse.data.logicAppUrl; } }, HeadersConfig(token = undefined) { return _HeadersConfig(token, this._devspacesName); }, }; export default ConfigService; ================================================ FILE: src/ContosoTraders.Ui.Website/src/services/index.js ================================================ import UserService from './userService'; import CartService from './cartService'; import ProductService from './productsService'; import ConfigService from './configService'; export { UserService, CartService, ProductService, ConfigService }; ================================================ FILE: src/ContosoTraders.Ui.Website/src/services/productsService.js ================================================ import axios from "axios"; import { ConfigService } from "./" const qs = require('qs'); require('../helpers/errorsHandler'); const ProductService = { async getHomePageData() { await ConfigService.loadSettings(); const response = await axios.get(`${ConfigService._apiUrl}/products/landing`, ConfigService.HeadersConfig(), { errorHandle: false }) return response; }, async getCouponsPageData(token) { await ConfigService.loadSettings(); const response = await axios.get(`${ConfigService._apiUrl}/coupons`, ConfigService.HeadersConfig(token), { errorHandle: false }); return response; }, async getFilteredProducts(filters = {}) { await ConfigService.loadSettings(); filters.type = filters.type.type === undefined ? filters.type : filters.type.type; const params = { 'params': filters, 'paramsSerializer': qs.stringify(filters, { arrayFormat: 'repeat' }) } const response = await axios.get(`${ConfigService._apiUrl}/products/?`+params.paramsSerializer, ConfigService.HeadersConfig(), { errorHandle: false }); return response; }, async getDetailProductData(productId) { await ConfigService.loadSettings(); const response = await axios.get(`${ConfigService._apiUrl}/products/${productId}`, ConfigService.HeadersConfig(), { errorHandle: false }); return response && response.data ? response.data : null; }, async getRelatedProducts(formData, token) { await ConfigService.loadSettings(); const response = await axios.post(`${ConfigService._apiUrl}/products/imageclassifier`, formData, ConfigService.HeadersConfig(token)); return response.data; }, async getSearchResults(term) { await ConfigService.loadSettings(); const response = await axios.get(`${ConfigService._apiUrl}/Products/search/${term}`, ConfigService.HeadersConfig(), { errorHandle: false }); return response.data; }, } export default ProductService; ================================================ FILE: src/ContosoTraders.Ui.Website/src/services/telemetryClient.js ================================================ import { ApplicationInsights } from '@microsoft/applicationinsights-web'; import { ReactPlugin } from '@microsoft/applicationinsights-react-js'; class TelemetryService { constructor() { this.reactPlugin = new ReactPlugin(); } initialize(appInsightsInstrumentationKey, reactPluginConfig) { this.appInsights = new ApplicationInsights({ config: { instrumentationKey: appInsightsInstrumentationKey, maxBatchInterval: 0, disableFetchTracking: false, extensions: [this.reactPlugin], extensionConfig: { [this.reactPlugin.identifier]: reactPluginConfig } } }); this.appInsights.loadAppInsights(); } } export let ai = new TelemetryService(); ================================================ FILE: src/ContosoTraders.Ui.Website/src/services/userService.js ================================================ import axios from "axios"; import { ConfigService } from "./" require('../helpers/errorsHandler'); const UserService = { async postLoginForm(formData) { await ConfigService.loadSettings(); const response = await axios.post(`${ConfigService._apiUrl}/login`, formData, ConfigService.HeadersConfig(), { errorHandle: false }); return response; }, async getUserInfoData(token) { await ConfigService.loadSettings(); const response = await axios.get(`${ConfigService._apiUrl}/profiles/me`, ConfigService.HeadersConfig(token), { errorHandle: false }); return response.data; }, async getProfileData(token) { await ConfigService.loadSettings(); const response = await axios.get(`${ConfigService._apiUrl}/profiles/navbar/me`, ConfigService.HeadersConfig(token), { errorHandle: false }); return response.data; } } export default UserService; ================================================ FILE: src/ContosoTraders.Ui.Website/src/setupTests.js ================================================ // jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; ================================================ FILE: src/ContosoTraders.Ui.Website/src/store.js ================================================ import { createStore } from 'redux'; import reducers from './reducers/reducers'; const store = createStore(reducers); export default store; ================================================ FILE: src/ContosoTraders.Ui.Website/src/styles/abstracts/_variables.scss ================================================ // ----------------------------------------------------------------------------- // This file contains all application-wide Sass variables. // ----------------------------------------------------------------------------- // // #Colors // // #Base-Color $color-base: #000000; $color-base-darker: lighten($color-base, 20); $color-base-dark: lighten($color-base, 40); $color-base-light: lighten($color-base, 60); $color-base-lighter: lighten($color-base, 80); // #Brand-Palette $color-primary: #2f4b66; $color-secondary: #36644a; $color-tertiary: #b4c8bd; // #Accents $color-accent-primary: #60524d; $color-accent-secondary: #f9ceb7; $color-accent-tertiary: #673c4f; $color-accent-tertiary-hover: #612c43; // #Alerts $color-warning: #8a001f; $color-success: #27ae60; // #Absolute-colors to avoid linting alerts on hardcoded values $color-white: #ffffff; $color-grey: #b2bac1; $color-grey-dark: #b4b4b4; $color-black: #000000; $color-background-light: $color-white; $color-background-medium: $color-grey; $color-background-primary: $color-primary; $color-background-secondary: $color-secondary; $color-background-tertiary: $color-tertiary; $color-background-accent-primary: $color-accent-primary; $color-background-accent-secondary: $color-accent-secondary; $color-background-accent-tertiary: $color-accent-tertiary; $color-background-accent-tertiary-hover: $color-accent-tertiary-hover; $color-background-success-altert: $color-success; $color-background-microsoft: #0076ff; $color-background-overlay: rgba($color-black, .3); $color-link: $color-secondary; $color-text-light: $color-white; $color-text-medium: $color-grey; $color-text-primary: $color-primary; $color-text-secondary: $color-secondary; $color-text-tertiary: $color-tertiary; $color-text-accent-tertiary: $color-accent-tertiary; $color-svg-primary: $color-primary; $color-svg-tertiary: $color-tertiary; $color-border: $color-primary; $color-element-hover: #4778d9; $box-shadow-xs: 2px 4px 10px 0 rgba($color-base, 0.6); $box-shadow-s: 4px 9px 20px 4px rgba($color-base, 0.6); $box-shadow-m: 8px 8px 26px 2px rgba($color-base, 0.6); $box-shadow-l: 10px 10px 52px 0 rgba($color-base, 0.2); $box-shadow-xl: 16px 16px 84px 0 rgba($color-base, 0.12); $border-radius-s: 0.5rem; $border-radius: 1rem; $linear-gradient-base: linear-gradient(to bottom, #f5f5f5, #f5f5f5); // // #Animations // $animation-speed-slow: 0.35s; $animation-speed-regular: 0.25s; $animation-speed-fast: 0.15s; $animation-type-regular: ease-in; // // #Fonts // // #Font-Family $font-family-body: "brandon-grotesque", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; $font-family-heading: "gin", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; // // #Font-Weight // $font-weight-light: 300; $font-weight-medium: 500; // // #Font-Size // $font-base: 1rem; $font-big: 1.125rem; // #Font-Variables $font-ratio: 1.15; $base-size: 16; $line-height: 24; // // #Spacing // $grid-gap-m: 1.5rem; $grid-gap-l: 2rem; $grid-item-min-size: 20.5rem; $grid-item-max-size: 24.5rem; $grid-max-width-s: $grid-item-min-size; $grid-max-width-m: 50.5rem; $grid-max-width-l: 76.625rem; $grid-max-width-xl: 87.5rem; $checkbox-size: 1.5rem; $container-width-xl: 76.5rem; $space-default: $font-base; // Is dependant on $font-base and should be 1em; $space-xxs: calc($space-default / 8); // 2px $space-xs: calc($space-default / 4); // 4px $space-s: calc($space-default / 2); // 8px $space-m: calc($space-default); // 16px $space-l: calc($space-default * 2); // 32px $space-xl: calc($space-default * 4); // 64px $space-xxl: calc($space-default * 6); // 96px // #Padding $padding-xxs: $space-xxs; // 2px $padding-xs: $space-xs; // 4px $padding-s: $space-s; // 8px $padding-m: $space-m; // 16px $padding-l: $space-l; // 32px $padding-xl: $space-xl; // 64px $history-padding: 4.875rem; // #Margin $margin-xxs: $space-xxs; // 2px $margin-xs: $space-xs; // 4px $margin-s: $space-s; // 8px $margin-m: $space-m; // 16px $margin-l: $space-l; // 32px $margin-xl: $space-xl; // 64px $margin-xxl: $space-xxl; // 96px // #Icon $icon-xxs: $space-xxs; // 2px $icon-xs: $space-xs; // 4px $icon-s: $space-s; // 8px $icon-m: $space-m; // 16px $icon-l: 1.75rem; // 28px $icon-xl: $space-xl; // 64px // // $Media-Queries // $mq-s: 23.4375em; // 375px $mq-m: 48em; // 768px $mq-sl: 65em; // 1280px $mq-r: 80em; // 1280px $mq-l: 85.375em; // 1366px $mq-xl: 120em; // 1920px // // $Z-Index // $z-index-s: 1; $z-index-m: 2; $z-index-l: 3; $z-index-xl: 4; $z-index-xxl: 5; // // #Loader // $loader-color: $color-background-accent-tertiary !default; $loader-size: $icon-xl !default; $loader-height: 1.25rem !default; $loader-border-size: 0.5rem !default; $loader-gap: 0.75rem !default; $loader-animation-duration: 1s !default; ================================================ FILE: src/ContosoTraders.Ui.Website/src/styles/abstracts/mixins/_ellipsis.scss ================================================ // Add ellipsis at the end of a line to avoid text overflow // @author Ignacio Villanueva // @param {String} $width @mixin ellipsis($width: 100%) { max-width: $width; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/styles/abstracts/mixins/_font-placeholders.scss ================================================ // Paragraph Extra-Extra Small @mixin text-xxs { font-family: $font-family-body; font-size: .875rem; // 14px } // Paragraph Extra Small @mixin text-xs { font-family: $font-family-body; font-size: 1rem; // 16px @media (min-width: $mq-l) { font-size: $font-big; // 18px } } // Paragraph Small @mixin text-s { font-family: $font-family-body; font-size: $font-big; // 18px @media (min-width: $mq-l) { font-size: 1.25rem; // 20px; } } // Paragraph Medium @mixin text-m { font-family: $font-family-body; font-size: 1.25rem; // 20px @media (min-width: $mq-l) { font-size: 1.5rem; // 24px } } // Price numbers Card componnent only @mixin text-l-price { font-family: $font-family-body; font-size: 2rem; // 32px @media (min-width: $mq-l) { font-size: 2.125rem; // 34px } } // Heading 3 & Price @mixin text-l { font-family: $font-family-body; font-size: 1.5rem; // 24px @media (min-width: $mq-l) { font-size: 2.125rem; // 34px } } // Heading 2 @mixin text-xl { font-family: $font-family-heading; font-size: 1.75rem; // 28px font-weight: $font-weight-light; @media (min-width: $mq-l) { font-size: 2.625rem; // 42px } } // Heading 1 @mixin text-xxl { font-family: $font-family-heading; font-size: 1.75rem; // 28px font-weight: $font-weight-light; @media (min-width: $mq-l) { font-size: 3.25rem; // 52px } } ================================================ FILE: src/ContosoTraders.Ui.Website/src/styles/abstracts/mixins/_font-scale.scss ================================================ @mixin font-size($font-size) { font-size: $font-size + px; font-size: #{$font-size} / #{$base-size} + rem; } @mixin line-height($font-size, $base-line-height) { // Sets the line-height to a multiple // of $base-line-height that's not smaller than // $font-size line-height: ceil($font-size / $base-line-height) * ($base-line-height / $font-size); } ================================================ FILE: src/ContosoTraders.Ui.Website/src/styles/abstracts/mixins/_fonts.scss ================================================ // @Author https://gist.github.com/jonathantneal/d0460e5c2d5d7f9bc5e6 // ============================================================================= // String Replace // ============================================================================= @function str-replace($string, $search, $replace: "") { $index: str-index($string, $search); @if $index { @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace); } @return $string; } // ============================================================================= // Font Face // ============================================================================= @mixin font-face($name, $path, $weight: null, $style: null, $exts: eot woff ttf svg) { $src: null; $extmods: ( eot: "?", svg: "#" + str-replace($name, " ", "_"), ); $formats: ( otf: "opentype", ttf: "truetype", ); @each $ext in $exts { $extmod: if(map-has-key($extmods, $ext), $ext + map-get($extmods, $ext), $ext); $format: if(map-has-key($formats, $ext), map-get($formats, $ext), $ext); $src: append($src, url(quote($path + "." + $extmod)) format(quote($format)), comma); } @font-face { font-family: quote($name); font-style: $style; font-weight: $weight; src: $src; } } @mixin font-include($font, $type, $weight: null, $style: null) { @include font-face( $font, "/assets/fonts/" + $font + "/" + $font + "-" + $type, $weight, $style ); } ================================================ FILE: src/ContosoTraders.Ui.Website/src/styles/abstracts/mixins/_loader.scss ================================================ @import '../variables'; @mixin loader-rotate { @keyframes loader-rotate { 0% { transform: rotate(0); } 100% { transform: rotate(360deg); } } } @mixin loader-scale { @keyframes loader-scale { 0% { transform: scale(0); opacity: 0; } 50% { opacity: 1; } 100% { transform: scale(1); opacity: 0; } } } @mixin loader( $size: $loader-size, $color: $loader-color, $border-size: $loader-border-size, $duration: $loader-animation-duration, $align: null ) { width: $size; height: $size; border: $border-size solid rgba($color, 0.25); border-top-color: $color; border-radius: 50%; position: relative; animation: loader-rotate $duration linear infinite; @if ($align == center) { margin: 0 auto; } @if ($align == middle) { top: 50%; margin: -$size / 2 auto 0; } @include loader-rotate; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/styles/base/_base.scss ================================================ // ----------------------------------------------------------------------------- // This file contains very basic styles. // ----------------------------------------------------------------------------- // // Set up a decent box model on the root element // html { box-sizing: border-box; } // // Make all elements from the DOM inherit from the parent box-sizing // Since `*` has a specificity of 0, it does not override the `html` value // making all elements inheriting from the root box-sizing value // See: https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ // *, *::before, *::after { box-sizing: inherit; } .App { overflow-x: hidden; } body.is-blocked { overflow: hidden; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/styles/base/_typography.scss ================================================ // // Basic typography style for copy text // body { color: $color-base; font-family: $font-family-body; font-size: 100%; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/styles/base/_utilities.scss ================================================ // ----------------------------------------------------------------------------- // This file contains CSS utiltiy classes. // ----------------------------------------------------------------------------- // // Clear inner floats // .u-clearfix::after { clear: both; content: ''; display: table; } // Hide overflowed content .u-overflow-hidden { overflow: hidden; } // Clean button default styles .u-empty { background-color: transparent; border: 0; &:focus:not(.main-nav__item) { outline: none; } } // // Hide text while making it readable for screen readers // 1. Needed in WebKit-based browsers because of an implementation bug; // See: https://code.google.com/p/chromium/issues/detail?id=457146 // .u-hide-text { overflow: hidden; padding: 0; // 1 text-indent: 101%; white-space: nowrap; } // // Hide element while making it readable for screen readers // Shamelessly borrowed from HTML5Boilerplate: // https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css#L119-L133 // .u-visually-hidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/styles/vendor/_normalize.scss ================================================ /*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */ /* Document ========================================================================== */ /** * 1. Correct the line height in all browsers. * 2. Prevent adjustments of font size after orientation changes in iOS. */ html { -webkit-text-size-adjust: 100%; /* 2 */ } /* Sections ========================================================================== */ /** * Remove the margin in all browsers. */ body { margin: 0; } /** * Correct the font size and margin on `h1` elements within `section` and * `article` contexts in Chrome, Firefox, and Safari. */ h1 { font-size: 2em; margin: 0.67em 0; } /* Grouping content ========================================================================== */ /** * 1. Add the correct box sizing in Firefox. * 2. Show the overflow in Edge and IE. */ hr { box-sizing: content-box; /* 1 */ height: 0; /* 1 */ overflow: visible; /* 2 */ } /** * 1. Correct the inheritance and scaling of font size in all browsers. * 2. Correct the odd `em` font sizing in all browsers. */ pre { font-family: monospace, monospace; /* 1 */ font-size: 1em; /* 2 */ } /* Text-level semantics ========================================================================== */ /** * Remove the gray background on active links in IE 10. */ a { background-color: transparent; } /** * 1. Remove the bottom border in Chrome 57- * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. */ abbr[title] { border-bottom: none; /* 1 */ text-decoration: underline; /* 2 */ text-decoration: underline dotted; /* 2 */ } /** * Add the correct font weight in Chrome, Edge, and Safari. */ b, strong { font-weight: bolder; } /** * 1. Correct the inheritance and scaling of font size in all browsers. * 2. Correct the odd `em` font sizing in all browsers. */ code, kbd, samp { font-family: monospace, monospace; /* 1 */ font-size: 1em; /* 2 */ } /** * Add the correct font size in all browsers. */ small { font-size: 80%; } /** * Prevent `sub` and `sup` elements from affecting the line height in * all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } /* Embedded content ========================================================================== */ /** * Remove the border on images inside links in IE 10. */ img { border-style: none; } /* Forms ========================================================================== */ /** * 1. Change the font styles in all browsers. * 2. Remove the margin in Firefox and Safari. */ button, input, optgroup, select, textarea { font-family: inherit; /* 1 */ // font-size: 100%; /* 1 */ line-height: 1.15; /* 1 */ margin: 0; /* 2 */ } /** * Show the overflow in IE. * 1. Show the overflow in Edge. */ button, input { /* 1 */ overflow: visible; } /** * Remove the inheritance of text transform in Edge, Firefox, and IE. * 1. Remove the inheritance of text transform in Firefox. */ button, select { /* 1 */ text-transform: none; } /** * Correct the inability to style clickable types in iOS and Safari. */ button, [type="button"], [type="reset"], [type="submit"] { -webkit-appearance: button; } /** * Remove the inner border and padding in Firefox. */ button::-moz-focus-inner, [type="button"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner, [type="submit"]::-moz-focus-inner { border-style: none; padding: 0; } /** * Restore the focus styles unset by the previous rule. */ button:-moz-focusring, [type="button"]:-moz-focusring, [type="reset"]:-moz-focusring, [type="submit"]:-moz-focusring { outline: 1px dotted ButtonText; } /** * Correct the padding in Firefox. */ fieldset { padding: 0.35em 0.75em 0.625em; } /** * 1. Correct the text wrapping in Edge and IE. * 2. Correct the color inheritance from `fieldset` elements in IE. * 3. Remove the padding so developers are not caught out when they zero out * `fieldset` elements in all browsers. */ legend { box-sizing: border-box; /* 1 */ color: inherit; /* 2 */ display: table; /* 1 */ max-width: 100%; /* 1 */ padding: 0; /* 3 */ white-space: normal; /* 1 */ } /** * Add the correct vertical alignment in Chrome, Firefox, and Opera. */ progress { vertical-align: baseline; } /** * Remove the default vertical scrollbar in IE 10+. */ textarea { overflow: auto; } /** * 1. Add the correct box sizing in IE 10. * 2. Remove the padding in IE 10. */ [type="checkbox"], [type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } /** * Correct the cursor style of increment and decrement buttons in Chrome. */ [type="number"]::-webkit-inner-spin-button, [type="number"]::-webkit-outer-spin-button { height: auto; } /** * 1. Correct the odd appearance in Chrome and Safari. * 2. Correct the outline style in Safari. */ [type="search"] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; /* 2 */ } /** * Remove the inner padding in Chrome and Safari on macOS. */ [type="search"]::-webkit-search-decoration { -webkit-appearance: none; } /** * 1. Correct the inability to style clickable types in iOS and Safari. * 2. Change font properties to `inherit` in Safari. */ ::-webkit-file-upload-button { -webkit-appearance: button; /* 1 */ font: inherit; /* 2 */ } /* Interactive ========================================================================== */ /* * Add the correct display in Edge, IE 10+, and Firefox. */ details { display: block; } /* * Add the correct display in all browsers. */ summary { display: list-item; } /* Misc ========================================================================== */ /** * Add the correct display in IE 10+. */ template { display: none; } /** * Add the correct display in IE 10. */ [hidden] { display: none; } ================================================ FILE: src/ContosoTraders.Ui.Website/src/types/types.js ================================================ export const FORM_EMAIL = '@@form/EMAIL'; export const SAVE_USER = '@@form/SUBMIT'; export const REMOVE_USER = '@@logout/CLICK'; export const THEME_CHANGE = 'THEME_CHANGE'; export const GET_QUANTITY = 'GET_QUANTITY'; ================================================ FILE: src/ContosoTraders.Ui.Website/tests/account.ts ================================================ import { test, expect } from '@playwright/test'; import fs from 'fs'; import path from 'path'; import { parse } from 'csv-parse/sync'; test.beforeEach(async () => { // skip test if environment variables for AAD creds are not set test.skip(!process.env.REACT_APP_AADUSERNAME || !process.env.REACT_APP_AADPASSWORD, 'AADUSERNAME and AADPASSWORD environment variables must be set'); }); test.describe('My Profile', () => { test('should be able to fill out personal info', async ({ page }) => { // Fill out the form using data from CSV const records = parse(fs.readFileSync(path.join(__dirname, 'test-data.csv')), { columns: true, skip_empty_lines: true }); // For each record in CSV, fill out the form for (const record of records) { // Fill out the form await page.goto('/profile/personal'); await page.locator('#firstName').fill(`${record.firstName}`); await page.locator('#lastName').fill(`${record.lastName}`); await page.getByLabel('Email').fill(`${record.email}`); await page.getByLabel('Mobile Number').fill(`${record.mobile}`); await page.getByLabel('Date of birth').fill(`${record.dob}`); await page.locator('#currentpassword').fill(`${record.currentpassword}`); await page.locator('#newpassword').fill(`${record.newpassword}`); await page.locator('#confirmpassword').fill(`${record.confirmpassword}`); await page.getByRole('button', { name: 'Save Changes' }).click(); // Verify no validation errors are present await expect(page.locator('.Mui-error')).toHaveCount(0); } }); }); test.describe('My Cart', () => { test('should be able to view cart', async ({ page }) => { await page.goto(''); await page.getByRole('button', { name: 'cart' }).click(); await expect(page).toHaveURL('/cart'); await expect(page.getByRole('heading', { name: 'My Cart' })).toBeVisible(); }); }); ================================================ FILE: src/ContosoTraders.Ui.Website/tests/api/cart.spec.ts ================================================ import { test, expect } from '@playwright/test'; // Shopping cart data const USER = 'fake@outlook.com'; const PRODUCT = 'Xbox Wireless Controller Lunar Shift Special Edition'; const PRODUCTID = 1; const PRODUCTIMAGE = 'PID1-1.jpg'; const PRODUCTPRICE = 99; const PRODUCTQUANTITY = 1; test.skip(() => !process.env.REACT_APP_APIURLSHOPPINGCART, 'requires REACT_APP_APIURLSHOPPINGCART'); test.use({ baseURL: process.env.REACT_APP_APIURLSHOPPINGCART + '/' }); // SETUP: Create a new cart test.beforeAll(async ({ request }) => { const newCart = await request.post('./ShoppingCart', { data: { cartItemId: "string", email: USER, productId: PRODUCTID, name: PRODUCT, price: PRODUCTPRICE, imageUrl: PRODUCTIMAGE, quantity: PRODUCTQUANTITY } }); expect(newCart.status()).toBe(201); }); test.describe('Shopping Cart API', () => { test('should be able to GET shopping cart', async ({ request }) => { const cart = await request.get('./ShoppingCart', { headers: { 'Accept': 'accept: */*', 'x-tt-email': USER, } }); await expect(cart).toBeOK(); expect(await cart.json()).toContainEqual(expect.objectContaining({ email: USER, productId: PRODUCTID, name: PRODUCT, price: PRODUCTPRICE, imageUrl: PRODUCTIMAGE, quantity: PRODUCTQUANTITY })); }); }); // TEARDOWN: Delete the cart test.afterAll(async ({ request }) => { const cart = await request.get('./ShoppingCart', { headers: { 'Accept': 'accept: */*', 'x-tt-email': USER, } }); await expect(cart).toBeOK(); // Loop through each cart item and delete it const cartBody = JSON.parse(await cart.text()); for (let i = 0; i < cartBody.length; i++) { const deleteCart = await request.delete('./ShoppingCart/product', { data: { cartItemId: cartBody[i].cartItemId, email: USER, productId: cartBody[i].productId, name: cartBody[i].name, price: cartBody[i].price, imageUrl: cartBody[i].imageUrl, quantity: cartBody[i].quantity } }); await expect(deleteCart).toBeOK(); } }); ================================================ FILE: src/ContosoTraders.Ui.Website/tests/api/data.spec.ts ================================================ import { test, expect } from '@playwright/test'; type Product = ({ type: { name: string } }); test.skip(() => !process.env.REACT_APP_APIURL, 'requires REACT_APP_APIURL'); test.use({ baseURL: process.env.REACT_APP_APIURL + '/', }); test.describe('API data Assertions - category', () => { // Filter products by category - Controller and then assert category test('should be able to filter products by category - Controllers and then assert category', async ({ request }) => { const response = await request.get('./products/?type=controllers'); await expect(response).toBeOK(); const productsFromResponse: Product[] = (await response.json()).products; // Assert if each product falls under the category - Controllers and not any other for (const product of productsFromResponse) { expect(product.type.name === "Controllers").toBeTruthy(); expect(product.type.name === "Desktops").toBeFalsy(); expect(product.type.name === "Laptops").toBeFalsy(); expect(product.type.name === "Mobiles").toBeFalsy(); expect(product.type.name === "Monitors").toBeFalsy(); } }); // Filter products by category - Desktops and then assert category test('should be able to filter products by category - Desktops and then assert category', async ({ request }) => { const response = await request.get('./products/?type=desktops'); await expect(response).toBeOK(); const productsFromResponse: Product[] = (await response.json()).products; // Assert if each product falls under the category - Desktops and not any other for (const product of productsFromResponse) { expect(product.type.name === "Controllers").toBeFalsy(); expect(product.type.name === "Desktops").toBeTruthy(); expect(product.type.name === "Laptops").toBeFalsy(); expect(product.type.name === "Mobiles").toBeFalsy(); expect(product.type.name === "Monitors").toBeFalsy(); } }); // Filter products by category - Laptops and then assert category test('should be able to filter products by category - Laptops and then assert category', async ({ request }) => { const response = await request.get('./products/?type=laptops'); await expect(response).toBeOK(); const productsFromResponse: Product[] = (await response.json()).products; // Assert if each product falls under the category - Laptops and not any other for (const product of productsFromResponse) { expect(product.type.name === "Controllers").toBeFalsy(); expect(product.type.name === "Desktops").toBeFalsy(); expect(product.type.name === "Laptops").toBeTruthy(); expect(product.type.name === "Mobiles").toBeFalsy(); expect(product.type.name === "Monitors").toBeFalsy(); } }); // Filter products by category - Mobiles and then assert category test('should be able to filter products by category - Mobiles and then assert category', async ({ request }) => { const response = await request.get('./products/?type=mobiles'); await expect(response).toBeOK(); const productsFromResponse: Product[] = (await response.json()).products; // Assert if each product falls under the category - Mobiles and not any other for (const product of productsFromResponse) { expect(product.type.name === "Controllers").toBeFalsy(); expect(product.type.name === "Desktops").toBeFalsy(); expect(product.type.name === "Laptops").toBeFalsy(); expect(product.type.name === "Mobiles").toBeTruthy(); expect(product.type.name === "Monitors").toBeFalsy(); } }); // Filter products by category - Monitors and then assert category test('should be able to filter products by category - Monitors and then assert category', async ({ request }) => { const response = await request.get('./products/?type=monitors'); await expect(response).toBeOK(); const productsFromResponse: Product[] = (await response.json()).products; // Assert if each product falls under the category - Monitors and not any other for (const product of productsFromResponse) { expect(product.type.name === "Controllers").toBeFalsy(); expect(product.type.name === "Desktops").toBeFalsy(); expect(product.type.name === "Laptops").toBeFalsy(); expect(product.type.name === "Mobiles").toBeFalsy(); expect(product.type.name === "Monitors").toBeTruthy(); } }); }); test.describe('API data Assertions - count', () => { // Get all products, verify status and get and assert count of each product test('should be able to get all products, verify status and get and assert count of each product', async ({ request }) => { // Initialize total no of each product let totalNoOfControllers = 0; let totalNoOfDesktops = 0; let totalNoOfLaptops = 0; let totalNoOfMobiles = 0; let totalNoOfMonitors = 0; const response = await request.get('./products'); await expect(response).toBeOK(); const productsFromResponse: Product[] = (await response.json()).products; // Get count of each product for (const product of productsFromResponse) { if (product.type.name === "Controllers") ++totalNoOfControllers; if (product.type.name === "Desktops") ++totalNoOfDesktops; if (product.type.name === "Laptops") ++totalNoOfLaptops; if (product.type.name === "Mobiles") ++totalNoOfMobiles; if (product.type.name === "Monitors") ++totalNoOfMonitors; } // Assert total numbers of each product expect(totalNoOfControllers).toBeGreaterThanOrEqual(0); expect(totalNoOfDesktops).toBeGreaterThanOrEqual(0); expect(totalNoOfLaptops).toBeGreaterThanOrEqual(0); expect(totalNoOfMobiles).toBeGreaterThanOrEqual(0); expect(totalNoOfMonitors).toBeGreaterThanOrEqual(0); }); }); ================================================ FILE: src/ContosoTraders.Ui.Website/tests/api/products.spec.ts ================================================ import { test, expect } from '@playwright/test'; let _productid = 1; test.skip(() => !process.env.REACT_APP_APIURL, 'requires REACT_APP_APIURL'); test.use({ baseURL: process.env.REACT_APP_APIURL + '/', }); test.describe('Products API', () => { // Text search API test('should be able to load search by text data', async ({ request }) => { const response = await request.get('./Products/search/laptops'); await expect(response).toBeOK(); }); // Load products list test('should be able to load products list', async ({ request }) => { const response = await request.get('./Products/?type=laptops'); await expect(response).toBeOK(); }); // Load product details test('should be able to load product details', async ({ request }) => { const response = await request.get('./Products/' + _productid); await expect(response).toBeOK(); }); // Filter products by brands test('should be able to filter products by brands', async ({ request }) => { const response = await request.get('./Products/?type=laptops&brand=1'); await expect(response).toBeOK(); }); }); ================================================ FILE: src/ContosoTraders.Ui.Website/tests/auth.setup.ts ================================================ import { test as setup } from '@playwright/test'; const authFile = '.auth/user.json'; setup('authenticate', async ({ page }) => { // skip setup if environment variables for AAD creds are not set setup.skip(!process.env.REACT_APP_AADUSERNAME || !process.env.REACT_APP_AADPASSWORD, 'AADUSERNAME and AADPASSWORD environment variables must be set'); await page.goto(''); // Sign in using creds from env variables const dialogPromise = page.waitForEvent('popup'); await page.getByText('LOGIN').click(); const dialog = await dialogPromise; await dialog.getByPlaceholder('Email, phone, or Skype').fill(process.env.REACT_APP_AADUSERNAME!); await dialog.getByRole('button', { name: 'Next' }).click(); await dialog.getByPlaceholder('Password').fill(process.env.REACT_APP_AADPASSWORD!); await dialog.getByRole('button', { name: 'Sign in' }).click(); // Do not stay signed in await dialog.getByRole('button', { name: 'No' }).click(); // Use try catch block to handle the case where the consent dialog is shown try { await dialog.waitForURL('**/Consent/**'); await dialog.getByRole('button', { name: 'Yes' }).click(); } catch (e) { // Consent dialog was not shown } // Save auth state to file (.gitignore'd) await page.context().storageState({ path: authFile }); }); ================================================ FILE: src/ContosoTraders.Ui.Website/tests/cart.spec.ts ================================================ import { test, expect, } from '@playwright/test'; // SETUP: Get first 5 product ID's and add them all to cart test.beforeEach(async ({ page, request }) => { const response = await request.get(process.env.REACT_APP_APIURL + '/products') await expect(response).toBeOK(); const productsFromResponse = (await response.json()).products.slice(0, 5); for (const product of productsFromResponse) { await page.goto(`/product/detail/${product.id}`); await page.getByRole('button', { name: 'Add To Bag' }).click(); } await page.getByRole('button', { name: 'cart' }).click(); }); test.describe('Shopping Cart', () => { test('should be able to view cart', async ({ page }) => { await expect(page.getByRole('heading', { name: 'My Cart' })).toBeVisible(); await expect(page.getByText('Order Summary')).toBeVisible(); await expect(page.getByText('Product Name')).toBeVisible(); await expect(page.getByText('Price', { exact: true })).toBeVisible(); await expect(page.getByText('Qty', { exact: true })).toBeVisible(); await expect(page.getByText('Subtotal', { exact: true })).toBeVisible(); }); test('should be able to increase and decrease test quantity', async ({ page }) => { const subtotal = async () => Number((await page.getByTestId('subtotal').innerText()).replace('$', '')); const clickButton = async (name: string) => await page.getByRole('button', { name }).first().click(); const subtotalBefore = await subtotal(); await clickButton('+'); await expect(async () => { expect(await subtotal()).toBeGreaterThan(subtotalBefore); }).toPass(); const subtotalAfter = await subtotal(); await clickButton('-'); await expect(async () => { expect(await subtotal()).toBeLessThan(subtotalAfter); }).toPass(); }); test('should be able to remove items from cart', async ({ page }) => { while (!(await page.getByRole('heading', { name: 'Your Cart is empty' }).isVisible())) { await page.getByRole('link', { name: 'Remove' }).first().click(); } }); }); ================================================ FILE: src/ContosoTraders.Ui.Website/tests/darkmode.spec.ts ================================================ import { test, expect } from '@playwright/test'; test.beforeEach(async ({ page }) => { await page.goto('/'); }); test.describe.skip('Dark Mode', () => { test.skip(({ browserName }) => browserName !== 'chromium', 'Chromium only!'); test('should be able to toggle dark mode', async ({ page }) => { await page.getByLabel('Dark Mode').check(); await expect(page.locator('.App')).toHaveAttribute('class', 'App dark') await expect(page.locator('.App')).toHaveCSS('background-color', 'rgb(34, 34, 34)') await page.getByLabel('Dark Mode').uncheck(); await expect(page.locator('.App')).toHaveAttribute('class', 'App light') await expect(page.locator('.App')).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)') }) test('verify dark mode is pixel perfect - on', async ({ page }) => { await page.getByLabel('Dark Mode').check(); await expect(page.getByRole('banner').filter({ hasText: '0Dark Mode' })).toHaveScreenshot(); }) test('verify dark mode is pixel perfect - off', async ({ page }) => { await expect(page.getByRole('banner').filter({ hasText: '0Dark Mode' })).toHaveScreenshot(); }) }); ================================================ FILE: src/ContosoTraders.Ui.Website/tests/discounts.spec.ts ================================================ import { test, expect } from '@playwright/test'; const productId = 1; const validDiscountCodes = ['DISCOUNT15', 'DISCOUNT10']; const invalidDiscountCodes = ['DISCOUNT20', 'DISCOUNT30']; // SETUP: Add product to cart, view cart test.beforeEach(async ({ page }) => { await page.goto(`/product/detail/${productId}`); await page.getByRole('button', { name: 'Add To Bag' }).click(); await page.getByRole('button', { name: 'cart' }).click(); }) test.describe('Discount Codes', () => { for (const code of validDiscountCodes) { test(`should be able to use VALID discount code ${code}`, async ({ page }) => { await page.getByPlaceholder('Enter coupon code').fill(code); await page.getByRole('button', { name: 'CHECK' }).click(); await expect(page.getByRole('button', { name: code })).toBeVisible(); // check that correct discount is applied await expect(page.getByTestId('discount')).toHaveText(`-$${code.replace('DISCOUNT', '')}.00`); }); } for (const code of invalidDiscountCodes) { test(`should not be able to use INVALID discount code ${code}`, async ({ page }) => { await page.getByPlaceholder('Enter coupon code').fill(code); await page.getByRole('button', { name: 'CHECK' }).click(); await expect(page.getByText('This coupon is invalid')).toBeVisible(); }); } }); ================================================ FILE: src/ContosoTraders.Ui.Website/tests/fileupload.spec.ts ================================================ import { test, expect } from '@playwright/test'; import path from 'path'; const imgPath = path.join(__dirname, '../src/assets/images/original/Contoso_Assets/Grid_Products_Collection/product_image.png'); test.describe.skip('File Upload', () => { test('should be able to upload a file', async ({ page }) => { await page.goto("/"); await page.getByRole('button', { name: 'iconimage' }).click(); await page.locator('input[type="file"]').setInputFiles(imgPath); await expect(page).toHaveURL('/suggested-products-list'); }); }); ================================================ FILE: src/ContosoTraders.Ui.Website/tests/map.spec.ts ================================================ import { test, expect } from '@playwright/test'; // Emulate geolocation, language and timezone test.use({ geolocation: { latitude: 41.890221, longitude: 12.492348 }, locale: 'it-IT', permissions: ['geolocation'], timezoneId: 'Europe/Rome' }); test.beforeEach(async ({ page }) => { await page.goto('/'); await page.mouse.wheel(0, 4000); // scroll down to map }); test.describe('Map', () => { test.skip(({ browserName }) => browserName !== 'chromium', 'Chromium only!'); test('should display bing maps iframe', async ({ page, geolocation }) => { await expect.poll(() => page.locator('input#latitude').inputValue()).toEqual(geolocation?.latitude.toString()); await expect.poll(() => page.locator('input#longitude').inputValue()).toEqual(geolocation?.longitude.toString()); await expect(page.locator('#current-location')).toBeVisible(); await expect(async () => { const boundingBox = await page.frameLocator('iframe[title="geolocation"]').locator('canvas[aria-label="Interactive Map"]').boundingBox(); if (!boundingBox || boundingBox?.width < 100 || boundingBox?.height < 100) { throw new Error('Map is too small'); } }).toPass(); }); test('should zoom in on bing maps iframe', async ({ page }) => { await page.frameLocator('iframe[title="geolocation"]').getByRole('button', { name: 'Zoom avanti' }).click(); }); test('should zoom out on bing maps iframe', async ({ page }) => { await page.frameLocator('iframe[title="geolocation"]').getByRole('button', { name: 'Zoom indietro' }).click(); }); }); ================================================ FILE: src/ContosoTraders.Ui.Website/tests/mocks.spec.ts ================================================ import { test, expect } from '@playwright/test'; test.describe('Mocks', () => { // Mock home page test('should be able to load mock home page', async ({ page }) => { await page.route('/', async route => { await route.fulfill({ status: 200, body: "Test Content" }); }); // Load home page await page.goto('/'); // Assert content of mock page await expect(page.locator('html').first()).toContainText("Test Content"); }); // Mock API - load product details test('should be able to load product details', async ({ page }) => { await page.route('/Products/1', async route => { const jsonResponse = { "id": 1, "name": "Test Product 01", "price": 10 }; await route.fulfill({ status: 200, json: jsonResponse }); }); await page.goto('/Products/1'); await expect(async () => { const data = await page.locator('pre').first().allInnerTexts(); const product = JSON.parse(data[0]); expect(product.name).toBe("Test Product 01"); }).toPass(); }); }); ================================================ FILE: src/ContosoTraders.Ui.Website/tests/pages.spec.ts ================================================ import { test, expect } from '@playwright/test'; let _productid = 1; test.beforeEach(async ({ page }) => { await page.goto('/'); }) test.describe('Header', () => { test('should be able to search by text', async ({ page }) => { await page.getByPlaceholder('Search by product name or search by image').fill('laptops'); await page.getByPlaceholder('Search by product name or search by image').press('Enter'); await expect(page).toHaveURL('/suggested-products-list'); }); test('should be able to select category', async ({ page }) => { await page.getByRole('button', { name: 'All Categories' }).click(); await page.getByRole('menuitem', { name: 'Laptops' }).click(); await expect(page).toHaveURL('/list/laptops'); }); test('should be able to hover over header menus', async ({ page }) => { await page.getByRole('navigation').getByRole('link', { name: 'All Products' }).hover(); await page.getByRole('navigation').getByRole('link', { name: 'Laptops' }).hover(); await page.getByRole('navigation').getByRole('link', { name: 'Controllers' }).hover(); await page.getByRole('navigation').getByRole('link', { name: 'Mobiles' }).hover(); await page.getByRole('navigation').getByRole('link', { name: 'Monitors' }).hover(); }); test('should be able to select header menu', async ({ page }) => { await page.getByRole('navigation').getByRole('link', { name: 'All Products' }).click(); await expect(page).toHaveURL('/list/all-products'); }); }); test.describe('Carousel', () => { test('prev and next buttons change slider', async ({ page }) => { const carousel = page.getByTestId('carousel'); await expect(carousel.getByText('The Fastest, Most Powerful Xbox Ever.')).toBeVisible(); await carousel.getByRole('button', { name: 'Next' }).click(); await expect(carousel.getByText('Xbox Wireless Controller - Mineral Camo Special Edition')).toBeVisible(); await carousel.getByRole('button', { name: 'Previous' }).click(); }) test('carousel buttons change slider', async ({ page }) => { const carousel = page.getByTestId('carousel'); await carousel.getByRole('button', { name: 'carousel indicator 2' }).click(); await expect(carousel.getByText('Xbox Wireless Controller - Mineral Camo Special Edition')).toBeVisible(); await carousel.getByRole('button', { name: 'carousel indicator 1' }).click(); await expect(carousel.getByText('The Fastest, Most Powerful Xbox Ever.')).toBeVisible(); }) test('buy now button links to product page', async ({ page }) => { await page.getByTestId('carousel').getByRole('button', { name: 'Buy Now' }).first().click(); await expect(page).toHaveURL('/product/detail/1'); }) test('more details links to list page', async ({ page }) => { await page.getByTestId('carousel').getByRole('button', { name: 'More Details' }).first().click(); await expect(page).toHaveURL('/list/controllers'); }) }); test.describe('CarouselVRT', () => { test.skip(({ browserName }) => browserName !== 'chromium', 'Chromium only!'); test('verify carousel is pixel perfect - slide 1', async ({ page }) => { await expect(page.getByTestId('carousel')).toHaveScreenshot(); }) test('verify carousel is pixel perfect - slide 2', async ({ page }) => { const carousel = page.getByTestId('carousel'); await carousel.getByRole('button', { name: 'Next' }).click(); await expect(carousel).toHaveScreenshot(); }) }); test.describe('Product Listing', () => { test('should be able to select product to view details', async ({ page }) => { await page.goto('/list/all-products'); await page.getByRole('img', { name: 'Xbox Wireless Controller Lunar Shift Special Edition' }).click(); await expect(page).toHaveURL('/product/detail/' + _productid); }); test('should be able to filter product by brands', async ({ page }) => { await page.goto('/list/all-products'); await page.locator('[id="\\32 "]').check(); }); }); test.describe('Product Details', () => { test('Image does not break UI', async ({ page }) => { // Navigate to the page with the image await page.getByRole('link', { name: 'Mobiles' }).click(); await page.getByRole('img', { name: 'Asus Zenfone 5Z' }).click(); // Get the dimensions of the image const image = page.locator('.productdetailsimage') const imageSize = await image.boundingBox(); let containerSize = { height: 600 }; // Assert that the image fits within the container expect(imageSize?.height).toBeLessThanOrEqual(containerSize?.height); }); }); test.describe('Footer', () => { test('should be able to select footer menu', async ({ page }) => { await page.getByRole('listitem').filter({ hasText: 'Monitors' }).getByRole('link', { name: 'Monitors' }).click(); await expect(page).toHaveURL('/list/monitors'); }); }); ================================================ FILE: src/ContosoTraders.Ui.Website/tests/test-data.csv ================================================ test,firstName,lastName,email,mobile,dob,currentpassword,newpassword,confirmpassword Test,Test1,Test1,notarealemail@outlook.com,2029182132,2022-01-01,NotarealPassword!!,NotarealPassword!!,NotarealPassword!! Test,Test2,Test2,notarealemail@microsoft.com,2029182132,2023-02-02,N0tarealP@ssword,N0tarealP@ssword,N0tarealP@ssword ================================================ FILE: src/ContosoTraders.Ui.Website/tsconfig.json ================================================ { "compilerOptions": { "target": "es5", "module": "esnext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react" }, } ================================================ FILE: src/ContosoTraders.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ContosoTraders.Api.Core", "ContosoTraders.Api.Core\ContosoTraders.Api.Core.csproj", "{07A99FA1-A7CF-4797-9120-937994D30FF7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ContosoTraders.Api.Products", "ContosoTraders.Api.Products\ContosoTraders.Api.Products.csproj", "{F867B7B7-2B68-401A-A072-ADFEB61F75D8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ContosoTraders.Api.Profiles", "ContosoTraders.Api.Profiles\ContosoTraders.Api.Profiles.csproj", "{470EC658-DAAD-47FC-9041-342F744C2D2F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ContosoTraders.Api.Carts", "ContosoTraders.Api.Carts\ContosoTraders.Api.Carts.csproj", "{8B124EFF-94AA-4A17-B15B-EBD601452D68}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {07A99FA1-A7CF-4797-9120-937994D30FF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {07A99FA1-A7CF-4797-9120-937994D30FF7}.Debug|Any CPU.Build.0 = Debug|Any CPU {07A99FA1-A7CF-4797-9120-937994D30FF7}.Release|Any CPU.ActiveCfg = Release|Any CPU {07A99FA1-A7CF-4797-9120-937994D30FF7}.Release|Any CPU.Build.0 = Release|Any CPU {F867B7B7-2B68-401A-A072-ADFEB61F75D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F867B7B7-2B68-401A-A072-ADFEB61F75D8}.Debug|Any CPU.Build.0 = Debug|Any CPU {F867B7B7-2B68-401A-A072-ADFEB61F75D8}.Release|Any CPU.ActiveCfg = Release|Any CPU {F867B7B7-2B68-401A-A072-ADFEB61F75D8}.Release|Any CPU.Build.0 = Release|Any CPU {470EC658-DAAD-47FC-9041-342F744C2D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {470EC658-DAAD-47FC-9041-342F744C2D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU {470EC658-DAAD-47FC-9041-342F744C2D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {470EC658-DAAD-47FC-9041-342F744C2D2F}.Release|Any CPU.Build.0 = Release|Any CPU {8B124EFF-94AA-4A17-B15B-EBD601452D68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8B124EFF-94AA-4A17-B15B-EBD601452D68}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B124EFF-94AA-4A17-B15B-EBD601452D68}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B124EFF-94AA-4A17-B15B-EBD601452D68}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0F32E022-174B-4EDC-9D7B-58A6FD7C213D} EndGlobalSection EndGlobal ================================================ FILE: src/ContosoTraders.sln.DotSettings ================================================  True True True True True True True True True True True True