diff --git a/.artifactignore b/.artifactignore index 2179dc1f0eb3..8cdc8fa8e5bf 100644 --- a/.artifactignore +++ b/.artifactignore @@ -1,4 +1,4 @@ **/* -!**/bin/** -!**/obj/** +!tests/Umbraco.Tests.Integration/bin/** +!tests/Umbraco.Tests.UnitTests/bin/** **/node_modules diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index b86a1d597805..20fda2335c86 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -5,6 +5,10 @@ parameters: displayName: Run SQL Server Integration Tests type: boolean default: false + - name: sqlServerAcceptanceTests + displayName: Run SQL Server Acceptance Tests + type: boolean + default: false - name: myGetDeploy displayName: Deploy to MyGet type: boolean @@ -38,11 +42,11 @@ parameters: type: string default: ' ' - name: nonWindowsIntegrationNonReleaseTestFilter - displayName: TestFilter used for non-release type builds on non windows agents + displayName: TestFilter used for non-release type builds on non Windows agents type: string default: '--filter TestCategory!=LongRunning&TestCategory!=NonCritical' - name: nonWindowsIntegrationReleaseTestFilter - displayName: TestFilter used for release type builds on non windows agents + displayName: TestFilter used for release type builds on non Windows agents type: string default: ' ' - name: isNightly @@ -52,8 +56,6 @@ parameters: variables: nodeVersion: 20 - dotnetVersion: 8.x - dotnetIncludePreviewVersions: true solution: umbraco.sln buildConfiguration: Release UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042 @@ -116,11 +118,9 @@ stages: displayName: Run Login Build (Bellissima) workingDirectory: src/Umbraco.Web.UI.New.Client/apps/auth - task: UseDotNet@2 - displayName: Use .NET $(dotnetVersion) + displayName: Use .NET SDK from global.json inputs: - version: $(dotnetVersion) - performMultiLevelLookup: true - includePreviewVersions: $(dotnetIncludePreviewVersions) + useGlobalJson: true - task: DotNetCoreCLI@2 displayName: Run dotnet restore inputs: @@ -294,16 +294,14 @@ stages: artifact: build_output path: $(Build.SourcesDirectory) - task: UseDotNet@2 - displayName: Use .NET $(dotnetVersion) + displayName: Use .NET SDK from global.json inputs: - version: $(dotnetVersion) - performMultiLevelLookup: true - includePreviewVersions: $(dotnetIncludePreviewVersions) + useGlobalJson: true - task: DotNetCoreCLI@2 displayName: Run dotnet test inputs: command: test - projects: '**/*.Tests.UnitTests.csproj' + projects: 'tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj' arguments: '--configuration $(buildConfiguration) --no-build' testRunTitle: Unit Tests - $(Agent.OS) @@ -326,48 +324,36 @@ stages: vmImage: 'macOS-latest' pool: vmImage: $(vmImage) + variables: + Tests__Database__DatabaseType: 'Sqlite' steps: - - checkout: self - submodules: true + # Setup test environment - task: DownloadPipelineArtifact@2 displayName: Download build artifacts inputs: artifact: build_output path: $(Build.SourcesDirectory) + - task: UseDotNet@2 - displayName: Use .NET $(dotnetVersion) + displayName: Use .NET SDK from global.json inputs: - version: $(dotnetVersion) - performMultiLevelLookup: true - includePreviewVersions: $(dotnetIncludePreviewVersions) + useGlobalJson: true + + # Test - task: DotNetCoreCLI@2 - displayName: Run dotnet test Windows - condition: eq(variables['Agent.OS'],'Windows_NT') + displayName: Run dotnet test inputs: command: test - projects: '**/*.Tests.Integration.csproj' + projects: 'tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj' testRunTitle: Integration Tests SQLite - $(Agent.OS) - ${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}: + ${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}: arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}' - ${{ else }}: - arguments: '--configuration $(buildConfiguration) ${{parameters.integrationNonReleaseTestFilter}}' - env: - Tests__Database__DatabaseType: 'Sqlite' - Umbraco__CMS__Global__MainDomLock: 'FileSystemMainDomLock' - - task: DotNetCoreCLI@2 - displayName: Run dotnet test Non Windows - condition: ne(variables['Agent.OS'],'Windows_NT') - inputs: - command: test - projects: '**/*.Tests.Integration.csproj' - testRunTitle: Integration Tests SQLite - $(Agent.OS) - ${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}: + ${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}: + arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}' + ${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}: arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}' ${{ else }}: - arguments: '--configuration $(buildConfiguration) ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}' - env: - Tests__Database__DatabaseType: 'Sqlite' - Umbraco__CMS__Global__MainDomLock: 'FileSystemMainDomLock' + arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}' # Integration Tests (SQL Server) - job: @@ -378,208 +364,363 @@ stages: matrix: Windows: vmImage: 'windows-latest' - testDb: LocalDb - connectionString: N/A + Tests__Database__DatabaseType: LocalDb + Tests__Database__SQLServerMasterConnectionString: N/A Linux: vmImage: 'ubuntu-latest' - testDb: SqlServer - connectionString: 'Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=true' + SA_PASSWORD: UmbracoIntegration123! + Tests__Database__DatabaseType: SqlServer + Tests__Database__SQLServerMasterConnectionString: 'Server=(local);User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True' pool: vmImage: $(vmImage) - variables: - SA_PASSWORD: UmbracoIntegration123! steps: + # Setup test environment - task: DownloadPipelineArtifact@2 displayName: Download build artifacts inputs: artifact: build_output path: $(Build.SourcesDirectory) + - task: UseDotNet@2 - displayName: Use .NET $(dotnetVersion) + displayName: Use .NET SDK from global.json inputs: - version: $(dotnetVersion) - includePreviewVersions: $(dotnetIncludePreviewVersions) - - powershell: sqllocaldb start mssqllocaldb - displayName: Start localdb (Windows only) - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - - powershell: docker run --name mssql -d -p 1433:1433 -e ACCEPT_EULA=Y -e SA_PASSWORD=$(SA_PASSWORD) -e MSSQL_PID=Developer mcr.microsoft.com/mssql/server:2019-latest - displayName: Start SQL Server (Linux only) + useGlobalJson: true + + # Start SQL Server + - powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest + displayName: Start SQL Server Docker image (Linux) condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + + - pwsh: SqlLocalDB start MSSQLLocalDB + displayName: Start SQL Server LocalDB (Windows) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + + # Test - task: DotNetCoreCLI@2 - displayName: Run dotnet test Windows - condition: eq(variables['Agent.OS'],'Windows_NT') + displayName: Run dotnet test inputs: command: test - projects: '**/*.Tests.Integration.csproj' + projects: 'tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj' testRunTitle: Integration Tests SQL Server - $(Agent.OS) - ${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}: + ${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}: arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}' - ${{ else }}: + ${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}: arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}' - env: - Tests__Database__DatabaseType: $(testDb) - Tests__Database__SQLServerMasterConnectionString: $(connectionString) - Umbraco__CMS__Global__MainDomLock: 'SqlMainDomLock' - - task: DotNetCoreCLI@2 - displayName: Run dotnet test NonWindows - condition: ne(variables['Agent.OS'],'Windows_NT') - inputs: - command: test - projects: '**/*.Tests.Integration.csproj' - testRunTitle: Integration Tests SQL Server - $(Agent.OS) - ${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}: + ${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}: arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}' ${{ else }}: arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}' - env: - Tests__Database__DatabaseType: $(testDb) - Tests__Database__SQLServerMasterConnectionString: $(connectionString) - Umbraco__CMS__Global__MainDomLock: 'SqlMainDomLock' + + # Stop SQL Server + - pwsh: docker stop mssql + displayName: Stop SQL Server Docker image (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + + - pwsh: SqlLocalDB stop MSSQLLocalDB + displayName: Stop SQL Server LocalDB (Windows) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - stage: E2E - variables: - npm_config_cache: $(Pipeline.Workspace)/.npm_e2e displayName: E2E Tests dependsOn: Build + variables: + npm_config_cache: $(Pipeline.Workspace)/.npm_e2e + # Enable console logging in Release mode + SERILOG__WRITETO__0__NAME: Async + SERILOG__WRITETO__0__ARGS__CONFIGURE__0__NAME: Console + # Set unattended install settings + UMBRACO__CMS__UNATTENDED__INSTALLUNATTENDED: true + UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERNAME: Playwright Test + UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD: UmbracoAcceptance123! + UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL: playwright@umbraco.com + # Custom Umbraco settings + UMBRACO__CMS__CONTENT__CONTENTVERSIONCLEANUPPOLICY__ENABLECLEANUP: false + UMBRACO__CMS__GLOBAL__DISABLEELECTIONFORSINGLESERVER: true + UMBRACO__CMS__GLOBAL__INSTALLMISSINGDATABASE: true + UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042 + UMBRACO__CMS__GLOBAL__VERSIONCHECKPERIOD: 0 + UMBRACO__CMS__GLOBAL__USEHTTPS: true + UMBRACO__CMS__HEALTHCHECKS__NOTIFICATION__ENABLED: false + UMBRACO__CMS__KEEPALIVE__DISABLEKEEPALIVETASK: true + UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL: https://localhost:44331/ + ASPNETCORE_URLS: https://localhost:44331 jobs: # E2E Tests - job: - displayName: E2E Tests - timeoutInMinutes: 120 + displayName: E2E Tests (SQLite) variables: - Umbraco__CMS__Unattended__UnattendedUserName: Playwright Test - Umbraco__CMS__Unattended__UnattendedUserPassword: UmbracoAcceptance123! - Umbraco__CMS__Unattended__UnattendedUserEmail: playwright@umbraco.com - ASPNETCORE_URLS: https://localhost:8443 + # Connection string + CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True + CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite strategy: matrix: Linux: vmImage: 'ubuntu-latest' - dockerfile: umbraco-linux.docker - dockerImageName: umbraco-linux Windows: vmImage: 'windows-latest' - DOTNET_GENERATE_ASPNET_CERTIFICATE: true # Automatically generate HTTPS development certificate on Windows - # Enable console logging in Release mode - Serilog__WriteTo__0__Name: Async - Serilog__WriteTo__0__Args__configure__0__Name: Console - # Set unattended install settings - Umbraco__CMS__Unattended__InstallUnattended: true - Umbraco__CMS__Global__InstallMissingDatabase: true - UmbracoDatabaseServer: (LocalDB)\MSSQLLocalDB - UmbracoDatabaseName: AcceptanceTestDB - ConnectionStrings__umbracoDbDSN: Server=$(UmbracoDatabaseServer);Database=$(UmbracoDatabaseName);Integrated Security=true; - # Custom Umbraco settings - Umbraco__CMS__Global__VersionCheckPeriod: 0 - Umbraco__CMS__Global__UseHttps: true - Umbraco__CMS__HealthChecks__Notification__Enabled: false - Umbraco__CMS__KeepAlive__DisableKeepAliveTask: true pool: vmImage: $(vmImage) steps: + # Setup test environment - task: DownloadPipelineArtifact@2 - displayName: Download nupkg + displayName: Download NuGet artifacts inputs: artifact: nupkg - path: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/misc/nupkg + path: $(Agent.BuildDirectory)/app/nupkg + - task: NodeTool@0 displayName: Use Node.js $(nodeVersion) retryCountOnTaskFailure: 3 inputs: versionSpec: $(nodeVersion) + + - task: UseDotNet@2 + displayName: Use .NET SDK from global.json + inputs: + useGlobalJson: true + + - pwsh: | + "UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL) + UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD) + URL=$(ASPNETCORE_URLS)" | Out-File .env + displayName: Generate .env + workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest + + # Cache and restore NPM packages - task: Cache@2 - displayName: Cache node_modules + displayName: Cache NPM packages inputs: - key: '"npm_e2e" | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json' + key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json' restoreKeys: | - "npm_e2e" | "$(Agent.OS)" - "npm_e2e" + npm_e2e | "$(Agent.OS)" + npm_e2e path: $(npm_config_cache) - - pwsh: | - New-Item -Path "." -Name ".env" -ItemType "file" -Value "UMBRACO_USER_LOGIN=$(Umbraco__CMS__Unattended__UnattendedUserEmail) - UMBRACO_USER_PASSWORD=$(Umbraco__CMS__Unattended__UnattendedUserPassword) - URL=$(ASPNETCORE_URLS)" - displayName: Generate .env - workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/ + - script: npm ci --no-fund --no-audit --prefer-offline - workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/ - displayName: Run npm ci - - pwsh: sqllocaldb start mssqllocaldb - displayName: Start localdb (Windows only) - condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - - pwsh: Invoke-Sqlcmd -Query "CREATE DATABASE $env:UmbracoDatabaseName" -ServerInstance $env:UmbracoDatabaseServer - displayName: Create database (Windows only) + workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest + displayName: Restore NPM packages + + # Build application + - pwsh: | + $cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g" + dotnet new nugetconfig + dotnet nuget add source ./nupkg --name Local + dotnet new install Umbraco.Templates::$cmsVersion + dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check + dotnet restore UmbracoProject + cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject + dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore + dotnet dev-certs https + displayName: Build application + workingDirectory: $(Agent.BuildDirectory)/app + + # Run application + - bash: | + nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 & + echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!" + displayName: Run application (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + workingDirectory: $(Agent.BuildDirectory)/app + + - pwsh: | + $process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log + Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)" + displayName: Run application (Windows) condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + workingDirectory: $(Agent.BuildDirectory)/app + + # Wait for application to start responding to requests + - pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS) + displayName: Wait for application + workingDirectory: tests/Umbraco.Tests.AcceptanceTest + + # Install Playwright and dependencies + - pwsh: npx playwright install --with-deps + displayName: Install Playwright + workingDirectory: tests/Umbraco.Tests.AcceptanceTest + + # Test + - pwsh: npm run test --ignore-certificate-errors + displayName: Run Playwright tests + continueOnError: true + workingDirectory: tests/Umbraco.Tests.AcceptanceTest + env: + CI: true + CommitId: $(Build.SourceVersion) + AgentOs: $(Agent.OS) + + # Stop application + - bash: kill -15 $(AcceptanceTestProcessId) + displayName: Stop application (Linux) + condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux')) + + - pwsh: Stop-Process -Id $(AcceptanceTestProcessId) + displayName: Stop application (Windows) + condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT')) + + # Copy artifacts + - pwsh: | + if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) { + Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse + } + displayName: Copy Playwright results + condition: succeededOrFailed() + + # Publish + - task: PublishPipelineArtifact@1 + displayName: Publish test artifacts + condition: succeededOrFailed() + inputs: + targetPath: $(Build.ArtifactStagingDirectory) + artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)' + + - job: + displayName: E2E Tests (SQL Server) + condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerAcceptanceTests}}) + variables: + # Connection string + CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True + CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient + strategy: + matrix: + Linux: + vmImage: 'ubuntu-latest' + SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD) + CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True' + Windows: + vmImage: 'windows-latest' + pool: + vmImage: $(vmImage) + steps: + # Setup test environment + - task: DownloadPipelineArtifact@2 + displayName: Download NuGet artifacts + inputs: + artifact: nupkg + path: $(Agent.BuildDirectory)/app/nupkg + + - task: NodeTool@0 + displayName: Use Node.js $(nodeVersion) + inputs: + versionSpec: $(nodeVersion) + - task: UseDotNet@2 - displayName: Use .NET $(dotnetVersion) + displayName: Use .NET SDK from global.json inputs: - version: $(dotnetVersion) - performMultiLevelLookup: true - includePreviewVersions: $(dotnetIncludePreviewVersions) + useGlobalJson: true + - pwsh: | - $sha = 'g$(Build.SourceVersion)'.substring(0, 8) - docker build -t $(dockerImageName):$sha -f $(dockerfile) . - mkdir -p $(Build.ArtifactStagingDirectory)/docker-images - docker save -o $(Build.ArtifactStagingDirectory)/docker-images/$(dockerImageName).$sha.tar $(dockerImageName):$sha - dotnet dev-certs https -ep ${HOME}/.aspnet/https/aspnetapp.pfx -p $(Umbraco__CMS__Unattended__UnattendedUserPassword) - docker run --name $(dockerImageName) -dp 8080:5000 -dp 8443:5001 -e UMBRACO__CMS__GLOBAL__ID=$(UMBRACO__CMS__GLOBAL__ID) -e ASPNETCORE_Kestrel__Certificates__Default__Password="$(Umbraco__CMS__Unattended__UnattendedUserPassword)" -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx -v ${HOME}/.aspnet/https:/https/ $(dockerImageName):$sha - docker ps - condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) - displayName: Build and run container (Linux only) - workingDirectory: tests/Umbraco.Tests.AcceptanceTest/misc + "UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL) + UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD) + URL=$(ASPNETCORE_URLS)" | Out-File .env + displayName: Generate .env + workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest + + # Cache and restore NPM packages + - task: Cache@2 + displayName: Cache NPM packages + inputs: + key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json' + restoreKeys: | + npm_e2e | "$(Agent.OS)" + npm_e2e + path: $(npm_config_cache) + + - script: npm ci --no-fund --no-audit --prefer-offline + workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest + displayName: Restore NPM packages + + # Build application - pwsh: | - dotnet new --install ./nupkg/Umbraco.Templates.*.nupkg - dotnet new umbraco --name AcceptanceTestProject --no-restore --output . - dotnet restore --configfile ./nuget.config - dotnet build --configuration $(buildConfiguration) --no-restore + $cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g" + dotnet new nugetconfig + dotnet nuget add source ./nupkg --name Local + dotnet new install Umbraco.Templates::$cmsVersion + dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check + dotnet restore UmbracoProject + cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject + dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore dotnet dev-certs https - $process = Start-Process -FilePath "dotnet" -ArgumentList "run --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log + displayName: Build application + workingDirectory: $(Agent.BuildDirectory)/app + + # Start SQL Server + - powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest + displayName: Start SQL Server Docker image (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + + - pwsh: SqlLocalDB start MSSQLLocalDB + displayName: Start SQL Server LocalDB (Windows) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + + # Run application + - bash: | + nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 & + echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!" + displayName: Run application (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + workingDirectory: $(Agent.BuildDirectory)/app + + - pwsh: | + $process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)" + displayName: Run application (Windows) condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - displayName: Build and run app (Windows only) - workingDirectory: tests/Umbraco.Tests.AcceptanceTest/misc + workingDirectory: $(Agent.BuildDirectory)/app + + # Wait for application to start responding to requests - pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS) - displayName: Wait for app + displayName: Wait for application workingDirectory: tests/Umbraco.Tests.AcceptanceTest + + # Install Playwright and dependencies - pwsh: npx playwright install --with-deps displayName: Install Playwright workingDirectory: tests/Umbraco.Tests.AcceptanceTest + + # Test - pwsh: npm run test --ignore-certificate-errors - displayName: Run Playwright (Desktop) + displayName: Run Playwright tests continueOnError: true workingDirectory: tests/Umbraco.Tests.AcceptanceTest env: CI: true CommitId: $(Build.SourceVersion) AgentOs: $(Agent.OS) + + # Stop application + - bash: kill -15 $(AcceptanceTestProcessId) + displayName: Stop application (Linux) + condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux')) + + - pwsh: Stop-Process -Id $(AcceptanceTestProcessId) + displayName: Stop application (Windows) + condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT')) + + # Stop SQL Server + - pwsh: docker stop mssql + displayName: Stop SQL Server Docker image (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + + - pwsh: SqlLocalDB stop MSSQLLocalDB + displayName: Stop SQL Server LocalDB (Windows) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) + + # Copy artifacts - pwsh: | - docker logs $(dockerImageName) > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 - docker stop $(dockerImageName) - condition: eq(variables['Agent.OS'], 'Linux') - displayName: Stop app (Linux only) - - pwsh: Stop-Process $env:AcceptanceTestProcessId - condition: eq(variables['Agent.OS'], 'Windows_NT') - displayName: Stop app (Windows only) - - task: PowerShell@2 - displayName: Check if artifacts folder exists - inputs: - targetType: inline - script: | - $MyVariable = Test-Path -Path $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/results - Write-Host "##vso[task.setvariable variable=resultFolderExists;]$MyVariable" - - task: CopyFiles@2 - displayName: Prepare artifacts - condition: eq(variables.resultFolderExists, 'True') - inputs: - sourceFolder: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/results/ - targetFolder: $(Build.ArtifactStagingDirectory)/playwright + if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) { + Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse + } + displayName: Copy Playwright results + condition: succeededOrFailed() + + # Publish - task: PublishPipelineArtifact@1 - condition: always() displayName: Publish test artifacts + condition: succeededOrFailed() inputs: targetPath: $(Build.ArtifactStagingDirectory) - artifact: 'E2E artifacts - $(Agent.OS) - Attempt #$(System.JobAttempt)' - + artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)' ############################################### ## Release diff --git a/global.json b/global.json index 36394634bd13..391ba3c2a30b 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,6 @@ { "sdk": { - "version": "8.0.0", - "rollForward": "latestFeature", - "allowPrerelease": false + "version": "8.0.100", + "rollForward": "latestFeature" } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedPropertyIndexValueFactoryBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedPropertyIndexValueFactoryBase.cs index a675b38b2ca5..173b5eb69367 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedPropertyIndexValueFactoryBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedPropertyIndexValueFactoryBase.cs @@ -112,7 +112,8 @@ protected NestedPropertyIndexValueFactoryBase( published, propertyTypeDictionary, nestedContentRowValue, - availableCultures)); + availableCultures, + contentTypeDictionary)); index++; } @@ -213,7 +214,8 @@ private static string GetResumeFromAllContent(List propertyTypeDictionary, TItem nestedContentRowValue, - IEnumerable availableCultures) + IEnumerable availableCultures, + IDictionary contentTypeDictionary) { foreach ((var propertyAlias, var propertyValue) in GetRawProperty(nestedContentRowValue)) { @@ -238,7 +240,7 @@ private static string GetResumeFromAllContent(List nestedValue) in indexValues) diff --git a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs index 676b05317e51..ee89a7e80751 100644 --- a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs @@ -520,7 +520,7 @@ public static class ImageCropperTemplateCoreExtensions throw new ArgumentNullException(nameof(mediaItem)); } - if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) + if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(publishedValueFallback, propertyAlias) == false) { return null; } diff --git a/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/SQLiteMemoryComposer.cs b/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/SQLiteMemoryComposer.cs new file mode 100644 index 000000000000..63f330a564b1 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/SQLiteMemoryComposer.cs @@ -0,0 +1,42 @@ +using Microsoft.Data.Sqlite; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Extensions; + +namespace UmbracoProject; + +/// +/// Ensures a SQLite in-memory database is persisted for the whole application duration. +/// +public sealed class SQLiteMemoryComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + var connectionString = builder.Config.GetUmbracoConnectionString(out var providerName); + if (!string.IsNullOrEmpty(connectionString) && + Constants.ProviderNames.SQLLite.InvariantEquals(providerName) && + connectionString.InvariantContains("Mode=Memory")) + { + // Open new SQLite connection to ensure in-memory database is persisted for the whole application duration + var connection = new SqliteConnection(connectionString); + connection.Open(); + + // And ensure connection is kept open (by keeping a reference) and gets gracefully closed/disposed when application stops + builder.Services.AddHostedService(_ => new SQLiteMemoryHostedService(connection)); + } + } + + private sealed class SQLiteMemoryHostedService : IHostedService, IAsyncDisposable + { + private readonly SqliteConnection _connection; + + public SQLiteMemoryHostedService(SqliteConnection connection) => _connection = connection; + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public async Task StopAsync(CancellationToken cancellationToken) => await _connection.CloseAsync(); + + public async ValueTask DisposeAsync() => await _connection.DisposeAsync(); + } +} diff --git a/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/SqlServerDelayedDurabilityComposer.cs b/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/SqlServerDelayedDurabilityComposer.cs new file mode 100644 index 000000000000..956a101fbcbc --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/SqlServerDelayedDurabilityComposer.cs @@ -0,0 +1,44 @@ +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Extensions; + +namespace UmbracoProject; + +/// +/// Disable waiting on log IO to finish when commiting a transaction (we can tolerate some data loss) on SQL Server. +/// +public sealed class SqlServerDelayedDurabilityComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + var connectionString = builder.Config.GetUmbracoConnectionString(out var providerName); + if (!string.IsNullOrEmpty(connectionString) && + Constants.ProviderNames.SQLServer.InvariantEquals(providerName)) + { + builder.AddNotificationAsyncHandler(); + } + } + + private sealed class SqlServerDelayedDurabilityInstallNotification : INotificationAsyncHandler + { + private readonly IOptions _connectionStrings; + + public SqlServerDelayedDurabilityInstallNotification(IOptions connectionStrings) => _connectionStrings = connectionStrings; + + public async Task HandleAsync(UnattendedInstallNotification notification, CancellationToken cancellationToken) + { + using var connection = new SqlConnection(_connectionStrings.Value.ConnectionString); + await connection.OpenAsync(cancellationToken); + + // Disable waiting on log IO to finish when commiting a transaction (we can tolerate some data loss) + var command = new SqlCommand("ALTER DATABASE CURRENT SET DELAYED_DURABILITY = FORCED;", connection); + await command.ExecuteNonQueryAsync(cancellationToken); + } + } +} diff --git a/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/SuspendScheduledPublishingComposer.cs b/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/SuspendScheduledPublishingComposer.cs new file mode 100644 index 000000000000..ba9c2207400e --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/SuspendScheduledPublishingComposer.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Infrastructure; + +namespace UmbracoProject; + +/// +/// Suspends/disables scheduled publishing, because that takes an eager write lock every minute, resulting in flaky test runs on SQLite. +/// +public sealed class SuspendScheduledPublishingComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) => Suspendable.ScheduledPublishing.Suspend(); +} diff --git a/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/Umbraco.Tests.AcceptanceTest.UmbracoProject.csproj b/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/Umbraco.Tests.AcceptanceTest.UmbracoProject.csproj new file mode 100644 index 000000000000..ac71f98e2926 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/Umbraco.Tests.AcceptanceTest.UmbracoProject.csproj @@ -0,0 +1,10 @@ + + + + Library + UmbracoProject + + + + + diff --git a/tests/Umbraco.Tests.AcceptanceTest/misc/Directory.Build.props b/tests/Umbraco.Tests.AcceptanceTest/misc/Directory.Build.props deleted file mode 100644 index 33a5cccb0b1a..000000000000 --- a/tests/Umbraco.Tests.AcceptanceTest/misc/Directory.Build.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/misc/nuget.config b/tests/Umbraco.Tests.AcceptanceTest/misc/nuget.config deleted file mode 100644 index 08b6b81193cd..000000000000 --- a/tests/Umbraco.Tests.AcceptanceTest/misc/nuget.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/tests/Umbraco.Tests.AcceptanceTest/misc/umbraco-linux.docker b/tests/Umbraco.Tests.AcceptanceTest/misc/umbraco-linux.docker deleted file mode 100644 index 8161ad062fad..000000000000 --- a/tests/Umbraco.Tests.AcceptanceTest/misc/umbraco-linux.docker +++ /dev/null @@ -1,51 +0,0 @@ -############################################ -## Build -############################################ - -FROM mcr.microsoft.com/dotnet/nightly/sdk:8.0.100-rc.2-jammy AS build - -COPY nuget.config . - -COPY nuget.config . - -WORKDIR /nupkg -COPY nupkg . - -WORKDIR /build -RUN dotnet new --install /nupkg/Umbraco.Templates.*.nupkg -RUN dotnet new umbraco --name AcceptanceTestProject --no-restore --output . -RUN dotnet restore --configfile /nuget.config -RUN dotnet build --configuration Release --no-restore -RUN dotnet publish --configuration Release --no-build --output /dist - -############################################ -## Run -############################################ - -FROM mcr.microsoft.com/dotnet/nightly/aspnet:8.0.0-rc.2-jammy AS run - -WORKDIR /app -COPY --from=build dist . - -# Enable console logging in Release mode -ENV Serilog__WriteTo__0__Name=Async -ENV Serilog__WriteTo__0__Args__configure__0__Name=Console - -# Set unattended install settings -ENV ConnectionStrings__umbracoDbDSN_ProviderName="Microsoft.Data.Sqlite" -ENV ConnectionStrings__umbracoDbDSN="Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True" -ENV Umbraco__CMS__Unattended__InstallUnattended="true" -ENV Umbraco__CMS__Unattended__UnattendedUserName="Playwright Test" -ENV Umbraco__CMS__Unattended__UnattendedUserEmail="playwright@umbraco.com" -ENV Umbraco__CMS__Unattended__UnattendedUserPassword="UmbracoAcceptance123!" - -# Custom Umbraco settings -ENV Umbraco__CMS__Global__VersionCheckPeriod="0" -ENV Umbraco__CMS__Global__UseHttps="true" -ENV Umbraco__CMS__HealthChecks__Notification__Enabled="false" -ENV Umbraco__CMS__KeepAlive__DisableKeepAliveTask="true" - -# Set application URL -ENV ASPNETCORE_URLS="http://0.0.0.0:5000;https://0.0.0.0:5001" - -CMD dotnet AcceptanceTestProject.dll diff --git a/umbraco.sln b/umbraco.sln index bfe5891cf5cc..3b581e089be7 100644 --- a/umbraco.sln +++ b/umbraco.sln @@ -140,14 +140,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution global.json = global.json icon.png = icon.png LICENSE.md = LICENSE.md - umbraco.sln.DotSettings = umbraco.sln.DotSettings nuget.config = nuget.config + umbraco.sln.DotSettings = umbraco.sln.DotSettings version.json = version.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{20CE9C97-9314-4A19-BCF1-D12CF49B7205}" ProjectSection(SolutionItems) = preProject build\azure-pipelines.yml = build\azure-pipelines.yml + build\nightly-build-trigger.yml = build\nightly-build-trigger.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "csharp-docs", "csharp-docs", "{F2BF84D9-0A14-40AF-A0F3-B9BBBBC16A44}" @@ -214,6 +215,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Persistence.EFC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Persistence.EFCore.SqlServer", "src\Umbraco.Cms.Persistence.EFCore.SqlServer\Umbraco.Cms.Persistence.EFCore.SqlServer.csproj", "{9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests.AcceptanceTest.UmbracoProject", "tests\Umbraco.Tests.AcceptanceTest.UmbracoProject\Umbraco.Tests.AcceptanceTest.UmbracoProject.csproj", "{A13FF0A0-69FA-468A-9F79-565401D5C341}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -408,6 +411,12 @@ Global {9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}.Release|Any CPU.Build.0 = Release|Any CPU {9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {9276C3F0-0DC9-46C9-BF32-9EE79D92AE02}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {A13FF0A0-69FA-468A-9F79-565401D5C341}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A13FF0A0-69FA-468A-9F79-565401D5C341}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A13FF0A0-69FA-468A-9F79-565401D5C341}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A13FF0A0-69FA-468A-9F79-565401D5C341}.Release|Any CPU.Build.0 = Release|Any CPU + {A13FF0A0-69FA-468A-9F79-565401D5C341}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {A13FF0A0-69FA-468A-9F79-565401D5C341}.SkipTests|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -429,6 +438,7 @@ Global {EA628ABD-624E-4AF3-B548-6710D4D66531} = {2B47AD9F-FFF1-448A-88F1-D4F568811738} {C55CA725-9F4E-4618-9435-6B8AE05DA14D} = {995D9EFA-8BB1-4333-80AD-C525A06FD984} {D88A926B-E8D6-495A-A2ED-8EFD0C847C62} = {995D9EFA-8BB1-4333-80AD-C525A06FD984} + {A13FF0A0-69FA-468A-9F79-565401D5C341} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC}