From 7a0b32df7ad673af754245549ed7817cdd35a57e Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Fri, 25 Sep 2026 10:41:54 +0200 Subject: [PATCH 01/22] Separate API test workflow infrastructure from AL uptake Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .github/AL-Go-Settings.json | 8 + .github/workflows/_BuildALGoProject.yaml | 6 + build/scripts/ApiTestCredential.psm1 | 112 ++ build/scripts/NewBcContainer.ps1 | 9 + build/scripts/ParallelTestExecution.psm1 | 675 +++++++++- build/scripts/Remove-ApiTestPassword.ps1 | 35 + build/scripts/RunTestsInBcContainer.ps1 | 51 +- .../scripts/tests/ApiTestCredential.Test.ps1 | 239 ++++ .../tests/ParallelTestExecution.Test.ps1 | 1100 ++++++++++++++++- .../tests/Remove-ApiTestPassword.Test.ps1 | 137 ++ 10 files changed, 2290 insertions(+), 82 deletions(-) create mode 100644 build/scripts/ApiTestCredential.psm1 create mode 100644 build/scripts/Remove-ApiTestPassword.ps1 create mode 100644 build/scripts/tests/ApiTestCredential.Test.ps1 create mode 100644 build/scripts/tests/Remove-ApiTestPassword.Test.ps1 diff --git a/.github/AL-Go-Settings.json b/.github/AL-Go-Settings.json index fbb001b96a5..68c886dd5f1 100644 --- a/.github/AL-Go-Settings.json +++ b/.github/AL-Go-Settings.json @@ -37,6 +37,14 @@ ] } }, + { + "buildModes": [ + "UncategorizedTests" + ], + "settings": { + "enableTaskScheduler": true + } + }, { "buildModes": [ "LegacyTestsBucket1" diff --git a/.github/workflows/_BuildALGoProject.yaml b/.github/workflows/_BuildALGoProject.yaml index 7d8c4d8af9c..2ddb7ae37f4 100644 --- a/.github/workflows/_BuildALGoProject.yaml +++ b/.github/workflows/_BuildALGoProject.yaml @@ -379,6 +379,12 @@ jobs: project: ${{ inputs.project }} testType: "pageScripting" + - name: Remove API test credential + if: always() && steps.DetermineBuildProject.outputs.BuildIt == 'True' && env.BCAppsApiTestPasswordPath != '' + timeout-minutes: 2 + run: | + & (Join-Path $env:GITHUB_WORKSPACE 'build/scripts/Remove-ApiTestPassword.ps1') -ContainerName $env:containerName -FilePath $env:BCAppsApiTestPasswordPath + - name: Cleanup if: always() && steps.DetermineBuildProject.outputs.BuildIt == 'True' uses: microsoft/AL-Go/Actions/PipelineCleanup@91b96c2b294be6f823277dafe6f03350abfb9d23 diff --git a/build/scripts/ApiTestCredential.psm1 b/build/scripts/ApiTestCredential.psm1 new file mode 100644 index 00000000000..8f0737a05dd --- /dev/null +++ b/build/scripts/ApiTestCredential.psm1 @@ -0,0 +1,112 @@ +function New-ApiTestPasswordFileStream { + param([Parameter(Mandatory = $true)][string]$FilePath) + + $ErrorActionPreference = 'Stop' + $security = [System.Security.AccessControl.FileSecurity]::new() + $security.SetAccessRuleProtection($true, $false) + $writerSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value + $entries = @( + @{ Sid = 'S-1-5-18'; Rights = [System.Security.AccessControl.FileSystemRights]::FullControl } + @{ Sid = 'S-1-5-20'; Rights = [System.Security.AccessControl.FileSystemRights]::Read } + @{ Sid = 'S-1-5-32-544'; Rights = [System.Security.AccessControl.FileSystemRights]::FullControl } + ) + if ($writerSid -notin @('S-1-5-18', 'S-1-5-32-544')) { + # The host identity writes the credential and deletes it during cleanup; it needs + # neither ReadData nor FullControl. Container service identities retain their access. + $entries += @{ + Sid = $writerSid + Rights = [System.Security.AccessControl.FileSystemRights]'Write, Delete' + } + } + foreach ($entry in $entries) { + $security.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new( + [System.Security.Principal.SecurityIdentifier]::new($entry.Sid), + $entry.Rights, + [System.Security.AccessControl.AccessControlType]::Allow)) + } + + # Supply the protected DACL to CreateFile itself, not Set-Acl after creation. + # CreateNew also refuses to follow or overwrite a pre-existing credential file. + if ($PSVersionTable.PSEdition -eq 'Core') { + return [System.IO.FileSystemAclExtensions]::Create( + [System.IO.FileInfo]::new($FilePath), [System.IO.FileMode]::CreateNew, + [System.Security.AccessControl.FileSystemRights]::Write, [System.IO.FileShare]::None, + 4096, [System.IO.FileOptions]::None, $security) + } + return [System.IO.FileStream]::new( + $FilePath, [System.IO.FileMode]::CreateNew, + [System.Security.AccessControl.FileSystemRights]::Write, [System.IO.FileShare]::None, + 4096, [System.IO.FileOptions]::None, $security) +} + +<# +.SYNOPSIS + Creates the API test credential directly in the container's shared my mount. +.DESCRIPTION + Registers workflow cleanup before atomically creating a new file with a protected + DACL. No plaintext staging copies are made. SYSTEM and Administrators retain full + access, NetworkService can read, and the host writer can write and delete. + An existing file or unresolved mount is an error, including outside CI. Workflow + cleanup (or local container teardown) must end consumers before removing the file. +#> +function Write-ApiTestPassword { + param( + [Parameter(Mandatory = $true)] + [ValidatePattern('^[a-zA-Z0-9][a-zA-Z0-9_.-]*$')] + [string]$ContainerName, + [Parameter(Mandatory = $true)] + [PSCredential]$Credential + ) + + $ErrorActionPreference = 'Stop' + $myFolders = @((Get-BcContainerSharedFolders -containerName $ContainerName).GetEnumerator() | + Where-Object { $_.Value.TrimEnd('\') -eq 'C:\Run\my' }) + if ($myFolders.Count -ne 1) { + throw "Cannot resolve the API test credential's container mount." + } + $filePath = Join-Path $myFolders[0].Key 'ApiTestPassword' + if ($env:GITHUB_ENV) { + # Persist cleanup before creation, including for setup failure or cancellation. + Add-Content -LiteralPath $env:GITHUB_ENV -Encoding UTF8 -Value "BCAppsApiTestPasswordPath=$filePath" -ErrorAction Stop + } + + $created = $false + $bytes = $null + $characters = $null + $passwordBuffer = [IntPtr]::Zero + try { + # Write directly to the shared mount. Copy-FileToBcContainer would introduce + # another host staging copy with inherited permissions. + $stream = New-ApiTestPasswordFileStream -FilePath $filePath + $created = $true + try { + $passwordBuffer = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password) + $characters = [char[]]::new($Credential.Password.Length) + [System.Runtime.InteropServices.Marshal]::Copy($passwordBuffer, $characters, 0, $characters.Length) + $bytes = [System.Text.UTF8Encoding]::new($false).GetBytes($characters) + $stream.Write($bytes, 0, $bytes.Length) + } + finally { + if ($null -ne $bytes) { [Array]::Clear($bytes, 0, $bytes.Length) } + if ($null -ne $characters) { [Array]::Clear($characters, 0, $characters.Length) } + if ($passwordBuffer -ne [IntPtr]::Zero) { + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($passwordBuffer) + } + $stream.Dispose() + } + } + catch { + $originalError = $_ + if ($created) { + try { + & (Join-Path $PSScriptRoot 'Remove-ApiTestPassword.ps1') -ContainerName $ContainerName -FilePath $filePath + } + catch { + Write-Warning 'Could not clean up the API test credential after setup failed; container teardown is still required.' + } + } + throw $originalError + } +} + +Export-ModuleMember -Function Write-ApiTestPassword diff --git a/build/scripts/NewBcContainer.ps1 b/build/scripts/NewBcContainer.ps1 index d6e98d84794..78871ab947b 100644 --- a/build/scripts/NewBcContainer.ps1 +++ b/build/scripts/NewBcContainer.ps1 @@ -24,6 +24,15 @@ if ($platformVersion) { New-BcContainer @parameters +if ($parameters.auth -in @('UserPassword', 'NavUserPassword')) { + if (-not $parameters.credential) { + throw "The BCApps UserPassword test container requires a credential." + } + + Import-Module (Join-Path $PSScriptRoot 'ApiTestCredential.psm1') -Force + Write-ApiTestPassword -ContainerName $parameters.ContainerName -Credential $parameters.credential +} + Set-BcContainerServerConfiguration -containerName $parameters.ContainerName -keyName "EnforceUserPathForAlFileOperations" -keyValue "false" Set-BcContainerServerConfiguration -containerName $parameters.ContainerName -keyName "UsePermissionSetsFromExtensions" -keyValue "true" Restart-BcContainer -containerName $parameters.ContainerName diff --git a/build/scripts/ParallelTestExecution.psm1 b/build/scripts/ParallelTestExecution.psm1 index 2d6e2b669e7..28ea00281c2 100644 --- a/build/scripts/ParallelTestExecution.psm1 +++ b/build/scripts/ParallelTestExecution.psm1 @@ -14,6 +14,133 @@ if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) { } Import-Module (Join-Path $PSScriptRoot "ALAppBuild.psm1" -Resolve) +<# +.SYNOPSIS + Gets disabled test entries for an app. +.PARAMETER AppName + Application name used to locate its DisabledTests folder. +#> +function Get-DisabledTestsForApp { + param( + [Parameter(Mandatory=$true)] + [string]$AppName + ) + + $appFolderName = $AppName -replace ' ', '_' + $disabledTests = @() + + $disabledTestsFolders = Get-ChildItem -Path (Get-BaseFolder) -Filter "DisabledTests" -Recurse -Directory + foreach ($disabledTestsFolder in $disabledTestsFolders) { + $appFolder = Join-Path $disabledTestsFolder.FullName $appFolderName + if (-not (Test-Path $appFolder)) { + continue + } + + foreach ($jsonFile in (Get-ChildItem -Path $appFolder -Filter "*.json")) { + $disabledTests += @( + Get-Content -Raw -Path $jsonFile.FullName | + ConvertFrom-Json + ) + } + } + + return @($disabledTests) +} + +function Get-ParametersForCommand { + param( + [Parameter(Mandatory=$true)] + [Hashtable]$Parameters, + [Parameter(Mandatory=$true)] + [string]$CommandName + ) + + $command = Get-Command $CommandName -ErrorAction Stop + $filtered = @{} + foreach ($key in $Parameters.Keys) { + if ($command.Parameters.ContainsKey($key)) { + $filtered[$key] = $Parameters[$key] + } + } + return $filtered +} + +function ConvertTo-RequiredDisabledWorkItems { + param( + [array]$DiscoveredTests, + [string]$AppName, + [string]$AppId + ) + + $codeunits = @() + foreach ($entry in @($DiscoveredTests)) { + if ($entry.PSObject.Properties.Name -contains "Codeunits") { + $codeunits += @($entry.Codeunits) + } else { + $codeunits += $entry + } + } + + return @( + $codeunits | + Where-Object { $_ -and $_.Id -and @($_.Tests).Count -gt 0 } | + ForEach-Object { + [PSCustomObject]@{ + Key = "${AppName}::$($_.Id)" + AppName = $AppName + AppId = $AppId + CodeunitId = [string]$_.Id + CodeunitName = [string]$_.Name + TestCount = @($_.Tests).Count + } + } + ) +} + +function Get-RequiredDisabledWorkItems { + param( + [Parameter(Mandatory=$true)] + [Hashtable]$Parameters, + [Parameter(Mandatory=$true)] + [string]$TestType, + [string[]]$AppNamesToTest, + [Parameter(Mandatory=$true)] + [Hashtable]$AppIdByName + ) + + # Without a test type, the test-tool extension selector ignores required isolation. + # Keep Legacy buckets on their existing execution path rather than treating every + # codeunit as requiring a clean tenant. + if ($TestType -eq 'Legacy') { + Write-Host 'Legacy buckets use ordinary execution; clean isolation discovery requires a typed lane.' + return @() + } + + $workItems = @() + foreach ($appName in $AppNamesToTest) { + $appId = $AppIdByName[$appName] + if (-not $appId) { + continue + } + + $discoveryParameters = Get-ParametersForCommand -Parameters $Parameters -CommandName "Get-TestsFromBcContainer" + $discoveryParameters["extensionId"] = $appId + $discoveryParameters["requiredTestIsolation"] = "Disabled" + $discoveryParameters["disabledTests"] = @(Get-DisabledTestsForApp -AppName $appName) + $discoveryParameters["testType"] = $TestType + + Write-Host "Discovering RequiredTestIsolation=Disabled codeunits in '$appName'..." + $discoveredTests = @(Get-TestsFromBcContainer @discoveryParameters) + $appWorkItems = @(ConvertTo-RequiredDisabledWorkItems -DiscoveredTests $discoveredTests -AppName $appName -AppId $appId) + if ($appWorkItems.Count -gt 0) { + Write-Host " Found $($appWorkItems.Count) codeunit(s), $((($appWorkItems | Measure-Object TestCount -Sum).Sum)) test method(s)." + $workItems += $appWorkItems + } + } + + return @($workItems) +} + <# .SYNOPSIS Returns the rerun budget for the current build. @@ -72,7 +199,7 @@ function Get-CachedTestRunResult { [string]$ContainerName ) - $tempDir = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { $env:TEMP } + $tempDir = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } elseif ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() } $stateFile = Join-Path $tempDir "parallelTests_$ContainerName.json" if (-not (Test-Path $stateFile)) { return $null } @@ -204,18 +331,167 @@ function Get-AppNamesForBucket { .OUTPUTS [string[]] Array of tenant IDs that are in Operational state. #> -function Get-AvailableBcTenants { +function Get-AvailableBcTenantInfo { param( [Parameter(Mandatory=$true)] [string]$containerName ) $tenants = Invoke-ScriptInBcContainer -containerName $containerName -scriptblock { - Get-NavTenant $ServerInstance | Where-Object { $_.State -eq "Operational" } | ForEach-Object { $_.Id } + Get-NavTenant $ServerInstance | + Where-Object { $_.State -eq "Operational" } | + ForEach-Object { + [PSCustomObject]@{ + Id = $_.Id + DatabaseName = $_.DatabaseName + } + } } return @($tenants) } +<# +.SYNOPSIS + Gets the IDs of operational tenants in a BC container. +.PARAMETER containerName + Name of the BC container. +#> +function Get-AvailableBcTenants { + param( + [Parameter(Mandatory=$true)] + [string]$containerName + ) + + return @( + Get-AvailableBcTenantInfo -containerName $containerName | + ForEach-Object { $_.Id } + ) +} + +function New-BcTestTenantTemplate { + param( + [Parameter(Mandatory=$true)] + [string]$ContainerName, + [Parameter(Mandatory=$true)] + [string]$SourceDatabaseName + ) + + $result = @(Invoke-ScriptInBcContainer -containerName $ContainerName -useSession $false -scriptblock { Param($sourceDatabaseName) + $templateDatabaseName = "$sourceDatabaseName-test-template" + $maxAttempts = 3 + $retryDelaySeconds = 5 + for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { + try { + if (Test-NAVDatabase -DatabaseName $templateDatabaseName) { + Remove-NAVDatabase -DatabaseName $templateDatabaseName | Out-Null + } + + Write-Host "Creating immutable test tenant template '$templateDatabaseName' from '$sourceDatabaseName' (attempt $attempt/$maxAttempts)..." + Copy-NAVDatabase -SourceDatabaseName $sourceDatabaseName -DestinationDatabaseName $templateDatabaseName -DatabaseServer "." | Out-Null + break + } catch { + Write-Host "WARNING: Template database copy failed on attempt $attempt/${maxAttempts}: $($_.Exception.Message)" + if ($attempt -eq $maxAttempts) { + throw "Failed to create a test tenant template from '$sourceDatabaseName' after $maxAttempts attempts. Last error: $($_.Exception.Message)" + } + Start-Sleep -Seconds $retryDelaySeconds + } + } + $templateDatabaseName + } -argumentList $SourceDatabaseName) + + if ($result.Count -eq 0) { + throw "Creating the clean test tenant template returned no database name." + } + return [string]$result[-1] +} + +<# +.SYNOPSIS + Replaces a tenant database with a copy of an idle source database. +.PARAMETER ContainerName + Name of the BC container. +.PARAMETER Tenant + Tenant ID to refresh. +.PARAMETER TenantDatabaseName + Database currently mounted for the tenant. +.PARAMETER TemplateDatabaseName + Immutable test template, or the untouched primary database used to restore discovery. +#> +function Reset-BcTestTenant { + param( + [Parameter(Mandatory=$true)] + [string]$ContainerName, + [Parameter(Mandatory=$true)] + [string]$Tenant, + [Parameter(Mandatory=$true)] + [string]$TenantDatabaseName, + [Parameter(Mandatory=$true)] + [string]$TemplateDatabaseName + ) + + Invoke-ScriptInBcContainer -containerName $ContainerName -useSession $false -scriptblock { + Param($tenant, $tenantDatabaseName, $templateDatabaseName) + + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + $mountedTenant = Get-NAVTenant -ServerInstance $ServerInstance -Tenant $tenant -ErrorAction SilentlyContinue + if ($mountedTenant) { + Dismount-NAVTenant -ServerInstance $ServerInstance -Tenant $tenant -Force | Out-Null + } + + $maxAttempts = 3 + $retryDelaySeconds = 5 + for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { + try { + if (Test-NAVDatabase -DatabaseName $tenantDatabaseName) { + Remove-NAVDatabase -DatabaseName $tenantDatabaseName | Out-Null + } + + Copy-NAVDatabase -SourceDatabaseName $templateDatabaseName -DestinationDatabaseName $tenantDatabaseName -DatabaseServer "." | Out-Null + break + } catch { + Write-Host "WARNING: Tenant database refresh failed on attempt $attempt/${maxAttempts}: $($_.Exception.Message)" + if ($attempt -eq $maxAttempts) { + throw "Failed to refresh tenant database '$tenantDatabaseName' after $maxAttempts attempts. Last error: $($_.Exception.Message)" + } + Start-Sleep -Seconds $retryDelaySeconds + } + } + Mount-NAVTenant -ServerInstance $ServerInstance -Id $tenant -DatabaseServer "." -DatabaseName $tenantDatabaseName -OverwriteTenantIdInDatabase -Force | Out-Null + + $maxWaitSeconds = 300 + while ((Get-NAVTenant -ServerInstance $ServerInstance -Tenant $tenant).State -eq "Mounting") { + if ($stopwatch.Elapsed.TotalSeconds -ge $maxWaitSeconds) { + throw "Tenant '$tenant' did not finish mounting within $maxWaitSeconds seconds." + } + Start-Sleep -Milliseconds 250 + } + + $state = (Get-NAVTenant -ServerInstance $ServerInstance -Tenant $tenant).State + if ($state -notin @("Operational", "OperationalWithWarnings")) { + throw "Tenant '$tenant' is '$state' after refresh; expected an operational state." + } + + $stopwatch.Stop() + Write-Host "Refreshed tenant '$tenant' from '$templateDatabaseName' in $([math]::Round($stopwatch.Elapsed.TotalSeconds, 2)) seconds." + } -argumentList $Tenant, $TenantDatabaseName, $TemplateDatabaseName +} + +function Remove-BcTestTenantTemplate { + param( + [Parameter(Mandatory=$true)] + [string]$ContainerName, + [Parameter(Mandatory=$true)] + [string]$TemplateDatabaseName + ) + + Invoke-ScriptInBcContainer -containerName $ContainerName -scriptblock { Param($templateDatabaseName) + if (Test-NAVDatabase -DatabaseName $templateDatabaseName) { + Remove-NAVDatabase -DatabaseName $templateDatabaseName | Out-Null + } + } -argumentList $TemplateDatabaseName +} + <# .SYNOPSIS Merges multiple test result XML files into a single file. @@ -297,7 +573,10 @@ function Merge-TestResultFiles { Any of these fingerprints is sufficient evidence: "Cannot open page 130455", "InvokeInteractions failed with status code 500", or a stack frame referencing "InteractionManager.cs:line N" (line number not pinned, so platform refactors do not - silently invalidate the match). + silently invalidate the match). The platform can also fail while page 130455 resolves an + extension's codeunit metadata; the known variants surface from ExtensionId_a45_OnValidate + as an invalid metadata BLOB range or a missing nullable metadata value. A generic + ClientSession InError state alone is not evidence of this race. .PARAMETER Output The combined output (stdout + stderr + verbose) captured from a finished background job. Null or empty returns $false. @@ -308,7 +587,12 @@ function Test-TransientTestFailure { ) if ([string]::IsNullOrEmpty($Output)) { return $false } - return [bool]($Output -match 'Cannot open page 130455|InvokeInteractions failed with status code 500|InteractionManager\.cs:line \d+') + return [bool]( + ($Output -match 'TRANSIENT TEST PLATFORM RACE') -or + ($Output -match 'Cannot open page 130455|InvokeInteractions failed with status code 500|InteractionManager\.cs:line \d+') -or + ($Output -match '(?s)ObjName:Command Line Test Tool.*MethodName:ExtensionId_a45_OnValidate.*(?:Offset and length were out of bounds|Nullable object must have a value)') -or + ($Output -match '(?s)GET request failed\..*Response code is 500.*Object reference not set to an instance of an object') + ) } <# @@ -403,6 +687,7 @@ function Start-TestAppDispatch { [string]$ScriptPath, [string]$TestType, $State, + [switch]$SkipAutomaticDisabledPass, [string]$Verb = 'Dispatching', [string]$FileSuffix ) @@ -414,12 +699,149 @@ function Start-TestAppDispatch { $appParams['extensionId'] = $AppId $appParams.Remove('ReRun') | Out-Null - $job = Start-TestJob -parameters $appParams -tenant $Tenant -scriptPath $ScriptPath -testType $TestType -fileSuffix $FileSuffix + $job = Start-TestJob -parameters $appParams -tenant $Tenant -scriptPath $ScriptPath -testType $TestType ` + -skipAutomaticDisabledPass:$SkipAutomaticDisabledPass -fileSuffix $FileSuffix $State.jobs = @($State.jobs) + @([PSCustomObject]@{ jobId = $job.Id; tenant = $Tenant; appName = $AppName }) Start-Sleep -Seconds 5 } +function Start-RequiredDisabledDispatch { + param( + [Hashtable]$Parameters, + $WorkItem, + $TenantInfo, + [string]$ScriptPath, + [string]$TestType, + $State, + [string]$Verb = "Dispatching" + ) + + Write-Host "$Verb RequiredTestIsolation=Disabled codeunit $($WorkItem.CodeunitId) '$($WorkItem.CodeunitName)' from '$($WorkItem.AppName)' on tenant '$($TenantInfo.Id)'" + + $codeunitParameters = $Parameters.Clone() + $codeunitParameters["appName"] = $WorkItem.AppName + $codeunitParameters["extensionId"] = $WorkItem.AppId + $codeunitParameters["testCodeunit"] = $WorkItem.CodeunitId + $codeunitParameters["requiredTestIsolation"] = "Disabled" + $codeunitParameters["testRunnerCodeunitId"] = "130451" + $codeunitParameters["disabledTests"] = @(Get-DisabledTestsForApp -AppName $WorkItem.AppName) + $codeunitParameters.Remove("ReRun") | Out-Null + if ($Verb -eq "Re-dispatching") { + $codeunitParameters["ReRun"] = $true + } + + $appendKeys = @{ + XUnitResultFileName = "AppendToXUnitResultFile" + JUnitResultFileName = "AppendToJUnitResultFile" + } + foreach ($resultKey in $appendKeys.Keys) { + if ($codeunitParameters.ContainsKey($resultKey) -and $codeunitParameters[$resultKey]) { + $codeunitParameters[$appendKeys[$resultKey]] = $true + } + } + + $job = Start-TestJob -parameters $codeunitParameters -tenant $TenantInfo.Id -scriptPath $ScriptPath ` + -testType $TestType -skipAutomaticDisabledPass + $State.jobs = @($State.jobs) + @( + [PSCustomObject]@{ + jobId = $job.Id + tenant = $TenantInfo.Id + appName = $WorkItem.Key + } + ) + Start-Sleep -Seconds 1 +} + +function Invoke-RequiredDisabledTestExecution { + param( + [Parameter(Mandatory=$true)] + [Hashtable]$Parameters, + [Parameter(Mandatory=$true)] + [array]$WorkItems, + [Parameter(Mandatory=$true)] + [array]$TenantInfo, + [Parameter(Mandatory=$true)] + [string]$TemplateDatabaseName, + [Parameter(Mandatory=$true)] + [string]$ScriptPath, + [Parameter(Mandatory=$true)] + [string]$TestType + ) + + if ($WorkItems.Count -eq 0) { + return $true + } + + $workItemByKey = @{} + foreach ($workItem in $WorkItems) { + $workItemByKey[$workItem.Key] = $workItem + } + + $state = [PSCustomObject]@{ + jobs = @() + hasFailures = $false + transient = @() + retried = @{} + retryTenant = @{} + } + $pending = @($WorkItems) + + while ($pending.Count -gt 0 -or $state.transient.Count -gt 0) { + if ($state.transient.Count -gt 0) { + $retryItems = @() + foreach ($transient in @($state.transient)) { + $key = $transient.Key + $state.retried[$key] = $true + $state.retryTenant[$key] = $transient.Tenant + $retryItems += $workItemByKey[$key] + } + $state.transient = @() + $pending = @($retryItems) + @($pending) + } + + $availableTenantInfo = @($TenantInfo) + $batch = @() + while ($pending.Count -gt 0 -and $availableTenantInfo.Count -gt 0) { + $workItem = $pending[0] + $pending = @($pending | Select-Object -Skip 1) + $selectedTenantInfo = if ($state.retryTenant.ContainsKey($workItem.Key)) { + $availableTenantInfo | + Where-Object { $_.Id -eq $state.retryTenant[$workItem.Key] } | + Select-Object -First 1 + } else { + $availableTenantInfo | Select-Object -First 1 + } + if (-not $selectedTenantInfo) { + throw "Could not reserve tenant for clean codeunit '$($workItem.Key)'." + } + $availableTenantInfo = @($availableTenantInfo | Where-Object { $_.Id -ne $selectedTenantInfo.Id }) + $verb = if ($state.retried.ContainsKey($workItem.Key)) { "Re-dispatching" } else { "Dispatching" } + + $batch += [PSCustomObject]@{ + WorkItem = $workItem + TenantInfo = $selectedTenantInfo + Verb = $verb + } + } + + # Finish every restore in the batch before any test starts. This keeps SQL backup/restore + # activity from overlapping page 130455 metadata enumeration and API cold starts. + foreach ($dispatch in $batch) { + Reset-BcTestTenant -ContainerName $Parameters.containerName -Tenant $dispatch.TenantInfo.Id ` + -TenantDatabaseName $dispatch.TenantInfo.DatabaseName -TemplateDatabaseName $TemplateDatabaseName + } + foreach ($dispatch in $batch) { + Start-RequiredDisabledDispatch -Parameters $Parameters -WorkItem $dispatch.WorkItem ` + -TenantInfo $dispatch.TenantInfo ` + -ScriptPath $ScriptPath -TestType $TestType -State $state -Verb $dispatch.Verb + } + $null = Wait-ForAllTestJobs -state $state + } + + return (-not $state.hasFailures) +} + <# .SYNOPSIS Records a finished job's outcome on the state object, queueing a rerun when one is warranted. @@ -442,10 +864,17 @@ function Register-TestJobOutcome { switch ($Result.Outcome) { 'Transient' { Write-Host "Transient platform race for '$($Result.AppName)' on '$($Result.Tenant)'. Queued for one retry." - $State.transient = @($State.transient) + @($Result.AppName) + $State.transient = @($State.transient) + @( + [PSCustomObject]@{ + Key = $Result.AppName + Tenant = $Result.Tenant + } + ) } 'Failed' { - $canRerun = ($State.rerunBudget -gt 0) -and + $supportsAppReruns = $null -ne $State.PSObject.Properties['rerunBudget'] + $canRerun = $supportsAppReruns -and + ($State.rerunBudget -gt 0) -and ($State.tenantCount -gt 1) -and (-not $State.rerunDone.ContainsKey($Result.AppName)) @@ -542,6 +971,28 @@ function Wait-ForFreeTenant { throw "Wait-ForFreeTenant: timed out after $timeoutSeconds seconds waiting for a free tenant. Running jobs: $($state.jobs | ForEach-Object { "$($_.appName) on $($_.tenant)" } | Out-String)" } +function Wait-ForSpecificTenant { + param( + $state, + $tenants, + [string]$tenant, + [int]$timeoutSeconds = 7200, + [int]$pollIntervalSeconds = 10 + ) + + $waited = 0 + while ($waited -lt $timeoutSeconds) { + $available = @(Get-FreeTenants -state $state -tenants $tenants) + if ($tenant -in $available) { + return $tenant + } + Start-Sleep -Seconds $pollIntervalSeconds + $waited += $pollIntervalSeconds + } + + throw "Wait-ForSpecificTenant: timed out after $timeoutSeconds seconds waiting for tenant '$tenant'." +} + <# .SYNOPSIS Starts a background job to run tests for a single app on a specific tenant. @@ -564,6 +1015,7 @@ function Start-TestJob { [string]$tenant, [string]$scriptPath, [string]$testType, + [switch]$skipAutomaticDisabledPass, [string]$fileSuffix ) @@ -587,15 +1039,19 @@ function Start-TestJob { $bchModule = Get-Module BcContainerHelper | Select-Object -First 1 $bchModulePath = if ($bchModule) { $bchModule.Path } else { "BcContainerHelper" } - return Start-Job -ScriptBlock { - param($params, $scriptPath, $testType, $bchPath) + $jobScript = { + param($params, $scriptPath, $testType, $bchPath, $skipDisabledPass) Import-Module $bchPath # Background jobs run a single app sequentially. Pass an empty $AppNamesToTest so the # shared script skips the parallel dispatch branch and falls through to running this # one app's tests directly. - $passed = . $scriptPath -parameters $params -TestType $testType -AppNamesToTest @() + $passed = . $scriptPath -parameters $params -TestType $testType -AppNamesToTest @() ` + -SkipAutomaticDisabledPass:$skipDisabledPass if (-not $passed) { throw "Test execution failed" } - } -ArgumentList $jobParams, $scriptPath, $testType, $bchModulePath + } + + return Start-Job -ScriptBlock $jobScript -ArgumentList $jobParams, $scriptPath, $testType, $bchModulePath, ` + $skipAutomaticDisabledPass.IsPresent } <# @@ -627,6 +1083,59 @@ function Wait-ForAllTestJobs { $state.jobs = @() } +function Add-MissingJUnitTestProperties { + param( + [Parameter(Mandatory=$true)] + [string]$ResultFile, + [array]$WorkItems = @() + ) + + if (-not (Test-Path $ResultFile) -or $WorkItems.Count -eq 0) { + return + } + + $workItemByCodeunitId = @{} + foreach ($workItem in $WorkItems) { + $workItemByCodeunitId[[string]$workItem.CodeunitId] = $workItem + } + + $xml = [xml](Get-Content $ResultFile -Raw) + $changed = $false + foreach ($suite in @($xml.testsuites.testsuite)) { + if ($suite.properties) { + continue + } + + $codeunitId = ([string]$suite.name -split ' ', 2)[0] + $workItem = $workItemByCodeunitId[$codeunitId] + if (-not $workItem) { + continue + } + + $properties = $xml.CreateElement("properties") + foreach ($propertyValue in @{ + extensionid = $workItem.AppId + appName = $workItem.AppName + }.GetEnumerator()) { + $property = $xml.CreateElement("property") + $property.SetAttribute("name", $propertyValue.Key) + $property.SetAttribute("value", [string]$propertyValue.Value) + $properties.AppendChild($property) | Out-Null + } + + if ($suite.FirstChild) { + $suite.InsertBefore($properties, $suite.FirstChild) | Out-Null + } else { + $suite.AppendChild($properties) | Out-Null + } + $changed = $true + } + + if ($changed) { + $xml.Save($ResultFile) + } +} + <# .SYNOPSIS Merges per-job test result files into the single file expected by Run-AlPipeline. @@ -642,6 +1151,7 @@ function Merge-TenantTestResults { param( [Hashtable]$parameters, [string[]]$tenants, + [array]$workItems = @(), [string[]]$rerunSuffixes = @() ) @@ -655,6 +1165,11 @@ function Merge-TenantTestResults { $ext = [System.IO.Path]::GetExtension($origFile) $tenantFiles = @($suffixes | ForEach-Object { Join-Path $dir "$name-$_$ext" }) + if ($resultKey -eq "JUnitResultFileName") { + foreach ($tenantFile in $tenantFiles) { + Add-MissingJUnitTestProperties -ResultFile $tenantFile -WorkItems $workItems + } + } Merge-TestResultFiles -targetFile $origFile -sourceFiles $tenantFiles # Clean up per-job files @@ -680,6 +1195,8 @@ function Merge-TenantTestResults { All available tenant ids. Warmup dispatches onto the first one. .PARAMETER State The parallel execution state object; mutated (jobs/hasFailures/transient) as the warmup runs. +.PARAMETER CleanTenantAppNames + Apps whose automatic Disabled-isolation pass must be deferred to clean-codeunit execution. .OUTPUTS [string[]] The remaining app names to dispatch (first app removed if it was warmed up). #> @@ -691,7 +1208,8 @@ function Invoke-WarmupDispatch { [Parameter(Mandatory=$true)][AllowEmptyCollection()][string[]]$Tenants, [Parameter(Mandatory=$true)][string]$ScriptPath, [string]$TestType, - [Parameter(Mandatory=$true)]$State + [Parameter(Mandatory=$true)]$State, + [string[]]$CleanTenantAppNames = @() ) # Only serialize when there is a fan-out to protect: >1 app AND >1 tenant. @@ -708,7 +1226,8 @@ function Invoke-WarmupDispatch { Write-Host "Warming up: dispatching first app '$warmupApp' on '$($Tenants[0])' alone and awaiting completion before parallel fan-out." Start-TestAppDispatch -Parameters $Parameters -AppName $warmupApp -AppId $warmupAppId -Tenant $Tenants[0] ` - -ScriptPath $ScriptPath -TestType $TestType -State $State -Verb 'Dispatching' + -ScriptPath $ScriptPath -TestType $TestType -State $State -Verb 'Dispatching' ` + -SkipAutomaticDisabledPass:($warmupApp -in $CleanTenantAppNames) # Await the single job so the process is warm before anything runs in parallel. A transient # failure here lands in $State.transient and the caller's loop re-queues it; a hard failure is @@ -790,8 +1309,8 @@ function Invoke-ParallelTestExecution { # GitHub Actions provides a per-job temp directory ($RUNNER_TEMP) that is cleaned up between # jobs, so a stale state file from a previous run cannot corrupt the current run. Fall back - # to $env:TEMP for local execution outside of CI. - $tempDir = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { $env:TEMP } + # to the local temp directory outside of CI. + $tempDir = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } elseif ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() } $stateFile = Join-Path $tempDir "parallelTests_$($parameters.containerName).json" # Short-circuit ONLY when a previous call ran to completion (wait+merge done). The @@ -808,7 +1327,8 @@ function Invoke-ParallelTestExecution { } } - $tenants = @(Get-AvailableBcTenants -containerName $parameters.containerName) + $tenantInfo = @(Get-AvailableBcTenantInfo -containerName $parameters.containerName) + $tenants = @($tenantInfo | ForEach-Object { $_.Id }) Write-Host "Available tenants: $($tenants -join ', ')" # Build a name -> appId map so we can set extensionId per dispatch. Run-TestsInBcContainer @@ -820,15 +1340,84 @@ function Invoke-ParallelTestExecution { Where-Object { $_.IsInstalled } | ForEach-Object { $appIdByName[$_.Name] = $_.AppId } + $cleanTenantInfo = @( + $tenantInfo | + Where-Object { $_.Id -ne $parameters.tenant } + ) + if ($testType -ne 'Legacy' -and $tenantInfo.Count -gt 1) { + $sourceTenantInfo = @($tenantInfo | Where-Object { $_.Id -eq $parameters.tenant }) + if ($sourceTenantInfo.Count -ne 1 -or [string]::IsNullOrWhiteSpace($sourceTenantInfo[0].DatabaseName)) { + throw "Could not determine the database name for source tenant '$($parameters.tenant)'." + } + if (@($tenantInfo | Where-Object { + [string]::IsNullOrWhiteSpace($_.Id) -or [string]::IsNullOrWhiteSpace($_.DatabaseName) + }).Count -gt 0 -or + @($tenantInfo.Id | Sort-Object -Unique).Count -ne $tenantInfo.Count -or + @($tenantInfo.DatabaseName | Sort-Object -Unique).Count -ne $tenantInfo.Count) { + throw "Discovery requires non-empty, unique tenant IDs and database names." + } + + # Discovery executes OnRun triggers. Keep the primary fixture untouched and restore + # the discovery worker before any template copy or test dispatch can observe its writes. + $discoveryTenant = $cleanTenantInfo[0] + $discoveryParameters = $parameters.Clone() + $discoveryParameters["tenant"] = $discoveryTenant.Id + try { + $requiredDisabledWorkItems = @( + Get-RequiredDisabledWorkItems -Parameters $discoveryParameters -TestType $testType ` + -AppNamesToTest $appNamesToTest -AppIdByName $appIdByName + ) + } + finally { + Reset-BcTestTenant -ContainerName $parameters.containerName -Tenant $discoveryTenant.Id ` + -TenantDatabaseName $discoveryTenant.DatabaseName -TemplateDatabaseName $sourceTenantInfo[0].DatabaseName + } + } else { + $requiredDisabledWorkItems = @( + Get-RequiredDisabledWorkItems -Parameters $parameters -TestType $testType ` + -AppNamesToTest $appNamesToTest -AppIdByName $appIdByName + ) + } + $templateDatabaseName = "" + try { + if ($requiredDisabledWorkItems.Count -gt 0) { + if ($cleanTenantInfo.Count -eq 0) { + throw "Clean RequiredTestIsolation=Disabled execution requires at least one secondary tenant." + } + Write-Host "Preparing clean-tenant execution for $($requiredDisabledWorkItems.Count) RequiredTestIsolation=Disabled codeunit(s)." + $sourceTenantInfo = @($tenantInfo | Where-Object { $_.Id -eq $parameters.tenant }) | Select-Object -First 1 + if (-not $sourceTenantInfo -or [string]::IsNullOrEmpty($sourceTenantInfo.DatabaseName)) { + throw "Could not determine the database name for source tenant '$($parameters.tenant)'." + } + $templateDatabaseName = New-BcTestTenantTemplate -ContainerName $parameters.containerName -SourceDatabaseName $sourceTenantInfo.DatabaseName + } + # dispatched=true marks "we started the foreach" - lets concurrent reads notice an in-flight # run. completed=false stays false until wait+merge finish; only then is finalResult valid. $state = [PSCustomObject]@{ jobs = @(); dispatched = $true; completed = $false; finalResult = $false; hasFailures = $false - transient = @(); retried = @{} + transient = @(); retried = @{}; retryTenant = @{} rerun = @(); rerunDone = @{}; rerunBudget = (Get-AppRerunBudget); tenantCount = $tenants.Count } $state | ConvertTo-Json -Depth 5 | Set-Content $stateFile -Force + if ($requiredDisabledWorkItems.Count -gt 0) { + try { + $requiredDisabledPassed = Invoke-RequiredDisabledTestExecution -Parameters $parameters ` + -WorkItems $requiredDisabledWorkItems -TenantInfo $cleanTenantInfo ` + -TemplateDatabaseName $templateDatabaseName -ScriptPath $scriptPath -TestType $testType + if (-not $requiredDisabledPassed) { + $state.hasFailures = $true + } + } + finally { + foreach ($cleanTenant in $cleanTenantInfo) { + Reset-BcTestTenant -ContainerName $parameters.containerName -Tenant $cleanTenant.Id ` + -TenantDatabaseName $cleanTenant.DatabaseName -TemplateDatabaseName $templateDatabaseName + } + } + } + # Single dispatch loop, FIFO. TestConfiguration.json lists the smallest app first (a cheap # serial warmup) and the rest longest-first (LPT, keeps the tail short). The retry cap lives in # Receive-TestJobResult: an app already in $state.retried is classified as Failed on a re-fail. @@ -837,7 +1426,8 @@ function Invoke-ParallelTestExecution { # Run the first app alone and await it to warm the container before parallelizing the rest. # No-op for single-app/single-tenant. $pending = @(Invoke-WarmupDispatch -Parameters $parameters -Pending $pending -AppIdByName $appIdByName ` - -Tenants $tenants -ScriptPath $scriptPath -TestType $testType -State $state) + -Tenants $tenants -ScriptPath $scriptPath -TestType $testType -State $state ` + -CleanTenantAppNames $appNamesToTest) $rerunSuffixes = @() @@ -850,9 +1440,12 @@ function Invoke-ParallelTestExecution { if ($state.transient.Count -gt 0) { $toRetry = @($state.transient) $state.transient = @() - Write-Host "Re-queueing $($toRetry.Count) app(s) after transient platform race: $($toRetry -join ', ')" - foreach ($appName in $toRetry) { + $retryAppNames = @($toRetry | ForEach-Object { $_.Key }) + Write-Host "Re-queueing $($toRetry.Count) app(s) after transient platform race: $($retryAppNames -join ', ')" + foreach ($transient in $toRetry) { + $appName = $transient.Key $state.retried[$appName] = $true + $state.retryTenant[$appName] = $transient.Tenant # A transient retry goes out through the normal queue and so writes to a TENANT # result file. Tenant files are merged before rerun files, so any rerun file this # app already produced would overwrite the newer result - drop it. @@ -860,7 +1453,7 @@ function Invoke-ParallelTestExecution { Remove-RerunResultFile -parameters $parameters -suffix $state.rerunDone[$appName] } } - $pending = @($toRetry) + @($pending) + $pending = @($retryAppNames) + @($pending) } # Reruns take priority over the normal queue, for the same tail-latency reason as above: @@ -875,7 +1468,8 @@ function Invoke-ParallelTestExecution { $tenant = Wait-ForFreeTenant -state $state -tenants $tenants -excludeTenant $rerunItem.excludeTenant Start-TestAppDispatch -Parameters $parameters -AppName $rerunItem.appName -AppId $appIdByName[$rerunItem.appName] ` -Tenant $tenant -ScriptPath $scriptPath -TestType $testType -State $state ` - -Verb 'Re-running' -FileSuffix $rerunItem.suffix + -Verb 'Re-running' -FileSuffix $rerunItem.suffix ` + -SkipAutomaticDisabledPass continue } @@ -890,9 +1484,14 @@ function Invoke-ParallelTestExecution { } $verb = if ($state.retried.ContainsKey($appName)) { 'Re-dispatching' } else { 'Dispatching' } - $tenant = Wait-ForFreeTenant -state $state -tenants $tenants + $tenant = if ($state.retryTenant.ContainsKey($appName)) { + Wait-ForSpecificTenant -state $state -tenants $tenants -tenant $state.retryTenant[$appName] + } else { + Wait-ForFreeTenant -state $state -tenants $tenants + } Start-TestAppDispatch -Parameters $parameters -AppName $appName -AppId $appId -Tenant $tenant ` - -ScriptPath $scriptPath -TestType $testType -State $state -Verb $verb + -ScriptPath $scriptPath -TestType $testType -State $state ` + -SkipAutomaticDisabledPass -Verb $verb } else { # Nothing left to dispatch; drain any still-running jobs. New transient failures and # reruns discovered here are picked up at the top of the next loop iteration. @@ -902,17 +1501,25 @@ function Invoke-ParallelTestExecution { } } - $allPassed = -not $state.hasFailures + $allPassed = -not $state.hasFailures - Merge-TenantTestResults -parameters $parameters -tenants $tenants -rerunSuffixes $rerunSuffixes + Merge-TenantTestResults -parameters $parameters -tenants $tenants ` + -workItems $requiredDisabledWorkItems -rerunSuffixes $rerunSuffixes - # Persist final result and mark complete so subsequent override invocations short-circuit - # to this value (and not the placeholder we wrote before dispatch). - $state.finalResult = $allPassed - $state.completed = $true - $state | ConvertTo-Json -Depth 5 | Set-Content $stateFile -Force + # Persist final result and mark complete so subsequent override invocations short-circuit + # to this value (and not the placeholder we wrote before dispatch). + $state.finalResult = $allPassed + $state.completed = $true + $state | ConvertTo-Json -Depth 5 | Set-Content $stateFile -Force - return $allPassed + return $allPassed + } + finally { + if ($templateDatabaseName) { + Remove-BcTestTenantTemplate -ContainerName $parameters.containerName ` + -TemplateDatabaseName $templateDatabaseName + } + } } <# @@ -957,4 +1564,4 @@ function Invoke-PerProjectTestRun { return (. $script -parameters $parameters -TestType $testType -AppNamesToTest $appNamesToTest) } -Export-ModuleMember -Function Invoke-ParallelTestExecution, Get-AvailableBcTenants, Get-CachedTestRunResult, Get-InstalledTestAppNames, Get-AppNamesForBucket, Invoke-PerProjectTestRun, Get-AppNameFromMetadata, Invoke-WarmupDispatch, Merge-TestResultFiles, Get-AppRerunBudget +Export-ModuleMember -Function Invoke-ParallelTestExecution, Get-AvailableBcTenants, Get-CachedTestRunResult, Get-InstalledTestAppNames, Get-AppNamesForBucket, Invoke-PerProjectTestRun, Get-AppNameFromMetadata, Get-DisabledTestsForApp, Reset-BcTestTenant, Invoke-WarmupDispatch, Merge-TestResultFiles, Get-AppRerunBudget, Test-TransientTestFailure diff --git a/build/scripts/Remove-ApiTestPassword.ps1 b/build/scripts/Remove-ApiTestPassword.ps1 new file mode 100644 index 00000000000..72da4aee86c --- /dev/null +++ b/build/scripts/Remove-ApiTestPassword.ps1 @@ -0,0 +1,35 @@ +Param( + [Parameter(Mandatory = $true)] + [ValidatePattern('^[a-zA-Z0-9][a-zA-Z0-9_.-]*$')] + [string]$ContainerName, + [Parameter(Mandatory = $true)] + [string]$FilePath +) + +$ErrorActionPreference = 'Stop' + +if ((Split-Path -Path $FilePath -Leaf) -ne 'ApiTestPassword') { + throw 'API test credential cleanup requires the exact ApiTestPassword file.' +} +if (-not (Test-Path -LiteralPath $FilePath -PathType Leaf)) { + return +} + +# Workflow cancellation can leave server-side test workers alive. End all consumers +# before deleting their shared credential, including when the build step was killed. +$containerIds = @(docker container ls --all --filter "name=^/$([regex]::Escape($ContainerName))$" --format '{{.ID}}') +if ($LASTEXITCODE -ne 0) { + throw 'Could not determine whether the API test container still exists.' +} +if ($containerIds.Count -gt 1) { + throw 'API test credential cleanup matched more than one container.' +} +if ($containerIds.Count -eq 1) { + docker stop --time 30 $containerIds[0] | Out-Null + if ($LASTEXITCODE -ne 0) { + throw 'Could not stop API test consumers; leaving credential removal to container teardown.' + } +} + +# The recorded mount path remains usable for stopped or already-removed containers. +Remove-Item -LiteralPath $FilePath -Force -ErrorAction Stop diff --git a/build/scripts/RunTestsInBcContainer.ps1 b/build/scripts/RunTestsInBcContainer.ps1 index 5ffd8c957bc..51067229314 100644 --- a/build/scripts/RunTestsInBcContainer.ps1 +++ b/build/scripts/RunTestsInBcContainer.ps1 @@ -5,40 +5,14 @@ Param( # Names of test apps to dispatch in parallel across tenants. Only set by the per-project # override on the parent invocation. Empty when called from inside a background job, which # forces the sequential single-app path further down. - [string[]] $AppNamesToTest = @() + [string[]] $AppNamesToTest = @(), + [switch] $SkipAutomaticDisabledPass ) Import-Module $PSScriptRoot\EnlistmentHelperFunctions.psm1 +Import-Module $PSScriptRoot\ParallelTestExecution.psm1 Import-Module $PSScriptRoot\TestTolerance\TestTolerance.psm1 -Force -function Get-DisabledTests -{ - param( - [string] $AppName - ) - - $baseFolder = Get-BaseFolder - - # Convert app name to folder name format (replace spaces with underscores) - $appFolderName = $AppName -replace ' ', '_' - - $disabledTests = @() - - # Look for DisabledTests folders and find the app-specific subfolder - $disabledTestsFolders = Get-ChildItem -Path $baseFolder -Filter "DisabledTests" -Recurse -Directory - foreach ($disabledTestsFolder in $disabledTestsFolders) { - $appFolder = Join-Path $disabledTestsFolder.FullName $appFolderName - if (Test-Path $appFolder) { - $jsonFiles = Get-ChildItem -Path $appFolder -Filter "*.json" - foreach ($jsonFile in $jsonFiles) { - $disabledTests += (Get-Content -Raw -Path $jsonFile.FullName | ConvertFrom-Json) - } - } - } - - return @($disabledTests) -} - <# .SYNOPSIS Runs Run-TestsInBcContainer and detects silent test truncation caused by BCH ERROR DIALOG events. @@ -84,15 +58,21 @@ function Invoke-RunTestsWithCancellationDetection { } $bchCancelled = $false + $transientPlatformRace = $false if ($transcriptStarted -and (Test-Path $transcriptFile)) { - if (Select-String -Path $transcriptFile -Pattern 'database command was cancelled|ERROR DIALOG' -Quiet) { + $transcriptContent = Get-Content -Path $transcriptFile -Raw + if ($transcriptContent -match 'database command was cancelled|ERROR DIALOG') { Write-Host "::warning::BCH client cancellation detected for app '$($parameters['appName'])' on tenant '$($parameters['tenant'])'. Tests were silently truncated - subsequent codeunits did not run." $bchCancelled = $true } + if (Test-TransientTestFailure -Output $transcriptContent) { + Write-Host "::warning::TRANSIENT TEST PLATFORM RACE detected for app '$($parameters['appName'])' on tenant '$($parameters['tenant'])'." + $transientPlatformRace = $true + } } Remove-Item $transcriptFile -Force -ErrorAction SilentlyContinue - return ($bchPassed -and -not $bchCancelled) + return ($bchPassed -and -not $bchCancelled -and -not $transientPlatformRace) } function Invoke-TestsWithReruns { @@ -126,7 +106,7 @@ if (($null -ne $TestType) -and ($TestType -ne "Legacy")) { $parameters["testType"] = $TestType } -$parameters["disabledTests"] = @(Get-DisabledTests -AppName $parameters["appName"]) # Add disabled tests to parameters +$parameters["disabledTests"] = @(Get-DisabledTestsForApp -AppName $parameters["appName"]) $parameters["renewClientContextBetweenTests"] = $true # When invoked from the per-project override on the parent process, $AppNamesToTest contains @@ -135,17 +115,18 @@ $parameters["renewClientContextBetweenTests"] = $true # circuit. When invoked from inside a background job (Start-TestJob), $AppNamesToTest is empty # and we fall through to the sequential single-app path below. if ($AppNamesToTest.Count -gt 0) { - Import-Module $PSScriptRoot\ParallelTestExecution.psm1 return Invoke-ParallelTestExecution -parameters $parameters -scriptPath $PSCommandPath -testType $TestType -appNamesToTest $AppNamesToTest } +$isRequiredDisabledRun = $parameters.ContainsKey("requiredTestIsolation") -and $parameters["requiredTestIsolation"] -eq "Disabled" # A failing app is retried once by the parallel dispatcher, on a different tenant (see # ParallelTestExecution.psm1). Retrying in place here would reuse the tenant the app just dirtied, # so the same residue could re-trigger the failure - hence a single attempt per dispatch. $result = Invoke-TestsWithReruns -parameters $parameters -maxAttempts 1 -# For UnitTests, also run with DisableTestIsolation on the same tenant -if ($TestType -eq "UnitTest") { +# Preserve the old sequential fallback. Parallel project execution defers this pass to the +# clean-tenant codeunit scheduler in ParallelTestExecution.psm1. +if ($TestType -eq "UnitTest" -and -not $SkipAutomaticDisabledPass -and -not $isRequiredDisabledRun) { Write-Host "Running DisableTestIsolation pass for UnitTest" $parameters["requiredTestIsolation"] = "Disabled" $parameters["testRunnerCodeunitId"] = "130451" diff --git a/build/scripts/tests/ApiTestCredential.Test.ps1 b/build/scripts/tests/ApiTestCredential.Test.ps1 new file mode 100644 index 00000000000..548c38fc562 --- /dev/null +++ b/build/scripts/tests/ApiTestCredential.Test.ps1 @@ -0,0 +1,239 @@ +Describe 'API test credential materialization' { + BeforeAll { + Import-Module (Join-Path $PSScriptRoot '..\ApiTestCredential.psm1') -Force + $script:previousGitHubEnv = $env:GITHUB_ENV + $script:previousExitCode = Get-Variable LASTEXITCODE -Scope Global -ValueOnly -ErrorAction SilentlyContinue + $script:createdMountStub = -not (Get-Command Get-BcContainerSharedFolders -ListImported -ErrorAction SilentlyContinue) + if ($script:createdMountStub) { + function global:Get-BcContainerSharedFolders { + param([string]$containerName) + $null = $containerName + throw 'Container mount lookup must be mocked.' + } + } + $script:createdDockerStub = -not (Get-Command docker -ErrorAction SilentlyContinue) + if ($script:createdDockerStub) { + function global:docker { + param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Arguments) + $null = $Arguments + throw 'Docker must be mocked.' + } + } + $script:fixturePassword = [System.Security.SecureString]::new() + foreach ($character in 'synthetic-fixture-only'.ToCharArray()) { + $script:fixturePassword.AppendChar($character) + } + $script:fixturePassword.MakeReadOnly() + $script:credential = [PSCredential]::new('unit-test', $script:fixturePassword) + $script:mount = Join-Path $PSScriptRoot 'unused-mount' + } + + AfterAll { + $script:fixturePassword.Dispose() + $env:GITHUB_ENV = $script:previousGitHubEnv + $global:LASTEXITCODE = $script:previousExitCode + if ($script:createdMountStub) { Remove-Item function:global:Get-BcContainerSharedFolders } + if ($script:createdDockerStub) { Remove-Item function:global:docker } + } + + BeforeEach { + $env:GITHUB_ENV = Join-Path $PSScriptRoot 'unused-github-env' + $script:events = [System.Collections.Generic.List[string]]::new() + $script:stream = [PSCustomObject]@{ + Events = $script:events + Buffer = $null + Written = $null + FailWrite = $false + FailDispose = $false + } + $script:stream | Add-Member ScriptMethod Write { + param($buffer, $offset, $count) + $this.Events.Add('write') + $this.Buffer = $buffer + $this.Written = [System.Text.Encoding]::UTF8.GetString($buffer, $offset, $count) + if ($this.FailWrite) { throw 'Synthetic write failure' } + } + $script:stream | Add-Member ScriptMethod Dispose { + $this.Events.Add('dispose') + if ($this.FailDispose) { throw 'Synthetic flush failure' } + } + Mock -ModuleName ApiTestCredential Get-BcContainerSharedFolders { @{ $script:mount = 'c:\run\my\' } } + Mock -ModuleName ApiTestCredential Add-Content { $script:events.Add('register') } + Mock -ModuleName ApiTestCredential New-ApiTestPasswordFileStream { + $script:events.Add('create') + $script:stream + } + Mock -ModuleName ApiTestCredential Test-Path { $true } + Mock -ModuleName ApiTestCredential docker { + $global:LASTEXITCODE = 0 + if ($args[0] -eq 'container') { 'synthetic-container-id' } + else { $script:events.Add('stop') } + } + Mock -ModuleName ApiTestCredential Remove-Item { $script:events.Add('delete') } + } + + It 'registers cleanup and creates the secured backing file before the first secret write' { + Write-ApiTestPassword -ContainerName 'unit-test' -Credential $script:credential + + ($script:events -join ',') | Should -Be 'register,create,write,dispose' + $script:stream.Written | Should -Be 'synthetic-fixture-only' + @($script:stream.Buffer | Where-Object { $_ -ne 0 }).Count | Should -Be 0 + Should -Invoke -ModuleName ApiTestCredential New-ApiTestPasswordFileStream -Times 1 -Exactly -ParameterFilter { + $FilePath -eq (Join-Path $script:mount 'ApiTestPassword') + } + Should -Invoke -ModuleName ApiTestCredential Add-Content -Times 1 -Exactly -ParameterFilter { + $LiteralPath -eq $env:GITHUB_ENV -and $ErrorAction -eq 'Stop' -and + $Value -eq "BCAppsApiTestPasswordPath=$(Join-Path $script:mount 'ApiTestPassword')" + } + Should -Invoke -ModuleName ApiTestCredential docker -Times 0 + } + + It 'uses the same secured mount locally without workflow registration' { + $env:GITHUB_ENV = '' + Write-ApiTestPassword -ContainerName 'unit-test' -Credential $script:credential + + ($script:events -join ',') | Should -Be 'create,write,dispose' + Should -Invoke -ModuleName ApiTestCredential Get-BcContainerSharedFolders -Times 1 -Exactly + Should -Invoke -ModuleName ApiTestCredential Add-Content -Times 0 + } + + It 'preserves Unicode passwords while clearing the written byte buffer' { + $characters = [char[]]@(0x0061, 0x00E9, 0xD83D, 0xDD10) + $password = [System.Security.SecureString]::new() + try { + foreach ($character in $characters) { $password.AppendChar($character) } + $password.MakeReadOnly() + $credential = [PSCredential]::new('unit-test', $password) + + Write-ApiTestPassword -ContainerName 'unit-test' -Credential $credential + + $script:stream.Written | Should -Be (-join $characters) + @($script:stream.Buffer | Where-Object { $_ -ne 0 }).Count | Should -Be 0 + } + finally { + $password.Dispose() + } + } + + It 'does not materialize an immutable plaintext password string in the writer' { + $source = Get-Content (Join-Path $PSScriptRoot '..\ApiTestCredential.psm1') -Raw + $source | Should -Not -Match 'GetNetworkCredential|PtrToString' + $source | Should -Match 'SecureStringToBSTR' + $source | Should -Match 'ZeroFreeBSTR' + } + + It 'fails closed on an unresolved mount in CI and locally' { + Mock -ModuleName ApiTestCredential Get-BcContainerSharedFolders { @{} } + foreach ($environmentPath in @('unused-github-env', '')) { + $env:GITHUB_ENV = $environmentPath + { Write-ApiTestPassword -ContainerName 'unit-test' -Credential $script:credential } | Should -Throw '*Cannot resolve*' + } + Should -Invoke -ModuleName ApiTestCredential New-ApiTestPasswordFileStream -Times 0 + Should -Invoke -ModuleName ApiTestCredential Add-Content -Times 0 + } + + It 'fails closed on ambiguous mounts' { + Mock -ModuleName ApiTestCredential Get-BcContainerSharedFolders { @{ 'C:\first' = 'C:\Run\my'; 'C:\second' = 'c:\run\my' } } + { Write-ApiTestPassword -ContainerName 'unit-test' -Credential $script:credential } | Should -Throw '*Cannot resolve*' + Should -Invoke -ModuleName ApiTestCredential New-ApiTestPasswordFileStream -Times 0 + } + + It 'does not materialize credentials if workflow cleanup registration fails' { + Mock -ModuleName ApiTestCredential Add-Content { throw 'Synthetic registration failure' } + { Write-ApiTestPassword -ContainerName 'unit-test' -Credential $script:credential } | Should -Throw '*Synthetic registration failure*' + Should -Invoke -ModuleName ApiTestCredential New-ApiTestPasswordFileStream -Times 0 + } + + It 'does not write or delete an existing file when secured creation fails' { + Mock -ModuleName ApiTestCredential New-ApiTestPasswordFileStream { throw 'Synthetic ACL or create failure' } + { Write-ApiTestPassword -ContainerName 'unit-test' -Credential $script:credential } | Should -Throw '*Synthetic ACL or create failure*' + ($script:events -join ',') | Should -Be 'register' + Should -Invoke -ModuleName ApiTestCredential Remove-Item -Times 0 + } + + It 'closes the failed writer and stops consumers before deleting a partial file' { + $script:stream.FailWrite = $true + { Write-ApiTestPassword -ContainerName 'unit-test' -Credential $script:credential } | Should -Throw '*Synthetic write failure*' + + ($script:events -join ',') | Should -Be 'register,create,write,dispose,stop,delete' + @($script:stream.Buffer | Where-Object { $_ -ne 0 }).Count | Should -Be 0 + } + + It 'also cleans up after a flush failure without reporting success' { + $script:stream.FailDispose = $true + { Write-ApiTestPassword -ContainerName 'unit-test' -Credential $script:credential } | Should -Throw '*Synthetic flush failure*' + ($script:events -join ',') | Should -Be 'register,create,write,dispose,stop,delete' + } + + It 'preserves the setup error and credential when consumers cannot be stopped' { + $script:stream.FailWrite = $true + Mock -ModuleName ApiTestCredential docker { $global:LASTEXITCODE = 1 } -ParameterFilter { $args[0] -eq 'stop' } + Mock -ModuleName ApiTestCredential Write-Warning {} + + { Write-ApiTestPassword -ContainerName 'unit-test' -Credential $script:credential } | Should -Throw '*Synthetic write failure*' + Should -Invoke -ModuleName ApiTestCredential Remove-Item -Times 0 + Should -Invoke -ModuleName ApiTestCredential Write-Warning -Times 1 -Exactly + } +} + +Describe 'API test credential atomic Windows ACL' { + BeforeAll { + Import-Module (Join-Path $PSScriptRoot '..\ApiTestCredential.psm1') -Force + $script:aclRoot = Join-Path $PSScriptRoot ("acl-fixture-" + [guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $script:aclRoot | Out-Null + } + + AfterAll { + Remove-Item -LiteralPath $script:aclRoot -Recurse -Force + } + + It 'has only required explicit SIDs on an empty file before writing synthetic bytes' { + $path = Join-Path $script:aclRoot 'ApiTestPassword' + $stream = $null + try { + $stream = & (Get-Module ApiTestCredential) { param($path) New-ApiTestPasswordFileStream -FilePath $path } $path + $stream.Length | Should -Be 0 + $acl = Get-Acl -LiteralPath $path + $acl.AreAccessRulesProtected | Should -BeTrue + $rules = @($acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) + @($rules | Where-Object IsInherited).Count | Should -Be 0 + $writerSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value + $expectedSids = @('S-1-5-18', 'S-1-5-20', 'S-1-5-32-544', $writerSid) | Select-Object -Unique + ($rules.IdentityReference.Value | Sort-Object) | Should -Be ($expectedSids | Sort-Object) + foreach ($sid in @('S-1-5-18', 'S-1-5-32-544')) { + ($rules | Where-Object { $_.IdentityReference.Value -eq $sid }).FileSystemRights | + Should -Be ([System.Security.AccessControl.FileSystemRights]::FullControl) + } + $networkServiceRights = [System.Security.AccessControl.FileSystemRights]'Read, Synchronize' + if ($writerSid -eq 'S-1-5-20') { + $networkServiceRights = $networkServiceRights -bor [System.Security.AccessControl.FileSystemRights]'Write, Delete' + } + ($rules | Where-Object { $_.IdentityReference.Value -eq 'S-1-5-20' }).FileSystemRights | + Should -Be $networkServiceRights + if ($writerSid -notin @('S-1-5-18', 'S-1-5-20', 'S-1-5-32-544')) { + ($rules | Where-Object { $_.IdentityReference.Value -eq $writerSid }).FileSystemRights | + Should -Be ([System.Security.AccessControl.FileSystemRights]'Write, Delete, Synchronize') + } + $stream.WriteByte(65) + $stream.Length | Should -Be 1 + } + finally { + if ($stream) { $stream.Dispose() } + Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue + } + } + + It 'refuses to overwrite an existing file or change its ACL' { + $path = Join-Path $script:aclRoot 'ApiTestPassword' + [System.IO.File]::WriteAllText($path, 'synthetic-existing-fixture') + $before = (Get-Acl -LiteralPath $path).Sddl + try { + { & (Get-Module ApiTestCredential) { param($path) New-ApiTestPasswordFileStream -FilePath $path } $path } | Should -Throw + [System.IO.File]::ReadAllText($path) | Should -Be 'synthetic-existing-fixture' + (Get-Acl -LiteralPath $path).Sddl | Should -Be $before + } + finally { + Remove-Item -LiteralPath $path -Force + } + } +} diff --git a/build/scripts/tests/ParallelTestExecution.Test.ps1 b/build/scripts/tests/ParallelTestExecution.Test.ps1 index 84c7d0e83ba..ce3f2482a3a 100644 --- a/build/scripts/tests/ParallelTestExecution.Test.ps1 +++ b/build/scripts/tests/ParallelTestExecution.Test.ps1 @@ -1,5 +1,33 @@ $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 +if (-not (Get-Command Get-TestsFromBcContainer -ErrorAction SilentlyContinue)) { + function global:Get-TestsFromBcContainer { + param( + [string]$containerName, + [string]$tenant, + [string]$extensionId, + [string]$requiredTestIsolation, + [string]$testType, + [array]$disabledTests + ) + $null = $containerName, $tenant, $extensionId, $requiredTestIsolation, $testType, $disabledTests + throw "Get-TestsFromBcContainer stub should never be called; a Pester mock must intercept it." + } +} + +if (-not (Get-Command Invoke-ScriptInBcContainer -ErrorAction SilentlyContinue)) { + function global:Invoke-ScriptInBcContainer { + param( + [string]$containerName, + [scriptblock]$scriptblock, + [object[]]$argumentList, + [bool]$useSession + ) + $null = $containerName, $scriptblock, $argumentList, $useSession + throw "Invoke-ScriptInBcContainer stub should never be called; a Pester mock must intercept it." + } +} + Import-Module (Join-Path $PSScriptRoot '../ParallelTestExecution.psm1') -Force Describe "ParallelTestExecution app-name resolution" { @@ -97,6 +125,122 @@ Describe "ParallelTestExecution app-name resolution" { $result | Should -Not -Contain 'Projects-Json-Key' } } + + It "uses docker exec for the long-running template database copy" { + InModuleScope ParallelTestExecution { + Mock Invoke-ScriptInBcContainer { 'default-test-template' } + + New-BcTestTenantTemplate -ContainerName 'bc' -SourceDatabaseName 'default' | Should -Be 'default-test-template' + + Should -Invoke Invoke-ScriptInBcContainer -Times 1 -ParameterFilter { + $containerName -eq 'bc' -and $useSession -eq $false + } + } + } + + It "uses docker exec for long-running tenant database refreshes" { + InModuleScope ParallelTestExecution { + Mock Invoke-ScriptInBcContainer { } + + Reset-BcTestTenant -ContainerName 'bc' -Tenant 'tenant2' ` + -TenantDatabaseName 'tenant2' -TemplateDatabaseName 'default-test-template' + + Should -Invoke Invoke-ScriptInBcContainer -Times 1 -ParameterFilter { + $containerName -eq 'bc' -and $useSession -eq $false + } + } + } + + It "removes the clean template when normal app dispatch aborts" { + InModuleScope ParallelTestExecution { + Mock Get-AvailableBcTenantInfo { + @( + [PSCustomObject]@{ Id = 'default'; DatabaseName = 'default' } + [PSCustomObject]@{ Id = 'tenant2'; DatabaseName = 'tenant2' } + ) + } + Mock Get-BcContainerAppInfo { + @([PSCustomObject]@{ IsInstalled = $true; Name = 'Tests'; AppId = 'tests-id' }) + } + Mock Get-RequiredDisabledWorkItems { + @([PSCustomObject]@{ + Key = 'Tests::500' + AppName = 'Tests' + AppId = 'tests-id' + CodeunitId = '500' + CodeunitName = 'API E2E' + TestCount = 2 + }) + } + Mock New-BcTestTenantTemplate { 'default-test-template' } + Mock Invoke-RequiredDisabledTestExecution { $true } + Mock Reset-BcTestTenant { } + Mock Invoke-WarmupDispatch { @($Pending) } + Mock Wait-ForFreeTenant { 'default' } + Mock Start-TestAppDispatch { throw 'dispatch failed' } + Mock Remove-BcTestTenantTemplate { } + + { + Invoke-ParallelTestExecution -parameters @{ + containerName = "ut-$([guid]::NewGuid().ToString('N'))" + tenant = 'default' + } -scriptPath 'unused.ps1' -testType 'IntegrationTest' -appNamesToTest @('Tests') + } | Should -Throw '*dispatch failed*' + + Should -Invoke Remove-BcTestTenantTemplate -Times 1 -ParameterFilter { + $TemplateDatabaseName -eq 'default-test-template' + } + } + } +} + +Describe "ParallelTestExecution background-task profile" { + BeforeAll { + $script:profileRepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path + $script:repoSettings = Get-Content (Join-Path $script:profileRepoRoot '.github\AL-Go-Settings.json') -Raw | + ConvertFrom-Json + } + + It "opts only UncategorizedTests into background task execution" { + $script:repoSettings.PSObject.Properties.Name | Should -Not -Contain 'enableTaskScheduler' + $rules = @($script:repoSettings.conditionalSettings | Where-Object { + $_.settings.PSObject.Properties.Name -contains 'enableTaskScheduler' + }) + $rules.Count | Should -Be 1 + @($rules[0].buildModes).Count | Should -Be 1 + $rules[0].buildModes[0] | Should -Be 'UncategorizedTests' + $rules[0].settings.enableTaskScheduler | Should -BeTrue + $rules[0].PSObject.Properties.Name | Should -Not -Contain 'projects' + } + + It "retains the profile for every country test project without scheduler overrides" { + $projects = @(Get-ChildItem (Join-Path $script:profileRepoRoot 'build\projects') -Directory -Filter 'Test Apps *') + $projects.Count | Should -BeGreaterThan 0 + foreach ($project in $projects) { + $settings = Get-Content (Join-Path $project.FullName '.AL-Go\settings.json') -Raw | ConvertFrom-Json + $settings.PSObject.Properties.Name | Should -Not -Contain 'enableTaskScheduler' + $profiles = @($settings.ConditionalSettings | Where-Object { $_.buildModes -contains 'UncategorizedTests' }) + $profiles.Count | Should -Be @($settings.buildModes | Where-Object { $_ -eq 'UncategorizedTests' }).Count + foreach ($testProfile in $profiles) { + $testProfile.settings.testType | Should -Be 'Uncategorized' + } + foreach ($rule in $settings.ConditionalSettings) { + $rule.settings.PSObject.Properties.Name | Should -Not -Contain 'enableTaskScheduler' + } + } + } + + It "runs APIV RapidStart polling tests in the background-enabled profile" -ForEach @( + @{ Version = '1' } + @{ Version = '2' } + ) { + $path = Join-Path $script:profileRepoRoot "src\Apps\W1\APIV$Version\test\src\APIV${Version}AutomationRSPackage.Codeunit.al" + $source = Get-Content $path -Raw + $source | Should -Match 'TestType\s*=\s*Uncategorized\s*;' + $source | Should -Match 'procedure TestImportRSPackage\(' + $source | Should -Match 'procedure TestImportWrongRSPackage\(' + $source | Should -Match 'procedure TestApplyRSPackage\(' + } } Describe "ParallelTestExecution transient retry scheduling" { @@ -104,6 +248,58 @@ Describe "ParallelTestExecution transient retry scheduling" { Import-Module (Join-Path $PSScriptRoot '../ParallelTestExecution.psm1') -Force } + It "classifies page 130455 metadata-resolution runtime failures as transient" { + InModuleScope ParallelTestExecution { + @( + "ObjName:Command Line Test Tool, ObjID:130455, Type:Form, MethodName:ExtensionId_a45_OnValidate`nOffset and length were out of bounds for the array" + "ObjName:Command Line Test Tool, ObjID:130455, Type:Form, MethodName:ExtensionId_a45_OnValidate`nNullable object must have a value." + "TRANSIENT TEST PLATFORM RACE detected for app 'Tests' on tenant 'tenant2'." + "Cannot open page 130455" + "InvokeInteractions failed with status code 500" + "at InteractionManager.InvokeInteractions in InteractionManager.cs:line 203" + "Cannot open page 130455`nClientSession State is InError (Wait time 25 seconds)" + "GET request failed. Response code is 500 (InternalServerError), expected code is 200. Error message: Object reference not set to an instance of an object." + ) | ForEach-Object { + Test-TransientTestFailure -Output $_ | Should -BeTrue + } + } + } + + It "does not classify generic session errors or unrelated failures as transient" { + InModuleScope ParallelTestExecution { + @( + $null + '' + 'ClientSession State is InError (Wait time 25 seconds)' + "Assertion failed: expected 2 but was 1`nClientSession State is InError" + "Permission denied opening page 42`nClientSession State is InError" + 'Cannot open page 130456' + 'InvokeInteractions failed with status code 403' + 'Nullable object must have a value.' + ) | ForEach-Object { + Test-TransientTestFailure -Output $_ | Should -BeFalse + } + } + } + + It "retries known races only once and never retries ordinary InError failures" { + InModuleScope ParallelTestExecution { + Mock Receive-Job { 'Cannot open page 130455' } -RemoveParameterType Job + Mock Remove-Job {} -RemoveParameterType Job + $entry = @{ appName = 'Tests'; tenant = 'default' } + $job = @{ State = 'Failed' } + + (Receive-TestJobResult -Entry $entry -Job $job -Retried @{}).Outcome | Should -Be 'Transient' + (Receive-TestJobResult -Entry $entry -Job $job -Retried @{ Tests = $true }).Outcome | Should -Be 'Failed' + + Mock Receive-Job { "Assertion failed`nClientSession State is InError" } -RemoveParameterType Job + (Receive-TestJobResult -Entry $entry -Job $job -Retried @{}).Outcome | Should -Be 'Failed' + $job.State = 'Completed' + (Receive-TestJobResult -Entry $entry -Job $job -Retried @{}).Outcome | Should -Be 'Passed' + Should -Invoke Remove-Job -Times 4 -Exactly + } + } + It "re-dispatches a transient platform-race victim ahead of the apps still queued" { # The dispatch order is what decides the critical path: a platform race normally kills a # job within a minute of dispatch, so the victim is one of the first (longest) apps. If the @@ -113,13 +309,20 @@ Describe "ParallelTestExecution transient retry scheduling" { $script:dispatched = [System.Collections.Generic.List[string]]::new() $script:raced = $false - Mock Get-AvailableBcTenants { @('default') } + Mock Get-AvailableBcTenantInfo { + @( + [PSCustomObject]@{ Id = 'default'; DatabaseName = 'default' } + [PSCustomObject]@{ Id = 'tenant2'; DatabaseName = 'tenant2' } + ) + } Mock Get-BcContainerAppInfo { @('Big', 'Medium', 'Small') | ForEach-Object { [PSCustomObject]@{ IsInstalled = $true; Name = $_; AppId = "id-$_" } } } + Mock Get-RequiredDisabledWorkItems { @() } Mock Wait-ForFreeTenant { 'default' } + Mock Wait-ForSpecificTenant { 'default' } Mock Wait-ForAllTestJobs { $true } Mock Merge-TenantTestResults { } Mock Start-TestAppDispatch { @@ -127,7 +330,9 @@ Describe "ParallelTestExecution transient retry scheduling" { # 'Big' loses the platform race on its very first dispatch, exactly once. if ($AppName -eq 'Big' -and -not $script:raced) { $script:raced = $true - $State.transient = @($State.transient) + @($AppName) + $State.transient = @($State.transient) + @( + [PSCustomObject]@{ Key = $AppName; Tenant = $Tenant } + ) } } @@ -140,24 +345,862 @@ Describe "ParallelTestExecution transient retry scheduling" { } } +Describe "ParallelTestExecution RequiredTestIsolation discovery" { + BeforeAll { + Import-Module (Join-Path $PSScriptRoot '../ParallelTestExecution.psm1') -Force + } + + It "flattens discovered codeunits and excludes codeunits without enabled methods" { + InModuleScope ParallelTestExecution { + $discovered = @( + [PSCustomObject]@{ Id = '100'; Name = 'Direct'; Tests = @('A', 'B') } + [PSCustomObject]@{ + Group = 'G' + Codeunits = @( + [PSCustomObject]@{ Id = '200'; Name = 'Grouped'; Tests = @('C') } + [PSCustomObject]@{ Id = '300'; Name = 'Disabled'; Tests = @() } + ) + } + ) + + $result = @(ConvertTo-RequiredDisabledWorkItems -DiscoveredTests $discovered -AppName 'API Tests' -AppId 'app-id') + + $result.Count | Should -Be 2 + $result[0].Key | Should -Be 'API Tests::100' + $result[0].TestCount | Should -Be 2 + $result[1].CodeunitId | Should -Be '200' + } + } + + It "discovers selected apps once each while preserving isolation, lane and exclusion filters" { + InModuleScope ParallelTestExecution { + Mock Get-ParametersForCommand { @{ containerName = 'c'; tenant = 'default' } } + Mock Get-DisabledTestsForApp { + @([PSCustomObject]@{ codeunitId = 999; method = 'DisabledMethod' }) + } + Mock Get-TestsFromBcContainer { + @([PSCustomObject]@{ Id = '500'; Name = 'API E2E'; Tests = @('Create', 'Modify') }) + } + + $result = @( + Get-RequiredDisabledWorkItems -Parameters @{ containerName = 'c'; tenant = 'default' } ` + -TestType 'IntegrationTest' -AppNamesToTest @('API Tests', 'New API Tests') ` + -AppIdByName @{ 'API Tests' = 'app-id'; 'New API Tests' = 'new-app-id'; 'Unselected Tests' = 'other-id' } + ) + + $result.Count | Should -Be 2 + $result[0].CodeunitId | Should -Be '500' + $result[1].AppName | Should -Be 'New API Tests' + Should -Invoke Get-TestsFromBcContainer -Times 1 -ParameterFilter { + $extensionId -eq 'app-id' -and + $requiredTestIsolation -eq 'Disabled' -and + $testType -eq 'IntegrationTest' -and + $disabledTests.Count -eq 1 + } + Should -Invoke Get-TestsFromBcContainer -Times 1 -Exactly -ParameterFilter { + $extensionId -eq 'new-app-id' -and + $requiredTestIsolation -eq 'Disabled' -and + $testType -eq 'IntegrationTest' -and + $disabledTests.Count -eq 1 + } + Should -Invoke Get-TestsFromBcContainer -Times 0 -ParameterFilter { + $extensionId -eq 'other-id' + } + } + } + + It "does not use unfiltered discovery for Legacy buckets" { + InModuleScope ParallelTestExecution { + Mock Get-ParametersForCommand { @{ containerName = 'c'; tenant = 'default'; testType = 'stale' } } + Mock Get-DisabledTestsForApp { @() } + Mock Get-TestsFromBcContainer { @() } + + $result = @(Get-RequiredDisabledWorkItems -Parameters @{ containerName = 'c' } ` + -TestType 'Legacy' -AppNamesToTest @('Legacy Tests') ` + -AppIdByName @{ 'Legacy Tests' = 'legacy-id' }) + + $result.Count | Should -Be 0 + Should -Invoke Get-TestsFromBcContainer -Times 0 + } + } +} + +Describe "ParallelTestExecution clean tenant scheduling" { + BeforeAll { + Import-Module (Join-Path $PSScriptRoot '../ParallelTestExecution.psm1') -Force + } + + BeforeEach { + Mock -ModuleName ParallelTestExecution Invoke-ScriptInBcContainer { + throw 'Unexpected unmocked container access.' + } + Mock -ModuleName ParallelTestExecution Reset-BcTestTenant { + throw 'Tenant resets require an explicit test mock.' + } + } + + It "writes and reads cached state without temp environment variables" { + InModuleScope ParallelTestExecution { + $previousRunnerTemp = $env:RUNNER_TEMP + $previousTemp = $env:TEMP + $containerName = "ut-$([guid]::NewGuid().ToString('N'))" + $stateFile = Join-Path ([System.IO.Path]::GetTempPath()) "parallelTests_$containerName.json" + Mock Get-AvailableBcTenantInfo { + @([PSCustomObject]@{ Id = 'default'; DatabaseName = 'default' }) + } + Mock Get-BcContainerAppInfo { + @([PSCustomObject]@{ IsInstalled = $true; Name = 'Tests'; AppId = 'tests-id' }) + } + Mock Get-RequiredDisabledWorkItems { @() } + Mock Wait-ForFreeTenant { 'default' } + Mock Start-TestAppDispatch { } + Mock Wait-ForAllTestJobs { $true } + Mock Merge-TenantTestResults { } + try { + $env:RUNNER_TEMP = $null + $env:TEMP = $null + $stateFile = Join-Path ([System.IO.Path]::GetTempPath()) "parallelTests_$containerName.json" + + Invoke-ParallelTestExecution -parameters @{ + containerName = $containerName + tenant = 'default' + } -scriptPath 'unused.ps1' -testType 'IntegrationTest' -appNamesToTest @('Tests') | + Should -BeTrue + + Test-Path $stateFile | Should -BeTrue + Get-CachedTestRunResult -ContainerName $containerName | Should -BeTrue + } finally { + $env:RUNNER_TEMP = $previousRunnerTemp + $env:TEMP = $previousTemp + if (Test-Path $stateFile) { + Remove-Item $stateFile -Force + } + } + } + } + + It "does not create a database template when no Disabled-isolation codeunits are enabled" { + InModuleScope ParallelTestExecution { + Mock Get-AvailableBcTenantInfo { + @([PSCustomObject]@{ Id = 'default'; DatabaseName = 'default' }) + } + Mock Get-BcContainerAppInfo { + @([PSCustomObject]@{ IsInstalled = $true; Name = 'Tests'; AppId = 'tests-id' }) + } + Mock Get-RequiredDisabledWorkItems { @() } + Mock New-BcTestTenantTemplate { throw 'Template must not be created' } + Mock Wait-ForFreeTenant { 'default' } + Mock Start-TestAppDispatch { } + Mock Wait-ForAllTestJobs { $true } + Mock Merge-TenantTestResults { } + + $result = Invoke-ParallelTestExecution -parameters @{ + containerName = "ut-$([guid]::NewGuid().ToString('N'))" + tenant = 'default' + } -scriptPath 'unused.ps1' -testType 'IntegrationTest' -appNamesToTest @('Tests') + + $result | Should -BeTrue + Should -Invoke New-BcTestTenantTemplate -Times 0 + Should -Invoke Get-RequiredDisabledWorkItems -Times 1 -Exactly -ParameterFilter { + $AppNamesToTest.Count -eq 1 -and $AppNamesToTest[0] -eq 'Tests' + } + Should -Invoke Start-TestAppDispatch -Times 1 -Exactly -ParameterFilter { + $SkipAutomaticDisabledPass + } + } + } + + Describe "ParallelTestExecution result metadata" { + BeforeAll { + Import-Module (Join-Path $PSScriptRoot '../ParallelTestExecution.psm1') -Force + } + + It "adds app properties to clean-codeunit JUnit suites" { + InModuleScope ParallelTestExecution { + $resultFile = Join-Path ([System.IO.Path]::GetTempPath()) "junit-$([guid]::NewGuid().ToString('N')).xml" + try { + @( + '' + '' + ' ' + ' ' + ' ' + '' + ) | Set-Content -Path $resultFile -Encoding utf8 + + Add-MissingJUnitTestProperties -ResultFile $resultFile -WorkItems @( + [PSCustomObject]@{ + CodeunitId = '139800' + AppId = 'app-id' + AppName = '_Exclude_APIV2_ Tests' + } + ) + + [xml]$xml = Get-Content $resultFile -Raw + $properties = @($xml.testsuites.testsuite.properties.property) + ($properties | Where-Object name -eq 'extensionid').value | Should -Be 'app-id' + ($properties | Where-Object name -eq 'appName').value | Should -Be '_Exclude_APIV2_ Tests' + } finally { + Remove-Item $resultFile -Force -ErrorAction SilentlyContinue + } + } + } + } + + Describe "API test authentication extensibility" { + BeforeAll { + $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path + $testLibraryRoot = Join-Path $repoRoot 'src\Layers\W1\Tests\TestLibraries' + $script:authenticationInterface = Get-Content (Join-Path $testLibraryRoot 'APITestAuthProvider.Interface.al') -Raw + $script:authenticationEnum = Get-Content (Join-Path $testLibraryRoot 'APITestAuthentication.Enum.al') -Raw + $script:authenticationContext = Get-Content (Join-Path $testLibraryRoot 'APITestAuthContext.Codeunit.al') -Raw + $script:graphManagement = Get-Content (Join-Path $testLibraryRoot 'LibraryGraphMgt.Codeunit.al') -Raw + $script:microsoftProvider = Get-Content (Join-Path $testLibraryRoot 'MicrosoftTestAuthProvider.Codeunit.al') -Raw + } + + It "exposes an extensible provider contract with a no-auth default" { + $script:authenticationInterface | Should -Match 'interface\s+"API Test Auth Provider"' + $script:authenticationInterface | Should -Match 'ConfigureAuthentication\(var\s+Authentication:\s*Codeunit\s+"API Test Auth Context"\)' + $script:authenticationInterface | Should -Not -Match 'TargetURL' + $script:authenticationEnum | Should -Match 'Extensible\s*=\s*true\s*;' + $script:authenticationEnum | Should -Match 'DefaultImplementation\s*=\s*"API Test Auth Provider"\s*=\s*"No API Test Auth Provider"\s*;' + $script:authenticationEnum | Should -Match 'value\(1;\s*"Microsoft Test Environment"\)' + $script:authenticationEnum | Should -Not -Match 'UnknownValueImplementation' + } + + It "keeps request mutation inside the authentication context" { + $script:authenticationContext | Should -Match 'procedure\s+SetBasicAuthentication\(UserName:\s*Text;\s*Password:\s*SecretText\)' + $script:authenticationContext | Should -Match 'HttpWebRequestMgt\.AddBasicAuthentication\(BasicUserName,\s*BasicPassword\)' + $script:microsoftProvider | Should -Not -Match 'HttpWebRequestMgt' + $script:microsoftProvider | Should -Match 'Authentication\.SetBasicAuthentication\(UserId\(\),\s*GetAuthenticationPassword\(\)\)' + $script:microsoftProvider | Should -Not -Match 'TargetURL|IsCurrentTestServiceURL' + } + + It "applies the selected provider after URL rewriting and before the final request event" { + $script:graphManagement | Should -Match 'AuthenticationProviderResolved:\s*Boolean\s*;' + $script:graphManagement | Should -Match 'HttpWebRequestMgt\.Initialize\(TargetURL\);\s*ApplyAuthentication\(HttpWebRequestMgt\);\s*OnAfterInitializeWebRequestWithURL\(HttpWebRequestMgt\);' + $script:graphManagement | Should -Match 'ConfigureAuthentication\(AuthenticationContext\)' + $script:graphManagement | Should -Not -Match 'Microsoft Test Auth Provider' + } + + It "preserves the provider instance when initialization selects the same provider again" { + $script:graphManagement | Should -Match 'if AuthenticationProviderResolved and \(Authentication = NewAuthentication\) then\s*exit;\s*Authentication := NewAuthentication;' + } + + It "checks platform authentication before reading credentials without querying the User table" { + $script:microsoftProvider | Should -Match 'SecurityGroup:\s*Codeunit "Security Group";' + $script:microsoftProvider | Should -Match 'if EnvironmentInfo\.IsSaaSInfrastructure\(\) then\s*exit;\s*if SecurityGroup\.IsWindowsAuthentication\(\) then\s*exit;\s*Authentication\.SetBasicAuthentication' + $script:microsoftProvider | Should -Not -Match 'Record User|Windows Security ID|User\.Get\(' + } + + It "caches only a successfully resolved password and keeps container precedence over Key Vault" { + $script:microsoftProvider | Should -Match 'if not CachedAuthenticationPassword\.IsEmpty\(\) then\s*exit\(CachedAuthenticationPassword\);' + $script:microsoftProvider | Should -Match 'if ContainerPasswordFileExists\(\) then begin\s*if not TryGetContainerPassword\(Password\) then\s*Error\(ContainerPasswordReadErr, ApiTestPasswordFileTok\);\s*end else\s*if not TryGetNavEnlistmentPassword\(Password\) then\s*Error\(KeyVaultPasswordReadErr, NavServerUserPasswordKeyTok\);\s*CachedAuthenticationPassword := Password;' + $script:microsoftProvider | Should -Not -Match 'AuthenticationRequired|AuthenticationResolved|CacheAuthenticationResult' + } + + } + + It "defers the automatic Unit disabled pass only for clean-tenant apps" { + InModuleScope ParallelTestExecution { + $script:skipValues = [System.Collections.Generic.List[bool]]::new() + Mock Start-Sleep { } + Mock Start-TestJob { + $script:skipValues.Add($skipAutomaticDisabledPass.IsPresent) + [PSCustomObject]@{ Id = $script:skipValues.Count } + } + + $state = [PSCustomObject]@{ jobs = @() } + Start-TestAppDispatch -Parameters @{} -AppName 'Normal Tests' -AppId 'normal-id' ` + -Tenant 'default' -ScriptPath 'runner.ps1' -TestType 'UnitTest' -State $state + Start-TestAppDispatch -Parameters @{} -AppName 'API Tests' -AppId 'api-id' ` + -Tenant 'tenant2' -ScriptPath 'runner.ps1' -TestType 'UnitTest' -State $state ` + -SkipAutomaticDisabledPass + + $script:skipValues | Should -Be @($false, $true) + } + } + + It "loads all app exclusions without consulting country settings" { + InModuleScope ParallelTestExecution { + $script:disabledTestRoot = Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString('N')) + Mock Get-BaseFolder { $script:disabledTestRoot } + Mock Get-ALGoSetting { throw 'Exclusions must not depend on country settings' } + try { + $appFolder = Join-Path $script:disabledTestRoot 'DisabledTests\Example_Tests' + $null = New-Item -Path $appFolder -ItemType Directory -Force + '[{"codeunitId":500,"method":"First"}]' | + Set-Content (Join-Path $appFolder 'first.json') + '[{"codeunitId":501,"method":"*"}]' | + Set-Content (Join-Path $appFolder 'second.json') + + $result = @(Get-DisabledTestsForApp -AppName 'Example Tests') + + $result.Count | Should -Be 2 + $result.codeunitId | Should -Contain 500 + $result.codeunitId | Should -Contain 501 + @(Get-DisabledTestsForApp -AppName 'Other Tests').Count | Should -Be 0 + Should -Invoke Get-ALGoSetting -Times 0 + } finally { + Remove-Item $script:disabledTestRoot -Recurse -Force + } + } + } + + It "dispatches one codeunit with Disabled isolation" { + InModuleScope ParallelTestExecution { + $script:capturedParameters = $null + + Mock Get-DisabledTestsForApp { @() } + Mock Start-Sleep { } + Mock Start-TestJob { + $script:capturedParameters = $parameters + [PSCustomObject]@{ Id = 42 } + } + + $state = [PSCustomObject]@{ jobs = @() } + $workItem = [PSCustomObject]@{ + Key = 'Tests::500' + AppName = 'Tests' + AppId = 'tests-id' + CodeunitId = '500' + CodeunitName = 'API E2E' + } + + Start-RequiredDisabledDispatch -Parameters @{ + containerName = 'c' + JUnitResultFileName = 'results.xml' + } -WorkItem $workItem -TenantInfo ([PSCustomObject]@{ + Id = 'tenant2' + DatabaseName = 'tenant2' + }) -ScriptPath 'runner.ps1' ` + -TestType 'IntegrationTest' -State $state + + $script:capturedParameters.testCodeunit | Should -Be '500' + $script:capturedParameters.requiredTestIsolation | Should -Be 'Disabled' + $script:capturedParameters.testRunnerCodeunitId | Should -Be '130451' + $script:capturedParameters.AppendToJUnitResultFile | Should -BeTrue + $state.jobs.Count | Should -Be 1 + } + } + + It "marks scheduler retries as reruns so existing XML entries are replaced" { + InModuleScope ParallelTestExecution { + $script:capturedParameters = $null + + Mock Get-DisabledTestsForApp { @() } + Mock Reset-BcTestTenant { } + Mock Start-Sleep { } + Mock Start-TestJob { + $script:capturedParameters = $parameters + [PSCustomObject]@{ Id = 42 } + } + + Start-RequiredDisabledDispatch -Parameters @{ containerName = 'c' } -WorkItem ([PSCustomObject]@{ + Key = 'Tests::500' + AppName = 'Tests' + AppId = 'tests-id' + CodeunitId = '500' + CodeunitName = 'API E2E' + }) -TenantInfo ([PSCustomObject]@{ + Id = 'tenant2' + DatabaseName = 'tenant2' + }) -ScriptPath 'runner.ps1' ` + -TestType 'IntegrationTest' -State ([PSCustomObject]@{ jobs = @() }) ` + -Verb 'Re-dispatching' + + $script:capturedParameters.ReRun | Should -BeTrue + } + } + + It "retries a clean codeunit on its original tenant" { + InModuleScope ParallelTestExecution { + $script:dispatchTenants = [System.Collections.Generic.List[string]]::new() + $script:firstDispatch = $true + + Mock Reset-BcTestTenant { } + Mock Wait-ForAllTestJobs { $true } + Mock Start-RequiredDisabledDispatch { + $script:dispatchTenants.Add($TenantInfo.Id) + if ($script:firstDispatch) { + $script:firstDispatch = $false + $State.transient = @( + [PSCustomObject]@{ Key = $WorkItem.Key; Tenant = $TenantInfo.Id } + ) + } + } + + $workItem = [PSCustomObject]@{ + Key = 'Tests::500' + AppName = 'Tests' + AppId = 'tests-id' + CodeunitId = '500' + CodeunitName = 'API E2E' + } + $tenantInfo = @( + [PSCustomObject]@{ Id = 'tenant2'; DatabaseName = 'tenant2' } + [PSCustomObject]@{ Id = 'tenant3'; DatabaseName = 'tenant3' } + ) + + $result = Invoke-RequiredDisabledTestExecution -Parameters @{ containerName = 'c' } ` + -WorkItems @($workItem) -TenantInfo $tenantInfo -TemplateDatabaseName 'template' ` + -ScriptPath 'runner.ps1' -TestType 'UnitTest' + + $result | Should -BeTrue + $script:dispatchTenants | Should -Be @('tenant2', 'tenant2') + } + } + + It "finishes all tenant resets before dispatching a clean-codeunit batch" { + InModuleScope ParallelTestExecution { + $script:events = [System.Collections.Generic.List[string]]::new() + Mock Reset-BcTestTenant { + $script:events.Add("reset:$Tenant") + } + Mock Start-RequiredDisabledDispatch { + $script:events.Add("dispatch:$($TenantInfo.Id)") + } + Mock Wait-ForAllTestJobs { } + + $workItems = @( + [PSCustomObject]@{ Key = 'Tests::500'; AppName = 'Tests'; AppId = 'id'; CodeunitId = '500'; CodeunitName = 'A' } + [PSCustomObject]@{ Key = 'Tests::501'; AppName = 'Tests'; AppId = 'id'; CodeunitId = '501'; CodeunitName = 'B' } + ) + $tenantInfo = @( + [PSCustomObject]@{ Id = 'tenant2'; DatabaseName = 'tenant2' } + [PSCustomObject]@{ Id = 'tenant3'; DatabaseName = 'tenant3' } + ) + + Invoke-RequiredDisabledTestExecution -Parameters @{ containerName = 'c' } ` + -WorkItems $workItems -TenantInfo $tenantInfo -TemplateDatabaseName 'template' ` + -ScriptPath 'runner.ps1' -TestType 'UnitTest' | Should -BeTrue + + $script:events | Should -Be @('reset:tenant2', 'reset:tenant3', 'dispatch:tenant2', 'dispatch:tenant3') + } + } + + It "preserves configured Task Scheduler state during clean and ordinary execution" -ForEach @( + @{ Enabled = $false } + @{ Enabled = $true } + ) { + InModuleScope ParallelTestExecution -Parameters @{ Enabled = $Enabled } { + param($Enabled) + + Mock Get-AvailableBcTenantInfo { + @( + [PSCustomObject]@{ Id = 'default'; DatabaseName = 'default' } + [PSCustomObject]@{ Id = 'tenant2'; DatabaseName = 'tenant2' } + ) + } + Mock Get-BcContainerAppInfo { + @([PSCustomObject]@{ IsInstalled = $true; Name = 'Tests'; AppId = 'tests-id' }) + } + Mock Get-RequiredDisabledWorkItems { + @([PSCustomObject]@{ + Key = 'Tests::500' + AppName = 'Tests' + AppId = 'tests-id' + CodeunitId = '500' + CodeunitName = 'API E2E' + TestCount = 2 + }) + } + $script:taskSchedulerEnabled = $Enabled + $script:expectedTaskSchedulerEnabled = $Enabled + Mock New-BcTestTenantTemplate { 'default-test-template' } + Mock Wait-ForFreeTenant { 'default' } + Mock Start-TestAppDispatch { + $script:taskSchedulerEnabled | Should -Be $script:expectedTaskSchedulerEnabled + } + Mock Wait-ForAllTestJobs { $true } + Mock Invoke-RequiredDisabledTestExecution { + $script:taskSchedulerEnabled | Should -Be $script:expectedTaskSchedulerEnabled + $true + } + Mock Reset-BcTestTenant { + $script:taskSchedulerEnabled | Should -Be $script:expectedTaskSchedulerEnabled + } + Mock Remove-BcTestTenantTemplate { } + Mock Merge-TenantTestResults { } + + $result = Invoke-ParallelTestExecution -parameters @{ + containerName = "ut-$([guid]::NewGuid().ToString('N'))" + tenant = 'default' + } -scriptPath 'unused.ps1' -testType 'IntegrationTest' -appNamesToTest @('Tests') + + $result | Should -BeTrue + Should -Invoke Get-RequiredDisabledWorkItems -Times 1 -Exactly -ParameterFilter { + $AppNamesToTest.Count -eq 1 -and $AppNamesToTest[0] -eq 'Tests' + } + Should -Invoke New-BcTestTenantTemplate -Times 1 -ParameterFilter { + $SourceDatabaseName -eq 'default' + } + Should -Invoke Invoke-ScriptInBcContainer -Times 0 + Should -Invoke Reset-BcTestTenant -Times 1 -Exactly -ParameterFilter { + $Tenant -eq 'tenant2' -and + $TenantDatabaseName -eq 'tenant2' -and + $TemplateDatabaseName -eq 'default' + } + Should -Invoke Reset-BcTestTenant -Times 1 -ParameterFilter { + $Tenant -eq 'tenant2' -and + $TenantDatabaseName -eq 'tenant2' -and + $TemplateDatabaseName -eq 'default-test-template' + } + Should -Invoke Invoke-RequiredDisabledTestExecution -Times 1 -ParameterFilter { + $TemplateDatabaseName -eq 'default-test-template' -and + $WorkItems.Count -eq 1 -and + $TenantInfo.Count -eq 1 -and + $TenantInfo[0].Id -eq 'tenant2' + } + Should -Invoke Remove-BcTestTenantTemplate -Times 1 -ParameterFilter { + $TemplateDatabaseName -eq 'default-test-template' + } + } + } + + Context "discovery fixture preservation" { + BeforeEach { + InModuleScope ParallelTestExecution { + $script:fixtureEvents = [System.Collections.Generic.List[string]]::new() + $script:fixtureTenants = @( + [PSCustomObject]@{ Id = 'worker-a'; DatabaseName = 'worker-a-db' } + [PSCustomObject]@{ Id = 'primary'; DatabaseName = 'primary-db' } + [PSCustomObject]@{ Id = 'worker-b'; DatabaseName = 'worker-b-db' } + ) + $script:fixtures = @{ + 'primary-db' = 'pristine' + 'worker-a-db' = 'pristine' + 'worker-b-db' = 'pristine' + } + $script:fixtureParameters = @{ + containerName = "ut-$([guid]::NewGuid().ToString('N'))" + tenant = 'primary' + extensionId = 'caller-app-id' + testSuite = 'CALLER' + disabledTests = @([PSCustomObject]@{ codeunitId = 999; method = 'Excluded' }) + } + $script:originalFixtureParameters = $script:fixtureParameters | ConvertTo-Json -Depth 5 + $script:fixtureWorkItems = @([PSCustomObject]@{ + Key = 'Tests::500'; AppName = 'Tests'; AppId = 'tests-id' + CodeunitId = '500'; CodeunitName = 'API E2E'; TestCount = 2 + }) + $script:discoveryThrows = $false + $script:cleanThrows = $false + $script:cleanPassed = $true + Mock Get-AvailableBcTenantInfo { $script:fixtureTenants } + Mock Get-BcContainerAppInfo { + @([PSCustomObject]@{ IsInstalled = $true; Name = 'Tests'; AppId = 'tests-id' }) + } + Mock Get-RequiredDisabledWorkItems { + $script:fixtureEvents.Add("discover:$($Parameters.tenant)") + [object]::ReferenceEquals($Parameters, $script:fixtureParameters) | Should -BeFalse + $expected = $script:fixtureParameters.Clone() + $expected.tenant = 'worker-a' + ($Parameters | ConvertTo-Json -Depth 5) | Should -Be ($expected | ConvertTo-Json -Depth 5) + $discoveryDatabase = ($script:fixtureTenants | + Where-Object Id -eq $Parameters.tenant).DatabaseName + $script:fixtures[$discoveryDatabase] = 'discovery-dirty' + if ($script:discoveryThrows) { + throw 'discovery failed' + } + $script:fixtureWorkItems + } + Mock Reset-BcTestTenant { + $Tenant | Should -Not -Be 'primary' + $TenantDatabaseName | Should -Not -Be 'primary-db' + $script:fixtureEvents.Add("restore:${Tenant}:$TemplateDatabaseName") + $script:fixtures[$TenantDatabaseName] = $script:fixtures[$TemplateDatabaseName] + } + Mock New-BcTestTenantTemplate { + $SourceDatabaseName | Should -Be 'primary-db' + $script:fixtures['primary-db'] | Should -Be 'pristine' + $script:fixtures['worker-a-db'] | Should -Be 'pristine' + $script:fixtureEvents.Add("template:$SourceDatabaseName") + $script:fixtures['template-db'] = $script:fixtures[$SourceDatabaseName] + 'template-db' + } + Mock Invoke-RequiredDisabledTestExecution { + $script:fixtureEvents.Add('clean') + $script:fixtures['primary-db'] | Should -Be 'pristine' + foreach ($worker in $TenantInfo) { + $script:fixtures[$worker.DatabaseName] = 'clean-dirty' + } + if ($script:cleanThrows) { + throw 'clean execution failed' + } + $script:cleanPassed + } + Mock Invoke-WarmupDispatch { + $script:fixtureEvents.Add('warmup') + @($Pending) + } + Mock Get-AppRerunBudget { 0 } + Mock Wait-ForFreeTenant { 'primary' } + Mock Start-TestAppDispatch { + $script:fixtureEvents.Add("ordinary:$Tenant") + foreach ($info in $script:fixtureTenants) { + $script:fixtures[$info.DatabaseName] | Should -Be 'pristine' + } + } + Mock Wait-ForAllTestJobs { $true } + Mock Merge-TenantTestResults { } + Mock Remove-BcTestTenantTemplate { + $script:fixtureEvents.Add('remove-template') + } + } + } + + It "restores secondary discovery before copying the pristine primary and dispatching tests" { + InModuleScope ParallelTestExecution { + Invoke-ParallelTestExecution -parameters $script:fixtureParameters -scriptPath 'unused.ps1' ` + -testType 'UnitTest' -appNamesToTest @('Tests') | Should -BeTrue + + $script:fixtureEvents | Should -Be @( + 'discover:worker-a', 'restore:worker-a:primary-db', 'template:primary-db', 'clean', + 'restore:worker-a:template-db', 'restore:worker-b:template-db', + 'warmup', 'ordinary:primary', 'remove-template' + ) + $script:fixtures['template-db'] | Should -Be 'pristine' + ($script:fixtureParameters | ConvertTo-Json -Depth 5) | Should -Be $script:originalFixtureParameters + Should -Invoke Get-RequiredDisabledWorkItems -Times 1 -Exactly -ParameterFilter { + $Parameters.tenant -eq 'worker-a' -and $TestType -eq 'UnitTest' -and + $AppNamesToTest.Count -eq 1 -and $AppIdByName.Tests -eq 'tests-id' + } + Should -Invoke Invoke-ScriptInBcContainer -Times 0 + } + } + + It "restores discovery even when there are no clean work items without creating a template" { + InModuleScope ParallelTestExecution { + $script:fixtureWorkItems = @() + Invoke-ParallelTestExecution -parameters $script:fixtureParameters -scriptPath 'unused.ps1' ` + -testType 'IntegrationTest' -appNamesToTest @('Tests') | Should -BeTrue + + $script:fixtureEvents | Should -Be @( + 'discover:worker-a', 'restore:worker-a:primary-db', 'warmup', 'ordinary:primary' + ) + Should -Invoke New-BcTestTenantTemplate -Times 0 + Should -Invoke Invoke-RequiredDisabledTestExecution -Times 0 + ($script:fixtureParameters | ConvertTo-Json -Depth 5) | Should -Be $script:originalFixtureParameters + } + } + + It "restores discovery after an exception without copying or dispatching tainted fixtures" { + InModuleScope ParallelTestExecution { + $script:discoveryThrows = $true + { + Invoke-ParallelTestExecution -parameters $script:fixtureParameters -scriptPath 'unused.ps1' ` + -testType 'IntegrationTest' -appNamesToTest @('Tests') + } | Should -Throw '*discovery failed*' + + $script:fixtureEvents | Should -Be @('discover:worker-a', 'restore:worker-a:primary-db') + $script:fixtures['primary-db'] | Should -Be 'pristine' + $script:fixtures['worker-a-db'] | Should -Be 'pristine' + Should -Invoke New-BcTestTenantTemplate -Times 0 + Should -Invoke Invoke-RequiredDisabledTestExecution -Times 0 + Should -Invoke Invoke-WarmupDispatch -Times 0 + ($script:fixtureParameters | ConvertTo-Json -Depth 5) | Should -Be $script:originalFixtureParameters + } + } + + It "stops before template creation or dispatch when restoring discovery fails" { + InModuleScope ParallelTestExecution { + Mock Reset-BcTestTenant { throw 'discovery restore failed' } + { + Invoke-ParallelTestExecution -parameters $script:fixtureParameters -scriptPath 'unused.ps1' ` + -testType 'IntegrationTest' -appNamesToTest @('Tests') + } | Should -Throw '*discovery restore failed*' + + Should -Invoke Reset-BcTestTenant -Times 1 -Exactly + Should -Invoke New-BcTestTenantTemplate -Times 0 + Should -Invoke Invoke-RequiredDisabledTestExecution -Times 0 + Should -Invoke Invoke-WarmupDispatch -Times 0 + ($script:fixtureParameters | ConvertTo-Json -Depth 5) | Should -Be $script:originalFixtureParameters + } + } + + It "restores every clean worker and removes the template when clean execution throws" { + InModuleScope ParallelTestExecution { + $script:cleanThrows = $true + { + Invoke-ParallelTestExecution -parameters $script:fixtureParameters -scriptPath 'unused.ps1' ` + -testType 'IntegrationTest' -appNamesToTest @('Tests') + } | Should -Throw '*clean execution failed*' + + $script:fixtureEvents | Should -Be @( + 'discover:worker-a', 'restore:worker-a:primary-db', 'template:primary-db', 'clean', + 'restore:worker-a:template-db', 'restore:worker-b:template-db', 'remove-template' + ) + $script:fixtures['worker-a-db'] | Should -Be 'pristine' + $script:fixtures['worker-b-db'] | Should -Be 'pristine' + Should -Invoke Invoke-WarmupDispatch -Times 0 + Should -Invoke Invoke-ScriptInBcContainer -Times 0 + } + } + + It "restores every clean worker while retaining a failed test result" { + InModuleScope ParallelTestExecution { + $script:cleanPassed = $false + Invoke-ParallelTestExecution -parameters $script:fixtureParameters -scriptPath 'unused.ps1' ` + -testType 'IntegrationTest' -appNamesToTest @('Tests') | Should -BeFalse + + $script:fixtures['worker-a-db'] | Should -Be 'pristine' + $script:fixtures['worker-b-db'] | Should -Be 'pristine' + $script:fixtureEvents.IndexOf('ordinary:primary') | + Should -BeGreaterThan $script:fixtureEvents.IndexOf('restore:worker-b:template-db') + Should -Invoke Remove-BcTestTenantTemplate -Times 1 -Exactly + } + } + + It "keeps Legacy discovery on the original parameters without copying or resetting tenants" { + InModuleScope ParallelTestExecution { + Mock Get-RequiredDisabledWorkItems { + [object]::ReferenceEquals($Parameters, $script:fixtureParameters) | Should -BeTrue + $TestType | Should -Be 'Legacy' + @() + } + Invoke-ParallelTestExecution -parameters $script:fixtureParameters -scriptPath 'unused.ps1' ` + -testType 'Legacy' -appNamesToTest @('Tests') | Should -BeTrue + + Should -Invoke Get-RequiredDisabledWorkItems -Times 1 -Exactly + Should -Invoke Reset-BcTestTenant -Times 0 + Should -Invoke New-BcTestTenantTemplate -Times 0 + Should -Invoke Invoke-RequiredDisabledTestExecution -Times 0 + ($script:fixtureParameters | ConvertTo-Json -Depth 5) | Should -Be $script:originalFixtureParameters + } + } + + It "preserves single-tenant discovery and ordinary execution without clean work items" { + InModuleScope ParallelTestExecution { + $script:fixtureTenants = @($script:fixtureTenants | Where-Object Id -eq 'primary') + Mock Get-RequiredDisabledWorkItems { + [object]::ReferenceEquals($Parameters, $script:fixtureParameters) | Should -BeTrue + @() + } + Invoke-ParallelTestExecution -parameters $script:fixtureParameters -scriptPath 'unused.ps1' ` + -testType 'IntegrationTest' -appNamesToTest @('Tests') | Should -BeTrue + + Should -Invoke Get-RequiredDisabledWorkItems -Times 1 -Exactly + Should -Invoke Reset-BcTestTenant -Times 0 + Should -Invoke New-BcTestTenantTemplate -Times 0 + Should -Invoke Start-TestAppDispatch -Times 1 -Exactly + } + } + + It "retains the single-tenant error when clean work items are discovered" { + InModuleScope ParallelTestExecution { + $script:fixtureTenants = @($script:fixtureTenants | Where-Object Id -eq 'primary') + Mock Get-RequiredDisabledWorkItems { $script:fixtureWorkItems } + { + Invoke-ParallelTestExecution -parameters $script:fixtureParameters -scriptPath 'unused.ps1' ` + -testType 'IntegrationTest' -appNamesToTest @('Tests') + } | Should -Throw '*at least one secondary tenant*' + + Should -Invoke Reset-BcTestTenant -Times 0 + Should -Invoke New-BcTestTenantTemplate -Times 0 + Should -Invoke Invoke-WarmupDispatch -Times 0 + } + } + + It "rejects unsafe tenant metadata before discovery: " -ForEach @( + @{ Fault = 'missing primary'; MessagePattern = '*source tenant*' } + @{ Fault = 'blank primary database'; MessagePattern = '*source tenant*' } + @{ Fault = 'blank worker ID'; MessagePattern = '*unique tenant IDs and database names*' } + @{ Fault = 'blank worker database'; MessagePattern = '*unique tenant IDs and database names*' } + @{ Fault = 'duplicate worker ID'; MessagePattern = '*unique tenant IDs and database names*' } + @{ Fault = 'duplicate worker database'; MessagePattern = '*unique tenant IDs and database names*' } + @{ Fault = 'worker aliases primary database'; MessagePattern = '*unique tenant IDs and database names*' } + @{ Fault = 'duplicate primary ID'; MessagePattern = '*source tenant*' } + ) { + InModuleScope ParallelTestExecution -Parameters @{ Fault = $Fault; ExpectedError = $MessagePattern } { + param($Fault, $ExpectedError) + + switch ($Fault) { + 'missing primary' { $script:fixtureParameters.tenant = 'missing' } + 'blank primary database' { $script:fixtureTenants[1].DatabaseName = ' ' } + 'blank worker ID' { $script:fixtureTenants[0].Id = ' ' } + 'blank worker database' { $script:fixtureTenants[0].DatabaseName = ' ' } + 'duplicate worker ID' { $script:fixtureTenants[2].Id = 'WORKER-A' } + 'duplicate worker database' { $script:fixtureTenants[2].DatabaseName = 'WORKER-A-DB' } + 'worker aliases primary database' { $script:fixtureTenants[0].DatabaseName = 'PRIMARY-DB' } + 'duplicate primary ID' { $script:fixtureTenants[2].Id = 'PRIMARY' } + } + { + Invoke-ParallelTestExecution -parameters $script:fixtureParameters -scriptPath 'unused.ps1' ` + -testType 'IntegrationTest' -appNamesToTest @('Tests') + } | Should -Throw $ExpectedError + + Should -Invoke Get-RequiredDisabledWorkItems -Times 0 + Should -Invoke Reset-BcTestTenant -Times 0 + Should -Invoke New-BcTestTenantTemplate -Times 0 + Should -Invoke Invoke-WarmupDispatch -Times 0 + Should -Invoke Invoke-ScriptInBcContainer -Times 0 + } + } + } + + It "contains no Task Scheduler override or server restart commands" { + $tokens = $null + $parseErrors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseFile( + (Get-Module ParallelTestExecution).Path, [ref]$tokens, [ref]$parseErrors) + $parseErrors.Count | Should -Be 0 + $overrides = @($ast.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.CommandAst] -and + $node.GetCommandName() -in @( + 'Set-NAVServerConfiguration', 'Set-NAVServerInstance', + 'Set-BcTestTaskScheduler', 'Enable-BcTestTaskScheduler', 'Disable-BcTestTaskScheduler' + ) + }, $true)) + $overrides.Count | Should -Be 0 + } +} + Describe "ParallelTestExecution warmup dispatch" { BeforeAll { Import-Module (Join-Path $PSScriptRoot '../ParallelTestExecution.psm1') -Force } + It "defers the Disabled pass when the warmup app uses clean-codeunit execution" { + InModuleScope ParallelTestExecution { + $script:skipDisabledPass = $false + Mock Start-TestAppDispatch { + $script:skipDisabledPass = $SkipAutomaticDisabledPass.IsPresent + } + Mock Wait-ForAllTestJobs { } + + $state = [PSCustomObject]@{ jobs = @(); hasFailures = $false; transient = @(); retried = @{} } + $result = Invoke-WarmupDispatch -Parameters @{ containerName = 'c' } ` + -Pending @('API Tests', 'Other Tests') ` + -AppIdByName @{ 'API Tests' = 'api-id'; 'Other Tests' = 'other-id' } ` + -Tenants @('default', 'tenant2') -ScriptPath 'unused.ps1' -TestType 'UnitTest' ` + -State $state -CleanTenantAppNames @('API Tests') + + $result | Should -Be @('Other Tests') + $script:skipDisabledPass | Should -BeTrue + } + } + It "dispatches the first app alone and awaits it before fanning out the rest" { # The first app must run alone and be awaited before any parallel dispatch. This asserts # exactly that ordering: dispatch(first) -> wait -> rest. InModuleScope ParallelTestExecution { $script:events = [System.Collections.Generic.List[string]]::new() - Mock Get-AvailableBcTenants { @('default', 'tenant2') } + Mock Get-AvailableBcTenantInfo { + @( + [PSCustomObject]@{ Id = 'default'; DatabaseName = 'default' } + [PSCustomObject]@{ Id = 'tenant2'; DatabaseName = 'tenant2' } + ) + } Mock Get-BcContainerAppInfo { @('Big', 'Medium', 'Small') | ForEach-Object { [PSCustomObject]@{ IsInstalled = $true; Name = $_; AppId = "id-$_" } } } Mock Wait-ForFreeTenant { 'tenant2' } + Mock Get-RequiredDisabledWorkItems { @() } Mock Merge-TenantTestResults { } Mock Start-TestAppDispatch { $script:events.Add("dispatch:$AppName") } Mock Wait-ForAllTestJobs { $script:events.Add('wait'); $true } @@ -179,7 +1222,9 @@ Describe "ParallelTestExecution warmup dispatch" { InModuleScope ParallelTestExecution { $script:events = [System.Collections.Generic.List[string]]::new() - Mock Get-AvailableBcTenants { @('default') } + Mock Get-AvailableBcTenantInfo { + @([PSCustomObject]@{ Id = 'default'; DatabaseName = 'default' }) + } Mock Get-BcContainerAppInfo { @('Big', 'Medium') | ForEach-Object { [PSCustomObject]@{ IsInstalled = $true; Name = $_; AppId = "id-$_" } @@ -187,6 +1232,7 @@ Describe "ParallelTestExecution warmup dispatch" { } Mock Wait-ForFreeTenant { 'default' } Mock Merge-TenantTestResults { } + Mock Get-RequiredDisabledWorkItems { @() } Mock Start-TestAppDispatch { $script:events.Add("dispatch:$AppName") } Mock Wait-ForAllTestJobs { $script:events.Add('wait'); $true } @@ -242,13 +1288,15 @@ Describe "ParallelTestExecution warmup dispatch" { InModuleScope ParallelTestExecution { Mock Start-TestAppDispatch { } # Simulate Wait-ForAllTestJobs classifying the warmup app as a transient race. - Mock Wait-ForAllTestJobs { $State.transient = @('A'); $true } + Mock Wait-ForAllTestJobs { + $State.transient = @([PSCustomObject]@{ Key = 'A'; Tenant = 'default' }) + } $state = [PSCustomObject]@{ jobs = @(); hasFailures = $false; transient = @(); retried = @{} } $result = Invoke-WarmupDispatch -Parameters @{ containerName = 'c' } ` -Pending @('A', 'B', 'C') -AppIdByName @{ A = 'id-A'; B = 'id-B'; C = 'id-C' } ` -Tenants @('default', 'tenant2') -ScriptPath 'unused.ps1' -TestType 'Legacy' -State $state $result | Should -Be @('B', 'C') - $state.transient | Should -Contain 'A' + $state.transient.Key | Should -Contain 'A' $state.hasFailures | Should -BeFalse } } @@ -267,12 +1315,18 @@ Describe "ParallelTestExecution failed-app rerun scheduling" { $script:dispatched = [System.Collections.Generic.List[object]]::new() $script:failed = $false - Mock Get-AvailableBcTenants { @('default', 'tenant2') } + Mock Get-AvailableBcTenantInfo { + @( + [PSCustomObject]@{ Id = 'default'; DatabaseName = 'default' } + [PSCustomObject]@{ Id = 'tenant2'; DatabaseName = 'tenant2' } + ) + } Mock Get-BcContainerAppInfo { @('Big', 'Small') | ForEach-Object { [PSCustomObject]@{ IsInstalled = $true; Name = $_; AppId = "id-$_" } } } + Mock Get-RequiredDisabledWorkItems { @() } Mock Invoke-WarmupDispatch { @($Pending) } Mock Get-AppRerunBudget { 1 } Mock Wait-ForAllTestJobs { $true } @@ -282,7 +1336,12 @@ Describe "ParallelTestExecution failed-app rerun scheduling" { @('default', 'tenant2') | Where-Object { $_ -ne $excludeTenant } | Select-Object -First 1 } Mock Start-TestAppDispatch { - $script:dispatched.Add([PSCustomObject]@{ App = $AppName; Tenant = $Tenant; Suffix = $FileSuffix }) + $script:dispatched.Add([PSCustomObject]@{ + App = $AppName + Tenant = $Tenant + Suffix = $FileSuffix + SkipDisabled = $SkipAutomaticDisabledPass.IsPresent + }) # 'Big' fails on its first dispatch, exactly once. if ($AppName -eq 'Big' -and -not $script:failed) { $script:failed = $true @@ -300,6 +1359,7 @@ Describe "ParallelTestExecution failed-app rerun scheduling" { $rerun.App | Should -Be 'Big' $rerun.Tenant | Should -Be 'tenant2' $rerun.Suffix | Should -Be 'rerun1' + $rerun.SkipDisabled | Should -BeTrue # The rerun passed, so the run as a whole passed. $result | Should -BeTrue } @@ -454,7 +1514,12 @@ Describe "ParallelTestExecution rerun budget is limited to pull request builds" Set-Content -Path $staleFile -Value '' $script:raced = $false - Mock Get-AvailableBcTenants { @('default', 'tenant2') } + Mock Get-AvailableBcTenantInfo { + @( + [PSCustomObject]@{ Id = 'default'; DatabaseName = 'default' } + [PSCustomObject]@{ Id = 'tenant2'; DatabaseName = 'tenant2' } + ) + } Mock Get-BcContainerAppInfo { @([PSCustomObject]@{ IsInstalled = $true; Name = 'A'; AppId = 'id-A' }) } Mock Invoke-WarmupDispatch { @($Pending) } Mock Wait-ForAllTestJobs { } @@ -464,7 +1529,9 @@ Describe "ParallelTestExecution rerun budget is limited to pull request builds" if ($FileSuffix -and -not $script:raced) { # The rerun job hits the platform race. $script:raced = $true - $State.transient = @($State.transient) + @($AppName) + $State.transient = @($State.transient) + @( + [PSCustomObject]@{ Key = $AppName; Tenant = $Tenant } + ) } } # Pre-seed the state as though 'A' already had its rerun dispatched. @@ -475,10 +1542,11 @@ Describe "ParallelTestExecution rerun budget is limited to pull request builds" rerun = @(); rerunDone = @{}; rerunBudget = 1; tenantCount = 2 } $state.rerunDone['A'] = 'rerun1' - $state.transient = @('A') + $state.transient = @([PSCustomObject]@{ Key = 'A'; Tenant = 'tenant2' }) # Exercise just the promotion path the loop performs. - foreach ($appName in @($state.transient)) { + foreach ($transient in @($state.transient)) { + $appName = $transient.Key if ($state.rerunDone.ContainsKey($appName)) { Remove-RerunResultFile -parameters $params -suffix $state.rerunDone[$appName] } @@ -518,7 +1586,12 @@ Describe "ParallelTestExecution rerun budget is limited to pull request builds" $script:dispatched = [System.Collections.Generic.List[object]]::new() $script:failed = $false - Mock Get-AvailableBcTenants { @('default', 'tenant2') } + Mock Get-AvailableBcTenantInfo { + @( + [PSCustomObject]@{ Id = 'default'; DatabaseName = 'default' } + [PSCustomObject]@{ Id = 'tenant2'; DatabaseName = 'tenant2' } + ) + } Mock Get-BcContainerAppInfo { @('Big', 'Small') | ForEach-Object { [PSCustomObject]@{ IsInstalled = $true; Name = $_; AppId = "id-$_" } @@ -526,6 +1599,7 @@ Describe "ParallelTestExecution rerun budget is limited to pull request builds" } Mock Invoke-WarmupDispatch { @($Pending) } Mock Wait-ForAllTestJobs { $true } + Mock Get-RequiredDisabledWorkItems { @() } Mock Merge-TenantTestResults { } Mock Wait-ForFreeTenant { 'default' } Mock Start-TestAppDispatch { diff --git a/build/scripts/tests/Remove-ApiTestPassword.Test.ps1 b/build/scripts/tests/Remove-ApiTestPassword.Test.ps1 new file mode 100644 index 00000000000..e67675bf7b5 --- /dev/null +++ b/build/scripts/tests/Remove-ApiTestPassword.Test.ps1 @@ -0,0 +1,137 @@ +Describe 'API test credential cleanup' { + BeforeAll { + $script:cleanupScript = Join-Path $PSScriptRoot '..\Remove-ApiTestPassword.ps1' + $script:passwordPath = Join-Path $PSScriptRoot 'unused\ApiTestPassword' + $script:previousExitCode = Get-Variable -Name LASTEXITCODE -Scope Global -ValueOnly -ErrorAction SilentlyContinue + function docker { + param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Arguments) + $null = $Arguments + throw 'Docker must be mocked; these tests never contact a container.' + } + } + + AfterAll { + $global:LASTEXITCODE = $script:previousExitCode + } + + BeforeEach { + $script:events = [System.Collections.Generic.List[string]]::new() + Mock Test-Path { $true } + Mock docker { + $global:LASTEXITCODE = 0 + if ($Arguments[0] -eq 'container') { + 'test-container-id' + } + else { + $script:events.Add('stop') + } + } + Mock Remove-Item { $script:events.Add('delete') } + } + + It 'stops all consumers before deleting the exact backing file' { + . $script:cleanupScript -ContainerName 'test-container' -FilePath $script:passwordPath + + ($script:events -join ',') | Should -Be 'stop,delete' + Should -Invoke docker -Times 1 -Exactly -ParameterFilter { + $Arguments[0] -eq 'container' -and $Arguments -contains '--all' -and + $Arguments -contains 'name=^/test-container$' + } + Should -Invoke Remove-Item -Times 1 -Exactly -ParameterFilter { + $LiteralPath -eq $script:passwordPath -and $Force -and $ErrorAction -eq 'Stop' + } + } + + It 'does nothing when the password file is missing' { + Mock Test-Path { $false } + + . $script:cleanupScript -ContainerName 'test-container' -FilePath $script:passwordPath + + Should -Invoke docker -Times 0 + Should -Invoke Remove-Item -Times 0 + } + + It 'removes a leftover backing file when the container is already absent' { + Mock docker { $global:LASTEXITCODE = 0 } + + . $script:cleanupScript -ContainerName 'test-container' -FilePath $script:passwordPath + + ($script:events -join ',') | Should -Be 'delete' + Should -Invoke docker -Times 0 -ParameterFilter { $Arguments[0] -eq 'stop' } + } + + It 'also stops an already-stopped container idempotently before deletion' { + . $script:cleanupScript -ContainerName 'test-container' -FilePath $script:passwordPath + + Should -Invoke docker -Times 1 -Exactly -ParameterFilter { + ($Arguments -join ' ') -eq 'stop --time 30 test-container-id' + } + Should -Invoke Remove-Item -Times 1 -Exactly + } + + It 'fails without deleting when Docker cannot enumerate containers' { + Mock docker { $global:LASTEXITCODE = 1 } + + { . $script:cleanupScript -ContainerName 'test-container' -FilePath $script:passwordPath } | + Should -Throw '*Could not determine*' + Should -Invoke Remove-Item -Times 0 + } + + It 'fails without deleting while consumers cannot be stopped' { + Mock docker { $global:LASTEXITCODE = 1 } -ParameterFilter { $Arguments[0] -eq 'stop' } + + { . $script:cleanupScript -ContainerName 'test-container' -FilePath $script:passwordPath } | + Should -Throw '*Could not stop API test consumers*' + Should -Invoke Remove-Item -Times 0 + } + + It 'fails without stopping or deleting if container identity is ambiguous' { + Mock docker { + $global:LASTEXITCODE = 0 + 'first-id', 'second-id' + } + + { . $script:cleanupScript -ContainerName 'test-container' -FilePath $script:passwordPath } | + Should -Throw '*more than one container*' + Should -Invoke docker -Times 0 -ParameterFilter { $Arguments[0] -eq 'stop' } + Should -Invoke Remove-Item -Times 0 + } + + It 'surfaces deletion failure rather than reporting successful cleanup' { + Mock Remove-Item { throw 'Access denied' } + + { . $script:cleanupScript -ContainerName 'test-container' -FilePath $script:passwordPath } | + Should -Throw '*Access denied*' + } + + It 'rejects a path that does not name the credential file' { + { . $script:cleanupScript -ContainerName 'test-container' -FilePath (Join-Path $TestDrive 'other-file') } | + Should -Throw '*exact ApiTestPassword file*' + Should -Invoke docker -Times 0 + Should -Invoke Remove-Item -Times 0 + } +} + +Describe 'API test credential workflow lifetime' { + BeforeAll { + $script:workflow = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\..\.github\workflows\_BuildALGoProject.yaml') -Raw + $script:setup = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\NewBcContainer.ps1') -Raw + } + + It 'delegates credential materialization without host or container staging copies' { + $script:setup | Should -Match "Import-Module .*'ApiTestCredential.psm1'" + $script:setup | Should -Match 'Write-ApiTestPassword -ContainerName \$parameters.ContainerName -Credential \$parameters.credential' + $script:setup | Should -Not -Match 'Copy-FileToBcContainer|WriteAllText|GetNetworkCredential|GetTempPath' + } + + It 'runs cleanup on success, failure and cancellation after all build consumers' { + $cleanupStep = [regex]::Match($script:workflow, '(?ms)^ - name: Remove API test credential\r?\n.*?(?=^ - name: Cleanup)').Value + $cleanupStep | Should -Match "if: always\(\) && steps\.DetermineBuildProject\.outputs\.BuildIt == 'True' && env\.BCAppsApiTestPasswordPath != ''" + $cleanupStep | Should -Match 'timeout-minutes: 2' + $cleanupStep | Should -Match 'Remove-ApiTestPassword.ps1' + $cleanupStep | Should -Not -Match 'continue-on-error' + $script:workflow.IndexOf('- name: Build') | + Should -BeLessThan $script:workflow.IndexOf('- name: Remove API test credential') + $script:workflow | Should -Match '(?s)- name: Cleanup\r?\n\s+if: always\(\).*?uses: microsoft/AL-Go/Actions/PipelineCleanup@' + } +} From 564bc06868496639548ea83b9ed6a654621ad9f7 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Fri, 25 Sep 2026 11:44:18 +0200 Subject: [PATCH 02/22] Remove relocated auth source-pattern checks from workflow tests The historical workflow patch relocated the original core checks. Remove that duplicate while retaining the behavioral workflow tests and downstream AL adoption metadata assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .../tests/ParallelTestExecution.Test.ps1 | 54 ------------------- 1 file changed, 54 deletions(-) diff --git a/build/scripts/tests/ParallelTestExecution.Test.ps1 b/build/scripts/tests/ParallelTestExecution.Test.ps1 index ce3f2482a3a..0997abdffb5 100644 --- a/build/scripts/tests/ParallelTestExecution.Test.ps1 +++ b/build/scripts/tests/ParallelTestExecution.Test.ps1 @@ -547,60 +547,6 @@ Describe "ParallelTestExecution clean tenant scheduling" { } } - Describe "API test authentication extensibility" { - BeforeAll { - $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path - $testLibraryRoot = Join-Path $repoRoot 'src\Layers\W1\Tests\TestLibraries' - $script:authenticationInterface = Get-Content (Join-Path $testLibraryRoot 'APITestAuthProvider.Interface.al') -Raw - $script:authenticationEnum = Get-Content (Join-Path $testLibraryRoot 'APITestAuthentication.Enum.al') -Raw - $script:authenticationContext = Get-Content (Join-Path $testLibraryRoot 'APITestAuthContext.Codeunit.al') -Raw - $script:graphManagement = Get-Content (Join-Path $testLibraryRoot 'LibraryGraphMgt.Codeunit.al') -Raw - $script:microsoftProvider = Get-Content (Join-Path $testLibraryRoot 'MicrosoftTestAuthProvider.Codeunit.al') -Raw - } - - It "exposes an extensible provider contract with a no-auth default" { - $script:authenticationInterface | Should -Match 'interface\s+"API Test Auth Provider"' - $script:authenticationInterface | Should -Match 'ConfigureAuthentication\(var\s+Authentication:\s*Codeunit\s+"API Test Auth Context"\)' - $script:authenticationInterface | Should -Not -Match 'TargetURL' - $script:authenticationEnum | Should -Match 'Extensible\s*=\s*true\s*;' - $script:authenticationEnum | Should -Match 'DefaultImplementation\s*=\s*"API Test Auth Provider"\s*=\s*"No API Test Auth Provider"\s*;' - $script:authenticationEnum | Should -Match 'value\(1;\s*"Microsoft Test Environment"\)' - $script:authenticationEnum | Should -Not -Match 'UnknownValueImplementation' - } - - It "keeps request mutation inside the authentication context" { - $script:authenticationContext | Should -Match 'procedure\s+SetBasicAuthentication\(UserName:\s*Text;\s*Password:\s*SecretText\)' - $script:authenticationContext | Should -Match 'HttpWebRequestMgt\.AddBasicAuthentication\(BasicUserName,\s*BasicPassword\)' - $script:microsoftProvider | Should -Not -Match 'HttpWebRequestMgt' - $script:microsoftProvider | Should -Match 'Authentication\.SetBasicAuthentication\(UserId\(\),\s*GetAuthenticationPassword\(\)\)' - $script:microsoftProvider | Should -Not -Match 'TargetURL|IsCurrentTestServiceURL' - } - - It "applies the selected provider after URL rewriting and before the final request event" { - $script:graphManagement | Should -Match 'AuthenticationProviderResolved:\s*Boolean\s*;' - $script:graphManagement | Should -Match 'HttpWebRequestMgt\.Initialize\(TargetURL\);\s*ApplyAuthentication\(HttpWebRequestMgt\);\s*OnAfterInitializeWebRequestWithURL\(HttpWebRequestMgt\);' - $script:graphManagement | Should -Match 'ConfigureAuthentication\(AuthenticationContext\)' - $script:graphManagement | Should -Not -Match 'Microsoft Test Auth Provider' - } - - It "preserves the provider instance when initialization selects the same provider again" { - $script:graphManagement | Should -Match 'if AuthenticationProviderResolved and \(Authentication = NewAuthentication\) then\s*exit;\s*Authentication := NewAuthentication;' - } - - It "checks platform authentication before reading credentials without querying the User table" { - $script:microsoftProvider | Should -Match 'SecurityGroup:\s*Codeunit "Security Group";' - $script:microsoftProvider | Should -Match 'if EnvironmentInfo\.IsSaaSInfrastructure\(\) then\s*exit;\s*if SecurityGroup\.IsWindowsAuthentication\(\) then\s*exit;\s*Authentication\.SetBasicAuthentication' - $script:microsoftProvider | Should -Not -Match 'Record User|Windows Security ID|User\.Get\(' - } - - It "caches only a successfully resolved password and keeps container precedence over Key Vault" { - $script:microsoftProvider | Should -Match 'if not CachedAuthenticationPassword\.IsEmpty\(\) then\s*exit\(CachedAuthenticationPassword\);' - $script:microsoftProvider | Should -Match 'if ContainerPasswordFileExists\(\) then begin\s*if not TryGetContainerPassword\(Password\) then\s*Error\(ContainerPasswordReadErr, ApiTestPasswordFileTok\);\s*end else\s*if not TryGetNavEnlistmentPassword\(Password\) then\s*Error\(KeyVaultPasswordReadErr, NavServerUserPasswordKeyTok\);\s*CachedAuthenticationPassword := Password;' - $script:microsoftProvider | Should -Not -Match 'AuthenticationRequired|AuthenticationResolved|CacheAuthenticationResult' - } - - } - It "defers the automatic Unit disabled pass only for clean-tenant apps" { InModuleScope ParallelTestExecution { $script:skipValues = [System.Collections.Generic.List[bool]]::new() From 6e1d8bb57bcb80b930c2d50499ad8060092c19a6 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Fri, 25 Sep 2026 17:48:13 +0200 Subject: [PATCH 03/22] Keep clean-codeunit lanes opt-in until authentication uptake Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- build/scripts/ParallelTestExecution.psm1 | 17 +++-- .../tests/ParallelTestExecution.Test.ps1 | 62 +++++++++++++++++-- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/build/scripts/ParallelTestExecution.psm1 b/build/scripts/ParallelTestExecution.psm1 index 28ea00281c2..485538d7d2f 100644 --- a/build/scripts/ParallelTestExecution.psm1 +++ b/build/scripts/ParallelTestExecution.psm1 @@ -1344,7 +1344,14 @@ function Invoke-ParallelTestExecution { $tenantInfo | Where-Object { $_.Id -ne $parameters.tenant } ) - if ($testType -ne 'Legacy' -and $tenantInfo.Count -gt 1) { + # Enable only with the AL authentication uptake; infrastructure alone must not add test lanes. + $cleanCodeunitExecution = (Get-ALGoSetting -Key 'enableCleanTestCodeunitExecution') -eq $true + $cleanTenantAppNames = @() + $requiredDisabledWorkItems = @() + if ($cleanCodeunitExecution) { + $cleanTenantAppNames = $appNamesToTest + } + if ($cleanCodeunitExecution -and $testType -ne 'Legacy' -and $tenantInfo.Count -gt 1) { $sourceTenantInfo = @($tenantInfo | Where-Object { $_.Id -eq $parameters.tenant }) if ($sourceTenantInfo.Count -ne 1 -or [string]::IsNullOrWhiteSpace($sourceTenantInfo[0].DatabaseName)) { throw "Could not determine the database name for source tenant '$($parameters.tenant)'." @@ -1372,7 +1379,7 @@ function Invoke-ParallelTestExecution { Reset-BcTestTenant -ContainerName $parameters.containerName -Tenant $discoveryTenant.Id ` -TenantDatabaseName $discoveryTenant.DatabaseName -TemplateDatabaseName $sourceTenantInfo[0].DatabaseName } - } else { + } elseif ($cleanCodeunitExecution) { $requiredDisabledWorkItems = @( Get-RequiredDisabledWorkItems -Parameters $parameters -TestType $testType ` -AppNamesToTest $appNamesToTest -AppIdByName $appIdByName @@ -1427,7 +1434,7 @@ function Invoke-ParallelTestExecution { # No-op for single-app/single-tenant. $pending = @(Invoke-WarmupDispatch -Parameters $parameters -Pending $pending -AppIdByName $appIdByName ` -Tenants $tenants -ScriptPath $scriptPath -TestType $testType -State $state ` - -CleanTenantAppNames $appNamesToTest) + -CleanTenantAppNames $cleanTenantAppNames) $rerunSuffixes = @() @@ -1469,7 +1476,7 @@ function Invoke-ParallelTestExecution { Start-TestAppDispatch -Parameters $parameters -AppName $rerunItem.appName -AppId $appIdByName[$rerunItem.appName] ` -Tenant $tenant -ScriptPath $scriptPath -TestType $testType -State $state ` -Verb 'Re-running' -FileSuffix $rerunItem.suffix ` - -SkipAutomaticDisabledPass + -SkipAutomaticDisabledPass:$cleanCodeunitExecution continue } @@ -1491,7 +1498,7 @@ function Invoke-ParallelTestExecution { } Start-TestAppDispatch -Parameters $parameters -AppName $appName -AppId $appId -Tenant $tenant ` -ScriptPath $scriptPath -TestType $testType -State $state ` - -SkipAutomaticDisabledPass -Verb $verb + -SkipAutomaticDisabledPass:$cleanCodeunitExecution -Verb $verb } else { # Nothing left to dispatch; drain any still-running jobs. New transient failures and # reruns discovered here are picked up at the top of the next loop iteration. diff --git a/build/scripts/tests/ParallelTestExecution.Test.ps1 b/build/scripts/tests/ParallelTestExecution.Test.ps1 index 0997abdffb5..31d01885e67 100644 --- a/build/scripts/tests/ParallelTestExecution.Test.ps1 +++ b/build/scripts/tests/ParallelTestExecution.Test.ps1 @@ -31,6 +31,11 @@ if (-not (Get-Command Invoke-ScriptInBcContainer -ErrorAction SilentlyContinue)) Import-Module (Join-Path $PSScriptRoot '../ParallelTestExecution.psm1') -Force Describe "ParallelTestExecution app-name resolution" { + BeforeEach { + Mock -ModuleName ParallelTestExecution Get-ALGoSetting { $true } -ParameterFilter { + $Key -eq 'enableCleanTestCodeunitExecution' + } + } BeforeAll { # Get-BcContainerAppInfo comes from BcContainerHelper, which is present when the module # runs inside a BC container but is NOT loaded in the "Run PS Tests" runner. Pester cannot @@ -431,6 +436,9 @@ Describe "ParallelTestExecution clean tenant scheduling" { } BeforeEach { + Mock -ModuleName ParallelTestExecution Get-ALGoSetting { $true } -ParameterFilter { + $Key -eq 'enableCleanTestCodeunitExecution' + } Mock -ModuleName ParallelTestExecution Invoke-ScriptInBcContainer { throw 'Unexpected unmocked container access.' } @@ -896,6 +904,37 @@ Describe "ParallelTestExecution clean tenant scheduling" { } } + It "preserves ordinary execution when clean-codeunit activation is " -ForEach @( + @{ Lane = 'IntegrationTest'; Activation = 'absent'; Enabled = $null } + @{ Lane = 'IntegrationTest'; Activation = 'false'; Enabled = $false } + @{ Lane = 'UnitTest'; Activation = 'absent'; Enabled = $null } + @{ Lane = 'UnitTest'; Activation = 'false'; Enabled = $false } + @{ Lane = 'Legacy'; Activation = 'absent'; Enabled = $null } + @{ Lane = 'Legacy'; Activation = 'false'; Enabled = $false } + ) { + InModuleScope ParallelTestExecution -Parameters @{ Lane = $Lane; Enabled = $Enabled } { + $script:activationUnderTest = $Enabled + Mock Get-ALGoSetting { $script:activationUnderTest } -ParameterFilter { + $Key -eq 'enableCleanTestCodeunitExecution' + } + Invoke-ParallelTestExecution -parameters $script:fixtureParameters -scriptPath 'unused.ps1' ` + -testType $Lane -appNamesToTest @('Tests') | Should -BeTrue + + $script:fixtureEvents | Should -Be @('warmup', 'ordinary:primary') + Should -Invoke Get-RequiredDisabledWorkItems -Times 0 + Should -Invoke Reset-BcTestTenant -Times 0 + Should -Invoke New-BcTestTenantTemplate -Times 0 + Should -Invoke Invoke-RequiredDisabledTestExecution -Times 0 + Should -Invoke Invoke-WarmupDispatch -Times 1 -Exactly -ParameterFilter { + $CleanTenantAppNames.Count -eq 0 + } + Should -Invoke Start-TestAppDispatch -Times 1 -Exactly -ParameterFilter { + -not $SkipAutomaticDisabledPass + } + ($script:fixtureParameters | ConvertTo-Json -Depth 5) | Should -Be $script:originalFixtureParameters + } + } + It "restores secondary discovery before copying the pristine primary and dispatching tests" { InModuleScope ParallelTestExecution { Invoke-ParallelTestExecution -parameters $script:fixtureParameters -scriptPath 'unused.ps1' ` @@ -912,6 +951,12 @@ Describe "ParallelTestExecution clean tenant scheduling" { $Parameters.tenant -eq 'worker-a' -and $TestType -eq 'UnitTest' -and $AppNamesToTest.Count -eq 1 -and $AppIdByName.Tests -eq 'tests-id' } + Should -Invoke Invoke-WarmupDispatch -Times 1 -Exactly -ParameterFilter { + $CleanTenantAppNames.Count -eq 1 -and $CleanTenantAppNames[0] -eq 'Tests' + } + Should -Invoke Start-TestAppDispatch -Times 1 -Exactly -ParameterFilter { + $SkipAutomaticDisabledPass + } Should -Invoke Invoke-ScriptInBcContainer -Times 0 } } @@ -1253,11 +1298,20 @@ Describe "ParallelTestExecution failed-app rerun scheduling" { Import-Module (Join-Path $PSScriptRoot '../ParallelTestExecution.psm1') -Force } - It "re-runs a failed app on a different tenant than the one it failed on" { + It "re-runs a failed app on a different tenant with clean activation " -ForEach @( + @{ Lane = 'Legacy'; Enabled = $false } + @{ Lane = 'UnitTest'; Enabled = $false } + @{ Lane = 'UnitTest'; Enabled = $true } + ) { # A failed app is retried once, and never on the tenant it failed on: tests are not # guaranteed to clean up after themselves, so residue from the failed run could # re-trigger the same failure and make the retry worthless. - InModuleScope ParallelTestExecution { + InModuleScope ParallelTestExecution -Parameters @{ Lane = $Lane; Enabled = $Enabled } { + $script:activationUnderTest = $Enabled + Mock Get-ALGoSetting { $script:activationUnderTest } -ParameterFilter { + $Key -eq 'enableCleanTestCodeunitExecution' + } + Mock Reset-BcTestTenant { } $script:dispatched = [System.Collections.Generic.List[object]]::new() $script:failed = $false @@ -1299,13 +1353,13 @@ Describe "ParallelTestExecution failed-app rerun scheduling" { $params = @{ containerName = "ut-$([guid]::NewGuid().ToString('N'))"; tenant = 'default' } $result = Invoke-ParallelTestExecution -parameters $params -scriptPath 'unused.ps1' ` - -testType 'Legacy' -appNamesToTest @('Big', 'Small') + -testType $Lane -appNamesToTest @('Big', 'Small') $rerun = $script:dispatched | Where-Object { $_.Suffix } $rerun.App | Should -Be 'Big' $rerun.Tenant | Should -Be 'tenant2' $rerun.Suffix | Should -Be 'rerun1' - $rerun.SkipDisabled | Should -BeTrue + $rerun.SkipDisabled | Should -Be $Enabled # The rerun passed, so the run as a whole passed. $result | Should -BeTrue } From 156bca035d09086d82c3b92470b4c55d53f6231b Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Fri, 2 Oct 2026 13:58:56 +0200 Subject: [PATCH 04/22] Do not classify ordinary API HTTP500 failures as platform races Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- build/scripts/ParallelTestExecution.psm1 | 3 +- .../tests/ParallelTestExecution.Test.ps1 | 66 ++++++++++++++++++- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/build/scripts/ParallelTestExecution.psm1 b/build/scripts/ParallelTestExecution.psm1 index 485538d7d2f..5ab0f3ecce7 100644 --- a/build/scripts/ParallelTestExecution.psm1 +++ b/build/scripts/ParallelTestExecution.psm1 @@ -590,8 +590,7 @@ function Test-TransientTestFailure { return [bool]( ($Output -match 'TRANSIENT TEST PLATFORM RACE') -or ($Output -match 'Cannot open page 130455|InvokeInteractions failed with status code 500|InteractionManager\.cs:line \d+') -or - ($Output -match '(?s)ObjName:Command Line Test Tool.*MethodName:ExtensionId_a45_OnValidate.*(?:Offset and length were out of bounds|Nullable object must have a value)') -or - ($Output -match '(?s)GET request failed\..*Response code is 500.*Object reference not set to an instance of an object') + ($Output -match '(?s)ObjName:Command Line Test Tool.*MethodName:ExtensionId_a45_OnValidate.*(?:Offset and length were out of bounds|Nullable object must have a value)') ) } diff --git a/build/scripts/tests/ParallelTestExecution.Test.ps1 b/build/scripts/tests/ParallelTestExecution.Test.ps1 index 31d01885e67..d700451a80d 100644 --- a/build/scripts/tests/ParallelTestExecution.Test.ps1 +++ b/build/scripts/tests/ParallelTestExecution.Test.ps1 @@ -263,7 +263,6 @@ Describe "ParallelTestExecution transient retry scheduling" { "InvokeInteractions failed with status code 500" "at InteractionManager.InvokeInteractions in InteractionManager.cs:line 203" "Cannot open page 130455`nClientSession State is InError (Wait time 25 seconds)" - "GET request failed. Response code is 500 (InternalServerError), expected code is 200. Error message: Object reference not set to an instance of an object." ) | ForEach-Object { Test-TransientTestFailure -Output $_ | Should -BeTrue } @@ -281,6 +280,10 @@ Describe "ParallelTestExecution transient retry scheduling" { 'Cannot open page 130456' 'InvokeInteractions failed with status code 403' 'Nullable object must have a value.' + 'Object reference not set to an instance of an object.' + 'Assertion failed: expected 2 but was 1' + 'GET request failed. Response code is 500 (InternalServerError), expected code is 200. Error message: Object reference not set to an instance of an object.' + "Opened page 130455 successfully.`nGET request failed. Response code is 500 (InternalServerError), expected code is 200. Error message: Object reference not set to an instance of an object." ) | ForEach-Object { Test-TransientTestFailure -Output $_ | Should -BeFalse } @@ -299,9 +302,11 @@ Describe "ParallelTestExecution transient retry scheduling" { Mock Receive-Job { "Assertion failed`nClientSession State is InError" } -RemoveParameterType Job (Receive-TestJobResult -Entry $entry -Job $job -Retried @{}).Outcome | Should -Be 'Failed' + Mock Receive-Job { 'GET request failed. Response code is 500 (InternalServerError), expected code is 200. Error message: Object reference not set to an instance of an object.' } -RemoveParameterType Job + (Receive-TestJobResult -Entry $entry -Job $job -Retried @{}).Outcome | Should -Be 'Failed' $job.State = 'Completed' (Receive-TestJobResult -Entry $entry -Job $job -Retried @{}).Outcome | Should -Be 'Passed' - Should -Invoke Remove-Job -Times 4 -Exactly + Should -Invoke Remove-Job -Times 5 -Exactly } } @@ -1148,6 +1153,63 @@ Describe "ParallelTestExecution clean tenant scheduling" { } } +Describe "RunTestsInBcContainer platform-race transcript classification" { + BeforeAll { + Import-Module (Join-Path $PSScriptRoot '../ParallelTestExecution.psm1') -Force + $tokens = $null + $parseErrors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseFile( + (Join-Path $PSScriptRoot '../RunTestsInBcContainer.ps1'), [ref]$tokens, [ref]$parseErrors) + $parseErrors.Count | Should -Be 0 + $function = $ast.Find({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and + $node.Name -eq 'Invoke-RunTestsWithCancellationDetection' + }, $true) + . ([scriptblock]::Create($function.Extent.Text)) + } + + BeforeEach { + $script:transcriptPath = $null + Mock Start-Transcript { + $script:transcriptPath = $Path + Set-Content -LiteralPath $Path -Value $script:transcript + } + Mock Stop-Transcript {} + Mock Run-TestsInBcContainer { $false } + Mock Write-Host {} + } + + AfterEach { + $script:transcriptPath | Should -Not -BeNullOrEmpty + Test-Path -LiteralPath $script:transcriptPath | Should -BeFalse + } + + It "does not emit a platform marker for an ordinary API failure or unrelated page mention" -ForEach @( + @{ Transcript = 'GET request failed. Response code is 500 (InternalServerError), expected code is 200. Error message: Object reference not set to an instance of an object.' } + @{ Transcript = "Opened page 130455 successfully.`nGET request failed. Response code is 500 (InternalServerError), expected code is 200. Error message: Object reference not set to an instance of an object." } + @{ Transcript = 'Assertion failed: expected 2 but was 1' } + ) { + $script:transcript = $Transcript + Invoke-RunTestsWithCancellationDetection -parameters @{ appName = 'API Tests'; tenant = 'default' } | + Should -BeFalse + Should -Invoke Write-Host -Times 0 -ParameterFilter { $Object -like '*TRANSIENT TEST PLATFORM RACE*' } + } + + It "emits the platform marker for established runtime races" -ForEach @( + @{ Transcript = 'Cannot open page 130455' } + @{ Transcript = 'InvokeInteractions failed with status code 500' } + @{ Transcript = 'at InteractionManager.InvokeInteractions in InteractionManager.cs:line 203' } + @{ Transcript = "ObjName:Command Line Test Tool, ObjID:130455, Type:Form, MethodName:ExtensionId_a45_OnValidate`nOffset and length were out of bounds for the array" } + @{ Transcript = "ObjName:Command Line Test Tool, ObjID:130455, Type:Form, MethodName:ExtensionId_a45_OnValidate`nNullable object must have a value." } + ) { + $script:transcript = $Transcript + Invoke-RunTestsWithCancellationDetection -parameters @{ appName = 'API Tests'; tenant = 'default' } | + Should -BeFalse + Should -Invoke Write-Host -Times 1 -Exactly -ParameterFilter { $Object -like '*TRANSIENT TEST PLATFORM RACE*' } + } +} + Describe "ParallelTestExecution warmup dispatch" { BeforeAll { Import-Module (Join-Path $PSScriptRoot '../ParallelTestExecution.psm1') -Force From decc1212af3229b36933dce909da5cf79d7ead82 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Fri, 2 Oct 2026 14:05:46 +0200 Subject: [PATCH 05/22] Do not classify ordinary API HTTP500 failures as platform races Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- build/scripts/tests/ParallelTestExecution.Test.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build/scripts/tests/ParallelTestExecution.Test.ps1 b/build/scripts/tests/ParallelTestExecution.Test.ps1 index d700451a80d..53fbe2acad9 100644 --- a/build/scripts/tests/ParallelTestExecution.Test.ps1 +++ b/build/scripts/tests/ParallelTestExecution.Test.ps1 @@ -1156,6 +1156,11 @@ Describe "ParallelTestExecution clean tenant scheduling" { Describe "RunTestsInBcContainer platform-race transcript classification" { BeforeAll { Import-Module (Join-Path $PSScriptRoot '../ParallelTestExecution.psm1') -Force + function Run-TestsInBcContainer { + param([string]$appName, [string]$tenant) + $null = $appName, $tenant + throw 'Container test execution must be mocked.' + } $tokens = $null $parseErrors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile( From a439380184d543898ee04cb12a3137f10abe8f46 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Fri, 2 Oct 2026 14:17:43 +0200 Subject: [PATCH 06/22] Move API test credential cleanup to supported PipelineFinalize hook Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .github/workflows/_BuildALGoProject.yaml | 6 - .../Test Apps AT/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps AU/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps BE/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps CA/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps CH/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps CZ/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps DE/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps DK/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps ES/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps FI/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps FR/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps GB/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps IN/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps IS/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps IT/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps MX/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps NL/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps NO/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps NZ/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps RU/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps SE/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps US/.AL-Go/PipelineFinalize.ps1 | 1 + .../Test Apps W1/.AL-Go/PipelineFinalize.ps1 | 1 + build/scripts/ApiTestCredential.psm1 | 15 +- build/scripts/PipelineFinalize.ps1 | 13 ++ build/scripts/Remove-ApiTestPassword.ps1 | 3 +- .../Update-TestProjectPipelineFinalize.ps1 | 31 ++++ .../scripts/tests/ApiTestCredential.Test.ps1 | 12 +- .../tests/Remove-ApiTestPassword.Test.ps1 | 135 ++++++++++++++++-- 30 files changed, 216 insertions(+), 22 deletions(-) create mode 100644 build/projects/Test Apps AT/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps AU/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps BE/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps CA/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps CH/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps CZ/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps DE/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps DK/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps ES/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps FI/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps FR/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps GB/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps IN/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps IS/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps IT/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps MX/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps NL/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps NO/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps NZ/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps RU/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps SE/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps US/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/projects/Test Apps W1/.AL-Go/PipelineFinalize.ps1 create mode 100644 build/scripts/PipelineFinalize.ps1 create mode 100644 build/scripts/Update-TestProjectPipelineFinalize.ps1 diff --git a/.github/workflows/_BuildALGoProject.yaml b/.github/workflows/_BuildALGoProject.yaml index 2ddb7ae37f4..7d8c4d8af9c 100644 --- a/.github/workflows/_BuildALGoProject.yaml +++ b/.github/workflows/_BuildALGoProject.yaml @@ -379,12 +379,6 @@ jobs: project: ${{ inputs.project }} testType: "pageScripting" - - name: Remove API test credential - if: always() && steps.DetermineBuildProject.outputs.BuildIt == 'True' && env.BCAppsApiTestPasswordPath != '' - timeout-minutes: 2 - run: | - & (Join-Path $env:GITHUB_WORKSPACE 'build/scripts/Remove-ApiTestPassword.ps1') -ContainerName $env:containerName -FilePath $env:BCAppsApiTestPasswordPath - - name: Cleanup if: always() && steps.DetermineBuildProject.outputs.BuildIt == 'True' uses: microsoft/AL-Go/Actions/PipelineCleanup@91b96c2b294be6f823277dafe6f03350abfb9d23 diff --git a/build/projects/Test Apps AT/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps AT/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps AT/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps AU/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps AU/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps AU/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps BE/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps BE/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps BE/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps CA/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps CA/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps CA/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps CH/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps CH/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps CH/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps CZ/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps CZ/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps CZ/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps DE/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps DE/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps DE/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps DK/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps DK/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps DK/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps ES/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps ES/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps ES/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps FI/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps FI/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps FI/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps FR/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps FR/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps FR/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps GB/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps GB/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps GB/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps IN/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps IN/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps IN/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps IS/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps IS/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps IS/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps IT/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps IT/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps IT/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps MX/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps MX/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps MX/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps NL/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps NL/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps NL/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps NO/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps NO/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps NO/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps NZ/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps NZ/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps NZ/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps RU/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps RU/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps RU/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps SE/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps SE/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps SE/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps US/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps US/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps US/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/projects/Test Apps W1/.AL-Go/PipelineFinalize.ps1 b/build/projects/Test Apps W1/.AL-Go/PipelineFinalize.ps1 new file mode 100644 index 00000000000..a726947778c --- /dev/null +++ b/build/projects/Test Apps W1/.AL-Go/PipelineFinalize.ps1 @@ -0,0 +1 @@ +. (Join-Path $PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve) diff --git a/build/scripts/ApiTestCredential.psm1 b/build/scripts/ApiTestCredential.psm1 index 8f0737a05dd..5983d23d783 100644 --- a/build/scripts/ApiTestCredential.psm1 +++ b/build/scripts/ApiTestCredential.psm1 @@ -43,11 +43,11 @@ function New-ApiTestPasswordFileStream { .SYNOPSIS Creates the API test credential directly in the container's shared my mount. .DESCRIPTION - Registers workflow cleanup before atomically creating a new file with a protected + Records cleanup identity before atomically creating a new file with a protected DACL. No plaintext staging copies are made. SYSTEM and Administrators retain full access, NetworkService can read, and the host writer can write and delete. - An existing file or unresolved mount is an error, including outside CI. Workflow - cleanup (or local container teardown) must end consumers before removing the file. + An existing file or unresolved mount is an error, including outside CI. Successful + pipelines finalize cleanup; failed/cancelled pipelines rely on container teardown. #> function Write-ApiTestPassword { param( @@ -65,9 +65,14 @@ function Write-ApiTestPassword { throw "Cannot resolve the API test credential's container mount." } $filePath = Join-Path $myFolders[0].Key 'ApiTestPassword' + # PipelineFinalize runs in this process; GITHUB_ENV only affects later steps. + $env:BCAppsApiTestPasswordPath = $filePath + $env:BCAppsApiTestPasswordContainer = $ContainerName if ($env:GITHUB_ENV) { - # Persist cleanup before creation, including for setup failure or cancellation. - Add-Content -LiteralPath $env:GITHUB_ENV -Encoding UTF8 -Value "BCAppsApiTestPasswordPath=$filePath" -ErrorAction Stop + Add-Content -LiteralPath $env:GITHUB_ENV -Encoding UTF8 -Value @( + "BCAppsApiTestPasswordPath=$filePath" + "BCAppsApiTestPasswordContainer=$ContainerName" + ) -ErrorAction Stop } $created = $false diff --git a/build/scripts/PipelineFinalize.ps1 b/build/scripts/PipelineFinalize.ps1 new file mode 100644 index 00000000000..006bdc54b80 --- /dev/null +++ b/build/scripts/PipelineFinalize.ps1 @@ -0,0 +1,13 @@ +$ErrorActionPreference = 'Stop' + +# This hook is success-only. Failures/cancellation rely on normal container teardown, +# not guaranteed after runner loss. CI credentials are per-run; other callers may differ. +if (-not $env:BCAppsApiTestPasswordPath) { + return +} + +Write-Host 'API test credential cleanup (PipelineFinalize).' +& (Join-Path $PSScriptRoot 'Remove-ApiTestPassword.ps1') ` + -ContainerName $env:BCAppsApiTestPasswordContainer -FilePath $env:BCAppsApiTestPasswordPath +$env:BCAppsApiTestPasswordPath = $null +$env:BCAppsApiTestPasswordContainer = $null diff --git a/build/scripts/Remove-ApiTestPassword.ps1 b/build/scripts/Remove-ApiTestPassword.ps1 index 72da4aee86c..4c49720fe99 100644 --- a/build/scripts/Remove-ApiTestPassword.ps1 +++ b/build/scripts/Remove-ApiTestPassword.ps1 @@ -15,8 +15,7 @@ if (-not (Test-Path -LiteralPath $FilePath -PathType Leaf)) { return } -# Workflow cancellation can leave server-side test workers alive. End all consumers -# before deleting their shared credential, including when the build step was killed. +# End any remaining test workers before deleting their shared credential. $containerIds = @(docker container ls --all --filter "name=^/$([regex]::Escape($ContainerName))$" --format '{{.ID}}') if ($LASTEXITCODE -ne 0) { throw 'Could not determine whether the API test container still exists.' diff --git a/build/scripts/Update-TestProjectPipelineFinalize.ps1 b/build/scripts/Update-TestProjectPipelineFinalize.ps1 new file mode 100644 index 00000000000..56149f14a3d --- /dev/null +++ b/build/scripts/Update-TestProjectPipelineFinalize.ps1 @@ -0,0 +1,31 @@ +<# +.SYNOPSIS + Generates the success-only finalizer override for projects using shared container setup. +.DESCRIPTION + Run with -Check to validate committed wrappers without modifying them. +#> +param( + [string]$ProjectsPath = (Join-Path $PSScriptRoot '..\projects'), + [switch]$Check +) + +$ErrorActionPreference = 'Stop' +$wrapper = ". (Join-Path `$PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve)`n" +$projects = @(Get-ChildItem -LiteralPath $ProjectsPath -Directory | Where-Object { + Test-Path -LiteralPath (Join-Path $_.FullName '.AL-Go\NewBcContainer.ps1') -PathType Leaf +}) +if ($projects.Count -eq 0) { + throw 'No test container project overrides were found.' +} +foreach ($project in $projects) { + $path = Join-Path $project.FullName '.AL-Go\PipelineFinalize.ps1' + $current = if (Test-Path -LiteralPath $path -PathType Leaf) { + [IO.File]::ReadAllText($path).Replace("`r`n", "`n") + } else { '' } + if ($current -ne $wrapper) { + if ($Check) { + throw "PipelineFinalize override is missing or outdated for $($project.Name). Run Update-TestProjectPipelineFinalize.ps1." + } + [IO.File]::WriteAllText($path, $wrapper, [Text.UTF8Encoding]::new($false)) + } +} diff --git a/build/scripts/tests/ApiTestCredential.Test.ps1 b/build/scripts/tests/ApiTestCredential.Test.ps1 index 548c38fc562..f47051ac4e3 100644 --- a/build/scripts/tests/ApiTestCredential.Test.ps1 +++ b/build/scripts/tests/ApiTestCredential.Test.ps1 @@ -2,6 +2,8 @@ Describe 'API test credential materialization' { BeforeAll { Import-Module (Join-Path $PSScriptRoot '..\ApiTestCredential.psm1') -Force $script:previousGitHubEnv = $env:GITHUB_ENV + $script:previousPasswordPath = $env:BCAppsApiTestPasswordPath + $script:previousPasswordContainer = $env:BCAppsApiTestPasswordContainer $script:previousExitCode = Get-Variable LASTEXITCODE -Scope Global -ValueOnly -ErrorAction SilentlyContinue $script:createdMountStub = -not (Get-Command Get-BcContainerSharedFolders -ListImported -ErrorAction SilentlyContinue) if ($script:createdMountStub) { @@ -31,6 +33,8 @@ Describe 'API test credential materialization' { AfterAll { $script:fixturePassword.Dispose() $env:GITHUB_ENV = $script:previousGitHubEnv + $env:BCAppsApiTestPasswordPath = $script:previousPasswordPath + $env:BCAppsApiTestPasswordContainer = $script:previousPasswordContainer $global:LASTEXITCODE = $script:previousExitCode if ($script:createdMountStub) { Remove-Item function:global:Get-BcContainerSharedFolders } if ($script:createdDockerStub) { Remove-Item function:global:docker } @@ -38,6 +42,8 @@ Describe 'API test credential materialization' { BeforeEach { $env:GITHUB_ENV = Join-Path $PSScriptRoot 'unused-github-env' + $env:BCAppsApiTestPasswordPath = $null + $env:BCAppsApiTestPasswordContainer = $null $script:events = [System.Collections.Generic.List[string]]::new() $script:stream = [PSCustomObject]@{ Events = $script:events @@ -60,6 +66,8 @@ Describe 'API test credential materialization' { Mock -ModuleName ApiTestCredential Get-BcContainerSharedFolders { @{ $script:mount = 'c:\run\my\' } } Mock -ModuleName ApiTestCredential Add-Content { $script:events.Add('register') } Mock -ModuleName ApiTestCredential New-ApiTestPasswordFileStream { + $env:BCAppsApiTestPasswordPath | Should -Be (Join-Path $script:mount 'ApiTestPassword') + $env:BCAppsApiTestPasswordContainer | Should -Be 'unit-test' $script:events.Add('create') $script:stream } @@ -83,7 +91,9 @@ Describe 'API test credential materialization' { } Should -Invoke -ModuleName ApiTestCredential Add-Content -Times 1 -Exactly -ParameterFilter { $LiteralPath -eq $env:GITHUB_ENV -and $ErrorAction -eq 'Stop' -and - $Value -eq "BCAppsApiTestPasswordPath=$(Join-Path $script:mount 'ApiTestPassword')" + $Value.Count -eq 2 -and + $Value -contains "BCAppsApiTestPasswordPath=$(Join-Path $script:mount 'ApiTestPassword')" -and + $Value -contains 'BCAppsApiTestPasswordContainer=unit-test' } Should -Invoke -ModuleName ApiTestCredential docker -Times 0 } diff --git a/build/scripts/tests/Remove-ApiTestPassword.Test.ps1 b/build/scripts/tests/Remove-ApiTestPassword.Test.ps1 index e67675bf7b5..b799ab16816 100644 --- a/build/scripts/tests/Remove-ApiTestPassword.Test.ps1 +++ b/build/scripts/tests/Remove-ApiTestPassword.Test.ps1 @@ -124,14 +124,133 @@ Describe 'API test credential workflow lifetime' { $script:setup | Should -Not -Match 'Copy-FileToBcContainer|WriteAllText|GetNetworkCredential|GetTempPath' } - It 'runs cleanup on success, failure and cancellation after all build consumers' { - $cleanupStep = [regex]::Match($script:workflow, '(?ms)^ - name: Remove API test credential\r?\n.*?(?=^ - name: Cleanup)').Value - $cleanupStep | Should -Match "if: always\(\) && steps\.DetermineBuildProject\.outputs\.BuildIt == 'True' && env\.BCAppsApiTestPasswordPath != ''" - $cleanupStep | Should -Match 'timeout-minutes: 2' - $cleanupStep | Should -Match 'Remove-ApiTestPassword.ps1' - $cleanupStep | Should -Not -Match 'continue-on-error' - $script:workflow.IndexOf('- name: Build') | - Should -BeLessThan $script:workflow.IndexOf('- name: Remove API test credential') + It 'leaves generated workflow cleanup unmodified and uses supported project finalizers' { + $script:workflow | Should -Not -Match '- name: Remove API test credential|Remove-ApiTestPassword.ps1' $script:workflow | Should -Match '(?s)- name: Cleanup\r?\n\s+if: always\(\).*?uses: microsoft/AL-Go/Actions/PipelineCleanup@' + & (Join-Path $PSScriptRoot '..\Update-TestProjectPipelineFinalize.ps1') -Check + } +} + +Describe 'API test credential pipeline finalizer' { + BeforeAll { + $script:finalizer = Join-Path $PSScriptRoot '..\PipelineFinalize.ps1' + $script:previousPasswordPath = $env:BCAppsApiTestPasswordPath + $script:previousPasswordContainer = $env:BCAppsApiTestPasswordContainer + $script:previousExitCode = Get-Variable LASTEXITCODE -Scope Global -ValueOnly -ErrorAction SilentlyContinue + function docker { + param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Arguments) + $null = $Arguments + throw 'Docker must be mocked.' + } + } + + BeforeEach { + $env:BCAppsApiTestPasswordPath = Join-Path $TestDrive 'ApiTestPassword' + $env:BCAppsApiTestPasswordContainer = 'recorded-container' + Mock Test-Path { $true } + Mock docker { + $global:LASTEXITCODE = 0 + if ($Arguments[0] -eq 'container') { 'recorded-container-id' } + } + Mock Remove-Item { + Should -Invoke docker -Times 1 -ParameterFilter { $Arguments[0] -eq 'stop' } + } + Mock Write-Host {} + } + + AfterAll { + $env:BCAppsApiTestPasswordPath = $script:previousPasswordPath + $env:BCAppsApiTestPasswordContainer = $script:previousPasswordContainer + $global:LASTEXITCODE = $script:previousExitCode + } + + It 'accepts no arguments and stops the recorded consumers before deleting the backing file' { + $path = $env:BCAppsApiTestPasswordPath + . $script:finalizer + Should -Invoke docker -Times 1 -Exactly -ParameterFilter { + $Arguments -contains 'name=^/recorded-container$' + } + Should -Invoke Remove-Item -Times 1 -Exactly -ParameterFilter { $LiteralPath -eq $path } + $env:BCAppsApiTestPasswordPath | Should -BeNullOrEmpty + $env:BCAppsApiTestPasswordContainer | Should -BeNullOrEmpty + } + + It 'does nothing when no credential was provisioned' { + $env:BCAppsApiTestPasswordPath = $null + . $script:finalizer + Should -Invoke docker -Times 0 + Should -Invoke Remove-Item -Times 0 + } + + It 'is idempotent when standard teardown already removed the credential' { + Mock Test-Path { $false } + . $script:finalizer + . $script:finalizer + Should -Invoke docker -Times 0 + Should -Invoke Remove-Item -Times 0 + $env:BCAppsApiTestPasswordPath | Should -BeNullOrEmpty + } + + It 'preserves cleanup identity and propagates a failure without deleting while consumers remain' { + Mock docker { $global:LASTEXITCODE = 1 } -ParameterFilter { $Arguments[0] -eq 'stop' } + { . $script:finalizer } | Should -Throw '*Could not stop API test consumers*' + Should -Invoke Remove-Item -Times 0 + $env:BCAppsApiTestPasswordContainer | Should -Be 'recorded-container' + $env:BCAppsApiTestPasswordPath | Should -Be (Join-Path $TestDrive 'ApiTestPassword') + } + + It 'propagates deletion failure without forgetting the recorded path' { + Mock Remove-Item { throw 'Synthetic deletion failure' } + { . $script:finalizer } | Should -Throw '*Synthetic deletion failure*' + $env:BCAppsApiTestPasswordPath | Should -Be (Join-Path $TestDrive 'ApiTestPassword') + } + + It 'runs every registered project wrapper through the AL-Go scriptblock invocation contract' { + $projects = Join-Path $PSScriptRoot '..\..\projects' + $wrappers = @(Get-ChildItem -LiteralPath $projects -Directory | ForEach-Object { + $setup = Join-Path $_.FullName '.AL-Go\NewBcContainer.ps1' + if ([IO.File]::Exists($setup)) { + Get-Item -LiteralPath (Join-Path $_.FullName '.AL-Go\PipelineFinalize.ps1') + } + }) + $wrappers.Count | Should -BeGreaterThan 0 + foreach ($wrapper in $wrappers) { + $env:BCAppsApiTestPasswordPath = Join-Path $TestDrive 'ApiTestPassword' + $env:BCAppsApiTestPasswordContainer = 'recorded-container' + Invoke-Command -ScriptBlock (Get-Command $wrapper.FullName).ScriptBlock + } + Should -Invoke Remove-Item -Times $wrappers.Count -Exactly + } +} + +Describe 'API test pipeline finalizer generation' { + BeforeAll { + $script:generator = Join-Path $PSScriptRoot '..\Update-TestProjectPipelineFinalize.ps1' + } + + It 'generates only container-project wrappers and is stable when regenerated' { + $projects = Join-Path $TestDrive 'projects' + foreach ($name in @('Test Apps W1', 'Test Apps AT', 'Apps W1')) { + New-Item -Path (Join-Path $projects "$name\.AL-Go") -ItemType Directory -Force | Out-Null + } + foreach ($name in @('Test Apps W1', 'Test Apps AT')) { + Set-Content -LiteralPath (Join-Path $projects "$name\.AL-Go\NewBcContainer.ps1") -Value 'fixture' + } + & $script:generator -ProjectsPath $projects + $path = Join-Path $projects 'Test Apps W1\.AL-Go\PipelineFinalize.ps1' + $first = [IO.File]::ReadAllBytes($path) + & $script:generator -ProjectsPath $projects + [Convert]::ToBase64String([IO.File]::ReadAllBytes($path)) | Should -Be ([Convert]::ToBase64String($first)) + & $script:generator -ProjectsPath $projects -Check + Test-Path (Join-Path $projects 'Apps W1\.AL-Go\PipelineFinalize.ps1') | Should -BeFalse + } + + It 'fails check-only validation for a missing wrapper without writing it' { + $projects = Join-Path $TestDrive 'missing' + $folder = Join-Path $projects 'Test Apps W1\.AL-Go' + New-Item -Path $folder -ItemType Directory -Force | Out-Null + Set-Content -LiteralPath (Join-Path $folder 'NewBcContainer.ps1') -Value 'fixture' + { & $script:generator -ProjectsPath $projects -Check } | Should -Throw '*missing or outdated*' + Test-Path (Join-Path $folder 'PipelineFinalize.ps1') | Should -BeFalse } } From dd6207b6ae66a170796342545d44b50093e2dc4f Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Fri, 2 Oct 2026 14:53:35 +0200 Subject: [PATCH 07/22] Simplify finalizer maintenance by retaining committed wrappers Remove the wrapper generator and its generator-specific tests. Keep the shared finalizer and behavioral tests that execute every project wrapper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .../Update-TestProjectPipelineFinalize.ps1 | 31 ---------------- .../tests/Remove-ApiTestPassword.Test.ps1 | 35 +------------------ 2 files changed, 1 insertion(+), 65 deletions(-) delete mode 100644 build/scripts/Update-TestProjectPipelineFinalize.ps1 diff --git a/build/scripts/Update-TestProjectPipelineFinalize.ps1 b/build/scripts/Update-TestProjectPipelineFinalize.ps1 deleted file mode 100644 index 56149f14a3d..00000000000 --- a/build/scripts/Update-TestProjectPipelineFinalize.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -<# -.SYNOPSIS - Generates the success-only finalizer override for projects using shared container setup. -.DESCRIPTION - Run with -Check to validate committed wrappers without modifying them. -#> -param( - [string]$ProjectsPath = (Join-Path $PSScriptRoot '..\projects'), - [switch]$Check -) - -$ErrorActionPreference = 'Stop' -$wrapper = ". (Join-Path `$PSScriptRoot '../../../scripts/PipelineFinalize.ps1' -Resolve)`n" -$projects = @(Get-ChildItem -LiteralPath $ProjectsPath -Directory | Where-Object { - Test-Path -LiteralPath (Join-Path $_.FullName '.AL-Go\NewBcContainer.ps1') -PathType Leaf -}) -if ($projects.Count -eq 0) { - throw 'No test container project overrides were found.' -} -foreach ($project in $projects) { - $path = Join-Path $project.FullName '.AL-Go\PipelineFinalize.ps1' - $current = if (Test-Path -LiteralPath $path -PathType Leaf) { - [IO.File]::ReadAllText($path).Replace("`r`n", "`n") - } else { '' } - if ($current -ne $wrapper) { - if ($Check) { - throw "PipelineFinalize override is missing or outdated for $($project.Name). Run Update-TestProjectPipelineFinalize.ps1." - } - [IO.File]::WriteAllText($path, $wrapper, [Text.UTF8Encoding]::new($false)) - } -} diff --git a/build/scripts/tests/Remove-ApiTestPassword.Test.ps1 b/build/scripts/tests/Remove-ApiTestPassword.Test.ps1 index b799ab16816..72eab0f130c 100644 --- a/build/scripts/tests/Remove-ApiTestPassword.Test.ps1 +++ b/build/scripts/tests/Remove-ApiTestPassword.Test.ps1 @@ -124,10 +124,9 @@ Describe 'API test credential workflow lifetime' { $script:setup | Should -Not -Match 'Copy-FileToBcContainer|WriteAllText|GetNetworkCredential|GetTempPath' } - It 'leaves generated workflow cleanup unmodified and uses supported project finalizers' { + It 'leaves generated workflow cleanup unmodified' { $script:workflow | Should -Not -Match '- name: Remove API test credential|Remove-ApiTestPassword.ps1' $script:workflow | Should -Match '(?s)- name: Cleanup\r?\n\s+if: always\(\).*?uses: microsoft/AL-Go/Actions/PipelineCleanup@' - & (Join-Path $PSScriptRoot '..\Update-TestProjectPipelineFinalize.ps1') -Check } } @@ -222,35 +221,3 @@ Describe 'API test credential pipeline finalizer' { Should -Invoke Remove-Item -Times $wrappers.Count -Exactly } } - -Describe 'API test pipeline finalizer generation' { - BeforeAll { - $script:generator = Join-Path $PSScriptRoot '..\Update-TestProjectPipelineFinalize.ps1' - } - - It 'generates only container-project wrappers and is stable when regenerated' { - $projects = Join-Path $TestDrive 'projects' - foreach ($name in @('Test Apps W1', 'Test Apps AT', 'Apps W1')) { - New-Item -Path (Join-Path $projects "$name\.AL-Go") -ItemType Directory -Force | Out-Null - } - foreach ($name in @('Test Apps W1', 'Test Apps AT')) { - Set-Content -LiteralPath (Join-Path $projects "$name\.AL-Go\NewBcContainer.ps1") -Value 'fixture' - } - & $script:generator -ProjectsPath $projects - $path = Join-Path $projects 'Test Apps W1\.AL-Go\PipelineFinalize.ps1' - $first = [IO.File]::ReadAllBytes($path) - & $script:generator -ProjectsPath $projects - [Convert]::ToBase64String([IO.File]::ReadAllBytes($path)) | Should -Be ([Convert]::ToBase64String($first)) - & $script:generator -ProjectsPath $projects -Check - Test-Path (Join-Path $projects 'Apps W1\.AL-Go\PipelineFinalize.ps1') | Should -BeFalse - } - - It 'fails check-only validation for a missing wrapper without writing it' { - $projects = Join-Path $TestDrive 'missing' - $folder = Join-Path $projects 'Test Apps W1\.AL-Go' - New-Item -Path $folder -ItemType Directory -Force | Out-Null - Set-Content -LiteralPath (Join-Path $folder 'NewBcContainer.ps1') -Value 'fixture' - { & $script:generator -ProjectsPath $projects -Check } | Should -Throw '*missing or outdated*' - Test-Path (Join-Path $folder 'PipelineFinalize.ps1') | Should -BeFalse - } -} From 7003849216f4f4e9b2e705ebaec4aaf339370792 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Sun, 4 Oct 2026 17:58:03 +0200 Subject: [PATCH 08/22] Document tenant info object return contract Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- build/scripts/ParallelTestExecution.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/scripts/ParallelTestExecution.psm1 b/build/scripts/ParallelTestExecution.psm1 index 5ab0f3ecce7..f8ed562ccee 100644 --- a/build/scripts/ParallelTestExecution.psm1 +++ b/build/scripts/ParallelTestExecution.psm1 @@ -325,11 +325,11 @@ function Get-AppNamesForBucket { <# .SYNOPSIS - Gets the list of operational tenants in a BC container. + Gets IDs and database names for operational tenants in a BC container. .PARAMETER containerName Name of the BC container to query. .OUTPUTS - [string[]] Array of tenant IDs that are in Operational state. + [PSCustomObject[]] Operational tenant records with Id and DatabaseName properties. #> function Get-AvailableBcTenantInfo { param( From 304e5b13464f3f4d470b159880fa6f5996223635 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 5 Oct 2026 15:23:59 +0200 Subject: [PATCH 09/22] Enable reviewed Expense APIs and consolidate unique Expense regressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session:3952f078-a881-4da8-ad96-13b727e48a91 --- .github/AL-Go-Settings.json | 1 + .../TravelRequestApproval.Codeunit.al | 29 ++- .../API/ExpenseActivityLogAPITest.Codeunit.al | 21 +- .../API/ExpenseAgentSetupAPITest.Codeunit.al | 3 + .../ExpenseCapabilitiesAPITest.Codeunit.al | 5 +- .../ExpensePerDiemLocationsTest.Codeunit.al | 6 +- .../API/ExpenseProjectsAPITest.Codeunit.al | 5 +- .../src/API/ExpenseUsersAPITest.Codeunit.al | 5 +- .../src/API/ExpenseVATSpecAPITest.Codeunit.al | 5 +- .../API/PoliciesToEvaluateAPITest.Codeunit.al | 5 +- .../API/PolicyEvaluationsAPITest.Codeunit.al | 5 +- .../src/API/TravelRequestsAPITest.Codeunit.al | 5 +- .../src/ExpensePermissionsTest.Codeunit.al | 141 +++++++++++++- .../ExpenseAPITestAuthHelper.Codeunit.al | 61 ------ .../ExpDetailTest.PermissionSet.al | 20 ++ .../test/src/SpendRequestTest.Codeunit.al | 180 ++++++++++++++++-- .../Expense_Agent_Tests.DisabledTest.json | 160 ---------------- .../Tests/ERM/SpendRequestTests.Codeunit.al | 48 +++++ .../TestLibraries/LibraryGraphMgt.Codeunit.al | 6 + 19 files changed, 439 insertions(+), 272 deletions(-) delete mode 100644 src/Apps/W1/ExpenseAgent/test/src/Helper/ExpenseAPITestAuthHelper.Codeunit.al create mode 100644 src/Apps/W1/ExpenseAgent/test/src/Permissions/ExpDetailTest.PermissionSet.al diff --git a/.github/AL-Go-Settings.json b/.github/AL-Go-Settings.json index af49d63a0de..3afdd819f8f 100644 --- a/.github/AL-Go-Settings.json +++ b/.github/AL-Go-Settings.json @@ -15,6 +15,7 @@ "repoVersion": "30.0", "memoryLimit": "16G", "numberOfTenantsForTesting": 4, + "enableCleanTestCodeunitExecution": true, "maxTestAppReruns": 2, "failOn": "newWarning", "workspaceCompilation": { diff --git a/src/Apps/W1/ExpenseAgent/app/src/Travel Request/Codeunits/TravelRequestApproval.Codeunit.al b/src/Apps/W1/ExpenseAgent/app/src/Travel Request/Codeunits/TravelRequestApproval.Codeunit.al index b8ee5762dc7..8b290df2331 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Travel Request/Codeunits/TravelRequestApproval.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Travel Request/Codeunits/TravelRequestApproval.Codeunit.al @@ -6,7 +6,6 @@ namespace Microsoft.ExpenseAgent; using Microsoft.Finance.SpendRequest; using System.Telemetry; -using System.Text; codeunit 7133 "Travel Request Approval" { @@ -318,13 +317,17 @@ codeunit 7133 "Travel Request Approval" var ExpenseApprovalSetup: Record "Expense Approval Setup"; ExpenseAgentSetup: Record "Expense Agent Setup"; - SelectionFilterManagement: Codeunit SelectionFilterManagement; - RecRef: RecordRef; + AssignedFilter: TextBuilder; begin ExpenseApprovalSetup.SetCurrentKey("Approver No."); ExpenseApprovalSetup.SetRange("Approver No.", ApproverExpenseUserNo); - RecRef.GetTable(ExpenseApprovalSetup); - RequestedForFilter := SelectionFilterManagement.GetSelectionFilter(RecRef, ExpenseApprovalSetup.FieldNo("Expense User No.")); + ExpenseApprovalSetup.SetLoadFields("Expense User No."); + // Ranges between setup rows could include expense users without any approval setup. + if ExpenseApprovalSetup.FindSet() then + repeat + AppendSubmitterFilter(AssignedFilter, ExpenseApprovalSetup."Expense User No."); + until ExpenseApprovalSetup.Next() = 0; + RequestedForFilter := AssignedFilter.ToText(); ExpenseAgentSetup.GetRecordOnce(); if ExpenseAgentSetup."Default Approver No." = ApproverExpenseUserNo then @@ -337,7 +340,6 @@ codeunit 7133 "Travel Request Approval" local procedure AppendDefaultSubmitters(var RequestedForFilter: Text) var ExpenseUser: Record "Expense User"; - SelectionFilterManagement: Codeunit SelectionFilterManagement; DefaultFilter: TextBuilder; begin if RequestedForFilter <> '' then @@ -348,14 +350,23 @@ codeunit 7133 "Travel Request Approval" ExpenseUser.SetLoadFields("No."); if ExpenseUser.FindSet() then repeat - if DefaultFilter.Length > 0 then - DefaultFilter.Append('|'); - DefaultFilter.Append(SelectionFilterManagement.AddQuotes(ExpenseUser."No.")); + AppendSubmitterFilter(DefaultFilter, ExpenseUser."No."); until ExpenseUser.Next() = 0; RequestedForFilter := DefaultFilter.ToText(); end; + local procedure AppendSubmitterFilter(var SubmitterFilter: TextBuilder; ExpenseUserNo: Code[20]) + var + ExpenseUserFilter: Record "Expense User"; + begin + // Serialize an exact value so filter operators in user numbers remain literal. + ExpenseUserFilter.SetRange("No.", ExpenseUserNo); + if SubmitterFilter.Length > 0 then + SubmitterFilter.Append('|'); + SubmitterFilter.Append(ExpenseUserFilter.GetFilter("No.")); + end; + var FeatureTelemetry: Codeunit "Feature Telemetry"; ExpenseActivityLogMgt: Codeunit "Expense Activity Log Mgt."; diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseActivityLogAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseActivityLogAPITest.Codeunit.al index 90781c7634d..f0ec4e76f55 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseActivityLogAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseActivityLogAPITest.Codeunit.al @@ -22,7 +22,6 @@ codeunit 148343 "Expense Activity Log API Test" LibraryGraphMgt: Codeunit "Library - Graph Mgt"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; LibrarySetupStorage: Codeunit "Library - Setup Storage"; - APITestAuthHelper: Codeunit "Expense API Test Auth Helper"; IsInitialized: Boolean; ServiceNameTok: Label 'expenseActivityLogEntries', Locked = true; ExpenseReportsServiceNameTok: Label 'expenseReports', Locked = true; @@ -476,11 +475,14 @@ codeunit 148343 "Expense Activity Log API Test" RequestBody.WriteTo(RequestText); ReportURL := LibraryGraphMgt.CreateTargetURLWithSubpage( Format(SubmitterExpenseUser.SystemId), Page::"Expense Users API", ExpenseUsersServiceNameTok, ExpenseReportsServiceNameTok); - ReportURL += '(' + LibraryGraphMgt.StripBrackets(Format(ExpenseReportHeader.SystemId)) + ')'; + ReportURL := LibraryGraphMgt.AppendPathToTargetURL( + ReportURL, '(' + LibraryGraphMgt.StripBrackets(Format(ExpenseReportHeader.SystemId)) + ')'); Commit(); // [WHEN] The standard submission action is requested through the submitting user. - LibraryGraphMgt.PostToWebServiceAndCheckResponseCode(ReportURL + '/' + SubmitWithCommentActionTok, RequestText, ResponseText, 200); + Clear(ResponseText); + LibraryGraphMgt.PostToWebServiceAndCheckResponseCode( + LibraryGraphMgt.AppendPathToTargetURL(ReportURL, '/' + SubmitWithCommentActionTok), RequestText, ResponseText, 200); // [THEN] Exactly one structured snapshot is visible without a new callback or capability. ExpenseActivityLogEntry.SetRange("Subject System ID", ExpenseReportHeader.SystemId); @@ -488,7 +490,8 @@ codeunit 148343 "Expense Activity Log API Test" Assert.RecordCount(ExpenseActivityLogEntry, 1); ExpenseActivityLogEntry.FindFirst(); Clear(ResponseText); - LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, ReportURL + '/' + ServiceNameTok, 200); + LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode( + ResponseText, LibraryGraphMgt.AppendPathToTargetURL(ReportURL, '/' + ServiceNameTok), 200); VerifyPolicyCategoryCode(ResponseText, ExpenseCategory.Code); ResponseText := LowerCase(ResponseText); Assert.AreNotEqual(0, StrPos(ResponseText, '"eventtype":"policyevaluated"'), 'The event type must identify the policy snapshot.'); @@ -502,7 +505,8 @@ codeunit 148343 "Expense Activity Log API Test" HistoryURL := LibraryGraphMgt.CreateTargetURLWithSubpage( Format(SubmitterExpenseUser.SystemId), Page::"Expense Users API", ExpenseUsersServiceNameTok, ServiceNameTok); Clear(ResponseText); - LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, HistoryURL + '?$filter=historyActorRole eq ''Submitter''', 200); + LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode( + ResponseText, LibraryGraphMgt.AppendQueryParameterToTargetURL(HistoryURL, '$filter=historyActorRole eq ''Submitter'''), 200); // [THEN] The real submitter sees the agent snapshot in the complete timeline. Assert.AreNotEqual(0, StrPos(LowerCase(ResponseText), LowerCase(LibraryGraphMgt.StripBrackets(Format(ExpenseActivityLogEntry.SystemId)))), 'The submitter timeline must contain the snapshot.'); @@ -511,7 +515,8 @@ codeunit 148343 "Expense Activity Log API Test" HistoryURL := LibraryGraphMgt.CreateTargetURLWithSubpage( Format(ApproverExpenseUser.SystemId), Page::"Expense Users API", ExpenseUsersServiceNameTok, ServiceNameTok); Clear(ResponseText); - LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, HistoryURL + '?$filter=historyActorRole eq ''Approver''', 200); + LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode( + ResponseText, LibraryGraphMgt.AppendQueryParameterToTargetURL(HistoryURL, '$filter=historyActorRole eq ''Approver'''), 200); // [THEN] The agent event must not grant approver participation. Assert.AreEqual(0, StrPos(LowerCase(ResponseText), LowerCase(LibraryGraphMgt.StripBrackets(Format(ExpenseReportHeader.SystemId)))), 'The agent snapshot must not grant approver history access.'); @@ -803,7 +808,9 @@ codeunit 148343 "Expense Activity Log API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Expense Activity Log API Test"); LibraryERMCountryData.UpdateGeneralLedgerSetup(); if not ExpenseAgentSetup.Get() then begin diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseAgentSetupAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseAgentSetupAPITest.Codeunit.al index 50bb3595b39..c5201316389 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseAgentSetupAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseAgentSetupAPITest.Codeunit.al @@ -64,6 +64,9 @@ codeunit 148333 "Expense Agent Setup API Test" if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Expense Agent Setup API Test"); LibraryExpense.SetupNumberSeriesInExpenseMgmt(); IsInitialized := true; diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseCapabilitiesAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseCapabilitiesAPITest.Codeunit.al index 4436c873d54..ff80ab2eeb8 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseCapabilitiesAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseCapabilitiesAPITest.Codeunit.al @@ -18,7 +18,6 @@ codeunit 148318 "Expense Capabilities API Test" LibraryExpenseAgent: Codeunit "Library - Expense Agent"; LibraryGraphMgt: Codeunit "Library - Graph Mgt"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; - APITestAuthHelper: Codeunit "Expense API Test Auth Helper"; IsInitialized: Boolean; ServiceNameTok: Label 'expenseCapabilities', Locked = true; ActivityLogCapabilityNameTok: Label 'activityLog', Locked = true; @@ -248,7 +247,9 @@ codeunit 148318 "Expense Capabilities API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Expense Capabilities API Test"); IsInitialized := true; Commit(); diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/ExpensePerDiemLocationsTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/ExpensePerDiemLocationsTest.Codeunit.al index 40fdf025b82..88d251c4260 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpensePerDiemLocationsTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpensePerDiemLocationsTest.Codeunit.al @@ -17,7 +17,6 @@ codeunit 148349 "Expense PerDiem Locations Test" LibraryUtility: Codeunit "Library - Utility"; LibraryGraphMgt: Codeunit "Library - Graph Mgt"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; - APITestAuthHelper: Codeunit "Expense API Test Auth Helper"; IsInitialized: Boolean; [Test] @@ -82,11 +81,14 @@ codeunit 148349 "Expense PerDiem Locations Test" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibraryTestInitialize.OnTestInitialize(Codeunit::"Expense PerDiem Locations Test"); if IsInitialized then exit; - BindSubscription(APITestAuthHelper); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Expense PerDiem Locations Test"); IsInitialized := true; Commit(); diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseProjectsAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseProjectsAPITest.Codeunit.al index 45ed9ae6d25..63e7903018f 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseProjectsAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseProjectsAPITest.Codeunit.al @@ -24,7 +24,6 @@ codeunit 148331 "Expense Projects API Test" LibraryHumanResource: Codeunit "Library - Human Resource"; LibraryGraphMgt: Codeunit "Library - Graph Mgt"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; - APITestAuthHelper: Codeunit "Expense API Test Auth Helper"; IsInitialized: Boolean; ExpenseUsersServiceNameTok: Label 'expenseUsers', Locked = true; ProjectsServiceNameTok: Label 'expenseProjects', Locked = true; @@ -723,7 +722,9 @@ codeunit 148331 "Expense Projects API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Expense Projects API Test"); if not ExpenseAgentSetup.Get() then begin ExpenseAgentSetup.Init(); diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseUsersAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseUsersAPITest.Codeunit.al index f80c8eb6e80..1837a52d566 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseUsersAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseUsersAPITest.Codeunit.al @@ -21,7 +21,6 @@ codeunit 148315 "Expense Users API Test" LibraryUtility: Codeunit "Library - Utility"; LibraryGraphMgt: Codeunit "Library - Graph Mgt"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; - APITestAuthHelper: Codeunit "Expense API Test Auth Helper"; IsInitialized: Boolean; ServiceNameTok: Label 'expenseUsers', Locked = true; EmployeesServiceNameTok: Label 'employees', Locked = true; @@ -125,7 +124,9 @@ codeunit 148315 "Expense Users API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Expense Users API Test"); if not ExpenseAgentSetup.Get() then begin ExpenseAgentSetup.Init(); diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al index 7c30f7723fa..2fa5aa46229 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al @@ -19,7 +19,6 @@ codeunit 148348 "Expense VAT Spec. API Test" LibraryERM: Codeunit "Library - ERM"; LibraryGraphMgt: Codeunit "Library - Graph Mgt"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; - APITestAuthHelper: Codeunit "Expense API Test Auth Helper"; IsInitialized: Boolean; ServiceNameTok: Label 'expenseVATSpecifications', Locked = true; @@ -89,7 +88,9 @@ codeunit 148348 "Expense VAT Spec. API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Expense VAT Spec. API Test"); LibraryExpense.SetupNumberSeriesInExpenseMgmt(); LibraryERM.CreateVATBusinessPostingGroup(VATBusinessPostingGroup); diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/PoliciesToEvaluateAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/PoliciesToEvaluateAPITest.Codeunit.al index ef512122f3d..a382c57ee52 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/PoliciesToEvaluateAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/PoliciesToEvaluateAPITest.Codeunit.al @@ -18,7 +18,6 @@ codeunit 148345 "Policies To Evaluate API Test" LibraryExpense: Codeunit "Library - Expense"; LibraryGraphMgt: Codeunit "Library - Graph Mgt"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; - APITestAuthHelper: Codeunit "Expense API Test Auth Helper"; IsInitialized: Boolean; ExpenseReportLinesServiceNameTok: Label 'expenseReportLines', Locked = true; @@ -117,7 +116,9 @@ codeunit 148345 "Policies To Evaluate API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Policies To Evaluate API Test"); LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateGeneralPostingSetup(); diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/PolicyEvaluationsAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/PolicyEvaluationsAPITest.Codeunit.al index 97488f4294c..be07a540b99 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/PolicyEvaluationsAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/PolicyEvaluationsAPITest.Codeunit.al @@ -18,7 +18,6 @@ codeunit 148344 "Policy Evaluations API Test" LibraryExpense: Codeunit "Library - Expense"; LibraryGraphMgt: Codeunit "Library - Graph Mgt"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; - APITestAuthHelper: Codeunit "Expense API Test Auth Helper"; IsInitialized: Boolean; ServiceNameTok: Label 'expensePolicyEvaluations', Locked = true; BadRequestResponseErr: Label 'Response code is 400', Locked = true; @@ -121,7 +120,9 @@ codeunit 148344 "Policy Evaluations API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Policy Evaluations API Test"); LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateGeneralPostingSetup(); diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al index 2134bd14296..15eec688af5 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al @@ -31,7 +31,6 @@ codeunit 148347 "Travel Requests API Test" LibraryHumanResource: Codeunit "Library - Human Resource"; LibraryRandom: Codeunit "Library - Random"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; - APITestAuthHelper: Codeunit "Expense API Test Auth Helper"; IsInitialized: Boolean; ExpenseUsersServiceNameTok: Label 'expenseUsers', Locked = true; #if not CLEAN30 @@ -1291,7 +1290,9 @@ codeunit 148347 "Travel Requests API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Travel Requests API Test"); if not ExpenseAgentSetup.Get() then begin ExpenseAgentSetup.Init(); diff --git a/src/Apps/W1/ExpenseAgent/test/src/ExpensePermissionsTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/ExpensePermissionsTest.Codeunit.al index aa05633221e..89ecec0c706 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/ExpensePermissionsTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/ExpensePermissionsTest.Codeunit.al @@ -32,6 +32,9 @@ codeunit 148338 "Expense Permissions Test" AutomationPermissionSetTok: Label 'Exp. Auto Test', Locked = true; D365BasicPermissionSetTok: Label 'D365 BASIC', Locked = true; ExpenseAgentPermissionSetTok: Label 'Expense Agent', Locked = true; + DetailOnlyPermissionSetTok: Label 'Exp. Detail Test', Locked = true; + HeaderModifyPermissionErr: Label 'TableData %1 %2 IndirectModify', Comment = '%1 = Spend Request table ID, %2 = Spend Request table caption', Locked = true; + RequestMustBeOpenErr: Label 'The %1 %2 must have the status %3.', Comment = '%1 = document type description, %2 = document number, %3 = Open status'; ExpenseAgentAppIdTok: Label '66efe10c-8033-403b-a86d-77c0887178ba', Locked = true; ExpenseMgmtAdminPermissionSetTok: Label 'Expense Mgmt. Admin', Locked = true; SecurityPermissionSetTok: Label 'SECURITY', Locked = true; @@ -63,15 +66,100 @@ codeunit 148338 "Expense Permissions Test" [Test] procedure D365BasicCanUpdateTravelRequestDetailsIndirectly() begin + // [SCENARIO 646383] D365 BASIC applies successive detail amount deltas without direct header or detail writes. + Initialize(); VerifyTravelRequestDetailUpdateIndirectly(D365BasicPermissionSetTok); end; [Test] procedure ExpenseAgentCanUpdateTravelRequestDetailsIndirectly() begin + // [SCENARIO 646383] Expense Agent applies successive detail amount deltas without direct header or detail writes. + Initialize(); VerifyTravelRequestDetailUpdateIndirectly(ExpenseAgentPermissionSetTok); end; + [Test] + procedure DetailAmountUpdateFailsWithoutHeaderModifyPermission() + var + SpendRequest: Record "Spend Request"; + SpendRequestDetail: Record "Spend Request Detail"; + TravelRequestSubform: TestPage "Travel Request Subform"; + SpendRequestCanWrite: Boolean; + SpendRequestDetailCanWrite: Boolean; + PermissionErrorCode: Text; + PermissionErrorText: Text; + begin + // [SCENARIO 646383] A caller with only header read and indirect detail modify cannot update the header through an amount change. + Initialize(); + + // [GIVEN] An open request "R" with a committed detail "D" worth 10. + LibraryExpense.CreateSpendRequest(SpendRequest); + LibraryExpense.CreateSpendRequestDetail(SpendRequestDetail, SpendRequest."No.", 10); + VerifyTravelRequestAmounts(SpendRequest, SpendRequestDetail, 10); + Commit(); + LibraryLowerPermissions.StartLoggingNAVPermissions(); + LibraryLowerPermissions.SetExactPermissionSet(DetailOnlyPermissionSetTok); + SpendRequestCanWrite := SpendRequest.WritePermission(); + SpendRequestDetailCanWrite := SpendRequestDetail.WritePermission(); + TravelRequestSubform.OpenEdit(); + TravelRequestSubform.GoToRecord(SpendRequestDetail); + + // [WHEN] The caller changes the amount through the public page. + asserterror TravelRequestSubform.Amount.SetValue(20); + PermissionErrorCode := GetLastErrorCode(); + PermissionErrorText := GetLastErrorText(); + TravelRequestSubform.Close(); + RestoreFullPermissions(); + LibraryLowerPermissions.StopLoggingNAVPermissions(); + + // [THEN] The page reports the specific header modify denial and neither record changes. + VerifyCapturedPageError(PermissionErrorCode, PermissionErrorText, + StrSubstNo(HeaderModifyPermissionErr, Database::"Spend Request", SpendRequest.TableCaption())); + Assert.IsFalse(SpendRequestCanWrite, 'The caller must not have direct request write permission.'); + Assert.IsFalse(SpendRequestDetailCanWrite, 'The caller must not have direct detail write permission.'); + VerifyTravelRequestAmounts(SpendRequest, SpendRequestDetail, 10); + end; + + [Test] + procedure ReleasedTravelRequestDetailAmountCannotBeUpdatedIndirectly() + var + SpendRequest: Record "Spend Request"; + SpendRequestDetail: Record "Spend Request Detail"; + TravelRequestSubform: TestPage "Travel Request Subform"; + ValidationErrorCode: Text; + ValidationErrorText: Text; + begin + // [SCENARIO 646383] Indirect header modification does not bypass the Open status guard. + Initialize(); + + // [GIVEN] A released request "R" with a committed detail "D" worth 10. + LibraryExpense.CreateSpendRequest(SpendRequest); + LibraryExpense.CreateSpendRequestDetail(SpendRequestDetail, SpendRequest."No.", 10); + VerifyTravelRequestAmounts(SpendRequest, SpendRequestDetail, 10); + LibraryExpense.SetSpendRequestStatus(SpendRequest, SpendRequest.Status::Released); + Commit(); + LibraryLowerPermissions.StartLoggingNAVPermissions(); + LibraryLowerPermissions.SetExactPermissionSet(ExpenseAgentPermissionSetTok); + TravelRequestSubform.OpenEdit(); + TravelRequestSubform.GoToRecord(SpendRequestDetail); + + // [WHEN] The caller changes the amount through the public page. + asserterror TravelRequestSubform.Amount.SetValue(20); + ValidationErrorCode := GetLastErrorCode(); + ValidationErrorText := GetLastErrorText(); + TravelRequestSubform.Close(); + RestoreFullPermissions(); + LibraryLowerPermissions.StopLoggingNAVPermissions(); + + // [THEN] The status guard rejects the change and the released request retains both amounts. + VerifyCapturedPageError(ValidationErrorCode, ValidationErrorText, + StrSubstNo(RequestMustBeOpenErr, SpendRequest.GetDocumentTypeDescription(), SpendRequest."No.", SpendRequest.Status::Open)); + VerifyTravelRequestAmounts(SpendRequest, SpendRequestDetail, 10); + SpendRequest.Get(SpendRequest."No."); + Assert.AreEqual(SpendRequest.Status::Released, SpendRequest.Status, 'The denied amount change must preserve the Released status.'); + end; + [Test] procedure D365BasicCanInsertActivityIndirectly() begin @@ -139,20 +227,29 @@ codeunit 148338 "Expense Permissions Test" ExpenseUser: Record "Expense User"; Approver: Record "Expense User"; TravelRequestApproval: Codeunit "Travel Request Approval"; + ExpenseUserCanRead: Boolean; + PermissionErrorCode: Text; + PermissionErrorText: Text; begin // [SCENARIO] An employee-only caller cannot approve requests without access to Expense User data. Initialize(); CreateTravelRequestApprovalScenario(SpendRequest, ExpenseUser, Approver); + // Preserve the fixture for the post-denial checks when asserterror rolls back. Commit(); LibraryLowerPermissions.StartLoggingNAVPermissions(); - SetCallerPermissions(EmployeeOnlyPermissionSetTok, ExpenseUser); + LibraryLowerPermissions.SetExactPermissionSet(EmployeeOnlyPermissionSetTok); + ExpenseUserCanRead := ExpenseUser.ReadPermission(); asserterror TravelRequestApproval.Approve(SpendRequest, Approver."No."); - Assert.ExpectedErrorCode('DB:ClientReadDenied'); - Assert.ExpectedError(ExpenseUser.TableCaption()); + // Capture the denial before permission cleanup can change the last-error state. + PermissionErrorCode := GetLastErrorCode(); + PermissionErrorText := GetLastErrorText(); RestoreFullPermissions(); LibraryLowerPermissions.StopLoggingNAVPermissions(); + Assert.AreEqual('DB:ClientReadDenied', PermissionErrorCode, 'Approval must fail because Expense User read access is denied.'); + Assert.ExpectedMessage(ExpenseUser.TableCaption(), PermissionErrorText); + Assert.IsFalse(ExpenseUserCanRead, 'The caller must not have direct access to Expense User.'); SpendRequest.Get(SpendRequest."No."); Assert.AreEqual(SpendRequest.Status::Released, SpendRequest.Status, 'A denied approval must preserve the request status.'); ExpenseReportHeader.SetRange("Spend Request No.", SpendRequest."No."); @@ -563,8 +660,7 @@ codeunit 148338 "Expense Permissions Test" SpendRequestDetail: Record "Spend Request Detail"; TravelRequestSubform: TestPage "Travel Request Subform"; begin - // [SCENARIO] Editing a detail through its page can update both the line and its header total. - Initialize(); + // [GIVEN] An open request "R" with a detail "D" worth 10. LibraryExpense.CreateSpendRequest(SpendRequest); LibraryExpense.CreateSpendRequestDetail(SpendRequestDetail, SpendRequest."No.", 10); @@ -583,10 +679,41 @@ codeunit 148338 "Expense Permissions Test" LibraryLowerPermissions.StopLoggingNAVPermissions(); // [THEN] Both the line change and the base table's header update are persisted. + VerifyTravelRequestAmounts(SpendRequest, SpendRequestDetail, 20); + + // [GIVEN] The same exact role still has no direct writes after the first update. + LibraryLowerPermissions.StartLoggingNAVPermissions(); + LibraryLowerPermissions.SetExactPermissionSet(PermissionSetId); + Assert.IsFalse(SpendRequest.WritePermission(), 'The caller must not have direct request write permission.'); + Assert.IsFalse(SpendRequestDetail.WritePermission(), 'The caller must not have direct detail write permission.'); + + // [WHEN] The persisted detail amount is increased again from 20 to 30. + Clear(TravelRequestSubform); + TravelRequestSubform.OpenEdit(); + TravelRequestSubform.GoToRecord(SpendRequestDetail); + TravelRequestSubform.Amount.SetValue(30); + TravelRequestSubform.Close(); + RestoreFullPermissions(); + LibraryLowerPermissions.StopLoggingNAVPermissions(); + + // [THEN] The second update applies only its delta, not the entire new amount. + VerifyTravelRequestAmounts(SpendRequest, SpendRequestDetail, 30); + end; + + local procedure VerifyTravelRequestAmounts(var SpendRequest: Record "Spend Request"; var SpendRequestDetail: Record "Spend Request Detail"; ExpectedAmount: Decimal) + begin SpendRequestDetail.Get(SpendRequestDetail."Spend Request No.", SpendRequestDetail."Line No."); SpendRequest.Get(SpendRequest."No."); - Assert.AreEqual(20, SpendRequestDetail."Expected Amount", 'The detail amount must be updated through indirect permissions.'); - Assert.AreEqual(20, SpendRequest."Total Expected Amount (LCY)", 'The detail update must also update the header total.'); + Assert.AreEqual(ExpectedAmount, SpendRequestDetail."Expected Amount", 'The detail amount must match the page operation.'); + Assert.AreEqual(ExpectedAmount, SpendRequestDetail."Expected Amount (LCY)", 'The LCY detail amount must match the page operation.'); + Assert.AreEqual(ExpectedAmount, SpendRequest."Total Expected Amount", 'The request amount must reflect the detail delta exactly once.'); + Assert.AreEqual(ExpectedAmount, SpendRequest."Total Expected Amount (LCY)", 'The LCY request amount must reflect the detail delta exactly once.'); + end; + + local procedure VerifyCapturedPageError(ErrorCode: Text; ErrorText: Text; ExpectedErrorText: Text) + begin + Assert.AreEqual('TestValidation', ErrorCode, 'The amount field must fail through the TestPage validation wrapper.'); + Assert.ExpectedMessage(ExpectedErrorText, ErrorText); end; local procedure VerifyCompanyEmailSynchronization(PermissionSetId: Code[20]) diff --git a/src/Apps/W1/ExpenseAgent/test/src/Helper/ExpenseAPITestAuthHelper.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/Helper/ExpenseAPITestAuthHelper.Codeunit.al deleted file mode 100644 index 4e34344be7a..00000000000 --- a/src/Apps/W1/ExpenseAgent/test/src/Helper/ExpenseAPITestAuthHelper.Codeunit.al +++ /dev/null @@ -1,61 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// ------------------------------------------------------------------------------------------------ -namespace Microsoft.Test.ExpenseAgent; - -using System.Azure.KeyVault; -using System.Environment; -using System.Integration; -using System.Security.AccessControl; - -/// -/// Test-only authentication helper for the API test codeunits in this app. -/// -codeunit 148332 "Expense API Test Auth Helper" -{ - Access = Internal; - EventSubscriberInstance = Manual; - - var - NavServerUserPasswordKeyTok: Label 'NavServerUserPassword', Locked = true; - - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Library - Graph Mgt", OnAfterInitializeWebRequestWithURL, '', false, false)] - local procedure OnAfterInitializeWebRequest(var HttpWebRequestMgt: Codeunit "Http Web Request Mgt.") - var - Password: SecretText; - begin - if not ShouldInjectBasicAuth() then - exit; - if not TryGetPassword(Password) then - exit; - HttpWebRequestMgt.AddBasicAuthentication(UserId(), Password); - end; - - local procedure ShouldInjectBasicAuth(): Boolean - var - User: Record User; - EnvironmentInfo: Codeunit "Environment Information"; - begin - if EnvironmentInfo.IsSaaSInfrastructure() then - exit(false); - - // If the current user authenticates via Windows - if not User.Get(UserSecurityId()) then - exit(false); - if User."Windows Security ID" <> '' then - exit(false); - - // Inject Basic auth when using username password authentication. - exit(true); - end; - - [TryFunction] - local procedure TryGetPassword(var Password: SecretText) - var - AzureKeyVault: Codeunit "Azure Key Vault"; - begin - AzureKeyVault.GetAzureKeyVaultSecret(NavServerUserPasswordKeyTok, Password); - end; -} - diff --git a/src/Apps/W1/ExpenseAgent/test/src/Permissions/ExpDetailTest.PermissionSet.al b/src/Apps/W1/ExpenseAgent/test/src/Permissions/ExpDetailTest.PermissionSet.al new file mode 100644 index 00000000000..9b2f1c33517 --- /dev/null +++ b/src/Apps/W1/ExpenseAgent/test/src/Permissions/ExpDetailTest.PermissionSet.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Test.ExpenseAgent; + +using Microsoft.Finance.SpendRequest; +using System.Security.AccessControl; + +permissionset 148303 "Exp. Detail Test" +{ + Access = Internal; + Assignable = false; + IncludedPermissionSets = "D365 READ"; + + // Keep the header read-only, while allowing the page to modify a detail indirectly. + Permissions = tabledata "Spend Request Detail" = m, + codeunit Assert = X, + codeunit "Library - Lower Permissions" = X; +} diff --git a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al index b18c4701d05..c93fa9338d1 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al @@ -62,6 +62,8 @@ codeunit 148339 "Spend Request Test" UnassignedTravelRequestHiddenMsg: Label 'The approver should not see a travel request assigned to another approver.'; DefaultTravelRequestVisibleMsg: Label 'The default approver should see travel requests without an assigned approver.'; ApproverWithoutRequestsMsg: Label 'An approver without assigned travel requests should receive an empty result.'; + ApproverFilterExpectedRequestErr: Label '%1 Requested For filter: %2. Expected request: %3.', Comment = '%1 = assertion message, %2 = Requested For filter, %3 = expected request number'; + ApproverFilterUnexpectedRequestErr: Label '%1 Requested For filter: %2. Expected request: %3. Unauthorized request: %4.', Comment = '%1 = assertion message, %2 = Requested For filter, %3 = expected request number, %4 = unauthorized request number'; SpendReqNoSetMsg: Label 'The Spend Request No. should be assigned to the expense report line.'; HeaderSpendReqNoSetMsg: Label 'The Spend Request No. should be assigned to the expense report header.'; HeaderCloseFlagMsg: Label 'The header should store the confirmed close flag.'; @@ -1877,21 +1879,108 @@ codeunit 148339 "Spend Request Test" procedure ApproverFilterReturnsDefaultApproverTravelRequests() begin // [SCENARIO] The default approver sees requests without an explicit approval assignment. - VerifyDefaultApproverFilter('', ''); + Initialize(); + + VerifyDefaultApproverFilter('', '', true); end; [Test] procedure DefaultApproverFilterQuotesWildcardUserNo() begin // [SCENARIO] A literal wildcard user number must not expose another approver's requests. - VerifyDefaultApproverFilter('*', 'TR-OTHER'); + Initialize(); + + VerifyDefaultApproverFilter('*', 'TR-OTHER', true); end; [Test] procedure DefaultApproverFilterQuotesPipeUserNo() begin // [SCENARIO] A pipe in a user number must not become an OR filter for other users. - VerifyDefaultApproverFilter('TR-A|TR-B', 'TR-A'); + Initialize(); + + VerifyDefaultApproverFilter('TR-A|TR-B', 'TR-A', true); + end; + + [Test] + procedure ApproverFilterQuotesExplicitWildcardUserNo() + begin + // [SCENARIO] An explicitly assigned literal wildcard user must not expose another approver's requests. + Initialize(); + + VerifyDefaultApproverFilter('*', 'TR-OTHER', false); + end; + + [Test] + procedure ApproverFilterQuotesExplicitPipeUserNo() + begin + // [SCENARIO] An explicitly assigned user with a pipe in its number must not expose another approver's requests. + Initialize(); + + VerifyDefaultApproverFilter('TR-A|TR-B', 'TR-A', false); + end; + + [Test] + procedure ApproverFilterDoesNotIncludeUnassignedUserBetweenAssignedUsers() + var + FirstTravelRequest: Record "Spend Request"; + LastTravelRequest: Record "Spend Request"; + UnassignedTravelRequest: Record "Spend Request"; + FilteredTravelRequest: Record "Spend Request"; + FirstExpenseUser: Record "Expense User"; + LastExpenseUser: Record "Expense User"; + UnassignedExpenseUser: Record "Expense User"; + AssignedApprover: Record "Expense User"; + DefaultApprover: Record "Expense User"; + ExpenseApprovalSetup: Record "Expense Approval Setup"; + TravelRequestApproval: Codeunit "Travel Request Approval"; + begin + // [SCENARIO] Adjacent approval setups must not expose a user between them who belongs to another default approver. + Initialize(); + + // [GIVEN] Approver "A" and default approver "B", with their user numbers outside the tested range. + ExpenseApprovalSetup.DeleteAll(); + LibraryExpense.UpdateEnableAgentInAgentSetup(true); + CreateApprover(AssignedApprover); + AssignedApprover.Rename('FILTER-APP-A'); + CreateApprover(DefaultApprover); + DefaultApprover.Rename('FILTER-APP-B'); + SetDefaultApprover(DefaultApprover."No."); + + // [GIVEN] Released requests for "TR-A" and "TR-C" explicitly assigned to "A". + CreateReleasableSpendRequest(FirstTravelRequest, FirstExpenseUser); + FirstExpenseUser.Rename('TR-A'); + FirstTravelRequest.Get(FirstTravelRequest."No."); + LibraryExpense.SetSpendRequestStatus(FirstTravelRequest, FirstTravelRequest.Status::Released); + ExpenseApprovalSetup.Get(FirstExpenseUser."No."); + ExpenseApprovalSetup.Validate("Approver No.", AssignedApprover."No."); + ExpenseApprovalSetup.Modify(true); + CreateReleasableSpendRequest(LastTravelRequest, LastExpenseUser); + LastExpenseUser.Rename('TR-C'); + LastTravelRequest.Get(LastTravelRequest."No."); + LibraryExpense.SetSpendRequestStatus(LastTravelRequest, LastTravelRequest.Status::Released); + ExpenseApprovalSetup.Get(LastExpenseUser."No."); + ExpenseApprovalSetup.Validate("Approver No.", AssignedApprover."No."); + ExpenseApprovalSetup.Modify(true); + + // [GIVEN] A released request for "TR-B" with no setup, so it belongs to default approver "B". + CreateReleasableSpendRequest(UnassignedTravelRequest, UnassignedExpenseUser); + UnassignedExpenseUser.Rename('TR-B'); + UnassignedTravelRequest.Get(UnassignedTravelRequest."No."); + LibraryExpense.SetSpendRequestStatus(UnassignedTravelRequest, UnassignedTravelRequest.Status::Released); + ExpenseApprovalSetup.Get(UnassignedExpenseUser."No."); + ExpenseApprovalSetup.Delete(true); + VerifyApproverFilterRangeGapSetup( + FirstTravelRequest, LastTravelRequest, UnassignedTravelRequest, AssignedApprover."No.", DefaultApprover."No."); + + // [WHEN] "A"'s filter is applied to pending travel requests. + FilteredTravelRequest.SetRange("Document Type", FilteredTravelRequest."Document Type"::"Travel Request"); + FilteredTravelRequest.SetRange(Status, FilteredTravelRequest.Status::Released); + TravelRequestApproval.ApplyApproverFilter(FilteredTravelRequest, AssignedApprover.SystemId); + + // [THEN] The requests for "TR-A" and "TR-C" are visible, but the request for "TR-B" is hidden. + VerifyApproverFilterRangeGapResults( + FilteredTravelRequest, FirstTravelRequest."No.", LastTravelRequest."No.", UnassignedTravelRequest."No."); end; [Test] @@ -3173,7 +3262,7 @@ codeunit 148339 "Spend Request Test" ExpenseAgentSetup.Modify(true); end; - local procedure VerifyDefaultApproverFilter(DefaultExpenseUserNo: Code[20]; OtherExpenseUserNo: Code[20]) + local procedure VerifyDefaultApproverFilter(DefaultExpenseUserNo: Code[20]; OtherExpenseUserNo: Code[20]; UseDefaultFallback: Boolean) var DefaultTravelRequest: Record "Spend Request"; OtherTravelRequest: Record "Spend Request"; @@ -3182,11 +3271,13 @@ codeunit 148339 "Spend Request Test" OtherExpenseUser: Record "Expense User"; DefaultApprover: Record "Expense User"; OtherApprover: Record "Expense User"; + ExpenseApprovalSetup: Record "Expense Approval Setup"; TravelRequestApproval: Codeunit "Travel Request Approval"; + ExpectedApproverNo: Code[20]; begin - Initialize(); - - // [GIVEN] A default approver, an unassigned request, and a request assigned to another approver. + // [GIVEN] Default approver "A", a request for "U", and another request assigned to approver "B". + // Initialize deletes expense users without their OnDelete trigger, leaving approval setups behind. + ExpenseApprovalSetup.DeleteAll(); LibraryExpense.UpdateEnableAgentInAgentSetup(true); CreateApprover(DefaultApprover); SetDefaultApprover(DefaultApprover."No."); @@ -3194,28 +3285,93 @@ codeunit 148339 "Spend Request Test" if DefaultExpenseUserNo <> '' then begin DefaultExpenseUser.Rename(DefaultExpenseUserNo); DefaultTravelRequest.Get(DefaultTravelRequest."No."); - DefaultTravelRequest.TestField("Requested For", DefaultExpenseUserNo); + Assert.AreEqual(DefaultExpenseUserNo, DefaultTravelRequest."Requested For", 'The request must retain the literal renamed user number.'); end; LibraryExpense.SetSpendRequestStatus(DefaultTravelRequest, DefaultTravelRequest.Status::Released); CreateReleasableSpendRequest(OtherTravelRequest, OtherExpenseUser); if OtherExpenseUserNo <> '' then begin OtherExpenseUser.Rename(OtherExpenseUserNo); OtherTravelRequest.Get(OtherTravelRequest."No."); - OtherTravelRequest.TestField("Requested For", OtherExpenseUserNo); + Assert.AreEqual(OtherExpenseUserNo, OtherTravelRequest."Requested For", 'The other request must retain the literal renamed user number.'); end; CreateApproverForExpenseUser(OtherApprover, OtherExpenseUser); LibraryExpense.SetSpendRequestStatus(OtherTravelRequest, OtherTravelRequest.Status::Released); + // [GIVEN] "U" is either explicitly assigned to "A" or has no setup and must use the default fallback. + // Both setting the default approver and inserting expense users create explicit assignments. + ExpenseApprovalSetup.Get(DefaultExpenseUser."No."); + Assert.AreEqual(DefaultApprover."No.", ExpenseApprovalSetup."Approver No.", 'The initial fixture must exercise the explicit-assignment path.'); + ExpectedApproverNo := DefaultApprover."No."; + if UseDefaultFallback then begin + ExpenseApprovalSetup.Delete(true); + ExpectedApproverNo := ''; + end; + DefaultExpenseUser.CalcFields("Approver No."); + Assert.AreEqual(ExpectedApproverNo, DefaultExpenseUser."Approver No.", 'The fixture must select the intended explicit-assignment or default-fallback path.'); + ExpenseApprovalSetup.Get(OtherExpenseUser."No."); + Assert.AreEqual(OtherApprover."No.", ExpenseApprovalSetup."Approver No.", 'The other request must remain explicitly assigned to another approver.'); + // [WHEN] The default approver's filter is applied to pending travel requests. FilteredTravelRequest.SetRange("Document Type", FilteredTravelRequest."Document Type"::"Travel Request"); FilteredTravelRequest.SetRange(Status, FilteredTravelRequest.Status::Released); TravelRequestApproval.ApplyApproverFilter(FilteredTravelRequest, DefaultApprover.SystemId); - // [THEN] Only the literal unassigned user's request is visible, not the other approver's request. + // [THEN] Only "U"'s literal request is visible, not the request assigned to "B". FilteredTravelRequest.SetRange("No.", DefaultTravelRequest."No."); - Assert.IsFalse(FilteredTravelRequest.IsEmpty(), DefaultTravelRequestVisibleMsg); + Assert.IsFalse( + FilteredTravelRequest.IsEmpty(), + StrSubstNo(ApproverFilterExpectedRequestErr, + DefaultTravelRequestVisibleMsg, FilteredTravelRequest.GetFilter("Requested For"), DefaultTravelRequest."No.")); FilteredTravelRequest.SetRange("No.", OtherTravelRequest."No."); - Assert.IsTrue(FilteredTravelRequest.IsEmpty(), UnassignedTravelRequestHiddenMsg); + Assert.IsTrue( + FilteredTravelRequest.IsEmpty(), + StrSubstNo(ApproverFilterUnexpectedRequestErr, + UnassignedTravelRequestHiddenMsg, FilteredTravelRequest.GetFilter("Requested For"), + DefaultTravelRequest."No.", OtherTravelRequest."No.")); + end; + + local procedure VerifyApproverFilterRangeGapSetup(FirstTravelRequest: Record "Spend Request"; LastTravelRequest: Record "Spend Request"; UnassignedTravelRequest: Record "Spend Request"; AssignedApproverNo: Code[20]; DefaultApproverNo: Code[20]) + var + ExpenseApprovalSetup: Record "Expense Approval Setup"; + ExpenseAgentSetup: Record "Expense Agent Setup"; + ExpenseUser: Record "Expense User"; + begin + Assert.AreEqual('TR-A', FirstTravelRequest."Requested For", 'The first request must use the lower range endpoint.'); + Assert.AreEqual('TR-C', LastTravelRequest."Requested For", 'The last request must use the upper range endpoint.'); + Assert.AreEqual('TR-B', UnassignedTravelRequest."Requested For", 'The unassigned request must lie between the assigned users.'); + ExpenseApprovalSetup.Get(FirstTravelRequest."Requested For"); + Assert.AreEqual(AssignedApproverNo, ExpenseApprovalSetup."Approver No.", 'The first user must be explicitly assigned.'); + ExpenseApprovalSetup.Get(LastTravelRequest."Requested For"); + Assert.AreEqual(AssignedApproverNo, ExpenseApprovalSetup."Approver No.", 'The last user must have the same explicit approver.'); + Assert.IsFalse(ExpenseApprovalSetup.Get(UnassignedTravelRequest."Requested For"), 'The middle user must have no approval setup row.'); + ExpenseUser.Get(UnassignedTravelRequest."Requested For"); + ExpenseUser.CalcFields("Approver No."); + Assert.AreEqual('', ExpenseUser."Approver No.", 'The middle user must use the default approver fallback.'); + ExpenseAgentSetup.Get(); + Assert.AreEqual(DefaultApproverNo, ExpenseAgentSetup."Default Approver No.", 'The middle user must belong to the other default approver.'); + Assert.AreNotEqual(AssignedApproverNo, DefaultApproverNo, 'The explicit and default approvers must differ.'); + ExpenseApprovalSetup.SetRange("Expense User No.", FirstTravelRequest."Requested For", LastTravelRequest."Requested For"); + Assert.AreEqual(2, ExpenseApprovalSetup.Count(), 'Only the two assigned setup rows may exist in the tested range.'); + end; + + local procedure VerifyApproverFilterRangeGapResults(var FilteredTravelRequest: Record "Spend Request"; FirstTravelRequestNo: Code[20]; LastTravelRequestNo: Code[20]; UnassignedTravelRequestNo: Code[20]) + begin + FilteredTravelRequest.SetRange("No.", FirstTravelRequestNo); + Assert.IsFalse( + FilteredTravelRequest.IsEmpty(), + StrSubstNo(ApproverFilterExpectedRequestErr, + AssignedTravelRequestVisibleMsg, FilteredTravelRequest.GetFilter("Requested For"), FirstTravelRequestNo)); + FilteredTravelRequest.SetRange("No.", LastTravelRequestNo); + Assert.IsFalse( + FilteredTravelRequest.IsEmpty(), + StrSubstNo(ApproverFilterExpectedRequestErr, + AssignedTravelRequestVisibleMsg, FilteredTravelRequest.GetFilter("Requested For"), LastTravelRequestNo)); + FilteredTravelRequest.SetRange("No.", UnassignedTravelRequestNo); + Assert.IsTrue( + FilteredTravelRequest.IsEmpty(), + StrSubstNo(ApproverFilterUnexpectedRequestErr, + UnassignedTravelRequestHiddenMsg, FilteredTravelRequest.GetFilter("Requested For"), + FirstTravelRequestNo, UnassignedTravelRequestNo)); end; local procedure VerifyPostedTravelRequestHistory(SpendRequest: Record "Spend Request"; ExpectedPostedExpenseReportHeader: Record "Posted Expense Report Header") diff --git a/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json b/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json index b23129476de..8c33fbff153 100644 --- a/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json +++ b/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json @@ -4,11 +4,6 @@ "codeunitName": "Expense Users API Test", "method": "EmployeesAPICanFilterExpenseUsers" }, - { - "codeunitId": 148315, - "codeunitName": "Expense Users API Test", - "method": "UnlinkedExpenseUserIsHiddenFromAPI" - }, { "codeunitId": 148347, "codeunitName": "Travel Requests API Test", @@ -89,166 +84,11 @@ "codeunitName": "Travel Requests API Test", "method": "LegacySpendRequestsAPIRejectsTravelRequestOwnerChange" }, - { - "codeunitId": 148318, - "codeunitName": "Expense Capabilities API Test", - "method": "CapabilitiesProjectsEnabledViaAPI" - }, - { - "codeunitId": 148318, - "codeunitName": "Expense Capabilities API Test", - "method": "CapabilitiesProjectsDisabledViaAPI" - }, - { - "codeunitId": 148318, - "codeunitName": "Expense Capabilities API Test", - "method": "CapabilitiesConsolidatedProjectsFollowsProjectFieldsViaAPI" - }, - { - "codeunitId": 148318, - "codeunitName": "Expense Capabilities API Test", - "method": "ActivityLogCapabilityEnabledViaAPI" - }, { "codeunitId": 148318, "codeunitName": "Expense Capabilities API Test", "method": "TravelRequestCapabilityEnabledViaAPI" }, - { - "codeunitId": 148343, - "codeunitName": "Expense Activity Log API Test", - "method": "ActivityLogEntryIsExposedThroughReadOnlyAPI" - }, - { - "codeunitId": 148343, - "codeunitName": "Expense Activity Log API Test", - "method": "ActivityLogAPIRejectsWrites" - }, - { - "codeunitId": 148343, - "codeunitName": "Expense Activity Log API Test", - "method": "NonSnapshotActivityDoesNotExposeCurrencies" - }, - { - "codeunitId": 148343, - "codeunitName": "Expense Activity Log API Test", - "method": "UnscopedActivityAPIHidesEntries" - }, - { - "codeunitId": 148343, - "codeunitName": "Expense Activity Log API Test", - "method": "SubmitterHistoryReturnsCompleteTimelinesForSubmittedReports" - }, - { - "codeunitId": 148343, - "codeunitName": "Expense Activity Log API Test", - "method": "ApproverHistoryReturnsCompleteTimelinesForActedOnReports" - }, - { - "codeunitId": 148343, - "codeunitName": "Expense Activity Log API Test", - "method": "E2EActivityLogScenario" - }, - { - "codeunitId": 148333, - "codeunitName": "Expense Agent Setup API Test", - "method": "EmailAddressFieldIsExposedThroughAPI" - }, - { - "codeunitId": 148349, - "codeunitName": "Expense PerDiem Locations Test", - "method": "PerDiemLocationsReturnsLinkedLocationViaAPI" - }, - { - "codeunitId": 148349, - "codeunitName": "Expense PerDiem Locations Test", - "method": "PerDiemLocationsExcludesNonPerDiemCategoryViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "AssignedByPersonResponsibleReturnsWholeProjectViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "AssignedByProjectManagerReturnsWholeProjectViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "AssignedByProjectLevelResourceReturnsWholeProjectViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "AssignedByTaskLevelResourceReturnsOnlyThatTaskViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "AssignedByPlanningLineReturnsOnlyThatTaskViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "WholeProjectSignalSupersedesTaskLevelViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "OverlappingSignalsForSameResourceAreDeduplicatedViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "CompletedProjectExcludedEvenWhenAssignedViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "EmptyResourceUserMatchesNothingViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "BlankUserIdDoesNotMatchBlankProjectManagerViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "ProjectManagerMatchWithBlankResourceReturnsProjectViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "TasklessProjectExcludedViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "WholeProjectAndTaskLevelAcrossProjectsNoLeakageViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "AllProjectsVisibilityReturnsAllProjectsViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "AllProjectsVisibilityIncludesActiveStatusesAndPostingTasksViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "AssignedProjectsRequiresScopeFilterViaAPI" - }, - { - "codeunitId": 148331, - "codeunitName": "Expense Projects API Test", - "method": "AssignedProjectsBlockedWhenProjectFieldsDisabledViaAPI" - }, { "codeunitId": 148305, "codeunitName": "Expense Test", diff --git a/src/Layers/W1/Tests/ERM/SpendRequestTests.Codeunit.al b/src/Layers/W1/Tests/ERM/SpendRequestTests.Codeunit.al index 11f5d08109a..ea889fe6e72 100644 --- a/src/Layers/W1/Tests/ERM/SpendRequestTests.Codeunit.al +++ b/src/Layers/W1/Tests/ERM/SpendRequestTests.Codeunit.al @@ -341,6 +341,37 @@ codeunit 134242 "Spend Request Tests" 'Deleting detail should reduce header total.'); end; + [Test] + procedure DeleteDetailLineDecrementsHeaderTotalExactlyOnce() + var + SpendRequest: Record "Spend Request"; + FirstSpendRequestDetail: Record "Spend Request Detail"; + RemainingSpendRequestDetail: Record "Spend Request Detail"; + begin + // [SCENARIO 646383] Real detail deletion applies each LCY delta exactly once without removing the other detail. + Initialize(); + + // [GIVEN] An open LCY request "R" initially has zero total and two details "D1" and "D2" worth 10 and 30. + CreateSpendRequest(SpendRequest); + VerifyDetailDeletionTotals(SpendRequest."No.", 0, 0); + CreateSpendRequestDetail(FirstSpendRequestDetail, SpendRequest."No.", 10); + CreateSpendRequestDetail(RemainingSpendRequestDetail, SpendRequest."No.", 30); + VerifyDetailDeletionTotals(SpendRequest."No.", 40, 2); + + // [WHEN] Deleting "D1" runs the production table trigger, not a page action or a permission test. + FirstSpendRequestDetail.Delete(true); + + // [THEN] "D2" survives and both its amount and the header total are 30. + RemainingSpendRequestDetail.Get(RemainingSpendRequestDetail."Spend Request No.", RemainingSpendRequestDetail."Line No."); + VerifyDetailDeletionTotals(SpendRequest."No.", 30, 1); + + // [WHEN] The remaining detail "D2" is deleted through its production trigger. + RemainingSpendRequestDetail.Delete(true); + + // [THEN] No details remain and the header total is exactly zero. + VerifyDetailDeletionTotals(SpendRequest."No.", 0, 0); + end; + [Test] [Scope('OnPrem')] procedure CannotInsertDetailOnReleasedRequest() @@ -827,4 +858,21 @@ codeunit 134242 "Spend Request Tests" // [THEN] An error is raised. Assert.ExpectedError('must have the status ' + Format(SpendRequest.Status::Open)); end; + + local procedure VerifyDetailDeletionTotals(SpendRequestNo: Code[20]; ExpectedAmount: Decimal; ExpectedLineCount: Integer) + var + SpendRequest: Record "Spend Request"; + SpendRequestDetail: Record "Spend Request Detail"; + begin + SpendRequest.Get(SpendRequestNo); + Assert.AreEqual(SpendRequest.Status::Open, SpendRequest.Status, 'Detail deletion must preserve the Open status.'); + Assert.AreEqual('', SpendRequest."Currency Code", 'The request must use LCY.'); + Assert.AreEqual(ExpectedAmount, SpendRequest."Total Expected Amount", 'The header amount must reflect each deleted detail exactly once.'); + Assert.AreEqual(ExpectedAmount, SpendRequest."Total Expected Amount (LCY)", 'The LCY header amount must reflect each deleted detail exactly once.'); + SpendRequestDetail.SetRange("Spend Request No.", SpendRequestNo); + Assert.AreEqual(ExpectedLineCount, SpendRequestDetail.Count(), 'Only the intended details must be deleted.'); + SpendRequestDetail.CalcSums("Expected Amount", "Expected Amount (LCY)"); + Assert.AreEqual(ExpectedAmount, SpendRequestDetail."Expected Amount", 'The remaining detail amounts must be unchanged.'); + Assert.AreEqual(ExpectedAmount, SpendRequestDetail."Expected Amount (LCY)", 'The remaining LCY detail amounts must be unchanged.'); + end; } \ No newline at end of file diff --git a/src/Layers/W1/Tests/TestLibraries/LibraryGraphMgt.Codeunit.al b/src/Layers/W1/Tests/TestLibraries/LibraryGraphMgt.Codeunit.al index 38b8c0f8bf8..bc37687e68c 100644 --- a/src/Layers/W1/Tests/TestLibraries/LibraryGraphMgt.Codeunit.al +++ b/src/Layers/W1/Tests/TestLibraries/LibraryGraphMgt.Codeunit.al @@ -31,6 +31,12 @@ codeunit 130618 "Library - Graph Mgt" AuthenticationProviderResolved := true; end; + /// Sets the work date to November 15 in the current year to stay within test-license date limits. + procedure SetLicenseSafeWorkDate() + begin + WorkDate := DMY2Date(15, 11, Date2DMY(Today, 3)); + end; + procedure EnsureWebServiceExist(ServiceNameTxt: Text[240]; PageNumber: Integer) var WebService: Record "Web Service"; From 5c21bd318f19a57ea57750fbcb60fd4d88001ee5 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 5 Oct 2026 15:24:00 +0200 Subject: [PATCH 10/22] Enable remaining general API authentication tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session:3952f078-a881-4da8-ad96-13b727e48a91 --- .../test/src/IRS1099APITests.Codeunit.al | 4 + .../test/src/APIV1AccountsE2E.Codeunit.al | 5 + .../APIV1/test/src/APIV1AgedAPE2E.Codeunit.al | 5 + .../APIV1/test/src/APIV1AgedARE2E.Codeunit.al | 5 + .../test/src/APIV1AttachmentsE2E.Codeunit.al | 78 + .../src/APIV1AutomationRSPackage.Codeunit.al | 4 + .../test/src/APIV1BalanceSheetE2E.Codeunit.al | 4 + .../src/APIV1CashFlowStatementE2E.Codeunit.al | 4 + .../test/src/APIV1CompanyInfoE2E.Codeunit.al | 5 + .../test/src/APIV1CurrenciesE2E.Codeunit.al | 4 + .../src/APIV1CustomerPaymentsE2E.Codeunit.al | 7 +- .../test/src/APIV1CustomersE2E.Codeunit.al | 4 + .../src/APIV1DefaultDimensionsE2E.Codeunit.al | 73 + .../src/APIV1DimensionLinesE2E.Codeunit.al | 14 +- .../test/src/APIV1DimensionsE2E.Codeunit.al | 5 + .../test/src/APIV1EmployeesE2E.Codeunit.al | 4 + .../test/src/APIV1GLEntriesE2E.Codeunit.al | 6 +- .../src/APIV1IncomeStatementE2E.Codeunit.al | 4 + .../src/APIV1ItemCategoriesE2E.Codeunit.al | 4 + .../test/src/APIV1ItemVariantsE2E.Codeunit.al | 33 + .../APIV1/test/src/APIV1ItemsE2E.Codeunit.al | 4 + .../test/src/APIV1JournalLinesE2E.Codeunit.al | 5 + .../test/src/APIV1JournalsE2E.Codeunit.al | 7 + .../test/src/APIV1PDFDocumentE2E.Codeunit.al | 6 +- .../test/src/APIV1PaymentTermsE2E.Codeunit.al | 5 + .../test/src/APIV1PicturesE2E.Codeunit.al | 60 + .../src/APIV1PurchaseInvLinesE2E.Codeunit.al | 11 +- .../src/APIV1PurchaseInvoicesE2E.Codeunit.al | 4 + .../test/src/APIV1RetEarningsE2E.Codeunit.al | 4 + .../src/APIV1SalesCrMemoLinesE2E.Codeunit.al | 11 +- .../src/APIV1SalesCreditMemosE2E.Codeunit.al | 49 +- .../src/APIV1SalesInvLinesE2E.Codeunit.al | 5 + .../src/APIV1SalesOrderLinesE2E.Codeunit.al | 13 +- .../test/src/APIV1SalesOrdersE2E.Codeunit.al | 36 + .../src/APIV1SalesQuoteLinesE2E.Codeunit.al | 11 +- .../test/src/APIV1SalesQuotesE2E.Codeunit.al | 38 + .../src/APIV1ShipmentMethodsE2E.Codeunit.al | 5 + .../test/src/APIV1TaxAreasE2E.Codeunit.al | 4 + .../test/src/APIV1TrialBalanceE2E.Codeunit.al | 5 + .../APIV1/test/src/APIV1UofME2E.Codeunit.al | 15 + .../test/src/APIV1VendorsE2E.Codeunit.al | 4 + .../test/src/CountriesRegionsE2E.Codeunit.al | 4 + .../test/src/PaymentMethodsE2E.Codeunit.al | 4 + .../test/src/SalesInvoicesE2E.Codeunit.al | 58 + .../APIV1/test/src/TaxGroupsE2E.Codeunit.al | 4 + .../src/APIV2AccountingPeriodsE2E.Codeunit.al | 9 + .../test/src/APIV2AccountsE2E.Codeunit.al | 7 +- .../APIV2/test/src/APIV2AgedAPE2E.Codeunit.al | 7 +- .../APIV2/test/src/APIV2AgedARE2E.Codeunit.al | 7 +- .../src/APIV2ApplyVendorEntE2E.Codeunit.al | 8 +- .../src/APIV2ApprovalEntriesE2E.Codeunit.al | 9 +- .../src/APIV2ApprovalUserSetupE2E.Codeunit.al | 9 +- .../src/APIV2AssemblyOrdersE2E.Codeunit.al | 21 + .../test/src/APIV2AttachmentsE2E.Codeunit.al | 100 + .../src/APIV2AutomationRSPackage.Codeunit.al | 7 +- .../test/src/APIV2BalanceSheetE2E.Codeunit.al | 6 +- .../src/APIV2BlPurchOrdersE2E.Codeunit.al | 20 + .../src/APIV2BlSalesOrdersE2E.Codeunit.al | 20 + .../src/APIV2CashFlowStatementE2E.Codeunit.al | 6 +- .../test/src/APIV2CompanyInfoE2E.Codeunit.al | 6 + .../test/src/APIV2ContactsE2E.Codeunit.al | 6 + .../test/src/APIV2ContactsInfE2E.Codeunit.al | 16 + .../src/APIV2CountriesRegionsE2E.Codeunit.al | 6 +- .../test/src/APIV2CurrenciesE2E.Codeunit.al | 6 +- .../src/APIV2CurrencyExchRateE2E.Codeunit.al | 5 + .../src/APIV2CustRetReasonsE2E.Codeunit.al | 5 + .../src/APIV2CustomerPaymentsE2E.Codeunit.al | 8 +- .../test/src/APIV2CustomersE2E.Codeunit.al | 5 + .../src/APIV2DefaultDimensionsE2E.Codeunit.al | 67 + .../test/src/APIV2DimSetLinesE2E.Codeunit.al | 66 + .../test/src/APIV2DimensionsE2E.Codeunit.al | 7 +- .../src/APIV2DocumentAttachE2E.Codeunit.al | 86 + .../test/src/APIV2EmployeesE2E.Codeunit.al | 6 +- .../test/src/APIV2FALocationsE2E.Codeunit.al | 5 + .../test/src/APIV2FixedAssetsE2E.Codeunit.al | 5 + .../test/src/APIV2GLEntriesE2E.Codeunit.al | 8 +- .../test/src/APIV2GLSetupE2E.Codeunit.al | 10 + .../src/APIV2GenProdPostGroupE2E.Codeunit.al | 14 +- .../src/APIV2IncomeStatementE2E.Codeunit.al | 6 +- .../APIV2InventoryPostGroupE2E.Codeunit.al | 14 +- .../src/APIV2ItemCategoriesE2E.Codeunit.al | 6 +- .../test/src/APIV2ItemVariantsE2E.Codeunit.al | 34 + .../APIV2/test/src/APIV2ItemsE2E.Codeunit.al | 8 +- .../src/APIV2JobQueueEntriesE2E.Codeunit.al | 15 + .../APIV2JobQueueLogEntriesE2E.Codeunit.al | 9 + .../test/src/APIV2JournalsE2E.Codeunit.al | 9 +- .../test/src/APIV2LocationsE2E.Codeunit.al | 6 + .../src/APIV2OpportunitiesE2E.Codeunit.al | 26 + .../test/src/APIV2PDFDocumentE2E.Codeunit.al | 6 +- .../src/APIV2PaymentMethodsE2E.Codeunit.al | 6 +- .../test/src/APIV2PaymentTermsE2E.Codeunit.al | 7 +- .../test/src/APIV2PicturesE2E.Codeunit.al | 77 +- .../APIV2PstdApprovalEntriesE2E.Codeunit.al | 8 + .../src/APIV2PurchCrMLinesE2E.Codeunit.al | 48 + .../test/src/APIV2PurchCrMemosE2E.Codeunit.al | 39 +- .../src/APIV2PurchOrderLinesE2E.Codeunit.al | 6 + .../test/src/APIV2PurchQuotesE2E.Codeunit.al | 20 + .../src/APIV2PurchRcptLinesE2E.Codeunit.al | 16 + .../src/APIV2PurchRetOrdersE2E.Codeunit.al | 20 + .../src/APIV2PurchRetShptsE2E.Codeunit.al | 14 + .../src/APIV2PurchaseInvLinesE2E.Codeunit.al | 7 +- .../src/APIV2PurchaseInvoicesE2E.Codeunit.al | 9 + .../src/APIV2PurchaseOrdersE2E.Codeunit.al | 21 +- .../src/APIV2PurchaseReceiptsE2E.Codeunit.al | 12 + .../test/src/APIV2RetEarningsE2E.Codeunit.al | 6 +- .../src/APIV2SalesCrMemoLinesE2E.Codeunit.al | 8 +- .../src/APIV2SalesCreditMemosE2E.Codeunit.al | 51 +- .../src/APIV2SalesInvLinesE2E.Codeunit.al | 6 + .../src/APIV2SalesInvoicesE2E.Codeunit.al | 66 + .../test/src/APIV2SalesOrdArchE2E.Codeunit.al | 14 + .../src/APIV2SalesOrderLinesE2E.Codeunit.al | 9 +- .../test/src/APIV2SalesOrdersE2E.Codeunit.al | 39 + .../src/APIV2SalesQuoteLinesE2E.Codeunit.al | 7 +- .../test/src/APIV2SalesQuotesE2E.Codeunit.al | 41 + .../src/APIV2SalesRetOrdersE2E.Codeunit.al | 20 + .../src/APIV2SalesShipLinesE2E.Codeunit.al | 16 + .../src/APIV2SalesShipmentsE2E.Codeunit.al | 12 + .../src/APIV2SalespersonPurchE2E.Codeunit.al | 5 + .../src/APIV2ShipmentMethodsE2E.Codeunit.al | 6 + .../test/src/APIV2TaxAreasE2E.Codeunit.al | 6 +- .../test/src/APIV2TaxGroupsE2E.Codeunit.al | 6 +- .../src/APIV2TransferOrdersE2E.Codeunit.al | 20 + .../test/src/APIV2TrialBalanceE2E.Codeunit.al | 7 +- .../APIV2/test/src/APIV2UofME2E.Codeunit.al | 17 +- .../src/APIV2VendorPaymentsE2E.Codeunit.al | 7 +- .../test/src/APIV2VendorsE2E.Codeunit.al | 5 + .../src/APIV2WorkflowApproversE2E.Codeunit.al | 9 +- .../test/src/APIV2WorkflowsE2E.Codeunit.al | 9 +- .../src/APIV2itemLedgEntriesE2E.Codeunit.al | 7 +- .../src/Processing/EDocAPITest.Codeunit.al | 16 +- .../Test/E2EPowerBIFinanceTest.Codeunit.al | 24 + .../Test/E2EPowerBIInventoryTest.Codeunit.al | 66 + .../E2EPowerBIManufacturingTest.Codeunit.al | 39 + .../Test/E2EPowerBIProjectTest.Codeunit.al | 20 + .../Test/E2EPowerBIPurchasesTest.Codeunit.al | 28 + .../Test/E2EPowerBISalesTest.Codeunit.al | 3 + .../src/QltyTestsInspectionsAPI.Codeunit.al | 3 + .../src/PowerBISustainabilityTest.Codeunit.al | 12 + .../src/SustainabilityAPITests.Codeunit.al | 3 + .../E-Document Core Tests.DisabledTest.json | 5 - .../_Exclude_APIV1__Tests.DisabledTest.json | 1451 +------ .../_Exclude_APIV2__Tests.DisabledTest.json | 3582 +---------------- .../ERMGeneralJournalUT.Codeunit.al | 4 +- .../Prepayment/ERMPrepaymentII.Codeunit.al | 1 - .../ERMGeneralJournalUT.Codeunit.al | 4 +- .../ERMGeneralJournalUT.Codeunit.al | 4 +- .../ERMGeneralJournalUT.Codeunit.al | 4 +- .../ERMGeneralJournalUT.Codeunit.al | 4 +- .../Prepayment/ERMPrepaymentII.Codeunit.al | 1 - .../ERMGeneralJournalUT.Codeunit.al | 4 +- .../IRS1099CodeEntityE2ETest.Codeunit.al | 4 +- .../WFWHSubscriptionE2ETests.Codeunit.al | 4 +- .../Integration/ApiRoutesE2E.Codeunit.al | 17 +- .../Integration/WebhookLogsE2E.Codeunit.al | 3 +- .../Misc/APITestAuthHTTPTests.Codeunit.al | 93 + .../Prepayment/ERMPrepaymentII.Codeunit.al | 2 - .../Report/ReportInboxAPIE2E.Codeunit.al | 2 + .../LibraryGraphDocumentTools.Codeunit.al | 5 - .../LibraryGraphJournalLines.Codeunit.al | 5 - 159 files changed, 2457 insertions(+), 5021 deletions(-) create mode 100644 src/Layers/W1/Tests/Misc/APITestAuthHTTPTests.Codeunit.al diff --git a/src/Apps/US/IRSForms/test/src/IRS1099APITests.Codeunit.al b/src/Apps/US/IRSForms/test/src/IRS1099APITests.Codeunit.al index 7495ec2d793..e114c4a8e6e 100644 --- a/src/Apps/US/IRSForms/test/src/IRS1099APITests.Codeunit.al +++ b/src/Apps/US/IRSForms/test/src/IRS1099APITests.Codeunit.al @@ -52,11 +52,15 @@ codeunit 148018 "IRS 1099 API Tests" IRS1099FormDocHeader: Record "IRS 1099 Form Doc. Header"; IRSReportingPeriod: Record "IRS Reporting Period"; begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IRS1099FormDocHeader.DeleteAll(true); IRSReportingPeriod.DeleteAll(true); LibraryTestInitialize.OnTestInitialize(Codeunit::"IRS 1099 API Tests"); if IsInitialized then exit; + + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"IRS 1099 API Tests"); IsInitialized := true; diff --git a/src/Apps/W1/APIV1/test/src/APIV1AccountsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1AccountsE2E.Codeunit.al index 9e3ef6f5e64..f66d536a7df 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1AccountsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1AccountsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139701 "APIV1 - Accounts E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -19,9 +20,13 @@ codeunit 139701 "APIV1 - Accounts E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; COMMIT(); end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1AgedAPE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1AgedAPE2E.Codeunit.al index 9c057dab240..033103a890c 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1AgedAPE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1AgedAPE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139719 "APIV1 - Aged AP E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -60,9 +61,13 @@ codeunit 139719 "APIV1 - Aged AP E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := TRUE; end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1AgedARE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1AgedARE2E.Codeunit.al index afdb729791c..a44f0646cba 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1AgedARE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1AgedARE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139720 "APIV1 - Aged AR E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -60,9 +61,13 @@ codeunit 139720 "APIV1 - Aged AR E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := TRUE; end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1AttachmentsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1AttachmentsE2E.Codeunit.al index 5e03d6caccc..27d6e6213f8 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1AttachmentsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1AttachmentsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139733 "APIV1 - Attachments E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -43,6 +44,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve all records from the Attachments API. + Initialize(); + // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreateGenJournalLine(DocumentRecordRef); CreateAttachments(DocumentRecordRef, AttachmentId); @@ -65,6 +68,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve all records from the Attachments API. + Initialize(); + // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreateGLEntry(DocumentRecordRef); CreateAttachments(DocumentRecordRef, AttachmentId); @@ -88,6 +93,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve all records from the Attachments API. + Initialize(); + // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreatePostedSalesInvoice(DocumentRecordRef, DocumentId); CreateAttachments(DocumentRecordRef, AttachmentId); @@ -110,6 +117,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve all records from the Attachments API. + Initialize(); + // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreateDraftSalesInvoice(DocumentRecordRef); CreateAttachments(DocumentRecordRef, AttachmentId); @@ -133,6 +142,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve all records from the Attachments API. + Initialize(); + // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreatePostedPurchaseInvoice(DocumentRecordRef, DocumentId); CreateAttachments(DocumentRecordRef, AttachmentId); @@ -155,6 +166,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve all records from the Attachments API. + Initialize(); + // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreateDraftPurchaseInvoice(DocumentRecordRef); CreateAttachments(DocumentRecordRef, AttachmentId); @@ -177,6 +190,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve all records from the Attachments API. + Initialize(); + // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreateSalesQuote(DocumentRecordRef); CreateAttachments(DocumentRecordRef, AttachmentId); @@ -199,6 +214,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve the Attachment record from the Attachment API. + Initialize(); + // [GIVEN] Attachment exists in the Incoming Document Attachment table CreateGenJournalLine(DocumentRecordRef); AttachmentId := CreateAttachment(DocumentRecordRef); @@ -224,6 +241,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve the Attachment record from the Attachment API. + Initialize(); + // [GIVEN] Attachment exists in the Incoming Document Attachment table JournalName := LibraryUtility.GenerateRandomCode(GenJournalBatch.FieldNo(Name), Database::"Gen. Journal Batch"); LibraryAPIGeneralJournal.EnsureGenJnlBatchExists(GraphMgtJournal.GetDefaultJournalLinesTemplateName(), JournalName); @@ -249,6 +268,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve the Attachment record from the Attachment API. + Initialize(); + // [GIVEN] Attachment exists in the Incoming Document Attachment table CreateGLEntry(DocumentRecordRef); AttachmentId := CreateAttachment(DocumentRecordRef); @@ -273,6 +294,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve the Attachment record from the Attachment API. + Initialize(); + // [GIVEN] Attachment exists in the Incoming Document Attachment table CreatePostedSalesInvoice(DocumentRecordRef, DocumentId); AttachmentId := CreateAttachment(DocumentRecordRef); @@ -296,6 +319,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve the Attachment record from the Attachment API. + Initialize(); + // [GIVEN] Attachment exists in the Incoming Document Attachment table CreateDraftSalesInvoice(DocumentRecordRef); AttachmentId := CreateAttachment(DocumentRecordRef); @@ -319,6 +344,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] User can retrieve the Attachment record from the Attachment API. + Initialize(); + // [GIVEN] Attachment exists in the Incoming Document Attachment table CreateSalesQuote(DocumentRecordRef); AttachmentId := CreateAttachment(DocumentRecordRef); @@ -346,6 +373,8 @@ codeunit 139733 "APIV1 - Attachments E2E" ActualBase64Content: Text; begin // [SCENARIO] User can update linked attachment binary content through the Attachment API. + Initialize(); + // [GIVEN] A linked attachment exists CreateGenJournalLine(DocumentRecordRef); AttachmentId := CreateAttachment(DocumentRecordRef); @@ -380,6 +409,8 @@ codeunit 139733 "APIV1 - Attachments E2E" ActualBase64Content: Text; begin // [SCENARIO] User can update linked attachment binary content through the Attachment API. + Initialize(); + // [GIVEN] A linked attachment exists CreateGLEntry(DocumentRecordRef); AttachmentId := CreateAttachment(DocumentRecordRef); @@ -407,6 +438,8 @@ codeunit 139733 "APIV1 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateGenJournalLine(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -416,6 +449,8 @@ codeunit 139733 "APIV1 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateGLEntry(DocumentRecordRef); TestCreateGLEAttachment(DocumentRecordRef); end; @@ -426,6 +461,8 @@ codeunit 139733 "APIV1 - Attachments E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedSalesInvoice(DocumentRecordRef, DocumentId); TestCreateAttachment(DocumentRecordRef, DocumentId); end; @@ -435,6 +472,8 @@ codeunit 139733 "APIV1 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftSalesInvoice(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -445,6 +484,8 @@ codeunit 139733 "APIV1 - Attachments E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedPurchaseInvoice(DocumentRecordRef, DocumentId); TestCreateAttachment(DocumentRecordRef, DocumentId); end; @@ -454,10 +495,19 @@ codeunit 139733 "APIV1 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftPurchaseInvoice(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + local procedure TestCreateAttachment(var DocumentRecordRef: RecordRef) var DocumentId: Guid; @@ -555,6 +605,8 @@ codeunit 139733 "APIV1 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateSalesQuote(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -564,6 +616,8 @@ codeunit 139733 "APIV1 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateGenJournalLine(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -573,6 +627,8 @@ codeunit 139733 "APIV1 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateGLEntry(DocumentRecordRef); TestDeleteGLEAttachment(DocumentRecordRef); end; @@ -583,6 +639,8 @@ codeunit 139733 "APIV1 - Attachments E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedSalesInvoice(DocumentRecordRef, DocumentId); TestDeleteAttachment(DocumentRecordRef, DocumentId); end; @@ -592,6 +650,8 @@ codeunit 139733 "APIV1 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftSalesInvoice(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -602,6 +662,8 @@ codeunit 139733 "APIV1 - Attachments E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedPurchaseInvoice(DocumentRecordRef, DocumentId); TestDeleteAttachment(DocumentRecordRef, DocumentId); end; @@ -611,6 +673,8 @@ codeunit 139733 "APIV1 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftPurchaseInvoice(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -620,6 +684,8 @@ codeunit 139733 "APIV1 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateSalesQuote(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -697,6 +763,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] The Attachment is transferred from a Draft Invoice the to Posted Invoice after posting. + Initialize(); + // [GIVEN] A draft sales invoice exists. CreateDraftSalesInvoice(DocumentRecordRef); DocumentId := GetDocumentId(DocumentRecordRef); @@ -737,6 +805,8 @@ codeunit 139733 "APIV1 - Attachments E2E" TargetURL: Text; begin // [SCENARIO] The Attachment is transferred from a Draft Invoice the to Posted Invoice after posting. + Initialize(); + // [GIVEN] A draft sales invoice exists. CreateDraftPurchaseInvoice(DocumentRecordRef); DocumentId := GetDocumentId(DocumentRecordRef); @@ -779,6 +849,8 @@ codeunit 139733 "APIV1 - Attachments E2E" NewBase64Content: Text; begin // [SCENARIO] Changing an attachment file name keeps other fields unchanged + Initialize(); + // [GIVEN] A sales quote exists. CreateSalesQuote(DocumentRecordRef); DocumentId := GetDocumentId(DocumentRecordRef); @@ -824,6 +896,8 @@ codeunit 139733 "APIV1 - Attachments E2E" NewFileName: Text; begin // [SCENARIO] Changing an attachment content keeps other fields unchanged + Initialize(); + // [GIVEN] A draft sales invoice exists. CreateDraftSalesInvoice(DocumentRecordRef); DocumentId := GetDocumentId(DocumentRecordRef); @@ -862,6 +936,8 @@ codeunit 139733 "APIV1 - Attachments E2E" JSONBody: Text; begin // [SCENARIO] User cannot change the linked attachment ID by making a PATCH request to the Attachments API + Initialize(); + // [GIVEN] A sales quote exists. CreateSalesQuote(DocumentRecordRef); @@ -891,6 +967,8 @@ codeunit 139733 "APIV1 - Attachments E2E" JSONBody: Text; begin // [SCENARIO] User cannot change the attachment ID by making a PATCH request to the Attachments API + Initialize(); + // [GIVEN] Two documents exist. CreateSalesQuote(DocumentRecordRef[1]); CreateDraftSalesInvoice(DocumentRecordRef[2]); diff --git a/src/Apps/W1/APIV1/test/src/APIV1AutomationRSPackage.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1AutomationRSPackage.Codeunit.al index fba368ca1dc..b3cfb39fa44 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1AutomationRSPackage.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1AutomationRSPackage.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139731 "APIV1 - Automation RS Package" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -29,11 +30,14 @@ codeunit 139731 "APIV1 - Automation RS Package" var TenantConfigPackageFile: Record "Tenant Config. Package File"; begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); TenantConfigPackageFile.DELETEALL(TRUE); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); SamplePackageFile := GenerateSamplePackageFile(); IsInitialized := TRUE; end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1BalanceSheetE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1BalanceSheetE2E.Codeunit.al index 9f700458c0e..4864b9df7d4 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1BalanceSheetE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1BalanceSheetE2E.Codeunit.al @@ -62,9 +62,13 @@ codeunit 139715 "APIV1 - Balance Sheet E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := TRUE; end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1CashFlowStatementE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1CashFlowStatementE2E.Codeunit.al index 2478bfb256f..b147dc3c1fd 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1CashFlowStatementE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1CashFlowStatementE2E.Codeunit.al @@ -62,9 +62,13 @@ codeunit 139717 "APIV1 - CashFlow Statement E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := TRUE; end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1CompanyInfoE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1CompanyInfoE2E.Codeunit.al index 6b3b4e66322..9cd8d21baa9 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1CompanyInfoE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1CompanyInfoE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139706 "APIV1 - Company Info. E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = IntegrationTest; TestPermissions = Disabled; @@ -22,9 +23,13 @@ codeunit 139706 "APIV1 - Company Info. E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; COMMIT(); end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1CurrenciesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1CurrenciesE2E.Codeunit.al index 5105b4c44bb..4552658a7cc 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1CurrenciesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1CurrenciesE2E.Codeunit.al @@ -155,9 +155,13 @@ codeunit 139713 "APIV1 - Currencies E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1CustomerPaymentsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1CustomerPaymentsE2E.Codeunit.al index 13e55ac2bb6..ccac81746e9 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1CustomerPaymentsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1CustomerPaymentsE2E.Codeunit.al @@ -1,6 +1,7 @@ codeunit 139743 "APIV1 - Customer Payments E2E" { Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -769,10 +770,14 @@ codeunit 139743 "APIV1 - Customer Payments E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibraryTestInitialize.OnTestInitialize(Codeunit::"APIV1 - Customer Payments E2E"); - if not isInitialized then + if not isInitialized then begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); isInitialized := true; + end; LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"APIV1 - Customer Payments E2E"); end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1CustomersE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1CustomersE2E.Codeunit.al index be1fe579b35..334cc5558cc 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1CustomersE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1CustomersE2E.Codeunit.al @@ -34,9 +34,13 @@ codeunit 139702 "APIV1 - Customers E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; COMMIT(); end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1DefaultDimensionsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1DefaultDimensionsE2E.Codeunit.al index 96621497106..a233371f061 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1DefaultDimensionsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1DefaultDimensionsE2E.Codeunit.al @@ -37,6 +37,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Customer: Record "Customer"; begin // [FEATURE] [Customer] + Initialize(); + // [GIVEN] a customer, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the customer // [THEN] The default dimension has been added to the customer @@ -50,6 +52,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Vendor: Record "Vendor"; begin // [FEATURE] [Vendor] + Initialize(); + // [GIVEN] a vendor, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the vendor // [THEN] The default dimension has been added to the vendor @@ -63,6 +67,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Item: Record "Item"; begin // [FEATURE] [Item] + Initialize(); + // [GIVEN] a Item, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Item // [THEN] The default dimension has been added to the Item @@ -89,6 +95,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Customer: Record "Customer"; begin // [FEATURE] [Customer] + Initialize(); + // [GIVEN] a customer, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the customer // [THEN] The default dimension has been added to the customer @@ -102,6 +110,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Vendor: Record "Vendor"; begin // [FEATURE] [Vendor] + Initialize(); + // [GIVEN] a Vendor, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Vendor // [THEN] The default dimension has been added to the Vendor @@ -115,6 +125,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Item: Record "Item"; begin // [FEATURE] [Item] + Initialize(); + // [GIVEN] a Item, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Item // [THEN] The default dimension has been added to the Item @@ -141,6 +153,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Customer: Record "Customer"; begin // [FEATURE] [Customer] + Initialize(); + // [GIVEN] a customer, a dimension and a dimension value // [WHEN] a user issues a http request to create a default dimension without dimension id // [THEN] You get an error @@ -154,6 +168,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Vendor: Record "Vendor"; begin // [FEATURE] [Vendor] + Initialize(); + // [GIVEN] a Vendor, a dimension and a dimension value // [WHEN] a user issues a http request to create a default dimension without dimension id // [THEN] You get an error @@ -167,6 +183,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Item: Record "Item"; begin // [FEATURE] [Item] + Initialize(); + // [GIVEN] a Item, a dimension and a dimension value // [WHEN] a user issues a http request to create a default dimension without dimension id // [THEN] You get an error @@ -193,6 +211,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Customer: Record "Customer"; begin // [FEATURE] [Customer] + Initialize(); + // [GIVEN] a customer, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the customer, with mismatching dimesnion and dimension value // [THEN] You get an error @@ -206,6 +226,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Vendor: Record "Vendor"; begin // [FEATURE] [Vendor] + Initialize(); + // [GIVEN] a Vendor, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Vendor, with mismatching dimesnion and dimension value // [THEN] You get an error @@ -219,6 +241,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Item: Record "Item"; begin // [FEATURE] [Item] + Initialize(); + // [GIVEN] a Item, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Item, with mismatching dimesnion and dimension value // [THEN] You get an error @@ -245,6 +269,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Customer: Record "Customer"; begin // [FEATURE] [Customer] + Initialize(); + // [GIVEN] a customer, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension on the customer // [THEN] You get an error @@ -258,6 +284,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Vendor: Record "Vendor"; begin // [FEATURE] [Vendor] + Initialize(); + // [GIVEN] a Vendor, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension on the Vendor // [THEN] You get an error @@ -271,6 +299,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Item: Record "Item"; begin // [FEATURE] [Item] + Initialize(); + // [GIVEN] a Item, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension on the Item // [THEN] You get an error @@ -297,6 +327,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Customer: Record "Customer"; begin // [FEATURE] [Customer] + Initialize(); + // [GIVEN] a customer, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension value on the customer // [THEN] You get an error @@ -310,6 +342,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Vendor: Record "Vendor"; begin // [FEATURE] [Vendor] + Initialize(); + // [GIVEN] a Vendor, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension value on the Vendor // [THEN] You get an error @@ -323,6 +357,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Item: Record "Item"; begin // [FEATURE] [Item] + Initialize(); + // [GIVEN] a Item, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension value on the Item // [THEN] You get an error @@ -349,6 +385,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Customer: Record "Customer"; begin // [FEATURE] [Customer] + Initialize(); + // [GIVEN] a customer with a default dimension // [WHEN] The user posts a http request to delete the default dimension on the customer // [THEN] The default dimension has been deleted from the customer's default dimensions @@ -362,6 +400,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Vendor: Record "Vendor"; begin // [FEATURE] [Vendor] + Initialize(); + // [GIVEN] a Vendor with a default dimension // [WHEN] The user posts a http request to delete a default dimension on the Vendor // [THEN] The default dimension has been deleted from the vendor's default dimensions @@ -375,6 +415,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Item: Record "Item"; begin // [FEATURE] [Item] + Initialize(); + // [GIVEN] a Item with a default dimension // [WHEN] The user posts a http request to delete a default dimension on the Item // [THEN] The default dimension has been deleted from the item's default dimensions @@ -401,6 +443,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Customer: Record "Customer"; begin // [FEATURE] [Customer] + Initialize(); + // [GIVEN] a customer with a default dimension // [WHEN] The user posts a http request to get the default dimension on the customer // [THEN] The response contains the default dimension that has been added to the customer @@ -414,6 +458,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Vendor: Record "Vendor"; begin // [FEATURE] [Vendor] + Initialize(); + // [GIVEN] a Vendor with a default dimension // [WHEN] The user posts a http request to get a default dimension on the Vendor // [THEN] The response contains the default dimension that has been added to the Vendor @@ -427,6 +473,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Item: Record "Item"; begin // [FEATURE] [Item] + Initialize(); + // [GIVEN] a Item with a default dimension // [WHEN] The user posts a http request to get a default dimension on the Item // [THEN] The response contains the default dimension that has been added to the Item @@ -453,6 +501,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Customer: Record "Customer"; begin // [FEATURE] [Customer] + Initialize(); + // [GIVEN] a customer with a default dimension // [WHEN] The user posts a http request to patch the default dimension on the customer // [THEN] The default dimension has been updated for the customer @@ -466,6 +516,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Vendor: Record "Vendor"; begin // [FEATURE] [Vendor] + Initialize(); + // [GIVEN] a Vendor with a default dimension // [WHEN] The user posts a http request to patch a default dimension on the Vendor // [THEN] The default dimension has been updated for the vendor @@ -479,6 +531,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Item: Record "Item"; begin // [FEATURE] [Item] + Initialize(); + // [GIVEN] a Item with a default dimension // [WHEN] The user posts a http request to patch a default dimension on the Item // [THEN] The default dimension has been updated for the item @@ -505,6 +559,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Customer: Record "Customer"; begin // [FEATURE] [Customer] + Initialize(); + // [GIVEN] a customer with a default dimension // [WHEN] The user posts a http request to patch the default dimension with a blocked dimension value on the customer // [THEN] You get an error @@ -518,6 +574,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Vendor: Record "Vendor"; begin // [FEATURE] [Vendor] + Initialize(); + // [GIVEN] a Vendor with a default dimension // [WHEN] The user posts a http request to patch a default dimension with a blocked dimension value on the Vendor // [THEN] You get an error @@ -531,6 +589,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Item: Record "Item"; begin // [FEATURE] [Item] + Initialize(); + // [GIVEN] a Item with a default dimension // [WHEN] The user posts a http request to patch a default dimension with a blocked dimension value on the Item // [THEN] You get an error @@ -557,6 +617,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Customer: Record "Customer"; begin // [FEATURE] [Customer] + Initialize(); + // [GIVEN] a customer with a default dimension // [WHEN] The user posts a http request to patch the default dimension with a blocked dimension value on the customer // [THEN] You get an error @@ -570,6 +632,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Vendor: Record "Vendor"; begin // [FEATURE] [Vendor] + Initialize(); + // [GIVEN] a vendor with a default dimension // [WHEN] The user posts a http request to patch the default dimension with a blocked dimension value on the vendor // [THEN] You get an error @@ -583,6 +647,8 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" Item: Record "Item"; begin // [FEATURE] [Item] + Initialize(); + // [GIVEN] a item with a default dimension // [WHEN] The user posts a http request to patch the default dimension with a blocked dimension value on the item // [THEN] You get an error @@ -603,6 +669,13 @@ codeunit 139732 "APIV1 - Default Dimensions E2E" TestPatchDefaultDimensionFailsWhenChangingDimensionCode(DATABASE::Employee, Employee."No.", Employee.SystemId); end; + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + local procedure TestCreateDefaultDimensionWithDimensionCode(TableNo: Integer; ParentNo: Code[20]; ParentId: Guid) var Dimension: Record "Dimension"; diff --git a/src/Apps/W1/APIV1/test/src/APIV1DimensionLinesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1DimensionLinesE2E.Codeunit.al index a0f0993cf2a..90bd765f3c4 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1DimensionLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1DimensionLinesE2E.Codeunit.al @@ -28,6 +28,9 @@ codeunit 139725 "APIV1 - Dimension Lines E2E" procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); end; [Test] @@ -44,6 +47,7 @@ codeunit 139725 "APIV1 - Dimension Lines E2E" ResponseText: Text; begin // [SCENARIO] Create a dimension line in a customer payment through a POST method and check if it was created + Initialize(); LibraryGraphJournalLines.Initialize(); // [GIVEN] a journal @@ -88,6 +92,7 @@ codeunit 139725 "APIV1 - Dimension Lines E2E" ResponseText: Text; begin // [SCENARIO] Creating a dimension line through a POST method without specifying a parent Id fails + Initialize(); LibraryGraphJournalLines.Initialize(); // [GIVEN] a dimension with a value @@ -119,6 +124,7 @@ codeunit 139725 "APIV1 - Dimension Lines E2E" TargetURL: Text; begin // [SCENARIO] Try to create a dimension line with an already existing code + Initialize(); LibraryGraphJournalLines.Initialize(); // [GIVEN] a journal @@ -171,6 +177,7 @@ codeunit 139725 "APIV1 - Dimension Lines E2E" TargetURL: Text; begin // [SCENARIO] Create dimension lines in a journal line and use a GET method to retrieve them + Initialize(); LibraryGraphJournalLines.Initialize(); // [GIVEN] a journal @@ -227,6 +234,7 @@ codeunit 139725 "APIV1 - Dimension Lines E2E" TargetURL: Text; begin // [SCENARIO] Create dimension lines in a journal line and use a GET method to retrieve them + Initialize(); // [GIVEN] a customer payment in the General Journal Table LibraryGraphJournalLines.Initialize(); @@ -276,6 +284,7 @@ codeunit 139725 "APIV1 - Dimension Lines E2E" TargetURL: Text; begin // [SCENARIO] Using a GET request to retrieve dimension lines without a filter fails + Initialize(); LibraryGraphJournalLines.Initialize(); AccountNo := LibraryGraphJournalLines.CreateAccount(); @@ -296,6 +305,7 @@ codeunit 139725 "APIV1 - Dimension Lines E2E" TargetURL: Text; begin // [SCENARIO] Using a GET request to retrieve dimension lines with a random entity fails + Initialize(); LibraryGraphJournalLines.Initialize(); // [GIVEN] a Target URL without filters @@ -323,6 +333,7 @@ codeunit 139725 "APIV1 - Dimension Lines E2E" TargetURL: Text; begin // [SCENARIO] Create a dimension line, use a PATCH method to change it and then verify the changes + Initialize(); LibraryGraphJournalLines.Initialize(); // [GIVEN] a journal @@ -386,6 +397,7 @@ codeunit 139725 "APIV1 - Dimension Lines E2E" TargetURL: Text; begin // [SCENARIO] Try to change the code of an existing dimension line + Initialize(); LibraryGraphJournalLines.Initialize(); // [GIVEN] a journal @@ -444,6 +456,7 @@ codeunit 139725 "APIV1 - Dimension Lines E2E" TargetURL: Text; begin // [SCENARIO] Create a dimension line, use a DELETE method to remove it and then verify the deletion + Initialize(); LibraryGraphJournalLines.Initialize(); // [GIVEN] a journal @@ -602,6 +615,5 @@ codeunit 139725 "APIV1 - Dimension Lines E2E" - diff --git a/src/Apps/W1/APIV1/test/src/APIV1DimensionsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1DimensionsE2E.Codeunit.al index f1aafb40161..1c92829400c 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1DimensionsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1DimensionsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139724 "APIV1 - Dimensions E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -20,9 +21,13 @@ codeunit 139724 "APIV1 - Dimensions E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; COMMIT(); end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1EmployeesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1EmployeesE2E.Codeunit.al index c2d3a8700fb..d64d5104e82 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1EmployeesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1EmployeesE2E.Codeunit.al @@ -25,12 +25,16 @@ codeunit 139722 "APIV1 - Employees E2E" var LibraryApplicationArea: Codeunit "Library - Application Area"; begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibraryApplicationArea.EnableBasicHRSetup(); // Lazy Setup. IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryHumanResource.SetupEmployeeNumberSeries(); IsInitialized := TRUE; diff --git a/src/Apps/W1/APIV1/test/src/APIV1GLEntriesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1GLEntriesE2E.Codeunit.al index f405f7a3ad5..db566bdf8bf 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1GLEntriesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1GLEntriesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139730 "APIV1 - GLEntries E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -24,10 +25,14 @@ codeunit 139730 "APIV1 - GLEntries E2E" LibraryERMCountryData: Codeunit "Library - ERM Country Data"; LibraryApplicationArea: Codeunit "Library - Application Area"; begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibraryApplicationArea.EnableFoundationSetup(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateVATPostingSetup(); LibraryERMCountryData.UpdateGeneralLedgerSetup(); @@ -163,4 +168,3 @@ codeunit 139730 "APIV1 - GLEntries E2E" - diff --git a/src/Apps/W1/APIV1/test/src/APIV1IncomeStatementE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1IncomeStatementE2E.Codeunit.al index a49c2db7b8b..d50fa6e693b 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1IncomeStatementE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1IncomeStatementE2E.Codeunit.al @@ -62,9 +62,13 @@ codeunit 139716 "APIV1 - Income Statement E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := TRUE; end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1ItemCategoriesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1ItemCategoriesE2E.Codeunit.al index 9035d86b098..fe9ac0f4d34 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1ItemCategoriesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1ItemCategoriesE2E.Codeunit.al @@ -155,9 +155,13 @@ codeunit 139707 "APIV1 - Item Categories E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1ItemVariantsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1ItemVariantsE2E.Codeunit.al index 7d404f44ee1..d621b0f771e 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1ItemVariantsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1ItemVariantsE2E.Codeunit.al @@ -22,6 +22,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Create an item variant through a POST method and check if it was created + Initialize(); + // [GIVEN] a JSON text with an item variant with itemId CreateItem(Item); Commit(); @@ -46,6 +48,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Cannot create an Item Variant with non-existing itemId + Initialize(); + // [GIVEN] a JSON text with an item variant with non-existing itemId ItemId := CreateGuid(); ItemVariantJSON := CreateItemVariantJsonWithItemId(ItemId, ItemVariantCode); @@ -67,6 +71,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Create an item variant through a POST method and check if it was created + Initialize(); + // [GIVEN] a JSON text with an item variant with itemNumber CreateItem(Item); Commit(); @@ -92,6 +98,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Cannot create an Item Variant with non-existing itemNumber + Initialize(); + // [GIVEN] a JSON text with an item variant with non-existing itemNumber ItemNo := LibraryUtility.GenerateRandomCode(ItemVariant.FieldNo(Code), Database::"Item Variant"); ItemVariantJSON := CreateItemVariantJsonWithItemNo(ItemNo, ItemVariantCode); @@ -113,6 +121,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Create an item variant through a POST method and check if it was created + Initialize(); + // [GIVEN] a JSON text with an item variant with itemNumber and itemId CreateItem(Item); Commit(); @@ -138,6 +148,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Cannot create an Item Variant with mismatching itemNumber and itemId + Initialize(); + // [GIVEN] a JSON text with an item variant with mismatching itemNumber and itemId CreateItem(Item1); CreateItem(Item2); @@ -161,6 +173,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Cannot create an Item Variant if there is an item variant with same itemNumber and code + Initialize(); + // [GIVEN] a JSON text with an item variant with same itemNumber and code CreateItem(Item); LibraryInventory.CreateItemVariant(ItemVariant, Item."No."); @@ -183,6 +197,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Get a simple customer with a GET request to the service. + Initialize(); + // [GIVEN] An item variant exists in the system CreateItem(Item); LibraryInventory.CreateItemVariant(ItemVariant, Item."No."); @@ -208,6 +224,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Create an item variant, use a PATCH method to change it and then verify the changes + Initialize(); + // [GIVEN] an item variant exists in the system CreateItem(Item); LibraryInventory.CreateItemVariant(ItemVariant, Item."No."); @@ -240,6 +258,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Create an item variant, use a PATCH method to change it and then verify the changes + Initialize(); + // [GIVEN] an item variant exists in the system CreateItem(Item1); CreateItem(Item2); @@ -273,6 +293,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Create an item variant, use a PATCH method to change it and then verify the changes + Initialize(); + // [GIVEN] an item variant exists in the system CreateItem(Item1); CreateItem(Item2); @@ -302,6 +324,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Create an item variant, use a DELETE method to remove it and then verify the deletion + Initialize(); + // [GIVEN] an item variant in the system CreateItem(Item); LibraryInventory.CreateItemVariant(ItemVariant, Item."No."); @@ -328,6 +352,8 @@ codeunit 139739 "APIV1 - Item Variants E2E" TargetURL: Text; begin // [SCENARIO] Cannot delete an item variant in use + Initialize(); + // [GIVEN] an item variant in the system and used in a sales order line LibraryInventory.CreateItem(Item); LibraryInventory.CreateItemVariant(ItemVariant, Item."No."); @@ -350,6 +376,13 @@ codeunit 139739 "APIV1 - Item Variants E2E" ServiceNameTxt: Label 'itemVariants'; + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + local procedure CreateItem(var Item: Record Item) begin Item.Init(); diff --git a/src/Apps/W1/APIV1/test/src/APIV1ItemsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1ItemsE2E.Codeunit.al index 4cddfa7b2ec..edba7c0d395 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1ItemsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1ItemsE2E.Codeunit.al @@ -33,9 +33,13 @@ codeunit 139700 "APIV1 - Items E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; COMMIT(); end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1JournalLinesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1JournalLinesE2E.Codeunit.al index c528b4c662a..2c449cfcf07 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1JournalLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1JournalLinesE2E.Codeunit.al @@ -1,6 +1,7 @@ codeunit 139745 "APIV1 - Journal Lines E2E" { Subtype = Test; + RequiredTestIsolation = Disabled; TestType = IntegrationTest; TestPermissions = Disabled; @@ -14,8 +15,12 @@ codeunit 139745 "APIV1 - Journal Lines E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); if IsInitialized then exit; + + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); IsInitialized := true; Commit(); end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1JournalsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1JournalsE2E.Codeunit.al index 6aab2759557..87d029fe1ff 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1JournalsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1JournalsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139727 "APIV1 - Journals E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -32,9 +33,13 @@ codeunit 139727 "APIV1 - Journals E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; COMMIT(); end; @@ -190,6 +195,8 @@ codeunit 139727 "APIV1 - Journals E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [GIVEN] A general journal batch with a general journal line BalAccountNo := LibraryERM.CreateGLAccountNoWithDirectPosting(); BalAccountType := GenJournalLine."Bal. Account Type"::"G/L Account"; diff --git a/src/Apps/W1/APIV1/test/src/APIV1PDFDocumentE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1PDFDocumentE2E.Codeunit.al index 3f795d60e48..5bc0a49d44b 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1PDFDocumentE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1PDFDocumentE2E.Codeunit.al @@ -1,6 +1,7 @@ codeunit 139744 "APIV1 - PDF Document E2E" { Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -24,7 +25,9 @@ codeunit 139744 "APIV1 - PDF Document E2E" var CompanyInformation: Record "Company Information"; begin - WorkDate := Today; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryGraphMgt.SetLicenseSafeWorkDate(); CompanyInformation.Get(); if CompanyInformation."Giro No." = '' then @@ -279,4 +282,3 @@ codeunit 139744 "APIV1 - PDF Document E2E" Commit(); end; } - diff --git a/src/Apps/W1/APIV1/test/src/APIV1PaymentTermsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1PaymentTermsE2E.Codeunit.al index 5c39f3bd02a..8e2d0e50f0b 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1PaymentTermsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1PaymentTermsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139704 "APIV1 - Payment Terms E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -19,9 +20,13 @@ codeunit 139704 "APIV1 - Payment Terms E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; COMMIT(); end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1PicturesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1PicturesE2E.Codeunit.al index 3913b1537e6..91bb68ca5b4 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1PicturesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1PicturesE2E.Codeunit.al @@ -28,9 +28,13 @@ codeunit 139742 "APIV1 - Pictures E2E" [Scope('OnPrem')] procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; end; @@ -46,6 +50,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Customer] // [SCENARIO] Get value picture from Customer Record + Initialize(); + // [GIVEN] a customer with image attached CreateTestCustomerWithImage(Customer, TempBlobExpected); Commit(); @@ -70,6 +76,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Customer] // [SCENARIO] Get a picture metadata from Customer Record + Initialize(); + // [GIVEN] a customer with image attached CreateTestCustomerWithImage(Customer, TempBlobExpected); Commit(); @@ -93,6 +101,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Customer] // [SCENARIO] Get value picture from Customer Record when no image exist + Initialize(); + // [GIVEN] a customer without image LibrarySales.CreateCustomer(Customer); Commit(); @@ -114,6 +124,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Customer] // [SCENARIO] Get a picture metadata from Customer Record when no image is present + Initialize(); + // [GIVEN] a customer with image attached LibrarySales.CreateCustomer(Customer); Commit(); @@ -139,6 +151,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Customer] // [SCENARIO] Set Picture on a customer without picture + Initialize(); + // [GIVEN] a customer with image attached LibrarySales.CreateCustomer(Customer); GetRedImageTempBlob(TempBlobExpected); @@ -171,6 +185,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Customer] // [SCENARIO] Replace the Picture on a customer + Initialize(); + // [GIVEN] a customer with image attached CreateTestCustomerWithImage(Customer, TempBlobOriginal); GetBlueImageTempBlob(TempBlobExpected); @@ -202,6 +218,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Customer] // [SCENARIO] Replace the Picture on a customer + Initialize(); + // [GIVEN] a customer with image attached CreateTestCustomerWithImage(Customer, TempBlobOriginal); GetBlueImageTempBlob(TempBlobExpected); @@ -229,6 +247,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Vendor] // [SCENARIO] Get value picture from Vendor Record + Initialize(); + // [GIVEN] a Vendor with image attached CreateTestVendorWithImage(Vendor, TempBlobExpected); Commit(); @@ -253,6 +273,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Vendor] // [SCENARIO] Get a picture metadata from Vendor Record + Initialize(); + // [GIVEN] a Vendor with image attached CreateTestVendorWithImage(Vendor, TempBlobExpected); Commit(); @@ -276,6 +298,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Vendor] // [SCENARIO] Get value picture from Vendor Record when no image exist + Initialize(); + // [GIVEN] a Vendor without image LibraryPurchase.CreateVendor(Vendor); Commit(); @@ -297,6 +321,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Vendor] // [SCENARIO] Get a picture metadata from Vendor Record when no image is present + Initialize(); + // [GIVEN] a Vendor with image attached LibraryPurchase.CreateVendor(Vendor); Commit(); @@ -322,6 +348,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Vendor] // [SCENARIO] Set Picture on a Vendor without picture + Initialize(); + // [GIVEN] a Vendor with image attached LibraryPurchase.CreateVendor(Vendor); GetRedImageTempBlob(TempBlobExpected); @@ -354,6 +382,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Vendor] // [SCENARIO] Replace the Picture on a Vendor + Initialize(); + // [GIVEN] a Vendor with image attached CreateTestVendorWithImage(Vendor, TempBlobOriginal); GetBlueImageTempBlob(TempBlobExpected); @@ -385,6 +415,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Vendor] // [SCENARIO] Replace the Picture on a Vendor + Initialize(); + // [GIVEN] a Vendor with image attached CreateTestVendorWithImage(Vendor, TempBlobOriginal); GetBlueImageTempBlob(TempBlobExpected); @@ -412,6 +444,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Employee] // [SCENARIO] Get value picture from Employee Record + Initialize(); + // [GIVEN] an Employee with image attached CreateTestEmployeeWithImage(Employee, TempBlobExpected); Commit(); @@ -436,6 +470,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Employee] // [SCENARIO] Get a picture metadata from Employee Record + Initialize(); + // [GIVEN] a Employee with image attached CreateTestEmployeeWithImage(Employee, TempBlobExpected); Commit(); @@ -459,6 +495,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Employee] // [SCENARIO] Get value picture from Employee Record when no image exist + Initialize(); + // [GIVEN] a Employee without image LibraryHumanResource.CreateEmployee(Employee); Commit(); @@ -480,6 +518,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Employee] // [SCENARIO] Get a picture metadata from Employee Record when no image is present + Initialize(); + // [GIVEN] a Employee with image attached LibraryHumanResource.CreateEmployee(Employee); Commit(); @@ -505,6 +545,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Employee] // [SCENARIO] Set Picture on a Employee without picture + Initialize(); + // [GIVEN] a Employee with image attached LibraryHumanResource.CreateEmployee(Employee); GetRedImageTempBlob(TempBlobExpected); @@ -537,6 +579,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Employee] // [SCENARIO] Replace the Picture on a Employee + Initialize(); + // [GIVEN] a Employee with image attached CreateTestEmployeeWithImage(Employee, TempBlobOriginal); GetBlueImageTempBlob(TempBlobExpected); @@ -568,6 +612,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Employee] // [SCENARIO] Replace the Picture on a Employee + Initialize(); + // [GIVEN] a Employee with image attached CreateTestEmployeeWithImage(Employee, TempBlobOriginal); GetBlueImageTempBlob(TempBlobExpected); @@ -595,6 +641,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Item] // [SCENARIO] Get value picture from Item Record + Initialize(); + // [GIVEN] a Item with image attached CreateTestItemWithImage(Item, TempBlobExpected); Commit(); @@ -619,6 +667,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Item] // [SCENARIO] Get a picture metadata from Item Record + Initialize(); + // [GIVEN] a Item with image attached CreateTestItemWithImage(Item, TempBlobExpected); Commit(); @@ -642,6 +692,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Item] // [SCENARIO] Get value picture from Item Record when no image exist + Initialize(); + // [GIVEN] a Item without image LibraryInventory.CreateItem(Item); Commit(); @@ -663,6 +715,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Item] // [SCENARIO] Get a picture metadata from Item Record when no image is present + Initialize(); + // [GIVEN] a Item with image attached LibraryInventory.CreateItem(Item); Commit(); @@ -688,6 +742,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Item] // [SCENARIO] Set Picture on a Item without picture + Initialize(); + // [GIVEN] a Item with image attached LibraryInventory.CreateItem(Item); GetRedImageTempBlob(TempBlobExpected); @@ -720,6 +776,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Item] // [SCENARIO] Replace the Picture on a Item + Initialize(); + // [GIVEN] a Item with image attached CreateTestItemWithImage(Item, TempBlobOriginal); GetBlueImageTempBlob(TempBlobExpected); @@ -751,6 +809,8 @@ codeunit 139742 "APIV1 - Pictures E2E" // [FEATURE] [Item] // [SCENARIO] Replace the Picture on a Item + Initialize(); + // [GIVEN] a Item with image attached CreateTestItemWithImage(Item, TempBlobOriginal); GetBlueImageTempBlob(TempBlobExpected); diff --git a/src/Apps/W1/APIV1/test/src/APIV1PurchaseInvLinesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1PurchaseInvLinesE2E.Codeunit.al index d3b95c6f61c..0a4f6a11fce 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1PurchaseInvLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1PurchaseInvLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139738 "APIV1 - Purchase Inv Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -31,9 +32,13 @@ codeunit 139738 "APIV1 - Purchase Inv Lines E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); LibraryERMCountryData.CreateGeneralPostingSetupData(); LibraryERMCountryData.UpdateGeneralPostingSetup(); @@ -1149,12 +1154,6 @@ codeunit 139738 "APIV1 - Purchase Inv Lines E2E" - - - - - - diff --git a/src/Apps/W1/APIV1/test/src/APIV1PurchaseInvoicesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1PurchaseInvoicesE2E.Codeunit.al index 8060fbf4d23..2914b56908b 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1PurchaseInvoicesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1PurchaseInvoicesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139729 "APIV1 - Purchase Invoices E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -34,6 +35,9 @@ codeunit 139729 "APIV1 - Purchase Invoices E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); end; [Test] diff --git a/src/Apps/W1/APIV1/test/src/APIV1RetEarningsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1RetEarningsE2E.Codeunit.al index 2bc76f8cb32..88a90cebf09 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1RetEarningsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1RetEarningsE2E.Codeunit.al @@ -62,9 +62,13 @@ codeunit 139721 "APIV1 - Ret. Earnings E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := TRUE; end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1SalesCrMemoLinesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1SalesCrMemoLinesE2E.Codeunit.al index 1d5fc4dabe1..615c7c0e9bb 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1SalesCrMemoLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1SalesCrMemoLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139737 "APIV1 - Sales CrMemo Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -30,9 +31,13 @@ codeunit 139737 "APIV1 - Sales CrMemo Lines E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibrarySales.SetStockoutWarning(FALSE); LibraryApplicationArea.EnableFoundationSetup(); @@ -1392,12 +1397,6 @@ codeunit 139737 "APIV1 - Sales CrMemo Lines E2E" - - - - - - diff --git a/src/Apps/W1/APIV1/test/src/APIV1SalesCreditMemosE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1SalesCreditMemosE2E.Codeunit.al index 8ab2dff2dbb..70b9714f81e 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1SalesCreditMemosE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1SalesCreditMemosE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -35,6 +36,13 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" CreditMemoStatusErr: Label 'The credit memo status is incorrect.'; MailingJobErr: Label 'The mailing job is not created.', Locked = true; + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + local procedure InitializeForSending() var TempEmailAccount: Record "Email Account"; @@ -60,6 +68,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" begin // [SCENARIO] Create posted and unposted sales credit memos and use a GET method to retrieve them + Initialize(); + // [GIVEN] 2 credit memos, one posted and one unposted CreateSalesCreditMemos(CreditMemoNo1, CreditMemoNo2); COMMIT(); @@ -93,6 +103,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" begin // [SCENARIO] Create posted and unposted Sales credit memos and use HTTP POST to delete them + Initialize(); + // [GIVEN] 2 credit memos, one posted and one unposted LibrarySales.CreateCustomerWithAddress(SellToCustomer); LibrarySales.CreateCustomerWithAddress(BillToCustomer); @@ -138,6 +150,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" begin // [SCENARIO] Create posted and unposted with specific currency set and use HTTP POST to create them + Initialize(); + // [GIVEN] an CreditMemo with a non-LCY currencyCode set LibrarySales.CreateCustomer(Customer); CustomerNo := Customer."No."; @@ -168,18 +182,24 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" [Test] procedure TestModifyCreditMemos() begin + Initialize(); + TestMultipleModifyCreditMemos(FALSE, FALSE); end; [Test] procedure TestEmptyModifyCreditMemos() begin + Initialize(); + TestMultipleModifyCreditMemos(TRUE, FALSE); end; [Test] procedure TestPartialModifyCreditMemos() begin + Initialize(); + TestMultipleModifyCreditMemos(FALSE, TRUE); end; @@ -263,6 +283,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" begin // [SCENARIO] Create unposted sales credit memo and use HTTP DELETE to delete it + Initialize(); + // [GIVEN] An unposted credit memo CreateDraftSalesCreditMemo(SalesHeader); CreditMemoNo := SalesHeader."No."; @@ -296,6 +318,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" // [SCENARIO] Create an credit memo both through the client UI and through the API // [SCENARIO] and compare them. They should be the same and have the same fields autocompleted wherever needed. + Initialize(); + // [GIVEN] An unposted credit memo LibraryGraphDocumentTools.InitializeUIPage(); @@ -329,6 +353,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" begin // [SCENARIO] When an credit memo is created,the GET Method should update the credit memo and assign a total + Initialize(); + // [GIVEN] 2 credit memos, one posted and one unposted without totals assigned LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending( SalesHeader, DiscountPct, SalesHeader."Document Type"::"Credit Memo"); @@ -362,6 +388,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" begin // [SCENARIO] When an credit memo is created, the GET Method should update the credit memo and assign a total + Initialize(); + // [GIVEN] 2 credit memos, one posted and one unposted with discount amount that should be redistributed LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending( SalesHeader, DiscountPct, SalesHeader."Document Type"::"Credit Memo"); @@ -401,6 +429,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" CreditMemoID: Text; begin // [SCENARIO 184721] Create Credit Memo, use a PATCH method to change it and then verify the changes + Initialize(); + LibrarySales.CreateCustomerWithAddress(Customer); // [GIVEN] an item with unit price and unit cost @@ -448,6 +478,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" begin // [SCENARIO 184721] Clearing manually set discount + Initialize(); + // [GIVEN] an item with unit price and unit cost LibraryInventory.CreateItemWithUnitPriceAndUnitCost( Item, LibraryRandom.RandDecInRange(1, 100, 2), LibraryRandom.RandDecInRange(1, 100, 2)); @@ -503,6 +535,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" begin // [SCENARIO] User can post a sales credit memo through the API. + Initialize(); + // [GIVEN] Draft sales credit memo exists CreateDraftSalesCreditMemo(SalesHeader); SetCustomerEmail(SalesHeader."Sell-to Customer No."); @@ -549,6 +583,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" TargetURL: Text; begin // [SCENARIO] User can post and send a sales credit memo through the API. + Initialize(); + InitializeForSending(); // [GIVEN] Draft sales credit memos exists @@ -588,6 +624,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" begin // [SCENARIO] User can cancel a posted sales credit memo through API. + Initialize(); + // [GIVEN] Non-corrective sales credit memo exists CreatePostedSalesCreditMemo(SalesCrMemoHeader); SetCustomerEmail(SalesCrMemoHeader."Sell-to Customer No."); @@ -617,6 +655,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" begin // [SCENARIO] User can cancel a posted sales credit memo through API. + Initialize(); + // [GIVEN] Corrective sales credit memo exists CreateCorrectiveSalesCreditMemo(SalesCrMemoHeader); SetCustomerEmail(SalesCrMemoHeader."Sell-to Customer No."); @@ -649,6 +689,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" TargetURL: Text; begin // [SCENARIO] User can cancel a posted sales credit memo through API. + Initialize(); + InitializeForSending(); // [GIVEN] Corrective sales credit memo exists @@ -685,6 +727,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" TargetURL: Text; begin // [SCENARIO] User can send a posted sales credit memo through the API. + Initialize(); + InitializeForSending(); // [GIVEN] Posted sales credit memo exists @@ -717,6 +761,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" TargetURL: Text; begin // [SCENARIO] Sending a draft sales credit memo through the API throws an error + Initialize(); + InitializeForSending(); // [GIVEN] Draft sales credit memo exists @@ -746,6 +792,8 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" TargetURL: Text; begin // [SCENARIO] User can send a draft sales credit memo through the API. + Initialize(); + InitializeForSending(); // [GIVEN] Cancelled sales credit memo exists @@ -1071,4 +1119,3 @@ codeunit 139728 "APIV1 - Sales Credit Memos E2E" JobQueueEntry.Cancel(); end; } - diff --git a/src/Apps/W1/APIV1/test/src/APIV1SalesInvLinesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1SalesInvLinesE2E.Codeunit.al index 86e95b3f302..75853b94e50 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1SalesInvLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1SalesInvLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139734 "APIV1 - Sales Inv. Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -33,9 +34,13 @@ codeunit 139734 "APIV1 - Sales Inv. Lines E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibrarySales.SetStockoutWarning(FALSE); LibraryApplicationArea.EnableFoundationSetup(); diff --git a/src/Apps/W1/APIV1/test/src/APIV1SalesOrderLinesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1SalesOrderLinesE2E.Codeunit.al index 2afa4cf7193..8615ed84e08 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1SalesOrderLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1SalesOrderLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139735 "APIV1 - Sales Order Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -31,9 +32,13 @@ codeunit 139735 "APIV1 - Sales Order Lines E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibrarySales.SetStockoutWarning(FALSE); LibraryApplicationArea.EnableFoundationSetup(); @@ -1279,14 +1284,6 @@ codeunit 139735 "APIV1 - Sales Order Lines E2E" - - - - - - - - diff --git a/src/Apps/W1/APIV1/test/src/APIV1SalesOrdersE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1SalesOrdersE2E.Codeunit.al index ec9fb22f169..f9a02ff432e 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1SalesOrdersE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1SalesOrdersE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139711 "APIV1 - Sales Orders E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -40,6 +41,8 @@ codeunit 139711 "APIV1 - Sales Orders E2E" begin // [SCENARIO 184721] Create Sales Orders and use a GET method to retrieve them + Initialize(); + // [GIVEN] 2 orders in the table LibrarySales.CreateSalesOrder(SalesHeader); OrderNo[1] := SalesHeader."No."; @@ -78,6 +81,8 @@ codeunit 139711 "APIV1 - Sales Orders E2E" begin // [SCENARIO 184721] Create sales orders JSON and use HTTP POST to create them + Initialize(); + // [GIVEN] a customer LibrarySales.CreateCustomerWithAddress(SellToCustomer); LibrarySales.CreateCustomerWithAddress(BillToCustomer); @@ -127,6 +132,8 @@ codeunit 139711 "APIV1 - Sales Orders E2E" begin // [SCENARIO 184721] Create sales order for customer with location and use HTTP POST to create it + Initialize(); + // [GIVEN] an order with customer with location code LibrarySales.CreateCustomer(Customer); LibraryWarehouse.CreateLocation(Location); @@ -170,6 +177,8 @@ codeunit 139711 "APIV1 - Sales Orders E2E" begin // [SCENARIO 184721] Create sales order with specific currency set and use HTTP POST to create it + Initialize(); + // [GIVEN] an order with a non-LCY currencyCode set LibrarySales.CreateCustomer(Customer); CustomerNo := Customer."No."; @@ -203,21 +212,34 @@ codeunit 139711 "APIV1 - Sales Orders E2E" [Test] procedure TestModifyOrders() begin + Initialize(); + TestMultipleModifyOrders(false, false); end; [Test] procedure TestEmptyModifyOrders() begin + Initialize(); + TestMultipleModifyOrders(true, false); end; [Test] procedure TestPartialModifyOrders() begin + Initialize(); + TestMultipleModifyOrders(false, true); end; + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + local procedure TestMultipleModifyOrders(EmptyData: Boolean; PartiallyEmptyData: Boolean) var SellToCustomer: Record "Customer"; @@ -291,6 +313,8 @@ codeunit 139711 "APIV1 - Sales Orders E2E" begin // [SCENARIO 184721] Create sales orders and use HTTP DELETE to delete them + Initialize(); + // [GIVEN] 2 orders in the table LibrarySales.CreateSalesOrder(SalesHeader); OrderNo[1] := SalesHeader."No."; @@ -336,6 +360,8 @@ codeunit 139711 "APIV1 - Sales Orders E2E" OrderWithComplexJSON: Text; begin // [SCENARIO 184721] Create an order both through the client UI and through the API and compare them. They should be the same and have the same fields autocompleted wherever needed. + Initialize(); + LibraryGraphDocumentTools.InitializeUIPage(); // [GIVEN] a customer @@ -396,6 +422,8 @@ codeunit 139711 "APIV1 - Sales Orders E2E" begin // [SCENARIO 184721] When an order is created, the GET Method should update the order and assign a total + Initialize(); + // [GIVEN] an order without totals assigned LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending(SalesHeader, DiscountPct, SalesHeader."Document Type"::Order); SalesHeader.CalcFields("Recalculate Invoice Disc."); @@ -426,6 +454,8 @@ codeunit 139711 "APIV1 - Sales Orders E2E" begin // [SCENARIO 184721] When an order is created, the GET Method should update the order and redistribute the discount amount + Initialize(); + // [GIVEN] an order with discount amount that should be redistributed LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending(SalesHeader, DiscountPct, SalesHeader."Document Type"::Order); SalesHeader.CalcFields(Amount); @@ -461,6 +491,8 @@ codeunit 139711 "APIV1 - Sales Orders E2E" begin // [SCENARIO 184721] Create Sales Order, use a PATCH method to change it and then verify the changes + Initialize(); + // [GIVEN] an order with lines CreateOrderWithLines(SalesHeader); OrderId := SalesHeader.SystemId; @@ -497,6 +529,8 @@ codeunit 139711 "APIV1 - Sales Orders E2E" begin // [SCENARIO 184721] Clearing manually set discount + Initialize(); + // [GIVEN] an order CreateOrderWithLines(SalesHeader); OrderNo := SalesHeader."No."; @@ -533,6 +567,8 @@ codeunit 139711 "APIV1 - Sales Orders E2E" begin // [SCENARIO] User can ship and invoice a sales order through the API. + Initialize(); + // [GIVEN] a sales order with lines CreateOrderWithLines(SalesHeader); OrderId := SalesHeader.SystemId; diff --git a/src/Apps/W1/APIV1/test/src/APIV1SalesQuoteLinesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1SalesQuoteLinesE2E.Codeunit.al index b4e577f634b..d1d53b26e59 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1SalesQuoteLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1SalesQuoteLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139736 "APIV1 - Sales Quote Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -29,9 +30,13 @@ codeunit 139736 "APIV1 - Sales Quote Lines E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibrarySales.SetStockoutWarning(FALSE); LibraryApplicationArea.EnableFoundationSetup(); @@ -1005,12 +1010,6 @@ codeunit 139736 "APIV1 - Sales Quote Lines E2E" - - - - - - diff --git a/src/Apps/W1/APIV1/test/src/APIV1SalesQuotesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1SalesQuotesE2E.Codeunit.al index b395b79b8e8..065c83792a9 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1SalesQuotesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1SalesQuotesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -39,6 +40,13 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" InvoiceIdErr: Label 'The invoice ID should differ from the quote ID.', Locked = true; MailingJobErr: Label 'The mailing job is not created.', Locked = true; + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + local procedure InitializeForSending() var TempEmailAccount: Record "Email Account"; @@ -64,6 +72,8 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" begin // [SCENARIO] Create Sales Quotes and use a GET method to retrieve them + Initialize(); + // [GIVEN] 2 quotes in the table CreateSalesQuoteWithLines(SalesHeader); QuoteID[1] := SalesHeader."No."; @@ -102,6 +112,8 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" begin // [SCENARIO] Create sales quotes JSON and use HTTP POST to create them + Initialize(); + // [GIVEN] a customer LibrarySales.CreateCustomerWithAddress(SellToCustomer); LibrarySales.CreateCustomerWithAddress(ShipToCustomer); @@ -147,6 +159,8 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" begin // [SCENARIO] Create sales quote with specific currency set and use HTTP POST to create it + Initialize(); + // [GIVEN] a quote with a non-LCY currencyCode set LibrarySales.CreateCustomer(Customer); CustomerNo := Customer."No."; @@ -176,18 +190,24 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" [Test] procedure TestModifyQuotes() begin + Initialize(); + TestMultipleModifyQuotes(false, false); end; [Test] procedure TestEmptyModifyQuotes() begin + Initialize(); + TestMultipleModifyQuotes(true, false); end; [Test] procedure TestPartialModifyQuotes() begin + Initialize(); + TestMultipleModifyQuotes(false, true); end; @@ -281,6 +301,8 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" begin // [SCENARIO] Create sales quotes and use HTTP DELETE to delete them + Initialize(); + // [GIVEN] 2 quotes in the table CreateSalesQuoteWithLines(SalesHeader); QuoteID[1] := SalesHeader."No."; @@ -326,6 +348,8 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" QuoteExists: Boolean; begin // [SCENARIO] Create a quote both through the client UI and through the API and compare them. They should be the same and have the same fields autocompleted wherever needed. + Initialize(); + LibraryGraphDocumentTools.InitializeUIPage(); // [GIVEN] a customer @@ -381,6 +405,8 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" begin // [SCENARIO] When a quote is created, the GET Method should update the quote and assign a total + Initialize(); + // [GIVEN] a quote without totals assigned LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending(SalesHeader, DiscountPct, SalesHeader."Document Type"::Quote); SalesHeader.CalcFields("Recalculate Invoice Disc."); @@ -411,6 +437,8 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" begin // [SCENARIO] When a quote is created, the GET Method should update the quote and redistribute the discount amount + Initialize(); + // [GIVEN] a quote with discount amount that should be redistributed LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending(SalesHeader, DiscountPct, SalesHeader."Document Type"::Quote); SalesHeader.CalcFields(Amount); @@ -447,6 +475,8 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" QuoteID: Text; begin // [SCENARIO 184721] Create Sales Quote, use a PATCH method to change it and then verify the changes + Initialize(); + LibrarySales.CreateCustomerWithAddress(Customer); // [GIVEN] an item with unit price and unit cost @@ -494,6 +524,8 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" begin // [SCENARIO 184721] Clearing manually set discount + Initialize(); + // [GIVEN] an item with unit price and unit cost LibraryInventory.CreateItemWithUnitPriceAndUnitCost( Item, LibraryRandom.RandDecInRange(1, 100, 2), LibraryRandom.RandDecInRange(1, 100, 2)); @@ -541,6 +573,8 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" TargetURL: Text; begin // [SCENARIO] User can send a sales quote through the API. + Initialize(); + InitializeForSending(); // [GIVEN] Draft sales quote exists @@ -585,6 +619,8 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" begin // [SCENARIO] User can convert a sales quote to a sales invoice through the API. + Initialize(); + // [GIVEN] Sales quote exists CreateSalesQuoteWithLines(SalesHeader); CreateEmailParameters(SalesHeader); @@ -643,6 +679,8 @@ codeunit 139723 "APIV1 - Sales Quotes E2E" begin // [SCENARIO] User can convert a sales quote to a sales order through the API. + Initialize(); + // [GIVEN] Sales quote exists CreateSalesQuoteWithLines(SalesHeader); CreateEmailParameters(SalesHeader); diff --git a/src/Apps/W1/APIV1/test/src/APIV1ShipmentMethodsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1ShipmentMethodsE2E.Codeunit.al index 09cf7ce2b3e..57274730dea 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1ShipmentMethodsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1ShipmentMethodsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139705 "APIV1 - Shipment Methods E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -21,9 +22,13 @@ codeunit 139705 "APIV1 - Shipment Methods E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; COMMIT(); end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1TaxAreasE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1TaxAreasE2E.Codeunit.al index bffc2b39d55..9bc959cb00b 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1TaxAreasE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1TaxAreasE2E.Codeunit.al @@ -24,9 +24,13 @@ codeunit 139726 "APIV1 - Tax Areas E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; COMMIT(); end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1TrialBalanceE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1TrialBalanceE2E.Codeunit.al index c495bc2ddfc..f03565d2c42 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1TrialBalanceE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1TrialBalanceE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139718 "APIV1 - Trial Balance E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -60,9 +61,13 @@ codeunit 139718 "APIV1 - Trial Balance E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := TRUE; end; diff --git a/src/Apps/W1/APIV1/test/src/APIV1UofME2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1UofME2E.Codeunit.al index b283bc61d9d..8eacc612fb0 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1UofME2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1UofME2E.Codeunit.al @@ -26,6 +26,8 @@ codeunit 139712 "APIV1 - UofM E2E" begin // [SCENARIO] User can get the units of measure. + Initialize(); + // [GIVEN] Units of measure. GenerateUnitsOfMeasure(UnitOfMeasure, 5); @@ -46,6 +48,8 @@ codeunit 139712 "APIV1 - UofM E2E" begin // [SCENARIO] User can update a unit of measure. + Initialize(); + // [GIVEN] Units of measure. GenerateUnitsOfMeasure(UnitOfMeasure, 5); @@ -75,6 +79,8 @@ codeunit 139712 "APIV1 - UofM E2E" begin // [SCENARIO] User can delete a unit of measure. + Initialize(); + // [GIVEN] Units of measure. GenerateUnitsOfMeasure(UnitOfMeasure, 5); @@ -96,6 +102,8 @@ codeunit 139712 "APIV1 - UofM E2E" begin // [SCENARIO] User can insert a unit of measure. + Initialize(); + // [GIVEN] A new unit of measure. GenerateUnitOfMeasure(UnitOfMeasure); Request := CreateInsertUnitOfMeasureRequest(UnitOfMeasure); @@ -111,6 +119,13 @@ codeunit 139712 "APIV1 - UofM E2E" ValidateUnitOfMeasure(UnitOfMeasure, Response); end; + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + local procedure GenerateUnitsOfMeasure(var UnitOfMeasure: Record "Unit of Measure"; "Count": Integer) var i: Integer; diff --git a/src/Apps/W1/APIV1/test/src/APIV1VendorsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/APIV1VendorsE2E.Codeunit.al index 44570b29311..05fdec9f1e3 100644 --- a/src/Apps/W1/APIV1/test/src/APIV1VendorsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/APIV1VendorsE2E.Codeunit.al @@ -25,9 +25,13 @@ codeunit 139703 "APIV1 - Vendors E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; COMMIT(); end; diff --git a/src/Apps/W1/APIV1/test/src/CountriesRegionsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/CountriesRegionsE2E.Codeunit.al index 1e4843ec39c..d2f70856614 100644 --- a/src/Apps/W1/APIV1/test/src/CountriesRegionsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/CountriesRegionsE2E.Codeunit.al @@ -155,9 +155,13 @@ codeunit 139710 "Countries/Regions E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; end; diff --git a/src/Apps/W1/APIV1/test/src/PaymentMethodsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/PaymentMethodsE2E.Codeunit.al index 005c5f0add8..b454803fcee 100644 --- a/src/Apps/W1/APIV1/test/src/PaymentMethodsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/PaymentMethodsE2E.Codeunit.al @@ -155,9 +155,13 @@ codeunit 139714 "Payment Methods E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; end; diff --git a/src/Apps/W1/APIV1/test/src/SalesInvoicesE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/SalesInvoicesE2E.Codeunit.al index dbf39f9a725..549feabec64 100644 --- a/src/Apps/W1/APIV1/test/src/SalesInvoicesE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/SalesInvoicesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139709 "Sales Invoices E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -48,6 +49,13 @@ codeunit 139709 "Sales Invoices E2E" NotEmptyParameterErr: Label 'Email parameter %1 is not empty.', Locked = true; MailingJobErr: Label 'The mailing job is not created.', Locked = true; + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + local procedure InitializeForSending() var TempEmailAccount: Record "Email Account"; @@ -73,6 +81,8 @@ codeunit 139709 "Sales Invoices E2E" begin // [SCENARIO 184721] Create posted and unposted Sales invoices and use a GET method to retrieve them + Initialize(); + // [GIVEN] 2 invoices, one posted and one unposted CreateSalesInvoices(InvoiceID1, InvoiceID2); COMMIT(); @@ -106,6 +116,8 @@ codeunit 139709 "Sales Invoices E2E" InvoiceWithComplexJSON: Text; begin // [SCENARIO 184721] Create posted and unposted Sales invoices and use HTTP POST to delete them + Initialize(); + // [GIVEN] 2 invoices, one posted and one unposted LibrarySales.CreateCustomer(SellToCustomer); @@ -157,6 +169,8 @@ codeunit 139709 "Sales Invoices E2E" begin // [SCENARIO 184721] Create posted and unposted with specific currency set and use HTTP POST to create them + Initialize(); + // [GIVEN] an Invoice with a non-LCY currencyCode set LibrarySales.CreateCustomer(Customer); CustomerNo := Customer."No."; @@ -203,6 +217,8 @@ codeunit 139709 "Sales Invoices E2E" CurrencyCode: Code[10]; begin // [SCENARIO 285872] Create posted and unposted with specific email set and use HTTP POST to create them + Initialize(); + Email := 'test@microsoft.com'; // [GIVEN] an Customer with no email set @@ -256,6 +272,8 @@ codeunit 139709 "Sales Invoices E2E" begin // [SCENARIO 184721] Create unposted with specific document and due date set and use HTTP POST to create them + Initialize(); + // [GIVEN] an Invoice with a document and due date set LibrarySales.CreateCustomer(Customer); CustomerNo := Customer."No."; @@ -294,18 +312,24 @@ codeunit 139709 "Sales Invoices E2E" [Test] procedure TestModifyInvoices() begin + Initialize(); + TestMultipleModifyInvoices(FALSE, FALSE); end; [Test] procedure TestEmptyModifyInvoices() begin + Initialize(); + TestMultipleModifyInvoices(TRUE, FALSE); end; [Test] procedure TestPartialModifyInvoices() begin + Initialize(); + TestMultipleModifyInvoices(FALSE, TRUE); end; @@ -405,6 +429,8 @@ codeunit 139709 "Sales Invoices E2E" InvoiceWithBlanksJSON: Text; begin // [SCENARIO 184721] Create Sales Invoice with all the Ids filled, use a PATCH method to blank the Ids and the Codes + Initialize(); + LibrarySales.CreateCustomerWithAddress(Customer); // [GIVEN] a currency @@ -470,6 +496,8 @@ codeunit 139709 "Sales Invoices E2E" begin // [SCENARIO 184721] Create draft invoice and issue a patch request to change the number + Initialize(); + // [GIVEN] 1 draft invoice and a json with a new number LibrarySales.CreateSalesInvoice(SalesHeader); NewInvoiceNumber := COPYSTR(CREATEGUID(), 1, MAXSTRLEN(SalesHeader."No.")); @@ -493,6 +521,8 @@ codeunit 139709 "Sales Invoices E2E" begin // [SCENARIO 184721] Create unposted Sales invoice and use HTTP DELETE to delete it + Initialize(); + // [GIVEN] An unposted invoice CreateDraftSalesInvoice(SalesHeader); InvoiceID := SalesHeader."No."; @@ -532,6 +562,8 @@ codeunit 139709 "Sales Invoices E2E" // [SCENARIO 184721] Create an invoice both through the client UI and through the API // [SCENARIO] and compare them. They should be the same and have the same fields autocompleted wherever needed. + Initialize(); + // [GIVEN] An unposted invoice LibraryGraphDocumentTools.InitializeUIPage(); LibraryApplicationArea.DisableApplicationAreaSetup(); @@ -594,6 +626,8 @@ codeunit 139709 "Sales Invoices E2E" begin // [SCENARIO 184721] When an invoice is created,the GET Method should update the invoice and assign a total + Initialize(); + // [GIVEN] 2 invoices, one posted and one unposted without totals assigned LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending( SalesHeader, DiscountPct, SalesHeader."Document Type"::Invoice); @@ -627,6 +661,8 @@ codeunit 139709 "Sales Invoices E2E" begin // [SCENARIO 184721] When an invoice is created, the GET Method should update the invoice and assign a total + Initialize(); + // [GIVEN] 2 invoices, one posted and one unposted with discount amount that should be redistributed LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending( SalesHeader, DiscountPct, SalesHeader."Document Type"::Invoice); @@ -663,6 +699,8 @@ codeunit 139709 "Sales Invoices E2E" begin // [SCENARIO 184721] Create an invoice wihtout Customer throws an error + Initialize(); + // [GIVEN] a sales invoice JSON with currency only Currency.SETFILTER(Code, '<>%1', ''); Currency.FINDFIRST(); @@ -690,6 +728,8 @@ codeunit 139709 "Sales Invoices E2E" InvoiceID: Text; begin // [SCENARIO 184721] Create Sales Invoice, use a PATCH method to change it and then verify the changes + Initialize(); + LibrarySales.CreateCustomerWithAddress(Customer); // [GIVEN] an item with unit price and unit cost @@ -737,6 +777,8 @@ codeunit 139709 "Sales Invoices E2E" begin // [SCENARIO 184721] Clearing manually set discount + Initialize(); + // [GIVEN] an item with unit price and unit cost LibraryInventory.CreateItemWithUnitPriceAndUnitCost( Item, LibraryRandom.RandDecInRange(1, 100, 2), LibraryRandom.RandDecInRange(1, 100, 2)); @@ -793,6 +835,8 @@ codeunit 139709 "Sales Invoices E2E" begin // [SCENARIO] User can post a sales invoice through the API. + Initialize(); + // [GIVEN] Draft sales invoice exists CreateDraftSalesInvoice(SalesHeader); DraftInvoiceRecordRef.GetTable(SalesHeader); @@ -838,6 +882,8 @@ codeunit 139709 "Sales Invoices E2E" TargetURL: Text; begin // [SCENARIO] User can post and send a sales invoice through the API. + Initialize(); + InitializeForSending(); // [GIVEN] Draft sales invoice exists @@ -877,6 +923,8 @@ codeunit 139709 "Sales Invoices E2E" begin // [SCENARIO] User can cancel a posted sales invoice through API. + Initialize(); + // [GIVEN] Posted sales invoice exists CreatePostedSalesInvoice(SalesInvoiceHeader); SetCustomerEmail(SalesInvoiceHeader."Sell-to Customer No."); @@ -912,6 +960,8 @@ codeunit 139709 "Sales Invoices E2E" TargetURL: Text; begin // [SCENARIO] User can cancel a posted sales invoice through API. + Initialize(); + InitializeForSending(); // [GIVEN] Posted sales invoice exists @@ -952,6 +1002,8 @@ codeunit 139709 "Sales Invoices E2E" TargetURL: Text; begin // [SCENARIO] User can send a posted sales invoice through the API. + Initialize(); + InitializeForSending(); // [GIVEN] Posted sales invoice exists @@ -984,6 +1036,8 @@ codeunit 139709 "Sales Invoices E2E" TargetURL: Text; begin // [SCENARIO] User can send a draft sales invoice through the API. + Initialize(); + InitializeForSending(); // [GIVEN] Draft sales invoice exists @@ -1016,6 +1070,8 @@ codeunit 139709 "Sales Invoices E2E" TargetURL: Text; begin // [SCENARIO] User can send a draft sales invoice through the API. + Initialize(); + InitializeForSending(); // [GIVEN] Cancelled sales invoice exists @@ -1057,6 +1113,8 @@ codeunit 139709 "Sales Invoices E2E" begin // [SCENARIO] User can create a corrective credit memo for the posted sales invoice through the API. + Initialize(); + // [GIVEN] A posted sales invoice exists CreatePostedSalesInvoice(SalesInvoiceHeader); PostedInvoiceRecordRef.GetTable(SalesInvoiceHeader); diff --git a/src/Apps/W1/APIV1/test/src/TaxGroupsE2E.Codeunit.al b/src/Apps/W1/APIV1/test/src/TaxGroupsE2E.Codeunit.al index 15b2166d131..cfa2ea66f1f 100644 --- a/src/Apps/W1/APIV1/test/src/TaxGroupsE2E.Codeunit.al +++ b/src/Apps/W1/APIV1/test/src/TaxGroupsE2E.Codeunit.al @@ -23,9 +23,13 @@ codeunit 139708 "Tax Groups E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); IF IsInitialized THEN EXIT; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := TRUE; COMMIT(); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2AccountingPeriodsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2AccountingPeriodsE2E.Codeunit.al index c578df54f2d..5cd61faa5b2 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2AccountingPeriodsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2AccountingPeriodsE2E.Codeunit.al @@ -16,6 +16,8 @@ codeunit 139859 "APIV2 - Accounting Periods E2E" TargetURL: Text; i: Integer; begin + Initialize(); + // [SCENARIO] Use a GET method to retrieve all accounting periods // [GIVEN] Accounting Periods for i := 1 to 10 do @@ -74,4 +76,11 @@ codeunit 139859 "APIV2 - Accounting Periods E2E" AccountingPeriod.Insert(true); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2AccountsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2AccountsE2E.Codeunit.al index 9389de358ba..fb16bde22f0 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2AccountsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2AccountsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139801 "APIV2 - Accounts E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -19,9 +20,14 @@ codeunit 139801 "APIV2 - Accounts E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; @@ -106,4 +112,3 @@ codeunit 139801 "APIV2 - Accounts E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2AgedAPE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2AgedAPE2E.Codeunit.al index 2418082ad5b..4d45914cd1b 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2AgedAPE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2AgedAPE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139819 "APIV2 - Aged AP E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -60,9 +61,14 @@ codeunit 139819 "APIV2 - Aged AP E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := true; end; @@ -82,4 +88,3 @@ codeunit 139819 "APIV2 - Aged AP E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2AgedARE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2AgedARE2E.Codeunit.al index 2a280d83eb5..2ff3d83ba67 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2AgedARE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2AgedARE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139820 "APIV2 - Aged AR E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -60,9 +61,14 @@ codeunit 139820 "APIV2 - Aged AR E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := true; end; @@ -81,4 +87,3 @@ codeunit 139820 "APIV2 - Aged AR E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2ApplyVendorEntE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2ApplyVendorEntE2E.Codeunit.al index 4b30e2db6ef..70032165f9a 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2ApplyVendorEntE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2ApplyVendorEntE2E.Codeunit.al @@ -1,6 +1,7 @@ codeunit 139867 "APIV2 - Apply Vendor Ent. E2E" { Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -170,10 +171,15 @@ codeunit 139867 "APIV2 - Apply Vendor Ent. E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryTestInitialize.OnTestInitialize(Codeunit::"APIV2 - Apply Vendor Ent. E2E"); - if not isInitialized then + if not isInitialized then begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); isInitialized := true; + end; LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"APIV2 - Apply Vendor Ent. E2E"); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2ApprovalEntriesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2ApprovalEntriesE2E.Codeunit.al index 9ae78501641..39745d47677 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2ApprovalEntriesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2ApprovalEntriesE2E.Codeunit.al @@ -1,6 +1,5 @@ codeunit 139917 "APIV2 - Approval Entries E2E" { - Subtype = Test; TestType = IntegrationTest; TestPermissions = Disabled; @@ -22,6 +21,8 @@ codeunit 139917 "APIV2 - Approval Entries E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO] Check that approval entries can be retrieved via API // [GIVEN] a Sales order exists @@ -44,4 +45,10 @@ codeunit 139917 "APIV2 - Approval Entries E2E" Assert.IsTrue(LibraryGraphMgt.GetObjectIDFromJSON(ResponseText, 'id', ApprovalEntryId), 'Could not find approval entry'); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } \ No newline at end of file diff --git a/src/Apps/W1/APIV2/test/src/APIV2ApprovalUserSetupE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2ApprovalUserSetupE2E.Codeunit.al index 2a259376141..148da74e16c 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2ApprovalUserSetupE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2ApprovalUserSetupE2E.Codeunit.al @@ -1,6 +1,5 @@ codeunit 139918 "APIV2 - Approval UserSetup E2E" { - Subtype = Test; TestType = IntegrationTest; TestPermissions = Disabled; @@ -20,6 +19,8 @@ codeunit 139918 "APIV2 - Approval UserSetup E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO] Check that approval user setup can be retrieved via API // [GIVEN] an Approval User Setup exists @@ -39,4 +40,10 @@ codeunit 139918 "APIV2 - Approval UserSetup E2E" Assert.IsTrue(LibraryGraphMgt.GetObjectIDFromJSON(ResponseText, 'id', UserSetupId), 'Could not find approval user setup'); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } \ No newline at end of file diff --git a/src/Apps/W1/APIV2/test/src/APIV2AssemblyOrdersE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2AssemblyOrdersE2E.Codeunit.al index 9de9e6343ae..9c274228e3e 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2AssemblyOrdersE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2AssemblyOrdersE2E.Codeunit.al @@ -26,6 +26,8 @@ codeunit 139925 "APIV2 - Assembly Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create an assembly order and use GET to retrieve it // [GIVEN] An assembly order LibraryInventory.CreateItem(Item); @@ -48,6 +50,8 @@ codeunit 139925 "APIV2 - Assembly Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create an assembly order and GET it by SystemId // [GIVEN] An assembly order LibraryInventory.CreateItem(Item); @@ -72,6 +76,8 @@ codeunit 139925 "APIV2 - Assembly Orders E2E" TargetURL: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] GET an assembly order with $expand=dimensionSetLines // [GIVEN] An assembly order if not AssemblyHeader.FindFirst() then begin @@ -99,6 +105,8 @@ codeunit 139925 "APIV2 - Assembly Orders E2E" TargetURL: Text; AttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET an assembly order with $expand=attachments // [GIVEN] An assembly order if not AssemblyHeader.FindFirst() then begin @@ -126,6 +134,8 @@ codeunit 139925 "APIV2 - Assembly Orders E2E" TargetURL: Text; DocumentAttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET an assembly order with $expand=documentAttachments // [GIVEN] An assembly order if not AssemblyHeader.FindFirst() then begin @@ -153,6 +163,8 @@ codeunit 139925 "APIV2 - Assembly Orders E2E" TargetURL: Text; PdfDocumentValue: Text; begin + Initialize(); + // [SCENARIO] GET an assembly order with $expand=pdfDocument // [GIVEN] An assembly order if not AssemblyHeader.FindFirst() then begin @@ -180,6 +192,8 @@ codeunit 139925 "APIV2 - Assembly Orders E2E" TargetURL: Text; LinesValue: Text; begin + Initialize(); + // [SCENARIO] GET an assembly order with $expand=assemblyOrderLines // [GIVEN] An assembly order if not AssemblyHeader.FindFirst() then begin @@ -204,4 +218,11 @@ codeunit 139925 "APIV2 - Assembly Orders E2E" exit(TargetURL + '&$expand=' + ExpandValue); exit(TargetURL + '?$expand=' + ExpandValue); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2AttachmentsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2AttachmentsE2E.Codeunit.al index 0ba1716ac2d..8aaa574e8c8 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2AttachmentsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2AttachmentsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139833 "APIV2 - Attachments E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -42,6 +43,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreateGenJournalLine(DocumentRecordRef); @@ -65,6 +68,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Incoming Document Attachment table GLEntryGuid := CreateGLEntry(DocumentRecordRef); @@ -88,6 +93,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreatePostedSalesInvoice(DocumentRecordRef, DocumentId); @@ -110,6 +117,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreateDraftSalesInvoice(DocumentRecordRef); @@ -132,6 +141,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreateDraftSalesOrder(DocumentRecordRef); @@ -156,6 +167,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreatePostedPurchaseInvoice(DocumentRecordRef, DocumentId); @@ -178,6 +191,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreateDraftPurchaseInvoice(DocumentRecordRef); @@ -200,6 +215,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreateSalesQuote(DocumentRecordRef); @@ -222,6 +239,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve the Attachment record from the Attachment API. // [GIVEN] Attachment exists in the Incoming Document Attachment table CreateGenJournalLine(DocumentRecordRef); @@ -246,6 +265,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve the Attachment record from the Attachment API. // [GIVEN] Attachment exists in the Incoming Document Attachment table JournalName := LibraryUtility.GenerateRandomCode(GenJournalBatch.FieldNo(Name), Database::"Gen. Journal Batch"); @@ -272,6 +293,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve the Attachment record from the Attachment API. // [GIVEN] Attachment exists in the Incoming Document Attachment table CreateGLEntry(DocumentRecordRef); @@ -295,6 +318,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve the Attachment record from the Attachment API. // [GIVEN] Attachment exists in the Incoming Document Attachment table CreatePostedSalesInvoice(DocumentRecordRef, DocumentId); @@ -317,6 +342,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve the Attachment record from the Attachment API. // [GIVEN] Attachment exists in the Incoming Document Attachment table CreateDraftSalesInvoice(DocumentRecordRef); @@ -339,6 +366,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve the Attachment record from the Attachment API. // [GIVEN] Attachment exists in the Incoming Document Attachment table CreateDraftSalesOrder(DocumentRecordRef); @@ -361,6 +390,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve the Attachment record from the Attachment API. // [GIVEN] Attachment exists in the Incoming Document Attachment table CreateSalesQuote(DocumentRecordRef); @@ -383,6 +414,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreateDraftPurchaseOrder(DocumentRecordRef); @@ -409,6 +442,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ExpectedBase64Content: Text; ActualBase64Content: Text; begin + Initialize(); + // [SCENARIO] User can update linked attachment binary content through the Attachment API. // [GIVEN] A linked attachment exists CreateGenJournalLine(DocumentRecordRef); @@ -442,6 +477,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ExpectedBase64Content: Text; ActualBase64Content: Text; begin + Initialize(); + // [SCENARIO] User can update linked attachment binary content through the Attachment API. // [GIVEN] A linked attachment exists CreateGLEntry(DocumentRecordRef); @@ -468,6 +505,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateGenJournalLine(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -477,6 +516,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateGLEntry(DocumentRecordRef); TestCreateGLEAttachment(DocumentRecordRef); end; @@ -487,6 +528,8 @@ codeunit 139833 "APIV2 - Attachments E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedSalesInvoice(DocumentRecordRef, DocumentId); TestCreateAttachment(DocumentRecordRef); end; @@ -496,6 +539,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftSalesInvoice(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -505,6 +550,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftSalesOrder(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -515,6 +562,8 @@ codeunit 139833 "APIV2 - Attachments E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedPurchaseInvoice(DocumentRecordRef, DocumentId); TestCreateAttachment(DocumentRecordRef); end; @@ -524,6 +573,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftPurchaseInvoice(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -533,6 +584,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftPurchaseOrder(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -634,6 +687,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateSalesQuote(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -643,6 +698,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateGenJournalLine(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -652,6 +709,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateGLEntry(DocumentRecordRef); TestDeleteGLEAttachment(DocumentRecordRef); end; @@ -662,6 +721,8 @@ codeunit 139833 "APIV2 - Attachments E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedSalesInvoice(DocumentRecordRef, DocumentId); TestDeleteAttachment2(DocumentRecordRef); end; @@ -671,6 +732,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftSalesInvoice(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -681,6 +744,8 @@ codeunit 139833 "APIV2 - Attachments E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedPurchaseInvoice(DocumentRecordRef, DocumentId); TestDeleteAttachment2(DocumentRecordRef); end; @@ -690,6 +755,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftPurchaseInvoice(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -699,6 +766,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateSalesQuote(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -773,6 +842,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] The Attachment is transferred from a Draft Invoice the to Posted Invoice after posting. // [GIVEN] A draft sales invoice exists. CreateDraftSalesInvoice(DocumentRecordRef); @@ -813,6 +884,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] The Attachment is transferred from a Draft Invoice the to Posted Invoice after posting. // [GIVEN] A draft sales invoice exists. CreateDraftPurchaseInvoice(DocumentRecordRef); @@ -855,6 +928,8 @@ codeunit 139833 "APIV2 - Attachments E2E" OldBase64Content: Text; NewBase64Content: Text; begin + Initialize(); + // [SCENARIO] Changing an attachment file name keeps other fields unchanged // [GIVEN] A sales quote exists. CreateSalesQuote(DocumentRecordRef); @@ -899,6 +974,8 @@ codeunit 139833 "APIV2 - Attachments E2E" OldFileName: Text; NewFileName: Text; begin + Initialize(); + // [SCENARIO] Changing an attachment content keeps other fields unchanged // [GIVEN] A draft sales invoice exists. CreateDraftSalesInvoice(DocumentRecordRef); @@ -937,6 +1014,8 @@ codeunit 139833 "APIV2 - Attachments E2E" TargetURL: Text; JSONBody: Text; begin + Initialize(); + // [SCENARIO] User cannot change the attachment ID by making a PATCH request to the Attachments API // [GIVEN] Two documents exist. CreateSalesQuote(DocumentRecordRef[1]); @@ -967,6 +1046,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreatePostedPurchaseCreditMemo(DocumentRecordRef, DocumentId); @@ -989,6 +1070,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Incoming Document Attachment table CreateDraftPurchaseCreditMemo(DocumentRecordRef); @@ -1009,6 +1092,8 @@ codeunit 139833 "APIV2 - Attachments E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedPurchaseCreditMemo(DocumentRecordRef, DocumentId); TestCreateAttachment(DocumentRecordRef); end; @@ -1018,6 +1103,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftPurchaseCreditMemo(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -1028,6 +1115,8 @@ codeunit 139833 "APIV2 - Attachments E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedPurchaseCreditMemo(DocumentRecordRef, DocumentId); TestDeleteAttachment2(DocumentRecordRef); end; @@ -1037,6 +1126,8 @@ codeunit 139833 "APIV2 - Attachments E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftPurchaseCreditMemo(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -1054,6 +1145,8 @@ codeunit 139833 "APIV2 - Attachments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] The Attachment is transferred from a Draft Credit Memo the to Posted Credit Memo after posting. // [GIVEN] A draft purchase credit memo exists. LibraryERMCountryData.UpdatePurchasesPayablesSetup(); @@ -1592,4 +1685,11 @@ codeunit 139833 "APIV2 - Attachments E2E" exit(StrSubstNo('%1.%2', Name, Extension)); exit(Name); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2AutomationRSPackage.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2AutomationRSPackage.Codeunit.al index 31c14c5d3ab..8da2f4675b5 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2AutomationRSPackage.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2AutomationRSPackage.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139831 "APIV2 - Automation RS Package" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -28,8 +29,11 @@ codeunit 139831 "APIV2 - Automation RS Package" var TenantConfigPackageFile: Record "Tenant Config. Package File"; begin - TenantConfigPackageFile.DELETEALL(true); + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + TenantConfigPackageFile.DELETEALL(true); SamplePackageFile := GenerateSamplePackageFile(); end; @@ -354,6 +358,5 @@ codeunit 139831 "APIV2 - Automation RS Package" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2BalanceSheetE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2BalanceSheetE2E.Codeunit.al index f99099da896..704b3a4619e 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2BalanceSheetE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2BalanceSheetE2E.Codeunit.al @@ -62,9 +62,14 @@ codeunit 139815 "APIV2 - Balance Sheet E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := true; end; @@ -88,4 +93,3 @@ codeunit 139815 "APIV2 - Balance Sheet E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2BlPurchOrdersE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2BlPurchOrdersE2E.Codeunit.al index 25dbc2bfd72..0509ddb1263 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2BlPurchOrdersE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2BlPurchOrdersE2E.Codeunit.al @@ -24,6 +24,8 @@ codeunit 139908 "APIV2 - Bl. Purch. Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create a blanket purchase order and use GET to retrieve it // [GIVEN] A blanket purchase order LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::"Blanket Order", ''); @@ -44,6 +46,8 @@ codeunit 139908 "APIV2 - Bl. Purch. Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create a blanket purchase order and GET it by SystemId // [GIVEN] A blanket purchase order LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::"Blanket Order", ''); @@ -66,6 +70,8 @@ codeunit 139908 "APIV2 - Bl. Purch. Orders E2E" TargetURL: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] GET a blanket purchase order with $expand=dimensionSetLines // [GIVEN] A blanket purchase order PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::"Blanket Order"); @@ -92,6 +98,8 @@ codeunit 139908 "APIV2 - Bl. Purch. Orders E2E" TargetURL: Text; AttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a blanket purchase order with $expand=attachments // [GIVEN] A blanket purchase order PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::"Blanket Order"); @@ -118,6 +126,8 @@ codeunit 139908 "APIV2 - Bl. Purch. Orders E2E" TargetURL: Text; DocumentAttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a blanket purchase order with $expand=documentAttachments // [GIVEN] A blanket purchase order PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::"Blanket Order"); @@ -144,6 +154,8 @@ codeunit 139908 "APIV2 - Bl. Purch. Orders E2E" TargetURL: Text; PdfDocumentValue: Text; begin + Initialize(); + // [SCENARIO] GET a blanket purchase order with $expand=pdfDocument // [GIVEN] A blanket purchase order PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::"Blanket Order"); @@ -170,6 +182,8 @@ codeunit 139908 "APIV2 - Bl. Purch. Orders E2E" TargetURL: Text; LinesValue: Text; begin + Initialize(); + // [SCENARIO] GET a blanket purchase order with $expand=blanketPurchaseOrderLines // [GIVEN] A blanket purchase order PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::"Blanket Order"); @@ -194,4 +208,10 @@ codeunit 139908 "APIV2 - Bl. Purch. Orders E2E" exit(TargetURL + '&$expand=' + ExpandValue); exit(TargetURL + '?$expand=' + ExpandValue); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2BlSalesOrdersE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2BlSalesOrdersE2E.Codeunit.al index e70ad8a2eb3..111a07def07 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2BlSalesOrdersE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2BlSalesOrdersE2E.Codeunit.al @@ -24,6 +24,8 @@ codeunit 139909 "APIV2 - Bl. Sales Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create a blanket sales order and use GET to retrieve it // [GIVEN] A blanket sales order LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::"Blanket Order", ''); @@ -44,6 +46,8 @@ codeunit 139909 "APIV2 - Bl. Sales Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create a blanket sales order and GET it by SystemId // [GIVEN] A blanket sales order LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::"Blanket Order", ''); @@ -66,6 +70,8 @@ codeunit 139909 "APIV2 - Bl. Sales Orders E2E" TargetURL: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] GET a blanket sales order with $expand=dimensionSetLines // [GIVEN] A blanket sales order SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Blanket Order"); @@ -92,6 +98,8 @@ codeunit 139909 "APIV2 - Bl. Sales Orders E2E" TargetURL: Text; AttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a blanket sales order with $expand=attachments // [GIVEN] A blanket sales order SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Blanket Order"); @@ -118,6 +126,8 @@ codeunit 139909 "APIV2 - Bl. Sales Orders E2E" TargetURL: Text; DocumentAttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a blanket sales order with $expand=documentAttachments // [GIVEN] A blanket sales order SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Blanket Order"); @@ -144,6 +154,8 @@ codeunit 139909 "APIV2 - Bl. Sales Orders E2E" TargetURL: Text; PdfDocumentValue: Text; begin + Initialize(); + // [SCENARIO] GET a blanket sales order with $expand=pdfDocument // [GIVEN] A blanket sales order SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Blanket Order"); @@ -170,6 +182,8 @@ codeunit 139909 "APIV2 - Bl. Sales Orders E2E" TargetURL: Text; LinesValue: Text; begin + Initialize(); + // [SCENARIO] GET a blanket sales order with $expand=blanketSalesOrderLines // [GIVEN] A blanket sales order SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Blanket Order"); @@ -194,4 +208,10 @@ codeunit 139909 "APIV2 - Bl. Sales Orders E2E" exit(TargetURL + '&$expand=' + ExpandValue); exit(TargetURL + '?$expand=' + ExpandValue); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2CashFlowStatementE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2CashFlowStatementE2E.Codeunit.al index 9ff2fa17758..7912cf90a97 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2CashFlowStatementE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2CashFlowStatementE2E.Codeunit.al @@ -62,9 +62,14 @@ codeunit 139817 "APIV2 - CashFlow Statement E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := true; end; @@ -93,4 +98,3 @@ codeunit 139817 "APIV2 - CashFlow Statement E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2CompanyInfoE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2CompanyInfoE2E.Codeunit.al index 295e9a9dc83..857e1986a88 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2CompanyInfoE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2CompanyInfoE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139806 "APIV2 - Company Info. E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = IntegrationTest; TestPermissions = Disabled; @@ -24,9 +25,14 @@ codeunit 139806 "APIV2 - Company Info. E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2ContactsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2ContactsE2E.Codeunit.al index 431c3a98b1c..f4515ca11e4 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2ContactsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2ContactsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139866 "APIV2 - Contacts E2E" // version Test,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -22,9 +23,14 @@ codeunit 139866 "APIV2 - Contacts E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2ContactsInfE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2ContactsInfE2E.Codeunit.al index 5612f0eb26c..e4978df62f7 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2ContactsInfE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2ContactsInfE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139853 "APIV2 - Contacts Inf. E2E" // version Test,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -30,6 +31,8 @@ codeunit 139853 "APIV2 - Contacts Inf. E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [FEATURE] [Customer] // [SCENARIO] Get all contact information for a customer @@ -60,6 +63,8 @@ codeunit 139853 "APIV2 - Contacts Inf. E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [FEATURE] [Customer] // [SCENARIO] Get all contact information for a customer @@ -92,6 +97,8 @@ codeunit 139853 "APIV2 - Contacts Inf. E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [FEATURE] [Vendor] // [SCENARIO] Get all contact information for a vendor @@ -122,6 +129,8 @@ codeunit 139853 "APIV2 - Contacts Inf. E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [FEATURE] [Vendor] // [SCENARIO] Get all contact information for a Vendor @@ -177,4 +186,11 @@ codeunit 139853 "APIV2 - Contacts Inf. E2E" Contact.Validate("Company No.", CompanyNo); Contact.Modify(true); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2CountriesRegionsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2CountriesRegionsE2E.Codeunit.al index 0bd63f98d47..b18c1157647 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2CountriesRegionsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2CountriesRegionsE2E.Codeunit.al @@ -155,9 +155,14 @@ codeunit 139810 "APIV2 - Countries/Regions E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; end; @@ -233,4 +238,3 @@ codeunit 139810 "APIV2 - Countries/Regions E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2CurrenciesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2CurrenciesE2E.Codeunit.al index 5e8bfb987bc..8b81ce31b02 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2CurrenciesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2CurrenciesE2E.Codeunit.al @@ -155,9 +155,14 @@ codeunit 139813 "APIV2 - Currencies E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; end; @@ -232,4 +237,3 @@ codeunit 139813 "APIV2 - Currencies E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2CurrencyExchRateE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2CurrencyExchRateE2E.Codeunit.al index 36e62a1b344..045094549f7 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2CurrencyExchRateE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2CurrencyExchRateE2E.Codeunit.al @@ -1,6 +1,7 @@ codeunit 139858 "APIV2 Currency Exch. Rate E2E" { Subtype = Test; + RequiredTestIsolation = Disabled; var LibraryERM: Codeunit "Library - ERM"; @@ -107,6 +108,10 @@ codeunit 139858 "APIV2 Currency Exch. Rate E2E" Currency: Record Currency; CurrencyExchangeRate: Record "Currency Exchange Rate"; begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + Currency.DeleteAll(); CurrencyExchangeRate.DeleteAll(); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2CustRetReasonsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2CustRetReasonsE2E.Codeunit.al index 5749e8a0fb7..806cc6bad24 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2CustRetReasonsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2CustRetReasonsE2E.Codeunit.al @@ -22,9 +22,14 @@ codeunit 139869 "APIV2 - Cust. Ret. Reasons E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2CustomerPaymentsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2CustomerPaymentsE2E.Codeunit.al index b2e150e30af..5f93c91aede 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2CustomerPaymentsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2CustomerPaymentsE2E.Codeunit.al @@ -1,6 +1,7 @@ codeunit 139840 "APIV2 - Customer Payments E2E" { Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -769,10 +770,14 @@ codeunit 139840 "APIV2 - Customer Payments E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibraryTestInitialize.OnTestInitialize(Codeunit::"APIV2 - Customer Payments E2E"); - if not isInitialized then + if not isInitialized then begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); isInitialized := true; + end; LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"APIV2 - Customer Payments E2E"); end; @@ -806,4 +811,3 @@ codeunit 139840 "APIV2 - Customer Payments E2E" exit(ServiceSubpageNameTxt + '(' + LibraryGraphMgt.StripBrackets(CustomerPaymentId) + ')'); end; } - diff --git a/src/Apps/W1/APIV2/test/src/APIV2CustomersE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2CustomersE2E.Codeunit.al index 567d0166f0a..f0156c35d4c 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2CustomersE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2CustomersE2E.Codeunit.al @@ -34,9 +34,14 @@ codeunit 139802 "APIV2 - Customers E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2DefaultDimensionsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2DefaultDimensionsE2E.Codeunit.al index 5a72973f69d..edc80adba49 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2DefaultDimensionsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2DefaultDimensionsE2E.Codeunit.al @@ -35,6 +35,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Customer: Record "Customer"; begin + Initialize(); // [FEATURE] [Customer] // [GIVEN] a customer, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the customer @@ -48,6 +49,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Vendor: Record "Vendor"; begin + Initialize(); // [FEATURE] [Vendor] // [GIVEN] a vendor, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the vendor @@ -61,6 +63,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Item: Record "Item"; begin + Initialize(); // [FEATURE] [Item] // [GIVEN] a Item, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Item @@ -74,6 +77,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Employee: Record "Employee"; begin + Initialize(); // [FEATURE] [Employee] // [GIVEN] a Employee, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Employee @@ -87,6 +91,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Customer: Record "Customer"; begin + Initialize(); // [FEATURE] [Customer] // [GIVEN] a customer, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the customer @@ -100,6 +105,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Vendor: Record "Vendor"; begin + Initialize(); // [FEATURE] [Vendor] // [GIVEN] a Vendor, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Vendor @@ -113,6 +119,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Item: Record "Item"; begin + Initialize(); // [FEATURE] [Item] // [GIVEN] a Item, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Item @@ -126,6 +133,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Employee: Record "Employee"; begin + Initialize(); // [FEATURE] [Employee] // [GIVEN] a Employee, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Employee @@ -139,6 +147,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Customer: Record "Customer"; begin + Initialize(); // [FEATURE] [Customer] // [GIVEN] a customer, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the customer, with mismatching dimesnion and dimension value @@ -152,6 +161,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Vendor: Record "Vendor"; begin + Initialize(); // [FEATURE] [Vendor] // [GIVEN] a Vendor, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Vendor, with mismatching dimesnion and dimension value @@ -165,6 +175,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Item: Record "Item"; begin + Initialize(); // [FEATURE] [Item] // [GIVEN] a Item, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Item, with mismatching dimesnion and dimension value @@ -178,6 +189,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Employee: Record "Employee"; begin + Initialize(); // [FEATURE] [Employee] // [GIVEN] a Employee, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension on the Employee, with mismatching dimesnion and dimension value @@ -191,6 +203,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Customer: Record "Customer"; begin + Initialize(); // [FEATURE] [Customer] // [GIVEN] a customer, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension on the customer @@ -204,6 +217,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Vendor: Record "Vendor"; begin + Initialize(); // [FEATURE] [Vendor] // [GIVEN] a Vendor, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension on the Vendor @@ -217,6 +231,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Item: Record "Item"; begin + Initialize(); // [FEATURE] [Item] // [GIVEN] a Item, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension on the Item @@ -230,6 +245,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Employee: Record "Employee"; begin + Initialize(); // [FEATURE] [Employee] // [GIVEN] a Employee, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension on the Employee @@ -243,6 +259,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Customer: Record "Customer"; begin + Initialize(); // [FEATURE] [Customer] // [GIVEN] a customer, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension value on the customer @@ -256,6 +273,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Vendor: Record "Vendor"; begin + Initialize(); // [FEATURE] [Vendor] // [GIVEN] a Vendor, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension value on the Vendor @@ -269,6 +287,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Item: Record "Item"; begin + Initialize(); // [FEATURE] [Item] // [GIVEN] a Item, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension value on the Item @@ -282,6 +301,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Employee: Record "Employee"; begin + Initialize(); // [FEATURE] [Employee] // [GIVEN] a Employee, a dimension and a dimension value // [WHEN] The user posts a http request to create a default dimension with a blocked dimension value on the Employee @@ -295,6 +315,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Customer: Record "Customer"; begin + Initialize(); // [FEATURE] [Customer] // [GIVEN] a customer with a default dimension // [WHEN] The user posts a http request to delete the default dimension on the customer @@ -308,6 +329,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Vendor: Record "Vendor"; begin + Initialize(); // [FEATURE] [Vendor] // [GIVEN] a Vendor with a default dimension // [WHEN] The user posts a http request to delete a default dimension on the Vendor @@ -321,6 +343,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Item: Record "Item"; begin + Initialize(); // [FEATURE] [Item] // [GIVEN] a Item with a default dimension // [WHEN] The user posts a http request to delete a default dimension on the Item @@ -334,6 +357,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Employee: Record "Employee"; begin + Initialize(); // [FEATURE] [Employee] // [GIVEN] a Employee with a default dimension // [WHEN] The user posts a http request to delete a default dimension on the Employee @@ -347,6 +371,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Customer: Record "Customer"; begin + Initialize(); // [FEATURE] [Customer] // [GIVEN] a customer with a default dimension // [WHEN] The user posts a http request to get the default dimension on the customer @@ -360,6 +385,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Vendor: Record "Vendor"; begin + Initialize(); // [FEATURE] [Vendor] // [GIVEN] a Vendor with a default dimension // [WHEN] The user posts a http request to get a default dimension on the Vendor @@ -373,6 +399,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Item: Record "Item"; begin + Initialize(); // [FEATURE] [Item] // [GIVEN] a Item with a default dimension // [WHEN] The user posts a http request to get a default dimension on the Item @@ -386,6 +413,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Employee: Record "Employee"; begin + Initialize(); // [FEATURE] [Employee] // [GIVEN] a Employee with a default dimension // [WHEN] The user posts a http request to get a default dimension on the Employee @@ -399,6 +427,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Customer: Record "Customer"; begin + Initialize(); // [FEATURE] [Customer] // [GIVEN] a customer with a default dimension // [WHEN] The user posts a http request to patch the default dimension on the customer @@ -412,6 +441,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Vendor: Record "Vendor"; begin + Initialize(); // [FEATURE] [Vendor] // [GIVEN] a Vendor with a default dimension // [WHEN] The user posts a http request to patch a default dimension on the Vendor @@ -425,6 +455,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Item: Record "Item"; begin + Initialize(); // [FEATURE] [Item] // [GIVEN] a Item with a default dimension // [WHEN] The user posts a http request to patch a default dimension on the Item @@ -438,6 +469,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Employee: Record "Employee"; begin + Initialize(); // [FEATURE] [Employee] // [GIVEN] a Employee with a default dimension // [WHEN] The user posts a http request to patch a default dimension on the Employee @@ -451,6 +483,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Customer: Record "Customer"; begin + Initialize(); // [FEATURE] [Customer] // [GIVEN] a customer with a default dimension // [WHEN] The user posts a http request to patch the default dimension with a blocked dimension value on the customer @@ -464,6 +497,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Vendor: Record "Vendor"; begin + Initialize(); // [FEATURE] [Vendor] // [GIVEN] a Vendor with a default dimension // [WHEN] The user posts a http request to patch a default dimension with a blocked dimension value on the Vendor @@ -477,6 +511,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Item: Record "Item"; begin + Initialize(); // [FEATURE] [Item] // [GIVEN] a Item with a default dimension // [WHEN] The user posts a http request to patch a default dimension with a blocked dimension value on the Item @@ -490,6 +525,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Employee: Record "Employee"; begin + Initialize(); // [FEATURE] [Employee] // [GIVEN] a Employee with a default dimension // [WHEN] The user posts a http request to patch a default dimension with a blocked dimension value on the Employee @@ -503,6 +539,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Customer: Record "Customer"; begin + Initialize(); // [FEATURE] [Customer] // [GIVEN] a customer with a default dimension // [WHEN] The user posts a http request to patch the default dimension with a blocked dimension value on the customer @@ -516,6 +553,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Vendor: Record "Vendor"; begin + Initialize(); // [FEATURE] [Vendor] // [GIVEN] a vendor with a default dimension // [WHEN] The user posts a http request to patch the default dimension with a blocked dimension value on the vendor @@ -529,6 +567,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Item: Record "Item"; begin + Initialize(); // [FEATURE] [Item] // [GIVEN] a item with a default dimension // [WHEN] The user posts a http request to patch the default dimension with a blocked dimension value on the item @@ -542,6 +581,7 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" var Employee: Record "Employee"; begin + Initialize(); // [FEATURE] [Employee] // [GIVEN] a employee with a default dimension // [WHEN] The user posts a http request to patch the default dimension with a blocked dimension value on the employee @@ -561,6 +601,8 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" DimensionValueId: Text; ParentIdAsText: Text; begin + Initialize(); + LibraryDimension.CreateDimension(Dimension); LibraryDimension.CreateDimensionValue(DimensionValue, Dimension.Code); ParentIdAsText := LowerCase(Format(ParentId)); @@ -592,6 +634,8 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" DimensionId: Text; DimensionValueId: Text; begin + Initialize(); + LibraryDimension.CreateDimension(Dimension); LibraryDimension.CreateDimensionValue(DimensionValue, Dimension.Code); ParentIdAsText := LowerCase(Format(ParentId)); @@ -624,6 +668,8 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" DimensionId: Text; DimensionValue2Id: Text; begin + Initialize(); + LibraryDimension.CreateDimension(Dimension); LibraryDimension.CreateDimensionValue(DimensionValue, Dimension.Code); LibraryDimension.CreateDimension(Dimension2); @@ -653,6 +699,8 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" DimensionId: Text; DimensionValueId: Text; begin + Initialize(); + LibraryDimension.CreateDimension(Dimension); LibraryDimension.CreateDimensionValue(DimensionValue, Dimension.Code); Dimension.Validate(Blocked, true); @@ -682,6 +730,8 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" DimensionId: Text; DimensionValueId: Text; begin + Initialize(); + LibraryDimension.CreateDimension(Dimension); LibraryDimension.CreateDimensionValue(DimensionValue, Dimension.Code); DimensionValue.Validate(Blocked, true); @@ -711,6 +761,8 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" ParentIdAsText: Text; DimensionId: Text; begin + Initialize(); + LibraryDimension.CreateDimension(Dimension); LibraryDimension.CreateDimensionValue(DimensionValue, Dimension.Code); DefaultDimension.Validate("Table ID", TableNo); @@ -748,6 +800,8 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" DimensionId: Text; DimensionValueId: Text; begin + Initialize(); + LibraryDimension.CreateDimension(Dimension); LibraryDimension.CreateDimensionValue(DimensionValue, Dimension.Code); DefaultDimension.Validate("Table ID", TableNo); @@ -785,6 +839,8 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" DimensionId: Text; DimensionValue2Id: Text; begin + Initialize(); + LibraryDimension.CreateDimension(Dimension); LibraryDimension.CreateDimensionValue(DimensionValue, Dimension.Code); LibraryDimension.CreateDimensionValue(DimensionValue2, Dimension.Code); @@ -826,6 +882,8 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" DimensionId: Text; DimensionValue2Id: Text; begin + Initialize(); + LibraryDimension.CreateDimension(Dimension); LibraryDimension.CreateDimensionValue(DimensionValue, Dimension.Code); LibraryDimension.CreateDimensionValue(DimensionValue2, Dimension.Code); @@ -866,6 +924,8 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" ParentIdAsText: Text; DimensionId: Text; begin + Initialize(); + LibraryDimension.CreateDimension(Dimension); LibraryDimension.CreateDimension(Dimension2); LibraryDimension.CreateDimensionValue(DimensionValue, Dimension.Code); @@ -975,4 +1035,11 @@ codeunit 139832 "APIV2 - Default Dimensions E2E" end; exit(DefaultDimensionParentType::" "); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2DimSetLinesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2DimSetLinesE2E.Codeunit.al index 503e68355db..fcdecceeafb 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2DimSetLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2DimSetLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -28,6 +29,9 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); end; [Test] @@ -38,6 +42,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" JournalName: Code[10]; JournalLineGUID: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in journal through a POST method and check if it was created LibraryGraphJournalLines.Initialize(); @@ -62,6 +67,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a sales order and a sales order line through a POST method and check if it was created // [GIVEN] A sales order LibrarySales.CreateSalesOrder(SalesHeader); @@ -90,6 +96,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineId: Guid; CustomerNo: Code[20]; begin + Initialize(); // [SCENARIO] Create a dimension line in a sales quote and sales quote line through a POST method and check if it was created // [GIVEN] A sales quote LibrarySales.CreateCustomer(Customer); @@ -118,6 +125,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a sales credit memo and a sales cr memo line through a POST method and check if it was created // [GIVEN] A sales credit memo LibrarySales.CreateSalesCreditMemo(SalesHeader); @@ -144,6 +152,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a sales invoice and a sales invoice line through a POST method and check if it was created // [GIVEN] A sales invoice LibrarySales.CreateSalesInvoice(SalesHeader); @@ -170,6 +179,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a purchase invoice and a purchase invoice line through a POST method and check if it was created // [GIVEN] A purchase invoice LibraryPurchase.CreatePurchaseInvoice(PurchaseHeader); @@ -196,6 +206,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a purchase order and a purchase order line through a POST method and check if it was created // [GIVEN] A purchase order LibraryPurchase.CreatePurchaseOrder(PurchaseHeader); @@ -222,6 +233,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" DocumentRecordRef: RecordRef; TimeSheetDetailId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a time registration entry through a POST method and check if it was created // [GIVEN] A Time registration entry UserSetup.DeleteAll(); @@ -242,6 +254,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO] Creating a dimension line through a POST method without specifying a parent Id fails LibraryGraphJournalLines.Initialize(); @@ -273,6 +287,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Try to create a dimension line with an already existing code LibraryGraphJournalLines.Initialize(); @@ -318,6 +334,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" JournalName: Code[10]; JournalLineGUID: Guid; begin + Initialize(); // [SCENARIO] Create dimension lines in a journal line and use a GET method to retrieve them // [GIVEN] a journal in the General Journal Table LibraryGraphJournalLines.Initialize(); @@ -335,6 +352,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" SalesLine: Record "Sales Line"; LineId: Guid; begin + Initialize(); // [SCENARIO] Create dimension lines in a sales order and sales order line and use a GET method to retrieve them // [GIVEN] a sales order with lines LibrarySales.CreateSalesOrder(SalesHeader); @@ -358,6 +376,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" CustomerNo: Code[20]; LineId: Guid; begin + Initialize(); // [SCENARIO] Create dimension lines in a sales quote and sales quote line and use a GET method to retrieve them // [GIVEN] a sales quote with lines LibrarySales.CreateCustomer(Customer); @@ -381,6 +400,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" SalesLine: Record "Sales Line"; LineId: Guid; begin + Initialize(); // [SCENARIO] Create dimension lines in a sales cr memo and sales cr memo line and use a GET method to retrieve them // [GIVEN] a sales cr memo with lines LibrarySales.CreateSalesCreditMemo(SalesHeader); @@ -402,6 +422,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" SalesLine: Record "Sales Line"; LineId: Guid; begin + Initialize(); // [SCENARIO] Create dimension lines in a sales invoice and sales invoice line and use a GET method to retrieve them // [GIVEN] a sales invoice with lines LibrarySales.CreateSalesInvoice(SalesHeader); @@ -423,6 +444,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" PurchaseLine: Record "Purchase Line"; LineId: Guid; begin + Initialize(); // [SCENARIO] Create dimension lines in a pucrhase invoice and purchase invoice line and use a GET method to retrieve them // [GIVEN] a purchase invoice with lines LibraryPurchase.CreatePurchaseInvoice(PurchaseHeader); @@ -444,6 +466,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" PurchaseLine: Record "Purchase Line"; LineId: Guid; begin + Initialize(); // [SCENARIO] Create dimension lines in a pucrhase order and purchase order line and use a GET method to retrieve them // [GIVEN] a purchase order with lines LibraryPurchase.CreatePurchaseOrder(PurchaseHeader); @@ -467,6 +490,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" DocumentRecordRef: RecordRef; TimeSheetDetailId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a time registration entry through a POST method and and use a GET method to retrieve them // [GIVEN] A Time registration entry UserSetup.DeleteAll(); @@ -491,6 +515,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" TargetURL: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] Create a G/L Entry with dimenions after Posting Sales Invoice and use a GET method to retreive them // [GIVEN] Create Customer, Items and Sales Invoice for different Items. @@ -522,6 +548,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Using a GET request to retrieve dimension lines without a filter fails LibraryGraphJournalLines.Initialize(); @@ -544,6 +572,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" JournalName: Code[10]; JournalLineGUID: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line, use a PATCH method to change it and then verify the changes LibraryGraphJournalLines.Initialize(); @@ -565,6 +594,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a sales order and a sales order line, use a PATCH method to change it and then verify the changes // [GIVEN] A sales order LibrarySales.CreateSalesOrder(SalesHeader); @@ -593,6 +623,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineId: Guid; CustomerNo: Code[20]; begin + Initialize(); // [SCENARIO] Create a dimension line in a sales quote and a sales quote line, use a PATCH method to change it and then verify the changes // [GIVEN] A sales quote LibrarySales.CreateCustomer(Customer); @@ -621,6 +652,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a sales credit memo and a sales cr memo line, use a PATCH method to change it and then verify the changes // [GIVEN] A sales credit memo LibrarySales.CreateSalesCreditMemo(SalesHeader); @@ -647,6 +679,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a sales invoice and a sales invoice line, use a PATCH method to change it and then verify the changes // [GIVEN] A sales invoice LibrarySales.CreateSalesInvoice(SalesHeader); @@ -673,6 +706,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a purchase invoice and a purchase invoice line, use a PATCH method to change it and then verify the changes // [GIVEN] A purchase invoice LibraryPurchase.CreatePurchaseInvoice(PurchaseHeader); @@ -699,6 +733,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a purchase order and a purchase order line, use a PATCH method to change it and then verify the changes // [GIVEN] A purchase order LibraryPurchase.CreatePurchaseOrder(PurchaseHeader); @@ -725,6 +760,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" DocumentRecordRef: RecordRef; TimeSheetDetailId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a time registration entry, use a PATCH method to change it and then verify the changes // [GIVEN] A Time registration entry UserSetup.DeleteAll(); @@ -745,6 +781,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a purchase credit memo and a purchase cr memo line through a POST method and check if it was created // [GIVEN] A purchase credit memo LibraryPurchase.CreatePurchaseCreditMemo(PurchaseHeader); @@ -769,6 +806,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" PurchaseLine: Record "Purchase Line"; LineId: Guid; begin + Initialize(); // [SCENARIO] Create dimension lines in a purchase cr memo and purchase cr memo line and use a GET method to retrieve them // [GIVEN] a purchase cr memo with lines LibraryPurchase.CreatePurchaseCreditMemo(PurchaseHeader); @@ -792,6 +830,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a purchase credit memo and a purchase cr memo line, use a PATCH method to change it and then verify the changes // [GIVEN] A purchase credit memo LibraryPurchase.CreatePurchaseCreditMemo(PurchaseHeader); @@ -818,6 +857,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a purchase credit memo and a purchase cr memo line, use a DELETE method to remove it and then verify the deletion // [GIVEN] A purchase credit memo LibraryPurchase.CreatePurchaseCreditMemo(PurchaseHeader); @@ -851,6 +891,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" TargetURL: Text; DimensionSetID: Integer; begin + Initialize(); + // [GIVEN] 2 dimension json texts LibraryDimension.CreateDimension(Dimension); DimensionCode := Dimension.Code; @@ -928,6 +970,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Try to change the code of an existing dimension line LibraryGraphJournalLines.Initialize(); @@ -978,6 +1022,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" JournalName: Code[10]; JournalLineGUID: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line, use a DELETE method to remove it and then verify the deletion LibraryGraphJournalLines.Initialize(); @@ -1000,6 +1045,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a sales order and a sales order, use a DELETE method to remove it and then verify the deletion // [GIVEN] A sales order LibrarySales.CreateSalesOrder(SalesHeader); @@ -1028,6 +1074,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineId: Guid; CustomerNo: Code[20]; begin + Initialize(); // [SCENARIO] Create a dimension line in a sales quote and sales quote line, use a DELETE method to remove it and then verify the deletion // [GIVEN] A sales quote LibrarySales.CreateCustomer(Customer); @@ -1056,6 +1103,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a sales credit memo and a sales cr memo line, use a DELETE method to remove it and then verify the deletion // [GIVEN] A sales credit memo LibrarySales.CreateSalesCreditMemo(SalesHeader); @@ -1082,6 +1130,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a sales invoice and a sales invoice line, use a DELETE method to remove it and then verify the deletion // [GIVEN] A sales invoice LibrarySales.CreateSalesInvoice(SalesHeader); @@ -1108,6 +1157,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a purchase invoice and a purchase invoice line, use a DELETE method to remove it and then verify the deletion LibraryPurchase.CreatePurchaseInvoice(PurchaseHeader); DocumentRecordRef.GetTable(PurchaseHeader); @@ -1133,6 +1183,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" LineRecordRef: RecordRef; LineId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a purchase order and a purchase order, use a DELETE method to remove it and then verify the deletion // [GIVEN] A purchase order LibraryPurchase.CreatePurchaseOrder(PurchaseHeader); @@ -1157,6 +1208,7 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" DocumentRecordRef: RecordRef; TimeSheetDetailId: Guid; begin + Initialize(); // [SCENARIO] Create a dimension line in a time registration entry, use a DELETE method to remove it and then verify the deletion // [GIVEN] A Time registration entry TimeSheetDetailId := CreateTimeSheet(); @@ -1179,6 +1231,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [GIVEN] a dimension line in the journal line LibraryDimension.CreateDimension(Dimension); DimensionValue.Reset(); @@ -1240,6 +1294,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [GIVEN] a dimension line in the journal line LibraryDimension.CreateDimension(Dimension); DimensionValue.Reset(); @@ -1296,6 +1352,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [GIVEN] a dimension with a value LibraryDimension.CreateDimension(Dimension); LibraryDimension.CreateDimensionValue(DimensionValue, Dimension.Code); @@ -1357,6 +1415,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [GIVEN] a dimension with a value LibraryDimension.CreateDimension(Dimension); LibraryDimension.CreateDimensionValue(DimensionValue, Dimension.Code); @@ -1406,6 +1466,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [GIVEN] 2 dimensions with dimension values LibraryDimension.CreateDimension(Dimension); DimensionCode[2] := Dimension.Code; @@ -1450,6 +1512,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [GIVEN] 2 dimensions with dimension values LibraryDimension.CreateDimension(Dimension); DimensionCode[2] := Dimension.Code; @@ -1501,6 +1565,8 @@ codeunit 139825 "APIV2 - Dim. Set Lines E2E" TargetURL: Text; DimensionSetID: Integer; begin + Initialize(); + // [GIVEN] 2 dimension json texts LibraryDimension.CreateDimension(Dimension); DimensionCode := Dimension.Code; diff --git a/src/Apps/W1/APIV2/test/src/APIV2DimensionsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2DimensionsE2E.Codeunit.al index 9b14ca531ea..a0c81c571cd 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2DimensionsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2DimensionsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139824 "APIV2 - Dimensions E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -20,9 +21,14 @@ codeunit 139824 "APIV2 - Dimensions E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; @@ -136,4 +142,3 @@ codeunit 139824 "APIV2 - Dimensions E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2DocumentAttachE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2DocumentAttachE2E.Codeunit.al index d38126db6a0..dc813383746 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2DocumentAttachE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2DocumentAttachE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139899 "APIV2 - Document Attach. E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -32,6 +33,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table LibrarySales.CreateCustomer(Customer); @@ -56,6 +59,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table LibrarySmallBusiness.CreateItem(Item); @@ -80,6 +85,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table LibraryHumanResources.CreateEmployee(Employee); @@ -104,6 +111,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table LibraryJob.CreateJob(Job); @@ -128,6 +137,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table LibrarySmallBusiness.CreateVendor(Vendor); @@ -152,6 +163,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table CreatePostedSalesInvoice(DocumentRecordRef, DocumentId); @@ -174,6 +187,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table CreateDraftSalesInvoice(DocumentRecordRef); @@ -197,6 +212,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table CreatePostedPurchaseInvoice(DocumentRecordRef, DocumentId); @@ -219,6 +236,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table CreateDraftPurchaseInvoice(DocumentRecordRef); @@ -241,6 +260,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table CreateSalesQuote(DocumentRecordRef); @@ -264,6 +285,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve the Attachment record from the Attachment API. // [GIVEN] Attachment exists in the Document Attachment table CreatePostedSalesInvoice(DocumentRecordRef, DocumentId); @@ -286,6 +309,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve the Attachment record from the Attachment API. // [GIVEN] Attachment exists in the Document Attachment table CreateDraftSalesInvoice(DocumentRecordRef); @@ -308,6 +333,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve the Attachment record from the Attachment API. // [GIVEN] Attachment exists in the Document Attachment table CreateDraftSalesOrder(DocumentRecordRef); @@ -330,6 +357,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve the Attachment record from the Attachment API. // [GIVEN] Attachment exists in the Document Attachment table CreateSalesQuote(DocumentRecordRef); @@ -352,6 +381,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table CreateDraftPurchaseOrder(DocumentRecordRef); @@ -379,6 +410,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ExpectedBase64Content: Text; ActualBase64Content: Text; begin + Initialize(); + // [SCENARIO] User can update linked attachment binary content through the Attachment API. // [GIVEN] A linked attachment exists LibrarySmallBusiness.CreateCustomer(Customer); @@ -407,6 +440,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" Customer: Record Customer; DocumentRecordRef: RecordRef; begin + Initialize(); + LibrarySmallBusiness.CreateCustomer(Customer); DocumentRecordRef.GetTable(Customer); TestCreateAttachment(DocumentRecordRef); @@ -418,6 +453,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" Vendor: Record Vendor; DocumentRecordRef: RecordRef; begin + Initialize(); + LibrarySmallBusiness.CreateVendor(Vendor); DocumentRecordRef.GetTable(Vendor); TestCreateAttachment(DocumentRecordRef); @@ -430,6 +467,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" DocumentId: Guid; DocumentType: Text; begin + Initialize(); + CreatePostedSalesInvoice(DocumentRecordRef, DocumentId); DocumentType := GetDocumentType(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef, DocumentId, DocumentType); @@ -440,6 +479,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftSalesInvoice(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -449,6 +490,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftSalesOrder(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -460,6 +503,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" DocumentId: Guid; DocumentType: Text; begin + Initialize(); + CreatePostedPurchaseInvoice(DocumentRecordRef, DocumentId); DocumentType := GetDocumentType(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef, DocumentId, DocumentType); @@ -470,6 +515,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftPurchaseInvoice(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -479,6 +526,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftPurchaseOrder(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -530,6 +579,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateSalesQuote(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -540,6 +591,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" Vendor: Record Vendor; DocumentRecordRef: RecordRef; begin + Initialize(); + LibrarySmallBusiness.CreateVendor(Vendor); DocumentRecordRef.GetTable(Vendor); CreateAttachment(DocumentRecordRef); @@ -552,6 +605,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" Employee: Record Employee; DocumentRecordRef: RecordRef; begin + Initialize(); + LibraryHumanResources.CreateEmployee(Employee); DocumentRecordRef.GetTable(Employee); CreateAttachment(DocumentRecordRef); @@ -564,6 +619,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" Job: Record Job; DocumentRecordRef: RecordRef; begin + Initialize(); + LibraryJob.CreateJob(Job); DocumentRecordRef.GetTable(Job); CreateAttachment(DocumentRecordRef); @@ -576,6 +633,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedSalesInvoice(DocumentRecordRef, DocumentId); TestDeleteAttachment2(DocumentRecordRef); end; @@ -585,6 +644,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftSalesInvoice(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -595,6 +656,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedPurchaseInvoice(DocumentRecordRef, DocumentId); TestDeleteAttachment2(DocumentRecordRef); end; @@ -604,6 +667,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftPurchaseInvoice(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -613,6 +678,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateSalesQuote(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -625,6 +692,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table CreateDraftPurchaseCreditMemo(DocumentRecordRef); @@ -648,6 +717,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can retrieve all records from the Attachments API. // [GIVEN] 2 Attachments in the Document Attachment table CreatePostedPurchaseCreditMemo(DocumentRecordRef, DocumentId); @@ -667,6 +738,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftPurchaseCreditMemo(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef); end; @@ -678,6 +751,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" DocumentId: Guid; DocumentType: Text; begin + Initialize(); + CreatePostedPurchaseCreditMemo(DocumentRecordRef, DocumentId); DocumentType := GetDocumentType(DocumentRecordRef); TestCreateAttachment(DocumentRecordRef, DocumentId, DocumentType); @@ -688,6 +763,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" var DocumentRecordRef: RecordRef; begin + Initialize(); + CreateDraftPurchaseCreditMemo(DocumentRecordRef); TestDeleteAttachment(DocumentRecordRef); end; @@ -698,6 +775,8 @@ codeunit 139899 "APIV2 - Document Attach. E2E" DocumentRecordRef: RecordRef; DocumentId: Guid; begin + Initialize(); + CreatePostedPurchaseCreditMemo(DocumentRecordRef, DocumentId); TestDeleteAttachment2(DocumentRecordRef); end; @@ -1035,4 +1114,11 @@ codeunit 139899 "APIV2 - Document Attach. E2E" exit(StrSubstNo('%1.%2', Name, Extension)); exit(Name); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2EmployeesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2EmployeesE2E.Codeunit.al index 9dca78748ec..ca757d49767 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2EmployeesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2EmployeesE2E.Codeunit.al @@ -25,12 +25,17 @@ codeunit 139822 "APIV2 - Employees E2E" var LibraryApplicationArea: Codeunit "Library - Application Area"; begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryApplicationArea.EnableBasicHRSetup(); // Lazy Setup. if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryHumanResource.SetupEmployeeNumberSeries(); IsInitialized := true; @@ -223,4 +228,3 @@ codeunit 139822 "APIV2 - Employees E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2FALocationsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2FALocationsE2E.Codeunit.al index 9a23cb8f5c7..9f7071a3e4f 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2FALocationsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2FALocationsE2E.Codeunit.al @@ -21,9 +21,14 @@ codeunit 139906 "APIV2 - FA Locations E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2FixedAssetsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2FixedAssetsE2E.Codeunit.al index 3efd9d6a789..0a2ded0e7f1 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2FixedAssetsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2FixedAssetsE2E.Codeunit.al @@ -23,9 +23,14 @@ codeunit 139907 "APIV2 - Fixed Assets E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2GLEntriesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2GLEntriesE2E.Codeunit.al index bb7b691b29f..d642ac47f98 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2GLEntriesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2GLEntriesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139830 "APIV2 - GLEntries E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -24,10 +25,15 @@ codeunit 139830 "APIV2 - GLEntries E2E" LibraryERMCountryData: Codeunit "Library - ERM Country Data"; LibraryApplicationArea: Codeunit "Library - Application Area"; begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryApplicationArea.EnableFoundationSetup(); if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateVATPostingSetup(); LibraryERMCountryData.UpdateGeneralLedgerSetup(); @@ -161,5 +167,3 @@ codeunit 139830 "APIV2 - GLEntries E2E" - - diff --git a/src/Apps/W1/APIV2/test/src/APIV2GLSetupE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2GLSetupE2E.Codeunit.al index 187cb7f3af7..213af4575c7 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2GLSetupE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2GLSetupE2E.Codeunit.al @@ -1,6 +1,7 @@ codeunit 139860 "APIV2 - G/L Setup E2E" { Subtype = Test; + RequiredTestIsolation = Disabled; var Assert: Codeunit "Assert"; @@ -13,6 +14,8 @@ codeunit 139860 "APIV2 - G/L Setup E2E" GeneralLedgerSetup: Record "General Ledger Setup"; Response, TargetURL : Text; begin + Initialize(); + GeneralLedgerSetup.Get(); TargetURL := LibraryGraphMgt.CreateTargetURL('', Page::"APIV2 - G/L Setup", ServiceNameTxt); LibraryGraphMgt.GetFromWebService(Response, TargetURL); @@ -60,4 +63,11 @@ codeunit 139860 "APIV2 - G/L Setup E2E" Assert.AreEqual(GeneralLedgerSetup."Shortcut Dimension 8 Code", PropertyJsonToken.AsValue().AsText(), 'Expected the same shortcutDimension8Code for generalLedgerSetup'); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } \ No newline at end of file diff --git a/src/Apps/W1/APIV2/test/src/APIV2GenProdPostGroupE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2GenProdPostGroupE2E.Codeunit.al index 6120bf37cdd..e594b1e6da8 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2GenProdPostGroupE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2GenProdPostGroupE2E.Codeunit.al @@ -1,12 +1,9 @@ codeunit 139856 "APIV2 - GenProdPostGroup E2E" { Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; - trigger OnRun() - begin - end; - var ERMVATToolHelper: Codeunit "ERM VAT Tool - Helper"; LibraryGraphMgt: Codeunit "Library - Graph Mgt"; @@ -21,6 +18,8 @@ codeunit 139856 "APIV2 - GenProdPostGroup E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [SCENARIO] Create a General Product Posting Group and use a GET method to retrieve it // [GIVEN] A Gen. Prod. Post. Group ERMVATToolHelper.CreateGenProdPostingGroup(GenProductPostingGroup, true); @@ -41,4 +40,11 @@ codeunit 139856 "APIV2 - GenProdPostGroup E2E" end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } \ No newline at end of file diff --git a/src/Apps/W1/APIV2/test/src/APIV2IncomeStatementE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2IncomeStatementE2E.Codeunit.al index 94109479bc6..49648b80327 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2IncomeStatementE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2IncomeStatementE2E.Codeunit.al @@ -62,9 +62,14 @@ codeunit 139816 "APIV2 - Income Statement E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := true; end; @@ -88,4 +93,3 @@ codeunit 139816 "APIV2 - Income Statement E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2InventoryPostGroupE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2InventoryPostGroupE2E.Codeunit.al index 3f357b613b8..7d3f5e05774 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2InventoryPostGroupE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2InventoryPostGroupE2E.Codeunit.al @@ -1,11 +1,8 @@ codeunit 139857 "APIV2 - InventoryPostGroup E2E" { Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; - trigger OnRun() - begin - end; - var LibraryInventory: Codeunit "Library - Inventory"; LibraryGraphMgt: Codeunit "Library - Graph Mgt"; @@ -21,6 +18,8 @@ codeunit 139857 "APIV2 - InventoryPostGroup E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [SCENARIO] Create an Inventory Posting Group and use a GET to retrieve it // [GIVEN] An Inventory Posting Group LibraryInventory.CreateInventoryPostingGroup(InventoryPostingGroup); @@ -40,4 +39,11 @@ codeunit 139857 "APIV2 - InventoryPostGroup E2E" 'Could not find created Inventory Posting Group on the response'); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } \ No newline at end of file diff --git a/src/Apps/W1/APIV2/test/src/APIV2ItemCategoriesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2ItemCategoriesE2E.Codeunit.al index 48a2175efe0..7e8a2532e28 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2ItemCategoriesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2ItemCategoriesE2E.Codeunit.al @@ -155,9 +155,14 @@ codeunit 139807 "APIV2 - Item Categories E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; end; @@ -231,4 +236,3 @@ codeunit 139807 "APIV2 - Item Categories E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2ItemVariantsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2ItemVariantsE2E.Codeunit.al index 2850134d790..064047b0f5a 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2ItemVariantsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2ItemVariantsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139839 "APIV2 - Item Variants E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -20,6 +21,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create an item variant through a POST method and check if it was created // [GIVEN] a JSON text with an item variant with itemId CreateItem(Item); @@ -44,6 +47,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Cannot create an Item Variant with non-existing itemId // [GIVEN] a JSON text with an item variant with non-existing itemId ItemId := CreateGuid(); @@ -65,6 +70,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create an item variant through a POST method and check if it was created // [GIVEN] a JSON text with an item variant with itemNumber CreateItem(Item); @@ -90,6 +97,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Cannot create an Item Variant with non-existing itemNumber // [GIVEN] a JSON text with an item variant with non-existing itemNumber ItemNo := LibraryUtility.GenerateRandomCode(ItemVariant.FieldNo(Code), Database::"Item Variant"); @@ -111,6 +120,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create an item variant through a POST method and check if it was created // [GIVEN] a JSON text with an item variant with itemNumber and itemId CreateItem(Item); @@ -136,6 +147,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Cannot create an Item Variant with mismatching itemNumber and itemId // [GIVEN] a JSON text with an item variant with mismatching itemNumber and itemId CreateItem(Item1); @@ -159,6 +172,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Cannot create an Item Variant if there is an item variant with same itemNumber and code // [GIVEN] a JSON text with an item variant with same itemNumber and code CreateItem(Item); @@ -181,6 +196,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Get a simple customer with a GET request to the service. // [GIVEN] An item variant exists in the system CreateItem(Item); @@ -206,6 +223,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create an item variant, use a PATCH method to change it and then verify the changes // [GIVEN] an item variant exists in the system CreateItem(Item); @@ -238,6 +257,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create an item variant, use a PATCH method to change it and then verify the changes // [GIVEN] an item variant exists in the system CreateItem(Item1); @@ -271,6 +292,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create an item variant, use a PATCH method to change it and then verify the changes // [GIVEN] an item variant exists in the system CreateItem(Item1); @@ -300,6 +323,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create an item variant, use a DELETE method to remove it and then verify the deletion // [GIVEN] an item variant in the system CreateItem(Item); @@ -326,6 +351,8 @@ codeunit 139839 "APIV2 - Item Variants E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Cannot delete an item variant in use // [GIVEN] an item variant in the system and used in a sales order line LibraryInventory.CreateItem(Item); @@ -440,4 +467,11 @@ codeunit 139839 "APIV2 - Item Variants E2E" exit(CopyStr('GRAPHITEM' + '00001', 1, 20)); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2ItemsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2ItemsE2E.Codeunit.al index 98f53fcb285..e26d64bb86d 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2ItemsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2ItemsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139800 "APIV2 - Items E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -30,9 +31,14 @@ codeunit 139800 "APIV2 - Items E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; @@ -936,8 +942,6 @@ codeunit 139800 "APIV2 - Items E2E" - - diff --git a/src/Apps/W1/APIV2/test/src/APIV2JobQueueEntriesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2JobQueueEntriesE2E.Codeunit.al index fd33be96961..82245a5b93e 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2JobQueueEntriesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2JobQueueEntriesE2E.Codeunit.al @@ -25,6 +25,8 @@ codeunit 139862 "APIV2JobQueueEntriesE2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO] Create 1 JobQueueEntry and use a GET method to retrieve it· // [GIVEN] Clean Job Queue Entries and create a new JobQueueEntry JobQueueEntry.DeleteAll(); @@ -51,6 +53,8 @@ codeunit 139862 "APIV2JobQueueEntriesE2E" ResponseText: Text; JobQueueEntryJSON: Text; begin + Initialize(); + // [SCENARIO] Create 1 JobQueueEntry and use a GET method to retrieve it. // [GIVEN] Clean Job Queue Entries and create a new JobQueueEntry and one corresponding JobQueueLogEntry JobQueueEntry.DeleteAll(); @@ -79,6 +83,8 @@ codeunit 139862 "APIV2JobQueueEntriesE2E" ResponseText: Text; JobQueueLogEntryJSON: Text; begin + Initialize(); + // [SCENARIO] Create 1 JobQueueEntry with 2 corresponding JobQueueLogEntries and use a GET method to retrieve it. // [GIVEN] Clean Job Queue Entries and create a new JobQueueEntry and 3 corresponding JobQueueLogEntries JobQueueEntry.DeleteAll(); @@ -105,6 +111,8 @@ codeunit 139862 "APIV2JobQueueEntriesE2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO] Create 1 JobQueueEntry with status error. Use an action to reschedule the JobQueueEntry // [GIVEN] Clean Job Queue Entries and create a new JobQueueEntry with status error JobQueueEntry.DeleteAll(); @@ -201,4 +209,11 @@ codeunit 139862 "APIV2JobQueueEntriesE2E" until (Index = LibraryGraphMgt.GetCollectionCountFromJSON(JobQueueLogEntryJSON)); Assert.AreEqual(Count, Index, 'The number of Job Queue Log Entries is wrong.'); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2JobQueueLogEntriesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2JobQueueLogEntriesE2E.Codeunit.al index 85e8931992b..2cdcbb60de4 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2JobQueueLogEntriesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2JobQueueLogEntriesE2E.Codeunit.al @@ -21,6 +21,8 @@ codeunit 139861 "APIV2JobQueueLogEntriesE2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO] Use a GET method to retrieve Job Queue Log Entries // [GIVEN] A new Job Queue Log Entry created JobQueueLogEntry.DeleteAll(); @@ -63,4 +65,11 @@ codeunit 139861 "APIV2JobQueueLogEntriesE2E" JobQueueLogEntry.Insert(); exit(JobQueueLogEntry); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2JournalsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2JournalsE2E.Codeunit.al index 5d6b35a0891..bce446d6a23 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2JournalsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2JournalsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139827 "APIV2 - Journals E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -32,9 +33,14 @@ codeunit 139827 "APIV2 - Journals E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; @@ -190,6 +196,8 @@ codeunit 139827 "APIV2 - Journals E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [GIVEN] A general journal batch with a general journal line BalAccountNo := LibraryERM.CreateGLAccountNoWithDirectPosting(); BalAccountType := GenJournalLine."Bal. Account Type"::"G/L Account"; @@ -272,4 +280,3 @@ codeunit 139827 "APIV2 - Journals E2E" GenJournalBatch.Modify(true); end; } - diff --git a/src/Apps/W1/APIV2/test/src/APIV2LocationsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2LocationsE2E.Codeunit.al index 667ae62e2f1..dc225782745 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2LocationsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2LocationsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139868 "APIV2 - Locations E2E" // version Test,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -24,9 +25,14 @@ codeunit 139868 "APIV2 - Locations E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2OpportunitiesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2OpportunitiesE2E.Codeunit.al index 50b6a4c45dd..40e1cb2ec3e 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2OpportunitiesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2OpportunitiesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139855 "APIV2 - Opportunities E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -31,6 +32,8 @@ codeunit 139855 "APIV2 - Opportunities E2E" ResponseText: Text; OpportunityId: Text; begin + Initialize(); + // [SCENARIO] Create an opportunity and use a GET method to retrieve them // [GIVEN] An opportunity ContactNo := LibraryMarketing.CreateCompanyContactNo(); @@ -60,6 +63,8 @@ codeunit 139855 "APIV2 - Opportunities E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO 184721] Create an opportunity through a POST method and check if it was created // [GIVEN] a JSON text with an Opportunity only with a Description property OpportunityJSON := CreateMinimalOpportunityJSON(OpportunityDescription); @@ -85,6 +90,8 @@ codeunit 139855 "APIV2 - Opportunities E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO 184721] Create an opportunity through a POST method and check if it was created // [GIVEN] a JSON text with an Opportunity only with a contact no ContactNo := LibraryMarketing.CreateCompanyContactNo(); @@ -125,6 +132,8 @@ codeunit 139855 "APIV2 - Opportunities E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO 184721] Create an opportunity through a POST method // [GIVEN] a JSON text with an Opportunity only with a contact number that does not exist ContactNo := 'wrongContNo'; @@ -146,6 +155,8 @@ codeunit 139855 "APIV2 - Opportunities E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO 184721] Create an opportunity through a POST method // [GIVEN] a JSON text with an Opportunity only with a salesperson number that does not exist SalespersonNo := 'wrongSPNo'; @@ -169,6 +180,8 @@ codeunit 139855 "APIV2 - Opportunities E2E" ResponseText: Text; OpportunityId: Guid; begin + Initialize(); + // [SCENARIO] Use a PATCH method to change the description of an opportunity // [GIVEN] An opportunity ContactNo := LibraryMarketing.CreateCompanyContactNo(); @@ -199,6 +212,8 @@ codeunit 139855 "APIV2 - Opportunities E2E" ResponseText: Text; OpportunityId: Guid; begin + Initialize(); + // [SCENARIO] Use a PATCH method to change the contact no of a Won opportunity // [GIVEN] An won opportunity CompanyContactNo := LibraryMarketing.CreateCompanyContactNo(); @@ -228,6 +243,8 @@ codeunit 139855 "APIV2 - Opportunities E2E" ResponseText: Text; OpportunityId: Guid; begin + Initialize(); + // [SCENARIO] Use a PATCH method to change the salesperson code of a Won opportunity // [GIVEN] An won opportunity SalesPersonCode := CreateSalesperson(SalespersonPurchaser); @@ -256,6 +273,8 @@ codeunit 139855 "APIV2 - Opportunities E2E" ResponseText: Text; OpportunityId: Guid; begin + Initialize(); + // [SCENARIO] Use a PATCH method to change the contact no of an opportunity in progress to a contact of a different company // [GIVEN] An won opportunity CompanyContactNo1 := LibraryMarketing.CreateCompanyContactNo(); @@ -408,4 +427,11 @@ codeunit 139855 "APIV2 - Opportunities E2E" Opportunity.SetRange(Description, DescriptionValue); Assert.IsFalse(Opportunity.IsEmpty(), 'Opportunity does not exist'); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2PDFDocumentE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PDFDocumentE2E.Codeunit.al index 63c5aabb741..f8603299edb 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PDFDocumentE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PDFDocumentE2E.Codeunit.al @@ -1,6 +1,7 @@ codeunit 139841 "APIV2 - PDF Document E2E" { Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -26,6 +27,10 @@ codeunit 139841 "APIV2 - PDF Document E2E" var CompanyInformation: Record "Company Information"; begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + CompanyInformation.Get(); if CompanyInformation."Giro No." = '' then CompanyInformation."Giro No." := '1234567'; @@ -381,4 +386,3 @@ codeunit 139841 "APIV2 - PDF Document E2E" Commit(); end; } - diff --git a/src/Apps/W1/APIV2/test/src/APIV2PaymentMethodsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PaymentMethodsE2E.Codeunit.al index ca63c08260f..5358c09d5ef 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PaymentMethodsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PaymentMethodsE2E.Codeunit.al @@ -155,9 +155,14 @@ codeunit 139814 "APIV2 - Payment Methods E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; end; @@ -231,4 +236,3 @@ codeunit 139814 "APIV2 - Payment Methods E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2PaymentTermsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PaymentTermsE2E.Codeunit.al index 0e56cd7e8dc..fb872702d9d 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PaymentTermsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PaymentTermsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139804 "APIV2 - Payment Terms E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -19,9 +20,14 @@ codeunit 139804 "APIV2 - Payment Terms E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; @@ -97,4 +103,3 @@ codeunit 139804 "APIV2 - Payment Terms E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2PicturesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PicturesE2E.Codeunit.al index 317006420bd..09fd579e2ab 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PicturesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PicturesE2E.Codeunit.al @@ -1,6 +1,7 @@ codeunit 139842 "APIV2 - Pictures E2E" { Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -29,9 +30,14 @@ codeunit 139842 "APIV2 - Pictures E2E" [Scope('OnPrem')] procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; end; @@ -44,6 +50,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TempBlobExpected: Codeunit "Temp Blob"; TargetURL: Text; begin + Initialize(); + // [FEATURE] [Customer] // [SCENARIO] Get value picture from Customer Record @@ -68,6 +76,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Customer] // [SCENARIO] Get a picture metadata from Customer Record @@ -91,6 +101,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TempBlobResponse: Codeunit "Temp Blob"; TargetURL: Text; begin + Initialize(); + // [FEATURE] [Customer] // [SCENARIO] Get value picture from Customer Record when no image exist @@ -112,6 +124,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Customer] // [SCENARIO] Get a picture metadata from Customer Record when no image is present @@ -137,6 +151,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Customer] // [SCENARIO] Set Picture on a customer without picture @@ -169,6 +185,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Customer] // [SCENARIO] Replace the Picture on a customer @@ -200,6 +218,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Customer] // [SCENARIO] Replace the Picture on a customer @@ -227,6 +247,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TempBlobExpected: Codeunit "Temp Blob"; TargetURL: Text; begin + Initialize(); + // [FEATURE] [Vendor] // [SCENARIO] Get value picture from Vendor Record @@ -251,6 +273,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Vendor] // [SCENARIO] Get a picture metadata from Vendor Record @@ -274,6 +298,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TempBlobResponse: Codeunit "Temp Blob"; TargetURL: Text; begin + Initialize(); + // [FEATURE] [Vendor] // [SCENARIO] Get value picture from Vendor Record when no image exist @@ -295,6 +321,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Vendor] // [SCENARIO] Get a picture metadata from Vendor Record when no image is present @@ -320,6 +348,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Vendor] // [SCENARIO] Set Picture on a Vendor without picture @@ -352,6 +382,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Vendor] // [SCENARIO] Replace the Picture on a Vendor @@ -383,6 +415,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Vendor] // [SCENARIO] Replace the Picture on a Vendor @@ -410,6 +444,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TempBlobExpected: Codeunit "Temp Blob"; TargetURL: Text; begin + Initialize(); + // [FEATURE] [Employee] // [SCENARIO] Get value picture from Employee Record @@ -434,6 +470,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Employee] // [SCENARIO] Get a picture metadata from Employee Record @@ -457,6 +495,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TempBlobResponse: Codeunit "Temp Blob"; TargetURL: Text; begin + Initialize(); + // [FEATURE] [Employee] // [SCENARIO] Get value picture from Employee Record when no image exist @@ -478,6 +518,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Employee] // [SCENARIO] Get a picture metadata from Employee Record when no image is present @@ -503,6 +545,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Employee] // [SCENARIO] Set Picture on a Employee without picture @@ -535,6 +579,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Employee] // [SCENARIO] Replace the Picture on a Employee @@ -566,6 +612,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Employee] // [SCENARIO] Replace the Picture on a Employee @@ -593,6 +641,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TempBlobExpected: Codeunit "Temp Blob"; TargetURL: Text; begin + Initialize(); + // [FEATURE] [Item] // [SCENARIO] Get value picture from Item Record @@ -617,6 +667,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Item] // [SCENARIO] Get a picture metadata from Item Record @@ -640,6 +692,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TempBlobResponse: Codeunit "Temp Blob"; TargetURL: Text; begin + Initialize(); + // [FEATURE] [Item] // [SCENARIO] Get value picture from Item Record when no image exist @@ -661,6 +715,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Item] // [SCENARIO] Get a picture metadata from Item Record when no image is present @@ -686,6 +742,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Item] // [SCENARIO] Set Picture on a Item without picture @@ -718,6 +776,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Item] // [SCENARIO] Replace the Picture on a Item @@ -749,6 +809,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Item] // [SCENARIO] Replace the Picture on a Item @@ -776,6 +838,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TempBlobExpected: Codeunit "Temp Blob"; TargetURL: Text; begin + Initialize(); + // [FEATURE] [Contact] // [SCENARIO] Get value picture from Contact Record @@ -800,6 +864,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Contact] // [SCENARIO] Get a picture metadata from Contact Record @@ -823,6 +889,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TempBlobResponse: Codeunit "Temp Blob"; TargetURL: Text; begin + Initialize(); + // [FEATURE] [Contact] // [SCENARIO] Get value picture from Contact Record when no image exist @@ -844,6 +912,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Contact] // [SCENARIO] Get a picture metadata from Contact Record when no image is present @@ -869,6 +939,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Contact] // [SCENARIO] Set Picture on a contact without picture @@ -901,6 +973,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Contact] // [SCENARIO] Replace the Picture on a contact @@ -932,6 +1006,8 @@ codeunit 139842 "APIV2 - Pictures E2E" TargetURL: Text; Response: Text; begin + Initialize(); + // [FEATURE] [Contact] // [SCENARIO] Replace the Picture on a contact @@ -1102,4 +1178,3 @@ codeunit 139842 "APIV2 - Pictures E2E" end; } - diff --git a/src/Apps/W1/APIV2/test/src/APIV2PstdApprovalEntriesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PstdApprovalEntriesE2E.Codeunit.al index 8db85c18246..838eaa29477 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PstdApprovalEntriesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PstdApprovalEntriesE2E.Codeunit.al @@ -21,6 +21,8 @@ codeunit 139919 "APIV2 PstdApprovalEntries E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO] Check that posted approval entries can be retrieved via API // [GIVEN] a Sales order exists @@ -60,4 +62,10 @@ codeunit 139919 "APIV2 PstdApprovalEntries E2E" PostedApprovalEntry.Insert(); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } \ No newline at end of file diff --git a/src/Apps/W1/APIV2/test/src/APIV2PurchCrMLinesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PurchCrMLinesE2E.Codeunit.al index 71212c1efab..6d13e266eb0 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PurchCrMLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PurchCrMLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -30,6 +31,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Call GET on the lines without providing a parent Credit Memo ID. // [GIVEN] the credit memo API exposedSetAutoCalcFields @@ -56,6 +59,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" LineNo: Integer; SequenceValue: Text; begin + Initialize(); + // [SCENARIO] Call GET on the Line of a credit memo // [GIVEN] a credit memo with a line.SetAutoCalcFields CreditMemoId := CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -86,6 +91,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" LineNo1: Text; LineNo2: Text; begin + Initialize(); + // [SCENARIO] Call GET on the Lines of a unposted Credit Memo // [GIVEN] An credit memo with lines.SetAutoCalcFields CreditMemoID := CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -115,6 +122,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" LineNo1: Text; LineNo2: Text; begin + Initialize(); + // [SCENARIO] Call GET on the Lines of a credit memo // [GIVEN] a credit memo with lines.SetAutoCalcFields CreditMemoId := CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -144,6 +153,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" LineNo1: Text; LineNo2: Text; begin + Initialize(); + // [SCENARIO] Call GET on the Lines of a posted Credit Memo // [GIVEN] A posted credit memo with lines.SetAutoCalcFields CreditMemoID := CreatePostedPurchaseCreditMemoWithLines(PurchCrMemoHdr); @@ -172,6 +183,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" CreditMemoID: Text; LineNo: Integer; begin + Initialize(); + // [SCENARIO] POST a new line to an unposted Credit Memo // [GIVEN] An existing unposted credit memo and a valid JSON describing the new credit memo lineSetAutoCalcFields CreditMemoID := CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -203,6 +216,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" CreditMemoID: Text; LineNo: Integer; begin + Initialize(); + // [SCENARIO] POST a new line to an unposted Credit Memo with a sequence number // [GIVEN] An existing unposted credit memo and a valid JSON describing the new credit memo lineSetAutoCalcFields CreditMemoID := CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -241,6 +256,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" PurchaseQuantity: Integer; PurchaseQuantityFromJSON: Text; begin + Initialize(); + // [SCENARIO] PATCH a line of an unposted Credit Memo // [GIVEN] An unposted credit memo with lines and a valid JSON describing the fields that we want to changeSetAutoCalcFields CreditMemoID := CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -278,6 +295,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" CreditMemoId: Text; NewSequence: Integer; begin + Initialize(); + // [SCENARIO] PATCH a line of a credit memo will fail if sequence is modified // [GIVEN] A credit memo with lines and a valid JSON describing the fields that we want to changeSetAutoCalcFields CreditMemoId := CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -318,6 +337,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" CreditMemoID: Text; LineNo: Integer; begin + Initialize(); + // [SCENARIO] DELETE a line from an unposted Credit Memo // [GIVEN] An unposted credit memo with linesSetAutoCalcFields CreditMemoID := CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -348,6 +369,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" CreditMemoID: Text; LineNo: Integer; begin + Initialize(); + // [SCENARIO] Call DELETE on a line of a posted Credit Memo // [GIVEN] A posted credit memo with linesSetAutoCalcFields CreditMemoID := CreatePostedPurchaseCreditMemoWithLines(PurchCrMemoHdr); @@ -384,6 +407,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" ItemNo: Code[20]; VendorNo: Code[20]; begin + Initialize(); + // [SCENARIO] Create an credit memo both through the client UI and through the API and compare their final values. // [GIVEN] An unposted credit memo and a JSON describing the line we want to create LibraryPurchase.CreateVendor(Vendor); @@ -422,6 +447,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" ResponseText: Text; LinesJSON: Text; begin + Initialize(); + // [SCENARIO] Getting a line through API lists all possible types // [GIVEN] An credit memo with lines of different typesSetAutoCalcFields CreateCreditMemoWithDifferentLineTypes(PurchaseHeader, ExpectedNumberOfLines); @@ -447,6 +474,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" ResponseText: Text; CreditMemoLineJSON: Text; begin + Initialize(); + // [SCENARIO] Posting a line with description only will get a type item // [GIVEN] A post request with description onlySetAutoCalcFields CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -482,6 +511,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" ResponseText: Text; CreditMemoLineJSON: Text; begin + Initialize(); + // [FEATURE] [Comment] // [SCENARIO] Posting a line with Type Comment and description will make a comment line // [GIVEN] A post request with type and descriptionSetAutoCalcFields @@ -520,6 +551,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" CreditMemoLineJSON: Text; CreditMemoID: Text; begin + Initialize(); + // [SCENARIO] PATCH a Type on a line of an unposted Credit Memo // [GIVEN] An unposted credit memo with lines and a valid JSON describing the fields that we want to changeSetAutoCalcFields CreditMemoID := CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -551,6 +584,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" CreditMemoLineJSON: Text; CreditMemoLineID: Text; begin + Initialize(); + // [SCENARIO] PATCH a Type on a line of an unposted Credit Memo // [GIVEN] An unposted credit memo with lines and a valid JSON describing the fields that we want to changeSetAutoCalcFields CreateCreditMemoWithDifferentLineTypes(PurchaseHeader, ExpectedNumberOfLines); @@ -588,6 +623,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" CreditMemoLineJSON: Text; CreditMemoID: Text; begin + Initialize(); + // [SCENARIO] PATCH a Type on a line of an unposted Credit Memo // [GIVEN] An unposted credit memo with lines and a valid JSON describing the fields that we want to changeSetAutoCalcFields CreditMemoID := CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -622,6 +659,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" CreditMemoID: Text; LineNo: Integer; begin + Initialize(); + // [SCENARIO] POST a new line to an unposted Credit Memo with item variant // [GIVEN] An existing unposted credit memo and a valid JSON describing the new credit memo line with item variantSetAutoCalcFields CreditMemoID := CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -658,6 +697,8 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" CreditMemoLineJSON: Text; CreditMemoID: Text; begin + Initialize(); + // [SCENARIO] POST a new line to an unposted Credit Memo with wrong item variant // [GIVEN] An existing unposted credit memo and a valid JSON describing the new credit memo line with item variantSetAutoCalcFields CreditMemoID := CreatePurchaseCreditMemoWithLines(PurchaseHeader); @@ -950,4 +991,11 @@ codeunit 139874 "APIV2 - Purch. Cr.M. Lines E2E" PurchaseLineComment.Description := 'Thank you for your business!'; PurchaseLineComment.Modify(); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2PurchCrMemosE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PurchCrMemosE2E.Codeunit.al index 99bf5468337..ef529ca543e 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PurchCrMemosE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PurchCrMemosE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -39,6 +40,8 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create posted and unposted purchase credit memos and use a GET method to retrieve them // [GIVEN] 2 credit memos, one posted and one unposted CreatePurchaseCreditMemos(CreditMemoNo1, CreditMemoNo2); @@ -71,8 +74,11 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" TargetURL: Text; CreditMemo: Text; begin + Initialize(); + // [SCENARIO] Create posted and unposted Purchase credit memos and use HTTP POST to delete them // [GIVEN] 2 credit memos, one posted and one unposted + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibraryPurchase.CreateVendorWithAddress(BuyFromVendor); LibraryPurchase.CreateVendorWithAddress(PayToVendor); VendorNo := BuyFromVendor."No."; @@ -115,6 +121,8 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" CreditMemoJSON: Text; CurrencyCode: Code[10]; begin + Initialize(); + // [SCENARIO] Create posted and unposted with specific currency set and use HTTP POST to create them // [GIVEN] an CreditMemo with a non-LCY currencyCode set @@ -156,6 +164,8 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" CreditMemoJSON: Text; VendorCreditMemoNumber: Text; begin + Initialize(); + // [SCENARIO] Create posted and unposted with specific vendor credit memo number set and use HTTP POST to create them // [GIVEN] A credit memo with vendor credit memo number set @@ -186,18 +196,24 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" [Test] procedure TestModifyCreditMemos() begin + Initialize(); + TestMultipleModifyCreditMemos(false, false); end; [Test] procedure TestEmptyModifyCreditMemos() begin + Initialize(); + TestMultipleModifyCreditMemos(true, false); end; [Test] procedure TestPartialModifyCreditMemos() begin + Initialize(); + TestMultipleModifyCreditMemos(false, true); end; @@ -274,6 +290,8 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" CreditMemoNo: Text; CreditMemoID: Text; begin + Initialize(); + // [SCENARIO] Create unposted purchase credit memo and use HTTP DELETE to delete it // [GIVEN] An unposted credit memo CreateDraftPurchaseCreditMemo(PurchaseHeader); @@ -305,9 +323,12 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" CreditMemoPostingDate: Date; CreditMemoJSON: Text; begin + Initialize(); + // [SCENARIO] Create an credit memo both through the client UI and through the API // [SCENARIO] and compare them. They should be the same and have the same fields autocompleted wherever needed. // [GIVEN] An unposted credit memo + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibraryGraphDocumentTools.InitializeUIPage(); LibraryPurchase.CreateVendor(BuyFromVendor); @@ -343,6 +364,8 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" ResponseText: Text; CreditMemoID: Text; begin + Initialize(); + // [SCENARIO 184721] Create Credit Memo, use a PATCH method to change it and then verify the changes LibraryPurchase.CreateVendorWithAddress(Vendor); @@ -389,6 +412,8 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" ResponseText: Text; CreditMemoID: Text; begin + Initialize(); + // [SCENARIO 184721] Clearing manually set discount // [GIVEN] an item with unit price and unit cost @@ -439,6 +464,8 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can post a purchase credit memo through the API. LibraryERMCountryData.UpdatePurchasesPayablesSetup(); // [GIVEN] Draft purchase credit memo exists @@ -472,6 +499,8 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can cancel a posted purchase credit memo through API. // [GIVEN] Non-corrective purchase credit memo exists @@ -500,6 +529,8 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can cancel a posted purchase credit memo through API. // [GIVEN] Corrective purchase credit memo exists @@ -714,5 +745,11 @@ codeunit 139865 "APIV2 - Purch. Cr. Memos E2E" Assert.IsTrue(PurchCrMemoEntityBuffer.FindFirst(), CannotFindPostedCreditMemoErr); Assert.AreEqual(Status, PurchCrMemoEntityBuffer.Status, CreditMemoStatusErr); end; -} + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; +} diff --git a/src/Apps/W1/APIV2/test/src/APIV2PurchOrderLinesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PurchOrderLinesE2E.Codeunit.al index 10c1f29cd26..15993ed317a 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PurchOrderLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PurchOrderLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139852 "APIV2 - Purch. Order Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -30,9 +31,14 @@ codeunit 139852 "APIV2 - Purch. Order Lines E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := true; diff --git a/src/Apps/W1/APIV2/test/src/APIV2PurchQuotesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PurchQuotesE2E.Codeunit.al index fff9ebbec3e..ff9a8586045 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PurchQuotesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PurchQuotesE2E.Codeunit.al @@ -24,6 +24,8 @@ codeunit 139922 "APIV2 - Purch. Quotes E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create a purchase quote and use GET to retrieve it // [GIVEN] A purchase quote LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Quote, ''); @@ -44,6 +46,8 @@ codeunit 139922 "APIV2 - Purch. Quotes E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create a purchase quote and GET it by SystemId // [GIVEN] A purchase quote LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Quote, ''); @@ -66,6 +70,8 @@ codeunit 139922 "APIV2 - Purch. Quotes E2E" TargetURL: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase quote with $expand=dimensionSetLines // [GIVEN] A purchase quote PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::Quote); @@ -92,6 +98,8 @@ codeunit 139922 "APIV2 - Purch. Quotes E2E" TargetURL: Text; AttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase quote with $expand=attachments // [GIVEN] A purchase quote PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::Quote); @@ -118,6 +126,8 @@ codeunit 139922 "APIV2 - Purch. Quotes E2E" TargetURL: Text; DocumentAttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase quote with $expand=documentAttachments // [GIVEN] A purchase quote PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::Quote); @@ -144,6 +154,8 @@ codeunit 139922 "APIV2 - Purch. Quotes E2E" TargetURL: Text; PdfDocumentValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase quote with $expand=pdfDocument // [GIVEN] A purchase quote PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::Quote); @@ -170,6 +182,8 @@ codeunit 139922 "APIV2 - Purch. Quotes E2E" TargetURL: Text; LinesValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase quote with $expand=purchaseQuoteLines // [GIVEN] A purchase quote PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::Quote); @@ -194,4 +208,10 @@ codeunit 139922 "APIV2 - Purch. Quotes E2E" exit(TargetURL + '&$expand=' + ExpandValue); exit(TargetURL + '?$expand=' + ExpandValue); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2PurchRcptLinesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PurchRcptLinesE2E.Codeunit.al index c3c96805fb3..fdff569f0f9 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PurchRcptLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PurchRcptLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139850 "APIV2 - Purch. Rcpt. Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -29,6 +30,8 @@ codeunit 139850 "APIV2 - Purch. Rcpt. Lines E2E" LineNo: Integer; SequenceValue: Text; begin + Initialize(); + // [SCENARIO] Call GET on the Line of a purchase receipt // [GIVEN] A receipt with a line. LibraryPurchase.CreatePurchaseOrder(PurchaseHeader); @@ -62,6 +65,8 @@ codeunit 139850 "APIV2 - Purch. Rcpt. Lines E2E" LineNo1: Text; LineNo2: Text; begin + Initialize(); + // [SCENARIO] Call GET on the Lines of a Purchase Receipt // [GIVEN] A purchase receipt with lines. CreatePurchaseOrderMultipleLines(PurchaseHeader); @@ -103,6 +108,8 @@ codeunit 139850 "APIV2 - Purch. Rcpt. Lines E2E" LineNo1: Text; LineNo2: Text; begin + Initialize(); + // [SCENARIO] Call GET on the Lines of a purchase receipt // [GIVEN] A purchase receipt with lines. CreatePurchaseOrderMultipleLines(PurchaseHeader); @@ -136,6 +143,8 @@ codeunit 139850 "APIV2 - Purch. Rcpt. Lines E2E" LineIdTxt: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] Call GET on the Line of a purchase receipt and expand the dimension set lines // [GIVEN] A receipt with a line. PurchRcptHeader.FindFirst(); @@ -250,4 +259,11 @@ codeunit 139850 "APIV2 - Purch. Rcpt. Lines E2E" exit(TargetURL); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2PurchRetOrdersE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PurchRetOrdersE2E.Codeunit.al index a80fc37128d..fb659365ef9 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PurchRetOrdersE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PurchRetOrdersE2E.Codeunit.al @@ -24,6 +24,8 @@ codeunit 139923 "APIV2 - Purch. Ret. Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create a purchase return order and use GET to retrieve it // [GIVEN] A purchase return order LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::"Return Order", ''); @@ -44,6 +46,8 @@ codeunit 139923 "APIV2 - Purch. Ret. Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create a purchase return order and GET it by SystemId // [GIVEN] A purchase return order LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::"Return Order", ''); @@ -66,6 +70,8 @@ codeunit 139923 "APIV2 - Purch. Ret. Orders E2E" TargetURL: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase return order with $expand=dimensionSetLines // [GIVEN] A purchase return order PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::"Return Order"); @@ -92,6 +98,8 @@ codeunit 139923 "APIV2 - Purch. Ret. Orders E2E" TargetURL: Text; AttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase return order with $expand=attachments // [GIVEN] A purchase return order PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::"Return Order"); @@ -118,6 +126,8 @@ codeunit 139923 "APIV2 - Purch. Ret. Orders E2E" TargetURL: Text; DocumentAttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase return order with $expand=documentAttachments // [GIVEN] A purchase return order PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::"Return Order"); @@ -144,6 +154,8 @@ codeunit 139923 "APIV2 - Purch. Ret. Orders E2E" TargetURL: Text; PdfDocumentValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase return order with $expand=pdfDocument // [GIVEN] A purchase return order PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::"Return Order"); @@ -170,6 +182,8 @@ codeunit 139923 "APIV2 - Purch. Ret. Orders E2E" TargetURL: Text; LinesValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase return order with $expand=purchaseReturnOrderLines // [GIVEN] A purchase return order PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::"Return Order"); @@ -194,4 +208,10 @@ codeunit 139923 "APIV2 - Purch. Ret. Orders E2E" exit(TargetURL + '&$expand=' + ExpandValue); exit(TargetURL + '?$expand=' + ExpandValue); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2PurchRetShptsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PurchRetShptsE2E.Codeunit.al index 7df56e4842f..5af2df6ab53 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PurchRetShptsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PurchRetShptsE2E.Codeunit.al @@ -24,6 +24,8 @@ codeunit 139928 "APIV2 - Purch. Ret. Shpts. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Post a purchase return order and use GET to retrieve the resulting return shipment // [GIVEN] A posted purchase return shipment EnsurePurchaseReturnShipment(ReturnShipmentHeader); @@ -44,6 +46,8 @@ codeunit 139928 "APIV2 - Purch. Ret. Shpts. E2E" TargetURL: Text; AttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase return shipment with $expand=attachments // [GIVEN] A posted purchase return shipment EnsurePurchaseReturnShipment(ReturnShipmentHeader); @@ -66,6 +70,8 @@ codeunit 139928 "APIV2 - Purch. Ret. Shpts. E2E" TargetURL: Text; DocumentAttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase return shipment with $expand=documentAttachments // [GIVEN] A posted purchase return shipment EnsurePurchaseReturnShipment(ReturnShipmentHeader); @@ -88,6 +94,8 @@ codeunit 139928 "APIV2 - Purch. Ret. Shpts. E2E" TargetURL: Text; PdfDocumentValue: Text; begin + Initialize(); + // [SCENARIO] GET a purchase return shipment with $expand=pdfDocument // [GIVEN] A posted purchase return shipment EnsurePurchaseReturnShipment(ReturnShipmentHeader); @@ -121,4 +129,10 @@ codeunit 139928 "APIV2 - Purch. Ret. Shpts. E2E" exit(TargetURL + '&$expand=' + ExpandValue); exit(TargetURL + '?$expand=' + ExpandValue); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2PurchaseInvLinesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PurchaseInvLinesE2E.Codeunit.al index 0dbd34f7443..c4a7bdfab0a 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PurchaseInvLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PurchaseInvLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139838 "APIV2 - Purchase Inv Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -30,9 +31,14 @@ codeunit 139838 "APIV2 - Purchase Inv Lines E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); LibraryERMCountryData.CreateGeneralPostingSetupData(); LibraryERMCountryData.UpdateGeneralPostingSetup(); @@ -1145,6 +1151,5 @@ codeunit 139838 "APIV2 - Purchase Inv Lines E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2PurchaseInvoicesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PurchaseInvoicesE2E.Codeunit.al index 2477d02b2ad..d3fbc749306 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PurchaseInvoicesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PurchaseInvoicesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139829 "APIV2 - Purchase Invoices E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -30,6 +31,10 @@ codeunit 139829 "APIV2 - Purchase Invoices E2E" local procedure Initialize() begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + + LibraryGraphMgt.SetLicenseSafeWorkDate(); end; [Test] @@ -75,6 +80,8 @@ codeunit 139829 "APIV2 - Purchase Invoices E2E" OrderIdValue: Text; OrderNoValue: Text; begin + Initialize(); + // [SCENARIO] Create a Purchase Invoice from a Purchase Order and use GET method to retrieve them and check the orderId and orderNumber // [GIVEN] A purchase invoice created by posting a purchase order LibraryPurchase.CreatePurchaseOrder(PurchaseHeader); @@ -444,6 +451,8 @@ codeunit 139829 "APIV2 - Purchase Invoices E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can map systemId with Draft Invoice SystemId // [GIVEN] Posted purchase invoice exists diff --git a/src/Apps/W1/APIV2/test/src/APIV2PurchaseOrdersE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PurchaseOrdersE2E.Codeunit.al index 1b799612203..d074fb583c0 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PurchaseOrdersE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PurchaseOrdersE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139851 "APIV2 - Purchase Orders E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -31,7 +32,10 @@ codeunit 139851 "APIV2 - Purchase Orders E2E" local procedure Initialize() begin - WorkDate := Today(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + + LibraryGraphMgt.SetLicenseSafeWorkDate(); end; [Test] @@ -92,8 +96,8 @@ codeunit 139851 "APIV2 - Purchase Orders E2E" ShipToVendor.Modify(true); Commit(); VendorNo := BuyFromVendor."No."; - OrderDate := Today(); - PostingDate := Today(); + OrderDate := WorkDate(); + PostingDate := WorkDate(); // [GIVEN] a JSON text with an order that contains the vendor and an address OrderJSON := CreateOrderJSONWithAddress(BuyFromVendor, PayToVendor, ShipToVendor, OrderDate, PostingDate); @@ -143,8 +147,8 @@ codeunit 139851 "APIV2 - Purchase Orders E2E" LibraryPurchase.CreateVendorWithAddress(BuyFromVendor); LibraryPurchase.CreateVendorWithAddress(PayToVendor); VendorNo := BuyFromVendor."No."; - OrderDate := Today(); - PostingDate := Today(); + OrderDate := WorkDate(); + PostingDate := WorkDate(); // [GIVEN] a JSON text with an order that contains the vendor and an address OrderJSON := CreateOrderJSONWithoutShipTo(BuyFromVendor, PayToVendor, OrderDate, PostingDate); @@ -402,8 +406,8 @@ codeunit 139851 "APIV2 - Purchase Orders E2E" // [GIVEN] a customer LibraryPurchase.CreateVendor(Vendor); VendorNo := Vendor."No."; - OrderDate := Today(); - PostingDate := Today(); + OrderDate := WorkDate(); + PostingDate := WorkDate(); // [GIVEN] a json describing our new order OrderJSON := CreateOrderJSONWithAddress(Vendor, Vendor, Vendor, OrderDate, PostingDate); @@ -603,6 +607,8 @@ codeunit 139851 "APIV2 - Purchase Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can recieve and invoice a purchase order through the API. // [GIVEN] Create vendors and a Purchase order with lines @@ -667,6 +673,7 @@ codeunit 139851 "APIV2 - Purchase Orders E2E" PurchaseOrder."Buy-from Vendor No.".SetValue(Vendor."No."); PurchaseOrder."Document Date".SetValue(DocumentDate); PurchaseOrder."Posting Date".SetValue(PostingDate); + PurchaseOrder."Order Date".SetValue(DocumentDate); end; local procedure CheckShippingDetailsNotEmpty(var PurchaseHeader: Record "Purchase Header") diff --git a/src/Apps/W1/APIV2/test/src/APIV2PurchaseReceiptsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2PurchaseReceiptsE2E.Codeunit.al index da2b96c946c..9e0f0eb4f20 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2PurchaseReceiptsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2PurchaseReceiptsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139849 "APIV2 - Purchase Receipts E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -26,6 +27,8 @@ codeunit 139849 "APIV2 - Purchase Receipts E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create 2 purchase receipts (post sales order) and use a GET method to retrieve them // [GIVEN] 2 posted purchase receipts LibraryPurchase.CreatePurchaseOrder(PurchaseHeader); @@ -56,6 +59,8 @@ codeunit 139849 "APIV2 - Purchase Receipts E2E" ReceiptIdTxt: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] Call GET on purchase receipt and expand the dimension set lines // [GIVEN] A purchase receipt. @@ -106,4 +111,11 @@ codeunit 139849 "APIV2 - Purchase Receipts E2E" exit(TargetURL); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2RetEarningsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2RetEarningsE2E.Codeunit.al index 771bc75dd2f..2e013840315 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2RetEarningsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2RetEarningsE2E.Codeunit.al @@ -62,9 +62,14 @@ codeunit 139821 "APIV2 - Ret. Earnings E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := true; end; @@ -93,4 +98,3 @@ codeunit 139821 "APIV2 - Ret. Earnings E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalesCrMemoLinesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalesCrMemoLinesE2E.Codeunit.al index 203a1e04eb3..97f5259ebc3 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalesCrMemoLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalesCrMemoLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139837 "APIV2 - Sales CrMemo Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -30,9 +31,14 @@ codeunit 139837 "APIV2 - Sales CrMemo Lines E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibrarySales.SetStockoutWarning(false); LibraryApplicationArea.EnableFoundationSetup(); @@ -1413,8 +1419,6 @@ codeunit 139837 "APIV2 - Sales CrMemo Lines E2E" - - diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalesCreditMemosE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalesCreditMemosE2E.Codeunit.al index 91631a22534..e37ba185768 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalesCreditMemosE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalesCreditMemosE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -56,6 +57,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create posted and unposted sales credit memos and use a GET method to retrieve them // [GIVEN] 2 credit memos, one posted and one unposted CreateSalesCreditMemos(CreditMemoNo1, CreditMemoNo2); @@ -88,9 +91,12 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" TargetURL: Text; CreditMemo: Text; begin + Initialize(); + // [SCENARIO] Create posted and unposted Sales credit memos and use HTTP POST to delete them // [GIVEN] 2 credit memos, one posted and one unposted + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibrarySales.CreateCustomerWithAddress(SellToCustomer); LibrarySales.CreateCustomerWithAddress(BillToCustomer); CustomerNo := SellToCustomer."No."; @@ -133,6 +139,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" CreditMemoJSON: Text; CurrencyCode: Code[10]; begin + Initialize(); + // [SCENARIO] Create posted and unposted with specific currency set and use HTTP POST to create them // [GIVEN] an CreditMemo with a non-LCY currencyCode set @@ -165,18 +173,24 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" [Test] procedure TestModifyCreditMemos() begin + Initialize(); + TestMultipleModifyCreditMemos(false, false); end; [Test] procedure TestEmptyModifyCreditMemos() begin + Initialize(); + TestMultipleModifyCreditMemos(true, false); end; [Test] procedure TestPartialModifyCreditMemos() begin + Initialize(); + TestMultipleModifyCreditMemos(false, true); end; @@ -252,6 +266,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" CreditMemoNo: Text; CreditMemoID: Text; begin + Initialize(); + // [SCENARIO] Create unposted sales credit memo and use HTTP DELETE to delete it // [GIVEN] An unposted credit memo CreateDraftSalesCreditMemo(SalesHeader); @@ -283,9 +299,12 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" CreditMemoPostingDate: Date; CreditMemoJSON: Text; begin + Initialize(); + // [SCENARIO] Create an credit memo both through the client UI and through the API // [SCENARIO] and compare them. They should be the same and have the same fields autocompleted wherever needed. // [GIVEN] An unposted credit memo + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibraryGraphDocumentTools.InitializeUIPage(); LibrarySales.CreateCustomer(SellToCustomer); @@ -316,6 +335,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" TargetURL: Text; DiscountPct: Decimal; begin + Initialize(); + // [SCENARIO] When an credit memo is created,the GET Method should update the credit memo and assign a total // [GIVEN] 2 credit memos, one posted and one unposted without totals assigned LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending( @@ -348,6 +369,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" DiscountAmt: Decimal; InvDiscAmount: Decimal; begin + Initialize(); + // [SCENARIO] When an credit memo is created, the GET Method should update the credit memo and assign a total // [GIVEN] 2 credit memos, one posted and one unposted with discount amount that should be redistributed LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending( @@ -387,6 +410,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" ResponseText: Text; CreditMemoID: Text; begin + Initialize(); + // [SCENARIO 184721] Create Credit Memo, use a PATCH method to change it and then verify the changes LibrarySales.CreateCustomerWithAddress(Customer); @@ -433,6 +458,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" ResponseText: Text; CreditMemoID: Text; begin + Initialize(); + // [SCENARIO 184721] Clearing manually set discount // [GIVEN] an item with unit price and unit cost @@ -488,6 +515,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" PostedCreditMemoEmailAddress: Text; PostedCreditMemoEmailSubject: Text; begin + Initialize(); + // [SCENARIO] User can post a sales credit memo through the API. // [GIVEN] Draft sales credit memo exists @@ -535,6 +564,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can post and send a sales credit memo through the API. InitializeForSending(); @@ -573,6 +604,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can cancel a posted sales credit memo through API. // [GIVEN] Non-corrective sales credit memo exists @@ -601,6 +634,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can cancel a posted sales credit memo through API. // [GIVEN] Corrective sales credit memo exists @@ -633,6 +668,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can cancel a posted sales credit memo through API. InitializeForSending(); @@ -669,6 +706,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can send a posted sales credit memo through the API. InitializeForSending(); @@ -701,6 +740,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Sending a draft sales credit memo through the API throws an error InitializeForSending(); @@ -730,6 +771,8 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can send a draft sales credit memo through the API. InitializeForSending(); @@ -1043,5 +1086,11 @@ codeunit 139828 "APIV2 - Sales Credit Memos E2E" while JobQueueEntry.FindJobQueueEntry(JobQueueEntry."Object Type to Run"::Codeunit, CodeunitID) do JobQueueEntry.Cancel(); end; -} + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; +} diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalesInvLinesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalesInvLinesE2E.Codeunit.al index 3a88184d347..395be869da7 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalesInvLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalesInvLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139834 "APIV2 - Sales Inv. Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -31,9 +32,14 @@ codeunit 139834 "APIV2 - Sales Inv. Lines E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibrarySales.SetStockoutWarning(false); LibraryApplicationArea.EnableFoundationSetup(); diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalesInvoicesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalesInvoicesE2E.Codeunit.al index 9438dd8d7fc..d3f4600a9e6 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalesInvoicesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalesInvoicesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -70,6 +71,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO 184721] Create posted and unposted Sales invoices and use a GET method to retrieve them // [GIVEN] 2 invoices, one posted and one unposted CreateSalesInvoices(InvoiceID1, InvoiceID2); @@ -102,6 +105,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" OrderIdValue: Text; OrderNoValue: Text; begin + Initialize(); + // [SCENARIO] Create a Sales Invoice from a Sales Order and use GET method to retrieve them and check the orderId and orderNumber // [GIVEN] A sales invoice created by posting a sales order LibrarySales.CreateSalesOrder(SalesHeader); @@ -145,9 +150,12 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" TargetURL: Text; InvoiceJSON: Text; begin + Initialize(); + // [SCENARIO 184721] Create posted and unposted Sales invoices and use HTTP POST to delete them // [GIVEN] 2 invoices, one posted and one unposted + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibrarySales.CreateCustomer(SellToCustomer); LibrarySales.CreateCustomer(BillToCustomer); LibrarySales.CreateCustomer(ShipToCustomer); @@ -195,6 +203,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" InvoiceJSON: Text; CurrencyCode: Code[10]; begin + Initialize(); + // [SCENARIO 184721] Create posted and unposted with specific currency set and use HTTP POST to create them // [GIVEN] an Invoice with a non-LCY currencyCode set @@ -242,6 +252,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" Email: Text; CurrencyCode: Code[10]; begin + Initialize(); + // [SCENARIO 285872] Create posted and unposted with specific email set and use HTTP POST to create them Email := 'test@microsoft.com'; // [GIVEN] an Customer with no email set @@ -293,8 +305,12 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" TargetURL: Text; InvoiceJSON: Text; begin + Initialize(); + // [SCENARIO 184721] Create unposted with specific document and due date set and use HTTP POST to create them + LibraryGraphMgt.SetLicenseSafeWorkDate(); + // [GIVEN] an Invoice with a document and due date set LibrarySales.CreateCustomer(Customer); CustomerNo := Customer."No."; @@ -333,18 +349,24 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" [Test] procedure TestModifyInvoices() begin + Initialize(); + TestMultipleModifyInvoices(false, false); end; [Test] procedure TestEmptyModifyInvoices() begin + Initialize(); + TestMultipleModifyInvoices(true, false); end; [Test] procedure TestPartialModifyInvoices() begin + Initialize(); + TestMultipleModifyInvoices(false, true); end; @@ -436,6 +458,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" TargetURL: Text; InvoiceWithBlanksJSON: Text; begin + Initialize(); + // [SCENARIO 184721] Create Sales Invoice with all the Ids filled, use a PATCH method to blank the Ids and the Codes LibrarySales.CreateCustomerWithAddress(Customer); @@ -500,6 +524,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" NewInvoiceNumber: Text; NewInvoiceNumberJSON: Text; begin + Initialize(); + // [SCENARIO 184721] Create draft invoice and issue a patch request to change the number // [GIVEN] 1 draft invoice and a json with a new number LibrarySales.CreateSalesInvoice(SalesHeader); @@ -522,6 +548,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO 184721] Create unposted Sales invoice and use HTTP DELETE to delete it // [GIVEN] An unposted invoice CreateDraftSalesInvoice(SalesHeader); @@ -559,9 +587,12 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" TargetURL: Text; InvoiceJSON: Text; begin + Initialize(); + // [SCENARIO 184721] Create an invoice both through the client UI and through the API // [SCENARIO] and compare them. They should be the same and have the same fields autocompleted wherever needed. // [GIVEN] An unposted invoice + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibraryGraphDocumentTools.InitializeUIPage(); LibraryApplicationArea.DisableApplicationAreaSetup(); @@ -621,6 +652,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" TargetURL: Text; DiscountPct: Decimal; begin + Initialize(); + // [SCENARIO 184721] When an invoice is created,the GET Method should update the invoice and assign a total // [GIVEN] 2 invoices, one posted and one unposted without totals assigned LibraryApplicationArea.EnableFoundationSetup(); @@ -654,6 +687,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" DiscountAmt: Decimal; InvDiscAmount: Decimal; begin + Initialize(); + // [SCENARIO 184721] When an invoice is created, the GET Method should update the invoice and assign a total // [GIVEN] 2 invoices, one posted and one unposted with discount amount that should be redistributed LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending( @@ -689,6 +724,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" InvoiceJSON: Text; CurrencyCode: Code[10]; begin + Initialize(); + // [SCENARIO 184721] Create an invoice wihtout Customer throws an error // [GIVEN] a sales invoice JSON with currency only @@ -717,6 +754,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" ResponseText: Text; InvoiceID: Text; begin + Initialize(); + // [SCENARIO 184721] Create Sales Invoice, use a PATCH method to change it and then verify the changes LibrarySales.CreateCustomerWithAddress(Customer); @@ -763,6 +802,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" ResponseText: Text; InvoiceID: Text; begin + Initialize(); + // [SCENARIO 184721] Clearing manually set discount // [GIVEN] an item with unit price and unit cost @@ -819,6 +860,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" PostedInvoiceEmailAddress: Text; PostedInvoiceEmailSubject: Text; begin + Initialize(); + // [SCENARIO] User can post a sales invoice through the API. // [GIVEN] Draft sales invoice exists @@ -865,6 +908,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can post and send a sales invoice through the API. InitializeForSending(); @@ -903,6 +948,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can cancel a posted sales invoice through API. // [GIVEN] Posted sales invoice exists @@ -939,6 +986,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can cancel a posted sales invoice through API. InitializeForSending(); @@ -979,6 +1028,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can send a posted sales invoice through the API. InitializeForSending(); @@ -1011,6 +1062,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can send a draft sales invoice through the API. InitializeForSending(); @@ -1043,6 +1096,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can send a draft sales invoice through the API. InitializeForSending(); @@ -1083,6 +1138,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" CreditMemoEmailAddress: Text; CreditMemoEmailSubject: Text; begin + Initialize(); + // [SCENARIO] User can create a corrective credit memo for the posted sales invoice through the API. // [GIVEN] A posted sales invoice exists @@ -1127,6 +1184,8 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can map systemId with Draft Invoice SystemId // [GIVEN] Posted sales invoice exists @@ -1380,4 +1439,11 @@ codeunit 139809 "APIV2 - Sales Invoices E2E" LibraryGraphMgt.VerifyIDInJson(ResponseText); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalesOrdArchE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalesOrdArchE2E.Codeunit.al index 43a0cf41433..fff3228995e 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalesOrdArchE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalesOrdArchE2E.Codeunit.al @@ -25,6 +25,8 @@ codeunit 139927 "APIV2 - Sales Ord. Arch. E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Archive a sales order and use GET to retrieve the archive // [GIVEN] An archived sales order EnsureSalesOrderArchive(SalesHeaderArchive); @@ -45,6 +47,8 @@ codeunit 139927 "APIV2 - Sales Ord. Arch. E2E" TargetURL: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] GET a sales order archive with $expand=dimensionSetLines // [GIVEN] An archived sales order EnsureSalesOrderArchive(SalesHeaderArchive); @@ -67,6 +71,8 @@ codeunit 139927 "APIV2 - Sales Ord. Arch. E2E" TargetURL: Text; AttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a sales order archive with $expand=attachments // [GIVEN] An archived sales order EnsureSalesOrderArchive(SalesHeaderArchive); @@ -89,6 +95,8 @@ codeunit 139927 "APIV2 - Sales Ord. Arch. E2E" TargetURL: Text; DocumentAttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a sales order archive with $expand=documentAttachments // [GIVEN] An archived sales order EnsureSalesOrderArchive(SalesHeaderArchive); @@ -121,4 +129,10 @@ codeunit 139927 "APIV2 - Sales Ord. Arch. E2E" exit(TargetURL + '&$expand=' + ExpandValue); exit(TargetURL + '?$expand=' + ExpandValue); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalesOrderLinesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalesOrderLinesE2E.Codeunit.al index 280882e872d..568adfee40d 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalesOrderLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalesOrderLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139835 "APIV2 - Sales Order Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -30,9 +31,14 @@ codeunit 139835 "APIV2 - Sales Order Lines E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibrarySales.SetStockoutWarning(false); LibraryApplicationArea.EnableFoundationSetup(); @@ -1477,9 +1483,6 @@ codeunit 139835 "APIV2 - Sales Order Lines E2E" - - - diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalesOrdersE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalesOrdersE2E.Codeunit.al index 16759bdf1b6..76098510107 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalesOrdersE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalesOrdersE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139811 "APIV2 - Sales Orders E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -38,6 +39,8 @@ codeunit 139811 "APIV2 - Sales Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO 184721] Create Sales Orders and use a GET method to retrieve them // [GIVEN] 2 orders in the table @@ -76,8 +79,12 @@ codeunit 139811 "APIV2 - Sales Orders E2E" TargetURL: Text; OrderJSON: Text; begin + Initialize(); + // [SCENARIO 184721] Create sales orders JSON and use HTTP POST to create them + LibraryGraphMgt.SetLicenseSafeWorkDate(); + // [GIVEN] a customer LibrarySales.CreateCustomerWithAddress(SellToCustomer); LibrarySales.CreateCustomerWithAddress(BillToCustomer); @@ -125,6 +132,8 @@ codeunit 139811 "APIV2 - Sales Orders E2E" TargetURL: Text; OrderJSON: Text; begin + Initialize(); + // [SCENARIO 184721] Create sales order for customer with location and use HTTP POST to create it // [GIVEN] an order with customer with location code @@ -168,6 +177,8 @@ codeunit 139811 "APIV2 - Sales Orders E2E" OrderJSON: Text; CurrencyCode: Code[10]; begin + Initialize(); + // [SCENARIO 184721] Create sales order with specific currency set and use HTTP POST to create it // [GIVEN] an order with a non-LCY currencyCode set @@ -203,18 +214,24 @@ codeunit 139811 "APIV2 - Sales Orders E2E" [Test] procedure TestModifyOrders() begin + Initialize(); + TestMultipleModifyOrders(false, false); end; [Test] procedure TestEmptyModifyOrders() begin + Initialize(); + TestMultipleModifyOrders(true, false); end; [Test] procedure TestPartialModifyOrders() begin + Initialize(); + TestMultipleModifyOrders(false, true); end; @@ -283,6 +300,8 @@ codeunit 139811 "APIV2 - Sales Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO 184721] Create sales orders and use HTTP DELETE to delete them // [GIVEN] 2 orders in the table @@ -329,7 +348,10 @@ codeunit 139811 "APIV2 - Sales Orders E2E" TargetURL: Text; OrderJSON: Text; begin + Initialize(); + // [SCENARIO 184721] Create an order both through the client UI and through the API and compare them. They should be the same and have the same fields autocompleted wherever needed. + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibraryGraphDocumentTools.InitializeUIPage(); // [GIVEN] a customer @@ -388,6 +410,8 @@ codeunit 139811 "APIV2 - Sales Orders E2E" TargetURL: Text; DiscountPct: Decimal; begin + Initialize(); + // [SCENARIO 184721] When an order is created, the GET Method should update the order and assign a total // [GIVEN] an order without totals assigned LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending(SalesHeader, DiscountPct, SalesHeader."Document Type"::Order); @@ -417,6 +441,8 @@ codeunit 139811 "APIV2 - Sales Orders E2E" DiscountAmt: Decimal; InvDiscAmount: Decimal; begin + Initialize(); + // [SCENARIO 184721] When an order is created, the GET Method should update the order and redistribute the discount amount // [GIVEN] an order with discount amount that should be redistributed LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending(SalesHeader, DiscountPct, SalesHeader."Document Type"::Order); @@ -451,6 +477,8 @@ codeunit 139811 "APIV2 - Sales Orders E2E" OrderNo: Text; OrderId: Guid; begin + Initialize(); + // [SCENARIO 184721] Create Sales Order, use a PATCH method to change it and then verify the changes // [GIVEN] an order with lines @@ -487,6 +515,8 @@ codeunit 139811 "APIV2 - Sales Orders E2E" ResponseText: Text; OrderNo: Text; begin + Initialize(); + // [SCENARIO 184721] Clearing manually set discount // [GIVEN] an order @@ -523,6 +553,8 @@ codeunit 139811 "APIV2 - Sales Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can ship and invoice a sales order through the API. // [GIVEN] a sales order with lines @@ -644,4 +676,11 @@ codeunit 139811 "APIV2 - Sales Orders E2E" LibraryGraphMgt.VerifyIDInJson(ResponseText); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalesQuoteLinesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalesQuoteLinesE2E.Codeunit.al index 803ae25520c..066bcc8c8e2 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalesQuoteLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalesQuoteLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139836 "APIV2 - Sales Quote Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -28,9 +29,14 @@ codeunit 139836 "APIV2 - Sales Quote Lines E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibrarySales.SetStockoutWarning(false); LibraryApplicationArea.EnableFoundationSetup(); @@ -998,6 +1004,5 @@ codeunit 139836 "APIV2 - Sales Quote Lines E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalesQuotesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalesQuotesE2E.Codeunit.al index e934a55669a..5d000a0d84f 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalesQuotesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalesQuotesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -60,6 +61,8 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create Sales Quotes and use a GET method to retrieve them // [GIVEN] 2 quotes in the table @@ -98,8 +101,12 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" QuoteJSON: Text; QuoteExists: Boolean; begin + Initialize(); + // [SCENARIO] Create sales quotes JSON and use HTTP POST to create them + LibraryGraphMgt.SetLicenseSafeWorkDate(); + // [GIVEN] a customer LibrarySales.CreateCustomerWithAddress(SellToCustomer); LibrarySales.CreateCustomerWithAddress(ShipToCustomer); @@ -143,6 +150,8 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" CurrencyCode: Code[10]; QuoteExists: Boolean; begin + Initialize(); + // [SCENARIO] Create sales quote with specific currency set and use HTTP POST to create it // [GIVEN] a quote with a non-LCY currencyCode set @@ -174,18 +183,24 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" [Test] procedure TestModifyQuotes() begin + Initialize(); + TestMultipleModifyQuotes(false, false); end; [Test] procedure TestEmptyModifyQuotes() begin + Initialize(); + TestMultipleModifyQuotes(true, false); end; [Test] procedure TestPartialModifyQuotes() begin + Initialize(); + TestMultipleModifyQuotes(false, true); end; @@ -269,6 +284,8 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create sales quotes and use HTTP DELETE to delete them // [GIVEN] 2 quotes in the table @@ -315,7 +332,10 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" QuoteJSON: Text; QuoteExists: Boolean; begin + Initialize(); + // [SCENARIO] Create a quote both through the client UI and through the API and compare them. They should be the same and have the same fields autocompleted wherever needed. + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibraryGraphDocumentTools.InitializeUIPage(); // [GIVEN] a customer @@ -369,6 +389,8 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" TargetURL: Text; DiscountPct: Decimal; begin + Initialize(); + // [SCENARIO] When a quote is created, the GET Method should update the quote and assign a total // [GIVEN] a quote without totals assigned LibraryGraphDocumentTools.CreateDocumentWithDiscountPctPending(SalesHeader, DiscountPct, SalesHeader."Document Type"::Quote); @@ -398,6 +420,8 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" DiscountAmt: Decimal; InvDiscAmount: Decimal; begin + Initialize(); + // [SCENARIO] When a quote is created, the GET Method should update the quote and redistribute the discount amount // [GIVEN] a quote with discount amount that should be redistributed @@ -435,6 +459,8 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" ResponseText: Text; QuoteID: Text; begin + Initialize(); + // [SCENARIO 184721] Create Sales Quote, use a PATCH method to change it and then verify the changes LibrarySales.CreateCustomerWithAddress(Customer); @@ -481,6 +507,8 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" ResponseText: Text; QuoteID: Text; begin + Initialize(); + // [SCENARIO 184721] Clearing manually set discount // [GIVEN] an item with unit price and unit cost @@ -529,6 +557,8 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] User can send a sales quote through the API. InitializeForSending(); @@ -572,6 +602,8 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" InvoiceEmailAddress: Text; InvoiceEmailSubject: Text; begin + Initialize(); + // [SCENARIO] User can convert a sales quote to a sales invoice through the API. // [GIVEN] Sales quote exists @@ -630,6 +662,8 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" OrderEmailAddress: Text; OrderEmailSubject: Text; begin + Initialize(); + // [SCENARIO] User can convert a sales quote to a sales order through the API. // [GIVEN] Sales quote exists @@ -847,4 +881,11 @@ codeunit 139823 "APIV2 - Sales Quotes E2E" end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalesRetOrdersE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalesRetOrdersE2E.Codeunit.al index c049e3837f6..fb3e557f46e 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalesRetOrdersE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalesRetOrdersE2E.Codeunit.al @@ -24,6 +24,8 @@ codeunit 139924 "APIV2 - Sales Ret. Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create a sales return order and use GET to retrieve it // [GIVEN] A sales return order LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::"Return Order", ''); @@ -44,6 +46,8 @@ codeunit 139924 "APIV2 - Sales Ret. Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create a sales return order and GET it by SystemId // [GIVEN] A sales return order LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::"Return Order", ''); @@ -66,6 +70,8 @@ codeunit 139924 "APIV2 - Sales Ret. Orders E2E" TargetURL: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] GET a sales return order with $expand=dimensionSetLines // [GIVEN] A sales return order SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Return Order"); @@ -92,6 +98,8 @@ codeunit 139924 "APIV2 - Sales Ret. Orders E2E" TargetURL: Text; AttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a sales return order with $expand=attachments // [GIVEN] A sales return order SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Return Order"); @@ -118,6 +126,8 @@ codeunit 139924 "APIV2 - Sales Ret. Orders E2E" TargetURL: Text; DocumentAttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a sales return order with $expand=documentAttachments // [GIVEN] A sales return order SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Return Order"); @@ -144,6 +154,8 @@ codeunit 139924 "APIV2 - Sales Ret. Orders E2E" TargetURL: Text; PdfDocumentValue: Text; begin + Initialize(); + // [SCENARIO] GET a sales return order with $expand=pdfDocument // [GIVEN] A sales return order SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Return Order"); @@ -170,6 +182,8 @@ codeunit 139924 "APIV2 - Sales Ret. Orders E2E" TargetURL: Text; LinesValue: Text; begin + Initialize(); + // [SCENARIO] GET a sales return order with $expand=salesReturnOrderLines // [GIVEN] A sales return order SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::"Return Order"); @@ -194,4 +208,10 @@ codeunit 139924 "APIV2 - Sales Ret. Orders E2E" exit(TargetURL + '&$expand=' + ExpandValue); exit(TargetURL + '?$expand=' + ExpandValue); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalesShipLinesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalesShipLinesE2E.Codeunit.al index 3081e6c6df6..343ee42123d 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalesShipLinesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalesShipLinesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139848 "APIV2 - Sales Ship. Lines E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -29,6 +30,8 @@ codeunit 139848 "APIV2 - Sales Ship. Lines E2E" LineNo: Integer; SequenceValue: Text; begin + Initialize(); + // [SCENARIO] Call GET on the Line of a sales shipment // [GIVEN] A shipment with a line. @@ -63,6 +66,8 @@ codeunit 139848 "APIV2 - Sales Ship. Lines E2E" LineNo1: Text; LineNo2: Text; begin + Initialize(); + // [SCENARIO] Call GET on the Lines of Sales Shipment // [GIVEN] An sales shipment with lines. @@ -105,6 +110,8 @@ codeunit 139848 "APIV2 - Sales Ship. Lines E2E" LineNo1: Text; LineNo2: Text; begin + Initialize(); + // [SCENARIO] Call GET on the Lines of a sales shipment // [GIVEN] An sales shipment with lines. @@ -139,6 +146,8 @@ codeunit 139848 "APIV2 - Sales Ship. Lines E2E" LineIdTxt: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] Call GET on the Line of a sales shipment and expand the dimension set lines // [GIVEN] A shipment with a line. @@ -255,4 +264,11 @@ codeunit 139848 "APIV2 - Sales Ship. Lines E2E" exit(TargetURL); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalesShipmentsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalesShipmentsE2E.Codeunit.al index 230e2af2a5b..e451cc22429 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalesShipmentsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalesShipmentsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139847 "APIV2 - Sales Shipments E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -26,6 +27,8 @@ codeunit 139847 "APIV2 - Sales Shipments E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create 2 sales shipments (post sales order) and use a GET method to retrieve them // [GIVEN] 2 posted sales shipments @@ -57,6 +60,8 @@ codeunit 139847 "APIV2 - Sales Shipments E2E" ShipmentIdTxt: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] Call GET on a sales shipment and expand the dimension set lines // [GIVEN] A sales shipment. @@ -108,4 +113,11 @@ codeunit 139847 "APIV2 - Sales Shipments E2E" exit(TargetURL); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2SalespersonPurchE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2SalespersonPurchE2E.Codeunit.al index 00b9b57cfd8..cfcf542c9fd 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2SalespersonPurchE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2SalespersonPurchE2E.Codeunit.al @@ -14,9 +14,14 @@ codeunit 139882 "APIV2 - Salesperson/Purch E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2ShipmentMethodsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2ShipmentMethodsE2E.Codeunit.al index b7f25bf36dd..745c9b7b301 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2ShipmentMethodsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2ShipmentMethodsE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139805 "APIV2 - Shipment Methods E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -21,9 +22,14 @@ codeunit 139805 "APIV2 - Shipment Methods E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2TaxAreasE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2TaxAreasE2E.Codeunit.al index 3e579576ff6..f56aea0d5ec 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2TaxAreasE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2TaxAreasE2E.Codeunit.al @@ -24,9 +24,14 @@ codeunit 139826 "APIV2 - Tax Areas E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; @@ -288,4 +293,3 @@ codeunit 139826 "APIV2 - Tax Areas E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2TaxGroupsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2TaxGroupsE2E.Codeunit.al index 71058576f58..426f57009bb 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2TaxGroupsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2TaxGroupsE2E.Codeunit.al @@ -23,9 +23,14 @@ codeunit 139808 "APIV2 - Tax Groups E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; @@ -288,4 +293,3 @@ codeunit 139808 "APIV2 - Tax Groups E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2TransferOrdersE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2TransferOrdersE2E.Codeunit.al index 93bda0b6eed..feee4b30a17 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2TransferOrdersE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2TransferOrdersE2E.Codeunit.al @@ -27,6 +27,8 @@ codeunit 139926 "APIV2 - Transfer Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create a transfer order and use GET to retrieve it // [GIVEN] A transfer order LibraryWarehouse.CreateTransferLocations(FromLocation, ToLocation, InTransitLocation); @@ -51,6 +53,8 @@ codeunit 139926 "APIV2 - Transfer Orders E2E" ResponseText: Text; TargetURL: Text; begin + Initialize(); + // [SCENARIO] Create a transfer order and GET it by SystemId // [GIVEN] A transfer order LibraryWarehouse.CreateTransferLocations(FromLocation, ToLocation, InTransitLocation); @@ -77,6 +81,8 @@ codeunit 139926 "APIV2 - Transfer Orders E2E" TargetURL: Text; DimensionSetValue: Text; begin + Initialize(); + // [SCENARIO] GET a transfer order with $expand=dimensionSetLines // [GIVEN] A transfer order if not TransferHeader.FindFirst() then begin @@ -106,6 +112,8 @@ codeunit 139926 "APIV2 - Transfer Orders E2E" TargetURL: Text; AttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a transfer order with $expand=attachments // [GIVEN] A transfer order if not TransferHeader.FindFirst() then begin @@ -135,6 +143,8 @@ codeunit 139926 "APIV2 - Transfer Orders E2E" TargetURL: Text; DocumentAttachmentsValue: Text; begin + Initialize(); + // [SCENARIO] GET a transfer order with $expand=documentAttachments // [GIVEN] A transfer order if not TransferHeader.FindFirst() then begin @@ -164,6 +174,8 @@ codeunit 139926 "APIV2 - Transfer Orders E2E" TargetURL: Text; PdfDocumentValue: Text; begin + Initialize(); + // [SCENARIO] GET a transfer order with $expand=pdfDocument // [GIVEN] A transfer order if not TransferHeader.FindFirst() then begin @@ -193,6 +205,8 @@ codeunit 139926 "APIV2 - Transfer Orders E2E" TargetURL: Text; LinesValue: Text; begin + Initialize(); + // [SCENARIO] GET a transfer order with $expand=transferOrderLines // [GIVEN] A transfer order if not TransferHeader.FindFirst() then begin @@ -217,4 +231,10 @@ codeunit 139926 "APIV2 - Transfer Orders E2E" exit(TargetURL + '&$expand=' + ExpandValue); exit(TargetURL + '?$expand=' + ExpandValue); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } diff --git a/src/Apps/W1/APIV2/test/src/APIV2TrialBalanceE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2TrialBalanceE2E.Codeunit.al index f94f316fc30..bd66781f754 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2TrialBalanceE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2TrialBalanceE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139818 "APIV2 - Trial Balance E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; trigger OnRun() @@ -60,9 +61,14 @@ codeunit 139818 "APIV2 - Trial Balance E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryApplicationArea.EnableFoundationSetup(); IsInitialized := true; end; @@ -85,4 +91,3 @@ codeunit 139818 "APIV2 - Trial Balance E2E" - diff --git a/src/Apps/W1/APIV2/test/src/APIV2UofME2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2UofME2E.Codeunit.al index 496d746235c..cad1e0faa0b 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2UofME2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2UofME2E.Codeunit.al @@ -24,6 +24,8 @@ codeunit 139812 "APIV2 - UofM E2E" UnitOfMeasure: Record "Unit of Measure"; Response: Text; begin + Initialize(); + // [SCENARIO] User can get the units of measure. // [GIVEN] Units of measure. @@ -44,6 +46,8 @@ codeunit 139812 "APIV2 - UofM E2E" Response: Text; OldDescription: Text; begin + Initialize(); + // [SCENARIO] User can update a unit of measure. // [GIVEN] Units of measure. @@ -73,6 +77,8 @@ codeunit 139812 "APIV2 - UofM E2E" UnitOfMeasure: Record "Unit of Measure"; Response: Text; begin + Initialize(); + // [SCENARIO] User can delete a unit of measure. // [GIVEN] Units of measure. @@ -94,6 +100,8 @@ codeunit 139812 "APIV2 - UofM E2E" Request: Text; "Code": Text; begin + Initialize(); + // [SCENARIO] User can insert a unit of measure. // [GIVEN] A new unit of measure. @@ -186,9 +194,14 @@ codeunit 139812 "APIV2 - UofM E2E" Format(UnitOfMeasure."International Standard Code"), InternationalStandardCode, StrSubstNo(WrongPropertyValueErr, 'internationalStandardCode')); end; -} - + local procedure Initialize() + begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; +} diff --git a/src/Apps/W1/APIV2/test/src/APIV2VendorPaymentsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2VendorPaymentsE2E.Codeunit.al index 0fc02556e63..b888df57ff4 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2VendorPaymentsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2VendorPaymentsE2E.Codeunit.al @@ -1,6 +1,7 @@ codeunit 139843 "APIV2 - Vendor Payments E2E" { Subtype = Test; + RequiredTestIsolation = Disabled; TestType = Uncategorized; TestPermissions = Disabled; @@ -884,10 +885,14 @@ codeunit 139843 "APIV2 - Vendor Payments E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); LibraryTestInitialize.OnTestInitialize(Codeunit::"APIV2 - Vendor Payments E2E"); - if not isInitialized then + if not isInitialized then begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); isInitialized := true; + end; LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"APIV2 - Vendor Payments E2E"); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2VendorsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2VendorsE2E.Codeunit.al index fe84521cbd5..d073fd35a34 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2VendorsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2VendorsE2E.Codeunit.al @@ -25,9 +25,14 @@ codeunit 139803 "APIV2 - Vendors E2E" local procedure Initialize() begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; diff --git a/src/Apps/W1/APIV2/test/src/APIV2WorkflowApproversE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2WorkflowApproversE2E.Codeunit.al index 4a11340aa53..05c9f4393ef 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2WorkflowApproversE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2WorkflowApproversE2E.Codeunit.al @@ -1,6 +1,5 @@ codeunit 139920 "APIV2 - Workflow Approvers E2E" { - Subtype = Test; TestType = IntegrationTest; TestPermissions = Disabled; @@ -29,6 +28,8 @@ codeunit 139920 "APIV2 - Workflow Approvers E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO] Check that workflow approvers can be retrieved via API // [GIVEN] A workflow exists LibraryWorkflow.CreateWorkflow(Workflow); @@ -64,4 +65,10 @@ codeunit 139920 "APIV2 - Workflow Approvers E2E" Assert.IsTrue(LibraryGraphMgt.GetObjectIDFromJSON(ResponseText, 'userId', UserId), 'Could not find workflow approver'); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } \ No newline at end of file diff --git a/src/Apps/W1/APIV2/test/src/APIV2WorkflowsE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2WorkflowsE2E.Codeunit.al index d5973d08348..802006851a8 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2WorkflowsE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2WorkflowsE2E.Codeunit.al @@ -1,6 +1,5 @@ codeunit 139921 "APIV2 - Workflows E2E" { - Subtype = Test; TestType = IntegrationTest; TestPermissions = Disabled; @@ -20,6 +19,8 @@ codeunit 139921 "APIV2 - Workflows E2E" TargetURL: Text; ResponseText: Text; begin + Initialize(); + // [SCENARIO] Check that workflows can be retrieved via API // [GIVEN] A workflow exists LibraryWorkflow.CreateWorkflow(Workflow); @@ -37,4 +38,10 @@ codeunit 139921 "APIV2 - Workflows E2E" Assert.IsTrue(LibraryGraphMgt.GetObjectIDFromJSON(ResponseText, 'id', WorkFlowId), 'Could not find workflow'); end; + + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; } \ No newline at end of file diff --git a/src/Apps/W1/APIV2/test/src/APIV2itemLedgEntriesE2E.Codeunit.al b/src/Apps/W1/APIV2/test/src/APIV2itemLedgEntriesE2E.Codeunit.al index f8f1374ccec..95c808218c0 100644 --- a/src/Apps/W1/APIV2/test/src/APIV2itemLedgEntriesE2E.Codeunit.al +++ b/src/Apps/W1/APIV2/test/src/APIV2itemLedgEntriesE2E.Codeunit.al @@ -3,6 +3,7 @@ codeunit 139854 "APIV2 - Item Ledg. Entries E2E" // version Test,ERM,W1,All Subtype = Test; + RequiredTestIsolation = Disabled; TestType = IntegrationTest; TestPermissions = Disabled; @@ -25,10 +26,15 @@ codeunit 139854 "APIV2 - Item Ledg. Entries E2E" LibraryERMCountryData: Codeunit "Library - ERM Country Data"; LibraryApplicationArea: Codeunit "Library - Application Area"; begin + LibraryGraphMgt.SetLicenseSafeWorkDate(); + LibraryApplicationArea.EnableFoundationSetup(); if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateVATPostingSetup(); LibraryERMCountryData.UpdateGeneralLedgerSetup(); @@ -121,4 +127,3 @@ codeunit 139854 "APIV2 - Item Ledg. Entries E2E" - diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocAPITest.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocAPITest.Codeunit.al index 80b678db590..c9c5959b565 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocAPITest.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocAPITest.Codeunit.al @@ -16,17 +16,20 @@ codeunit 135575 "E-Doc. API Test" TestType = IntegrationTest; TestPermissions = Disabled; + var + LibraryGraphMgt: Codeunit "Library - Graph Mgt"; + [Test] procedure GetEDocument() var PurchaseHeader: Record "Purchase Header"; EDocument: Record "E-Document"; - LibrarygraphMgt: Codeunit "Library - Graph Mgt"; TargetURL: Text; Response: Text; EDocsApiServiceNameTok: Label 'eDocuments', Locked = true; begin // [SCENARIO] Get E-Document from api page + Initialize(); // [GIVEN] Related document for e document CreatePurchaseHeader(PurchaseHeader); @@ -49,13 +52,13 @@ codeunit 135575 "E-Doc. API Test" EDocumentService: Record "E-Document Service"; Assert: Codeunit Assert; Any: Codeunit Any; - LibraryGraphMgt: Codeunit "Library - Graph Mgt"; JSONRequest: Text; TargetURL: Text; Response: Text; CreateEDocumentsServiceTxt: Label 'createEDocuments', Locked = true; begin // [SCENARIO] Create E-Document using api page + Initialize(); // [GIVEN] E-Document service EDocumentService.Init(); @@ -72,10 +75,15 @@ codeunit 135575 "E-Doc. API Test" Assert.AreEqual('', Response, 'Response should be empty.'); end; + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + local procedure VerifyEDocumentResponse(EDocument: Record "E-Document"; Response: Text) var Assert: Codeunit Assert; - LibraryGraphMgt: Codeunit "Library - Graph Mgt"; AmtInclVatTxt: Text; AmtExclVatTxt: Text; AmtInclVat: Decimal; @@ -152,8 +160,6 @@ codeunit 135575 "E-Doc. API Test" end; local procedure GetEDocumentCreateRequest(EDocumentServiceCode: Code[20]; var JSONRequest: Text) - var - LibraryGraphMgt: Codeunit "Library - Graph Mgt"; begin JSONRequest := LibraryGraphMgt.AddPropertytoJSON(JSONRequest, 'eDocumentService', EDocumentServiceCode); JSONRequest := LibraryGraphMgt.AddPropertytoJSON(JSONRequest, 'base64file', NavApp.GetResourceAsText('API/base64file.txt', TextEncoding::UTF8)); diff --git a/src/Apps/W1/PowerBIReports/Test/E2EPowerBIFinanceTest.Codeunit.al b/src/Apps/W1/PowerBIReports/Test/E2EPowerBIFinanceTest.Codeunit.al index c3d532f3176..ebcedcc041e 100644 --- a/src/Apps/W1/PowerBIReports/Test/E2EPowerBIFinanceTest.Codeunit.al +++ b/src/Apps/W1/PowerBIReports/Test/E2EPowerBIFinanceTest.Codeunit.al @@ -49,6 +49,12 @@ codeunit 139876 "E2E PowerBI Finance Test" PowerBIAPIEndpoints: Enum "PowerBI API Endpoints"; ResponseEmptyErr: Label 'Response should not be empty.'; + local procedure Initialize() + begin + LibGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + [Test] procedure TestGetVendorLedgerEntry() var @@ -61,6 +67,8 @@ codeunit 139876 "E2E PowerBI Finance Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A purchase invoice is posted with vendor ledger entry and detailed vendor ledger entry LibPurch.CreatePurchaseInvoice(PurchHeader); PurchInvHeader.Get(LibPurch.PostPurchaseDocument(PurchHeader, true, true)); @@ -150,6 +158,8 @@ codeunit 139876 "E2E PowerBI Finance Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A sales invoice is posted with customer ledger entry and detailed customer ledger entry LibSales.CreateSalesInvoice(SalesHeader); SalesInvHeader.Get(LibSales.PostSalesDocument(SalesHeader, true, true)); @@ -235,6 +245,8 @@ codeunit 139876 "E2E PowerBI Finance Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A G/L account is created LibERM.CreateGLAccount(GLAccount); Commit(); @@ -273,6 +285,8 @@ codeunit 139876 "E2E PowerBI Finance Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A G/L account category is created LibERM.CreateGLAccountCategory(GLAccountCategory); Commit(); @@ -308,6 +322,8 @@ codeunit 139876 "E2E PowerBI Finance Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A G/L budget is created LibERM.CreateGLBudgetName(GLBudgetName); @@ -343,6 +359,8 @@ codeunit 139876 "E2E PowerBI Finance Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] G/L budget entries are created LibERM.CreateGLBudgetName(GLBudgetName); LibERM.CreateGLBudgetEntry(GLBudgetEntry, WorkDate(), LibERM.CreateGLAccountNo(), GLBudgetName.Name); @@ -393,6 +411,8 @@ codeunit 139876 "E2E PowerBI Finance Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] General journal lines for income statement account is posted, with one line outside the date range PowerBICoreTest.AssignAdminPermissionSet(); if not PBISetup.Get() then begin @@ -442,6 +462,8 @@ codeunit 139876 "E2E PowerBI Finance Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] General journal lines for account is posted CreateGeneralJournalBatch(GenJournalBatch, GLAccount); GLAccount."Income/Balance" := GLAccount."Income/Balance"::"Balance Sheet"; @@ -478,6 +500,8 @@ codeunit 139876 "E2E PowerBI Finance Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] G/L Account and Entry outside of the query filter are created GLAccount.Init(); GLAccount."No." := LibUtility.GenerateRandomCode20(GLAccount.FieldNo("No."), Database::"G/L Account"); diff --git a/src/Apps/W1/PowerBIReports/Test/E2EPowerBIInventoryTest.Codeunit.al b/src/Apps/W1/PowerBIReports/Test/E2EPowerBIInventoryTest.Codeunit.al index 4260437c799..da59de3909c 100644 --- a/src/Apps/W1/PowerBIReports/Test/E2EPowerBIInventoryTest.Codeunit.al +++ b/src/Apps/W1/PowerBIReports/Test/E2EPowerBIInventoryTest.Codeunit.al @@ -67,6 +67,12 @@ codeunit 139877 "E2E PowerBI Inventory Test" PowerBIAPIEndpoints: Enum "PowerBI API Endpoints"; ResponseEmptyErr: Label 'Response should not be empty.'; + local procedure Initialize() + begin + LibGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + [Test] procedure TestGetZones() var @@ -76,6 +82,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A location with multiple zones is created LibWhse.CreateFullWMSLocation(Location, 1); Zone.SetRange("Location Code", Location.Code); @@ -116,6 +124,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A bin is created LibWhse.CreateLocation(Location); LibWhse.CreateBin(Bin, Location.Code, '', '', ''); @@ -160,6 +170,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A sales order is created with multiple types of sales lines LibSales.CreateSalesOrder(SalesHeader); LibInv.CreateItemWithUnitPriceAndUnitCost( @@ -216,6 +228,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Sales lines exists outside of the query filter SalesLine.Init(); SalesLine."Type" := SalesLine.Type::Item; @@ -256,6 +270,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A purchase order is created with multiple types of purchase lines LibPurch.CreatePurchaseOrder(PurchHeader); LibInv.CreateItemWithUnitPriceAndUnitCost( @@ -312,6 +328,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Purchase lines exists outside of the query filter PurchLine.Init(); PurchLine."Type" := PurchLine.Type::Item; @@ -356,6 +374,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + UpdateSalesReceivablesSetup(); // [GIVEN] Multiple items which require replenishment, and requisition lines are created @@ -464,6 +484,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A transfer order is posted LibInv.CreateTransferHeader(TransferHeader); LibInv.CreateItem(Item); @@ -514,6 +536,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Transfer lines exists outside of the query filter TransferLine.Init(); TransferLine."Document No." := LibUtility.GenerateRandomCode20(TransferLine.FieldNo("Document No."), Database::"Transfer Line"); @@ -545,6 +569,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A service order is created with service lines LibService.CreateServiceDocumentWithItemServiceLine(ServiceHeader, ServiceHeader."Document Type"::Order); LibService.CreateServiceItem(ServiceItem, ServiceHeader."Bill-to Customer No."); @@ -595,6 +621,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Service lines exists outside of the query filter ServiceLine.Init(); ServiceLine."Document No." := LibUtility.GenerateRandomCode20(ServiceLine.FieldNo("Document No."), Database::"Service Line"); @@ -636,6 +664,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Sales and purchase documents are created and posted with item ledgers LibSales.CreateSalesOrder(SalesHeader); SalesLine.SetRange("Document Type", SalesHeader."Document Type"); @@ -714,6 +744,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Wrehouse activity lines are created LibInv.CreateItem(Item); PostWarehouseActivity(Item."No."); @@ -771,6 +803,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Warehouse entries are created; LibInv.CreateItem(Item); PostWarehouseActivity(Item."No."); @@ -854,6 +888,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Warehouse journal lines are created LibInv.CreateItem(Item1); LibInv.CreateItem(Item2); @@ -903,6 +939,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Warehouse journal lines are created LibInv.CreateItem(Item1); LibInv.CreateItem(Item2); @@ -983,6 +1021,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Value entry are posted LibInv.CreateItem(Item); LibPurch.CreatePurchHeader(PurchHeader, PurchHeader."Document Type"::Order, LibPurch.CreateVendorNo()); @@ -1040,6 +1080,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Value entry exists outside of the query filter PermissionsMock.Assign('SUPER'); if ValueEntry.FindLast() then; @@ -1071,6 +1113,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Assembly headers are created LibAssembly.CreateAssemblyOrder(AssemblyHeader, CalcDate('<+1M>', WorkDate()), '', 1); LibAssembly.CreateAssemblyOrder(AssemblyHeader2, CalcDate('<+1M>', WorkDate()), '', 1); @@ -1119,6 +1163,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Assembly header exists outside of the query filter AssemblyHeader.Init(); AssemblyHeader."Document Type" := AssemblyHeader."Document Type"::Quote; @@ -1147,6 +1193,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Assembly lines are created LibAssembly.CreateAssemblyOrder(AssemblyHeader, CalcDate('<+1M>', WorkDate()), '', LibRandom.RandIntInRange(2, 5)); AssemblyLine.SetRange("Document Type", AssemblyHeader."Document Type"::Order); @@ -1192,6 +1240,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Assembly lines exist outside of the query filter AssemblyHeader.Init(); AssemblyHeader."Document Type" := AssemblyHeader."Document Type"::Order; @@ -1229,6 +1279,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Job planning lines are created LibInv.CreateItem(Item1); LibInv.CreateItem(Item2); @@ -1281,6 +1333,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Job planning line exists outside of the query filter JobPlanningLine.Init(); @@ -1325,6 +1379,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Production order lines are created LibManufacturing.CreateItemManufacturing( Item, @@ -1388,6 +1444,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Production order line exists outside of the query filter ProdOrderLine.Init(); ProdOrderLine.Status := ProdOrderLine.Status::Finished; @@ -1420,6 +1478,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Production order component lines are created LibManufacturing.CreateItemManufacturing( Item, @@ -1486,6 +1546,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Production order component line exists outside of the query filter ProdOrderComp.Init(); ProdOrderComp.Status := ProdOrderComp.Status::Finished; @@ -1519,6 +1581,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Planning component lines are created #pragma warning disable AA0210 ReqWkshTemplate.SetRange(Type, ReqWkshTemplate.Type::"Req."); @@ -1573,6 +1637,8 @@ codeunit 139877 "E2E PowerBI Inventory Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Planning component line exists outside of the query filter PlanningComponent.Init(); PlanningComponent."Worksheet Template Name" := LibUtility.GenerateRandomCode(PlanningComponent.FieldNo("Worksheet Template Name"), Database::"Planning Component"); diff --git a/src/Apps/W1/PowerBIReports/Test/E2EPowerBIManufacturingTest.Codeunit.al b/src/Apps/W1/PowerBIReports/Test/E2EPowerBIManufacturingTest.Codeunit.al index 33ae983a5bb..e9d9e761a9c 100644 --- a/src/Apps/W1/PowerBIReports/Test/E2EPowerBIManufacturingTest.Codeunit.al +++ b/src/Apps/W1/PowerBIReports/Test/E2EPowerBIManufacturingTest.Codeunit.al @@ -51,6 +51,12 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" PowerBIAPIEndpoints: Enum "PowerBI API Endpoints"; ResponseEmptyErr: Label 'Response should not be empty.'; + local procedure Initialize() + begin + LibGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + [Test] procedure TestInventoryAdjmtEntries() var @@ -62,6 +68,7 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" Response: Text; Index: Integer; begin + Initialize(); // [GIVEN] Inventory Adjmt. entries are created @@ -131,6 +138,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" Response: Text; Index: Integer; begin + Initialize(); + // [GIVEN] Calendar entries are created LibManufacturing.CreateWorkCenterWithCalendar(WorkCenter); CalendarEntry.SetRange("No.", WorkCenter."No."); @@ -178,6 +187,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Machine centers are created LibManufacturing.CreateWorkCenter(WorkCenter); LibManufacturing.CreateMachineCenter(MachineCenter, WorkCenter."No.", 1); @@ -220,6 +231,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Work centers are created LibManufacturing.CreateWorkCenterGroup(WorkCenterGroup); LibManufacturing.CreateWorkCenter(WorkCenter); @@ -268,6 +281,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Production order lines are created LibManufacturing.CreateItemManufacturing( Item, @@ -342,6 +357,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Production order components are created LibManufacturing.CreateItemManufacturing( Item, @@ -416,6 +433,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Production order routing lines are created LibInv.CreateItem(Item); CreateRoutingForItem(Item); @@ -496,6 +515,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Item is created with production BOM and routing LibManufacturing.CreateItemManufacturing( Item, @@ -569,6 +590,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Item ledger entries exist outside of the query filter PermissionsMock.Assign('SUPER'); if ItemLedgerEntry.FindLast() then; @@ -612,6 +635,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Item is created with routing LibManufacturing.CreateItemManufacturing( Item, @@ -702,6 +727,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" Response: Text; Index: Integer; begin + Initialize(); + // [GIVEN] Item is created with routing LibManufacturing.CreateItemManufacturing( Item, @@ -839,6 +866,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] "Show Capacity In" is set in Manufacturing Setup LibManufacturing.CreateCapacityUnitOfMeasure(CapacityUnitOfMeasure, Enum::"Capacity Unit of Measure"::Minutes); ManufacturingSetup.Get(); @@ -877,6 +906,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Production orders are created LibManufacturing.CreateItemManufacturing( Item, @@ -937,6 +968,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Routing links are created LibManufacturing.CreateRoutingLink(RoutingLink); @@ -974,6 +1007,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Routing headers are created LibManufacturing.CreateRoutingHeader(RoutingHeader, RoutingHeader.Type::Parallel); LibManufacturing.CreateRoutingHeader(RoutingHeader, RoutingHeader.Type::Serial); @@ -1014,6 +1049,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Work center groups are created LibManufacturing.CreateWorkCenterGroup(WorkCenterGroup); LibManufacturing.CreateWorkCenterGroup(WorkCenterGroup); @@ -1059,6 +1096,8 @@ codeunit 139878 "E2E PowerBI Manufacturing Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Manufacturing value entries are created LibManufacturing.CreateItemManufacturing( Item, diff --git a/src/Apps/W1/PowerBIReports/Test/E2EPowerBIProjectTest.Codeunit.al b/src/Apps/W1/PowerBIReports/Test/E2EPowerBIProjectTest.Codeunit.al index 5a71e714459..7b67a7dc12d 100644 --- a/src/Apps/W1/PowerBIReports/Test/E2EPowerBIProjectTest.Codeunit.al +++ b/src/Apps/W1/PowerBIReports/Test/E2EPowerBIProjectTest.Codeunit.al @@ -38,6 +38,12 @@ codeunit 139879 "E2E PowerBI Project Test" PowerBIEndpoints: Enum "PowerBI API Endpoints"; ResponseEmptyErr: Label 'Response should not be empty.'; + local procedure Initialize() + begin + LibGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + [Test] procedure TestGetJobs() var @@ -47,6 +53,8 @@ codeunit 139879 "E2E PowerBI Project Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Jobs are created LibJob.CreateJob(Job); Job.Validate("Starting Date", WorkDate()); @@ -108,6 +116,8 @@ codeunit 139879 "E2E PowerBI Project Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Job tasks are created LibJob.CreateJob(Job); LibJob.CreateJobTask(Job, JobTask); @@ -154,6 +164,8 @@ codeunit 139879 "E2E PowerBI Project Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Job planning lines are created LibJob.CreateJob(Job); LibJob.CreateJobTask(Job, JobTask); @@ -217,6 +229,8 @@ codeunit 139879 "E2E PowerBI Project Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Job ledger entries are posted LibJob.CreateJob(Job); LibJob.CreateJobTask(Job, JobTask); @@ -283,6 +297,8 @@ codeunit 139879 "E2E PowerBI Project Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Purchase order is created with outstanding lines LibJob.CreateJob(Job); CreatePOWithJob(PurchHeader, Job); @@ -335,6 +351,8 @@ codeunit 139879 "E2E PowerBI Project Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Purchase lines exist outside of the query filter PurchHeader.Init(); PurchHeader."Document Type" := PurchHeader."Document Type"::Invoice; @@ -399,6 +417,8 @@ codeunit 139879 "E2E PowerBI Project Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Purchase order is created with received not invoiced lines LibJob.CreateJob(Job); CreatePOWithJob(PurchHeader, Job); diff --git a/src/Apps/W1/PowerBIReports/Test/E2EPowerBIPurchasesTest.Codeunit.al b/src/Apps/W1/PowerBIReports/Test/E2EPowerBIPurchasesTest.Codeunit.al index 51e3fa3b43f..dde930c1758 100644 --- a/src/Apps/W1/PowerBIReports/Test/E2EPowerBIPurchasesTest.Codeunit.al +++ b/src/Apps/W1/PowerBIReports/Test/E2EPowerBIPurchasesTest.Codeunit.al @@ -43,6 +43,12 @@ codeunit 139880 "E2E PowerBI Purchases Test" PowerBIAPIEndpoints: Enum "PowerBI API Endpoints"; ResponseEmptyErr: Label 'Response should not be empty.'; + local procedure Initialize() + begin + LibGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + [Test] procedure TestGetOutstandingPurchOrderLine() var @@ -53,6 +59,8 @@ codeunit 139880 "E2E PowerBI Purchases Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] An outstanding purchase order with multiple lines exists LibPurch.CreatePurchaseOrder(PurchHeader); LibInv.CreateItemWithUnitPriceAndUnitCost( @@ -88,6 +96,8 @@ codeunit 139880 "E2E PowerBI Purchases Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Purchase lines exist outside of the query filter PurchHeader."Document Type" := PurchHeader."Document Type"::Invoice; PurchHeader."No." := LibUtility.GenerateRandomCode20(PurchHeader.FieldNo("No."), Database::"Purchase Header"); @@ -151,6 +161,8 @@ codeunit 139880 "E2E PowerBI Purchases Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] An item budget entry exists LibERM.CreateItemBudgetName(ItemBudgetName, "Analysis Area Type"::Purchase); LibInv.CreateItemBudgetEntry( @@ -198,6 +210,8 @@ codeunit 139880 "E2E PowerBI Purchases Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Item budget entries exist outside of the query filter if ItemBudgetEntry.FindLast() then; ItemBudgetEntry."Entry No." += 1; @@ -228,6 +242,8 @@ codeunit 139880 "E2E PowerBI Purchases Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A purchase order is posted with item ledger entry and value entry LibPurch.CreatePurchaseOrder(PurchaseHeader); PurchaseHeader.Validate("Payment Discount %", 3); @@ -289,6 +305,8 @@ codeunit 139880 "E2E PowerBI Purchases Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Value entries exist outside of the query filter PermissionsMock.Assign('SUPER'); if ItemLedgerEntry.FindLast() then; @@ -330,6 +348,8 @@ codeunit 139880 "E2E PowerBI Purchases Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] A purchase order with multiple lines is received but not invoiced LibPurch.CreatePurchaseOrder(PurchaseHeader); LibInv.CreateItemWithUnitPriceAndUnitCost( @@ -386,6 +406,8 @@ codeunit 139880 "E2E PowerBI Purchases Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Purchase lines exist outside of the query filter PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Invoice; PurchaseHeader."No." := LibUtility.GenerateRandomCode20(PurchaseHeader.FieldNo("No."), Database::"Purchase Header"); @@ -457,6 +479,8 @@ codeunit 139880 "E2E PowerBI Purchases Test" Response: Text; GLAccountCode: Code[20]; begin + Initialize(); + // [GIVEN] A purchase order with multiple lines for item, G/L account & resource. LibPurch.CreatePurchaseOrder(PurchaseHeader); PurchaseHeader."Posting Date" := WorkDate(); @@ -557,6 +581,8 @@ codeunit 139880 "E2E PowerBI Purchases Test" GLAccountCode: Code[20]; PurchInvoiceNo: Code[20]; begin + Initialize(); + // [GIVEN] A purchase invoice with multiple lines for G/L and Resource LibPurch.CreatePurchaseInvoice(PurchaseHeader); @@ -634,6 +660,8 @@ codeunit 139880 "E2E PowerBI Purchases Test" GLAccountCode: Code[20]; PurchCrMemoNo: Code[20]; begin + Initialize(); + // [GIVEN] A purchase credit memo with multiple lines for G/L and Resource LibPurch.CreatePurchaseCreditMemo(PurchaseHeader); diff --git a/src/Apps/W1/PowerBIReports/Test/E2EPowerBISalesTest.Codeunit.al b/src/Apps/W1/PowerBIReports/Test/E2EPowerBISalesTest.Codeunit.al index 80e1f147332..5a8912da008 100644 --- a/src/Apps/W1/PowerBIReports/Test/E2EPowerBISalesTest.Codeunit.al +++ b/src/Apps/W1/PowerBIReports/Test/E2EPowerBISalesTest.Codeunit.al @@ -53,6 +53,9 @@ codeunit 139881 "E2E PowerBI Sales Test" if IsInitialized then exit; + LibGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; Commit(); end; diff --git a/src/Apps/W1/Quality Management/test/src/QltyTestsInspectionsAPI.Codeunit.al b/src/Apps/W1/Quality Management/test/src/QltyTestsInspectionsAPI.Codeunit.al index b2a38ac3f7b..66004d3ce71 100644 --- a/src/Apps/W1/Quality Management/test/src/QltyTestsInspectionsAPI.Codeunit.al +++ b/src/Apps/W1/Quality Management/test/src/QltyTestsInspectionsAPI.Codeunit.al @@ -50,6 +50,9 @@ codeunit 139972 "Qlty. Tests - Inspections API" if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryERMCountryData.CreateVATData(); IsInitialized := true; Commit(); diff --git a/src/Apps/W1/Sustainability/test/src/PowerBISustainabilityTest.Codeunit.al b/src/Apps/W1/Sustainability/test/src/PowerBISustainabilityTest.Codeunit.al index e91d24174c6..c349a727223 100644 --- a/src/Apps/W1/Sustainability/test/src/PowerBISustainabilityTest.Codeunit.al +++ b/src/Apps/W1/Sustainability/test/src/PowerBISustainabilityTest.Codeunit.al @@ -31,6 +31,12 @@ codeunit 148215 "PowerBI Sustainability Test" FieldHiddenMsg: Label '''%1'' field should be hidden.', Comment = '%1 - field caption'; FieldShownMsg: Label '''%1'' field should be shown.', Comment = '%1 - field caption'; + local procedure Initialize() + begin + LibGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + [Test] procedure TestSustainLedgerEntry() var @@ -38,6 +44,8 @@ codeunit 148215 "PowerBI Sustainability Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Insert a Sustainability Ledger Entry SustainabilityLedgerEntry.Init(); SustainabilityLedgerEntry."Account No." := LibUtility.GenerateGUID(); @@ -103,6 +111,8 @@ codeunit 148215 "PowerBI Sustainability Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Insert a Sustainability Ledger Entry EmployeeLedgerEntry.Init(); DetailEmployLedgEntry.Init(); @@ -159,6 +169,8 @@ codeunit 148215 "PowerBI Sustainability Test" TargetURL: Text; Response: Text; begin + Initialize(); + // [GIVEN] Insert a Sustainability Goal if not UserSetup.Get(UserId()) then begin diff --git a/src/Apps/W1/Sustainability/test/src/SustainabilityAPITests.Codeunit.al b/src/Apps/W1/Sustainability/test/src/SustainabilityAPITests.Codeunit.al index 8c0064704ab..c29a972b139 100644 --- a/src/Apps/W1/Sustainability/test/src/SustainabilityAPITests.Codeunit.al +++ b/src/Apps/W1/Sustainability/test/src/SustainabilityAPITests.Codeunit.al @@ -275,6 +275,9 @@ codeunit 148185 "Sustainability API Tests" if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibrarySustainability.CleanUpBeforeTesting(); IsInitialized := true; diff --git a/src/DisabledTests/E-Document_Core_Tests/E-Document Core Tests.DisabledTest.json b/src/DisabledTests/E-Document_Core_Tests/E-Document Core Tests.DisabledTest.json index 2a0b6b498c3..1db9a8feddd 100644 --- a/src/DisabledTests/E-Document_Core_Tests/E-Document Core Tests.DisabledTest.json +++ b/src/DisabledTests/E-Document_Core_Tests/E-Document Core Tests.DisabledTest.json @@ -314,11 +314,6 @@ "codeunitName": "EDocCopilotPOGeneralHarms", "method": "*" }, - { - "codeunitId": 135575, - "codeunitName": "E-Doc. API Test", - "method": "*" - }, { "codeunitId": 139235, "codeunitName": "PEPPOL30 Management Tests", diff --git a/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.DisabledTest.json b/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.DisabledTest.json index ad692000a56..bcca968fa42 100644 --- a/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.DisabledTest.json +++ b/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.DisabledTest.json @@ -1,84 +1,4 @@ [ - { - "codeunitId": 139701, - "codeunitName": "APIV1 - Accounts E2E", - "method": "TestGetAccounts" - }, - { - "codeunitId": 139704, - "codeunitName": "APIV1 - Payment Terms E2E", - "method": "TestGetPaymentTerms" - }, - { - "codeunitId": 139705, - "codeunitName": "APIV1 - Shipment Methods E2E", - "method": "TestGetShipmentMethods" - }, - { - "codeunitId": 139706, - "codeunitName": "APIV1 - Company Info. E2E", - "method": "TestGetCompanyInformationWithComplexType" - }, - { - "codeunitId": 139706, - "codeunitName": "APIV1 - Company Info. E2E", - "method": "TestModifyCompanyInformation" - }, - { - "codeunitId": 139706, - "codeunitName": "APIV1 - Company Info. E2E", - "method": "TestModifyCompanyInformationWithComplexType" - }, - { - "codeunitId": 139706, - "codeunitName": "APIV1 - Company Info. E2E", - "method": "TestRemoveComplexTypeFromCompanyInformation" - }, - { - "codeunitId": 139707, - "codeunitName": "APIV1 - Item Categories E2E", - "method": "TestCreateItemCategory" - }, - { - "codeunitId": 139707, - "codeunitName": "APIV1 - Item Categories E2E", - "method": "TestDeleteItemCategory" - }, - { - "codeunitId": 139707, - "codeunitName": "APIV1 - Item Categories E2E", - "method": "TestGetItemCategories" - }, - { - "codeunitId": 139707, - "codeunitName": "APIV1 - Item Categories E2E", - "method": "TestModifyItemCategory" - }, - { - "codeunitId": 139708, - "codeunitName": "Tax Groups E2E", - "method": "TestCreateTaxGroup" - }, - { - "codeunitId": 139708, - "codeunitName": "Tax Groups E2E", - "method": "TestDeleteTaxGroup" - }, - { - "codeunitId": 139708, - "codeunitName": "Tax Groups E2E", - "method": "TestGetTaxGroup" - }, - { - "codeunitId": 139708, - "codeunitName": "Tax Groups E2E", - "method": "TestGetTaxGroups" - }, - { - "codeunitId": 139708, - "codeunitName": "Tax Groups E2E", - "method": "TestModifyTaxGroup" - }, { "codeunitId": 139709, "codeunitName": "Sales Invoices E2E", @@ -89,1369 +9,124 @@ "codeunitName": "Sales Invoices E2E", "method": "TestActionCancelInvoice" }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestActionMakeCorrectiveCreditMemo" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestActionPostAndSendInvoice" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestActionPostInvoice" - }, { "codeunitId": 139709, "codeunitName": "Sales Invoices E2E", "method": "TestActionSendCancelledInvoice" }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestActionSendDraftInvoice" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestActionSendPostedInvoice" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestClearingManualDiscounts" - }, { "codeunitId": 139709, "codeunitName": "Sales Invoices E2E", "method": "TestCreateInvoiceThroughPageAndAPI" }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestDeleteInvoice" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestEmptyModifyInvoices" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestGetInvoices" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestGetInvoicesAppliesDiscountPct" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestGetInvoicesRedistributesDiscountAmt" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestModifyingWithBlankIdEmptiesTheCodeAndTheId" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestModifyInvoiceNumberForDraftInvoice" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestModifyInvoices" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestModifyInvoiceSetManualDiscount" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestPartialModifyInvoices" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestPostInvoices" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestPostInvoiceWithCurrency" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestPostInvoiceWithDates" - }, - { - "codeunitId": 139709, - "codeunitName": "Sales Invoices E2E", - "method": "TestPostInvoiceWithEmail" - }, - { - "codeunitId": 139710, - "codeunitName": "Countries/Regions E2E", - "method": "TestCreateCountriesRegions" - }, - { - "codeunitId": 139710, - "codeunitName": "Countries/Regions E2E", - "method": "TestDeleteCountriesRegions" - }, - { - "codeunitId": 139710, - "codeunitName": "Countries/Regions E2E", - "method": "TestGetCountriesRegions" - }, - { - "codeunitId": 139710, - "codeunitName": "Countries/Regions E2E", - "method": "TestModifyCountriesRegions" - }, - { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestActionShipAndInvoice" - }, - { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestClearingManualDiscounts" - }, { "codeunitId": 139711, "codeunitName": "APIV1 - Sales Orders E2E", "method": "TestCreateOrderThroughPageAndAPI" }, { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestDeleteOrders" - }, - { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestEmptyModifyOrders" - }, - { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestGetOrders" - }, - { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestGetOrdersAppliesDiscountPct" - }, - { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestGetOrdersRedistributesDiscountAmt" - }, - { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestModifyOrders" - }, - { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestModifyOrderSetManualDiscount" - }, - { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestPartialModifyOrders" - }, - { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestPostOrderForCustomerWithLocationCode" - }, - { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestPostOrders" - }, - { - "codeunitId": 139711, - "codeunitName": "APIV1 - Sales Orders E2E", - "method": "TestPostOrderWithCurrency" - }, - { - "codeunitId": 139712, - "codeunitName": "APIV1 - UofM E2E", - "method": "TestDeleteUnitOfMeasure" - }, - { - "codeunitId": 139712, - "codeunitName": "APIV1 - UofM E2E", - "method": "TestGetUnitOfMeasure" - }, - { - "codeunitId": 139712, - "codeunitName": "APIV1 - UofM E2E", - "method": "TestInsertUnitOfMeasure" - }, - { - "codeunitId": 139712, - "codeunitName": "APIV1 - UofM E2E", - "method": "TestModifyUnitOfMeasure" - }, - { - "codeunitId": 139713, - "codeunitName": "APIV1 - Currencies E2E", - "method": "TestCreateCurrency" - }, - { - "codeunitId": 139713, - "codeunitName": "APIV1 - Currencies E2E", - "method": "TestDeleteCurrency" - }, - { - "codeunitId": 139713, - "codeunitName": "APIV1 - Currencies E2E", - "method": "TestGetCurrencies" - }, - { - "codeunitId": 139713, - "codeunitName": "APIV1 - Currencies E2E", - "method": "TestModifyCurrency" + "codeunitId": 139723, + "codeunitName": "APIV1 - Sales Quotes E2E", + "method": "TestActionMakeOrderFromQuote" }, { - "codeunitId": 139714, - "codeunitName": "Payment Methods E2E", - "method": "TestCreatePaymentMethods" + "codeunitId": 139723, + "codeunitName": "APIV1 - Sales Quotes E2E", + "method": "TestCreateQuoteThroughPageAndAPI" }, { - "codeunitId": 139714, - "codeunitName": "Payment Methods E2E", - "method": "TestDeletePaymentMethods" + "codeunitId": 139728, + "codeunitName": "APIV1 - Sales Credit Memos E2E", + "method": "TestActionCancelAndSendCorrectiveCreditMemo" }, { - "codeunitId": 139714, - "codeunitName": "Payment Methods E2E", - "method": "TestGetPaymentMethods" + "codeunitId": 139728, + "codeunitName": "APIV1 - Sales Credit Memos E2E", + "method": "TestActionCancelCorrectiveCreditMemo" }, { - "codeunitId": 139714, - "codeunitName": "Payment Methods E2E", - "method": "TestModifyPaymentMethods" + "codeunitId": 139728, + "codeunitName": "APIV1 - Sales Credit Memos E2E", + "method": "TestCreateCreditMemoThroughPageAndAPI" }, { - "codeunitId": 139715, - "codeunitName": "APIV1 - Balance Sheet E2E", - "method": "TestGetBalanceSheetRecords" + "codeunitId": 139729, + "codeunitName": "APIV1 - Purchase Invoices E2E", + "method": "TestPurchaseInvoiceWithPaymentDiscount" }, { - "codeunitId": 139716, - "codeunitName": "APIV1 - Income Statement E2E", - "method": "TestGetIncomeStatementRecords" + "codeunitId": 139734, + "codeunitName": "APIV1 - Sales Inv. Lines E2E", + "method": "TestGettingLinesWithDifferentTypes" }, { - "codeunitId": 139717, - "codeunitName": "APIV1 - CashFlow Statement E2E", - "method": "TestGetCashFlowRecords" + "codeunitId": 139734, + "codeunitName": "APIV1 - Sales Inv. Lines E2E", + "method": "TestPatchingTheIdToItemChangesLineType" }, { - "codeunitId": 139718, - "codeunitName": "APIV1 - Trial Balance E2E", - "method": "TestGetTrialBalanceRecords" + "codeunitId": 139735, + "codeunitName": "APIV1 - Sales Order Lines E2E", + "method": "TestGettingLinesWithDifferentTypes" }, { - "codeunitId": 139719, - "codeunitName": "APIV1 - Aged AP E2E", - "method": "TestGetAgedAPRecords" + "codeunitId": 139735, + "codeunitName": "APIV1 - Sales Order Lines E2E", + "method": "TestPatchingTheIdToItemChangesLineType" }, { - "codeunitId": 139720, - "codeunitName": "APIV1 - Aged AR E2E", - "method": "TestGetAgedARRecords" + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestDeleteInUse" }, { - "codeunitId": 139721, - "codeunitName": "APIV1 - Ret. Earnings E2E", - "method": "TestRetainedEarningsStatementRecords" + "codeunitId": 139734, + "codeunitName": "APIV1 - Sales Inv. Lines E2E", + "method": "TestPostingBlankLineDefaultsToItemType" }, { - "codeunitId": 139722, - "codeunitName": "APIV1 - Employees E2E", - "method": "TestCreateEmployee" + "codeunitId": 139735, + "codeunitName": "APIV1 - Sales Order Lines E2E", + "method": "TestPostingBlankLineDefaultsToItemType" }, { - "codeunitId": 139722, - "codeunitName": "APIV1 - Employees E2E", - "method": "TestDeleteEmployee" + "codeunitId": 139736, + "codeunitName": "APIV1 - Sales Quote Lines E2E", + "method": "TestPostingBlankLineDefaultsToItemType" }, { - "codeunitId": 139722, - "codeunitName": "APIV1 - Employees E2E", - "method": "TestGetEmployee" + "codeunitId": 139737, + "codeunitName": "APIV1 - Sales CrMemo Lines E2E", + "method": "TestPostingBlankLineDefaultsToItemType" }, { - "codeunitId": 139722, - "codeunitName": "APIV1 - Employees E2E", - "method": "TestModifyEmployee" + "codeunitId": 139738, + "codeunitName": "APIV1 - Purchase Inv Lines E2E", + "method": "TestPostingBlankLineDefaultsToItemType" }, { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestActionMakeInvoiceFromQuote" + "codeunitId": 139731, + "codeunitName": "APIV1 - Automation RS Package", + "method": "TestImportRSPackage" }, { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestActionMakeOrderFromQuote" + "codeunitId": 139731, + "codeunitName": "APIV1 - Automation RS Package", + "method": "TestImportWrongRSPackage" }, { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestActionSendQuote" + "codeunitId": 139731, + "codeunitName": "APIV1 - Automation RS Package", + "method": "TestApplyRSPackage" }, { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestClearingManualDiscounts" + "codeunitId": 139730, + "codeunitName": "APIV1 - GLEntries E2E", + "method": "TestGetGLEntries" }, { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestCreateQuoteThroughPageAndAPI" - }, - { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestDeleteQuotes" - }, - { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestEmptyModifyQuotes" - }, - { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestGetQuotes" - }, - { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestGetQuotesAppliesDiscountPct" - }, - { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestGetQuotesRedistributesDiscountAmt" - }, - { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestModifyQuotes" - }, - { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestModifyQuoteSetManualDiscount" - }, - { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestPartialModifyQuotes" - }, - { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestPostQuotes" - }, - { - "codeunitId": 139723, - "codeunitName": "APIV1 - Sales Quotes E2E", - "method": "TestPostQuoteWithCurrency" - }, - { - "codeunitId": 139724, - "codeunitName": "APIV1 - Dimensions E2E", - "method": "TestGetDimensions" - }, - { - "codeunitId": 139724, - "codeunitName": "APIV1 - Dimensions E2E", - "method": "TestGetDimensionValues" - }, - { - "codeunitId": 139727, - "codeunitName": "APIV1 - Journals E2E", - "method": "TestActionPostGenJournalBatch" - }, - { - "codeunitId": 139727, - "codeunitName": "APIV1 - Journals E2E", - "method": "TestCreateJournal" - }, - { - "codeunitId": 139727, - "codeunitName": "APIV1 - Journals E2E", - "method": "TestDeleteJournal" - }, - { - "codeunitId": 139727, - "codeunitName": "APIV1 - Journals E2E", - "method": "TestGetJournals" - }, - { - "codeunitId": 139727, - "codeunitName": "APIV1 - Journals E2E", - "method": "TestModifyJournal" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestActionCancelAndSendCorrectiveCreditMemo" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestActionCancelCorrectiveCreditMemo" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestActionPostAndSendCreditMemo" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestActionPostCreditMemo" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestActionSendCancelledCreditMemo" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestActionSendPostedCreditMemo" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestClearingManualDiscounts" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestCreateCreditMemoThroughPageAndAPI" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestDeleteCreditMemo" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestEmptyModifyCreditMemos" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestGetCreditMemos" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestGetCreditMemosAppliesDiscountPct" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestGetCreditMemosRedistributesDiscountAmt" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestModifyCreditMemos" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestModifyCreditMemoSetManualDiscount" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestPartialModifyCreditMemos" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestPostCreditMemos" - }, - { - "codeunitId": 139728, - "codeunitName": "APIV1 - Sales Credit Memos E2E", - "method": "TestPostCreditMemoWithCurrency" - }, - { - "codeunitId": 139729, - "codeunitName": "APIV1 - Purchase Invoices E2E", - "method": "TestActionPostPurchaseInvoice" - }, - { - "codeunitId": 139729, - "codeunitName": "APIV1 - Purchase Invoices E2E", - "method": "TestCreateInvoiceThroughPageAndAPI" - }, - { - "codeunitId": 139729, - "codeunitName": "APIV1 - Purchase Invoices E2E", - "method": "TestDeleteInvoice" - }, - { - "codeunitId": 139729, - "codeunitName": "APIV1 - Purchase Invoices E2E", - "method": "TestEmptyModifyInvoices" - }, - { - "codeunitId": 139729, - "codeunitName": "APIV1 - Purchase Invoices E2E", - "method": "TestGetInvoices" - }, - { - "codeunitId": 139729, - "codeunitName": "APIV1 - Purchase Invoices E2E", - "method": "TestModifyInvoices" - }, - { - "codeunitId": 139729, - "codeunitName": "APIV1 - Purchase Invoices E2E", - "method": "TestPartialModifyInvoices" - }, - { - "codeunitId": 139729, - "codeunitName": "APIV1 - Purchase Invoices E2E", - "method": "TestPostInvoices" - }, - { - "codeunitId": 139729, - "codeunitName": "APIV1 - Purchase Invoices E2E", - "method": "TestPostPurchaseInvoiceWithCurrency" - }, - { - "codeunitId": 139729, - "codeunitName": "APIV1 - Purchase Invoices E2E", - "method": "TestPurchaseInvoiceWithPaymentDiscount" - }, - { - "codeunitId": 139730, - "codeunitName": "APIV1 - GLEntries E2E", - "method": "TestGetGLEntries" - }, - { - "codeunitId": 139731, - "codeunitName": "APIV1 - Automation RS Package", - "method": "TestApplyRSPackage" - }, - { - "codeunitId": 139731, - "codeunitName": "APIV1 - Automation RS Package", - "method": "TestCreateRSPackage" - }, - { - "codeunitId": 139731, - "codeunitName": "APIV1 - Automation RS Package", - "method": "TestImportRSPackage" - }, - { - "codeunitId": 139731, - "codeunitName": "APIV1 - Automation RS Package", - "method": "TestImportWrongRSPackage" - }, - { - "codeunitId": 139731, - "codeunitName": "APIV1 - Automation RS Package", - "method": "TestUploadRSPackageFile" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestCreateDraftInvoiceAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestCreateDraftPurchaseInvoiceAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestCreateGLEntryAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestCreateJournalLineAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestCreatePostedInvoiceAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestCreatePostedPurchaseInvoiceAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestCreateQuoteAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestDeleteDraftInvoiceAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestDeleteDraftPurchaseInvoiceAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestDeleteGLEntryAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestDeleteJournalLineAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestDeletePostedInvoiceAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestDeletePostedPurchaseInvoiceAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestDeleteQuoteAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetDraftInvoiceAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetDraftInvoiceAttachments" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetDraftPurchaseInvoiceAttachments" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetGLEntryAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetGLEntryAttachments" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetJournalAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetJournalLineAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetJournalLineAttachments" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetPostedInvoiceAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetPostedInvoiceAttachments" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetPostedPurchaseInvoiceAttachments" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetQuoteAttachment" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestGetQuoteAttachments" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestLinkedAttachmentContentChangeKeepsOtherFieldsUnchanged" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestLinkedAttachmentDocumentIdChangeNotAllowed" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestLinkedAttachmentFileNameChangeKeepsOtherFieldsUnchanged" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestLinkedAttachmentIdChangeNotAllowed" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestTransferAttachmentFromDraftToPostedInvoice" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestTransferAttachmentFromDraftToPostedPurchaseInvoice" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestUpdateGenJournalLineAttachmentBinaryContent" - }, - { - "codeunitId": 139733, - "codeunitName": "APIV1 - Attachments E2E", - "method": "TestUpdateGLEntryAttachmentBinaryContent" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestCreateLineThroughPageAndAPI" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestDeleteInvoiceLine" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestDeletingLineKeepsInvoiceDiscountAmt" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestDeletingLineMovesInvoiceDiscountPct" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestDeletingLineRemovesInvoiceDiscountPct" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestGetInvoiceLineDirectly" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestGetInvoiceLines" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestGetInvoiceLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestGetPostedInvoiceLines" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestGettingLinesWithDifferentTypes" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestInsertingLineUpdatesInvoiceDiscountPct" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestModifyingLineKeepsInvoiceDiscountAmt" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestModifyingLineUpdatesInvoiceDiscountPct" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestModifyInvoiceLine" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestModifyInvoiceLineWithUoM" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestPatchingTheIdToAccountChangesLineType" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestPatchingTheIdToItemChangesLineType" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestPatchingTheTypeBlanksIds" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestPostingBlankLineDefaultsToItemType" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestPostingCommentLine" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestPostInvoiceLines" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestPostInvoiceLinesWithItemVariant" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestPostInvoiceLinesWithUoM" - }, - { - "codeunitId": 139734, - "codeunitName": "APIV1 - Sales Inv. Lines E2E", - "method": "TestPostInvoiceLineWithSequence" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestCreateLineThroughPageAndAPI" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestDeleteOrderLine" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestDeletingLineKeepsOrderDiscountAmt" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestDeletingLineMovesOrderDiscountPct" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestDeletingLineRemovesOrderDiscountPct" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestGetOrderLineDirectly" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestGetOrderLines" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestGetOrderLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestGettingLinesWithDifferentTypes" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestInsertingLineUpdatesOrderDiscountPct" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestModifyingLineKeepsOrderDiscountAmt" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestModifyingLineUpdatesOrderDiscountPct" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestModifyOrderLines" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestPatchingTheIdToAccountChangesLineType" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestPatchingTheIdToItemChangesLineType" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestPatchingTheTypeBlanksIds" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestPostingBlankLineDefaultsToItemType" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestPostingCommentLine" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestPostOrderLines" - }, - { - "codeunitId": 139735, - "codeunitName": "APIV1 - Sales Order Lines E2E", - "method": "TestPostOrderLineWithItemVariant" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestDeleteQuoteLine" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestDeletingLineKeepsQuoteDiscountAmt" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestDeletingLineMovesQuoteDiscountPct" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestDeletingLineRemovesQuoteDiscountPct" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestGetQuoteLineDirectly" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestGetQuoteLines" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestGetQuoteLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestInsertingLineKeepsQuoteDiscountAmt" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestInsertingLineUpdatesQuoteDiscountPct" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestModifyingLineKeepsQuoteDiscountAmt" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestModifyingLineUpdatesQuoteDiscountPct" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestModifyQuoteLines" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestPatchingTheTypeBlanksIds" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestPostingBlankLineDefaultsToItemType" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestPostingCommentLine" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestPostQuoteLines" - }, - { - "codeunitId": 139736, - "codeunitName": "APIV1 - Sales Quote Lines E2E", - "method": "TestPostQuoteLinesWithItemVariant" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestCreateLineThroughPageAndAPI" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestDeleteCreditMemoLine" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestDeletePostedCreditMemoLine" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestDeletingLineKeepsCreditMemoDiscountAmt" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestDeletingLineMovesCreditMemoDiscountPct" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestDeletingLineRemovesCreditMemoDiscountPct" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestGetCreditMemoLineDirectly" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestGetCreditMemoLines" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestGetCreditMemoLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestGetPostedCreditMemoLines" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestGettingLinesWithDifferentTypes" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestInsertingLineUpdatesCreditMemoDiscountPct" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestModifyCreditMemoLines" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestModifyingLineKeepsCreditMemoDiscountAmt" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestModifyingLineUpdatesCreditMemoDiscountPct" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestPatchingTheIdToAccountChangesLineType" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestPatchingTheIdToItemChangesLineType" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestPatchingTheTypeBlanksIds" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestPostCreditMemoLines" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestPostCreditMemoLinesWithItemVariant" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestPostCreditMemoLineWithSequence" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestPostingBlankLineDefaultsToItemType" - }, - { - "codeunitId": 139737, - "codeunitName": "APIV1 - Sales CrMemo Lines E2E", - "method": "TestPostingCommentLine" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestCreateLineThroughPageAndAPI" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestDeleteInvoiceLine" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestDeletingLineRemovesInvoiceDiscountPct" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestGetInvoiceLineDirectly" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestGetInvoiceLines" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestGetInvoiceLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestGetPostedInvoiceLines" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestInsertingLineUpdatesInvoiceDiscountPct" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestModifyingLineUpdatesInvoiceDiscountPct" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestModifyInvoiceLines" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestPatchingTheTypeBlanksIds" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestPostingBlankLineDefaultsToItemType" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestPostingCommentLine" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestPostInvoiceLines" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestPostInvoiceLinesWithItemVariant" - }, - { - "codeunitId": 139738, - "codeunitName": "APIV1 - Purchase Inv Lines E2E", - "method": "TestPostInvoiceLineWithSequence" - }, - { - "codeunitId": 139743, - "codeunitName": "APIV1 - Customer Payments E2E", - "method": "TestAppliesToInvoiceNoAndIdSync" - }, - { - "codeunitId": 139743, - "codeunitName": "APIV1 - Customer Payments E2E", - "method": "TestCreateCustomerPayment" - }, - { - "codeunitId": 139743, - "codeunitName": "APIV1 - Customer Payments E2E", - "method": "TestCreateCustomerPaymentWithInvoiceId" - }, - { - "codeunitId": 139743, - "codeunitName": "APIV1 - Customer Payments E2E", - "method": "TestCreateCustomerPaymentWithoutDocNo" - }, - { - "codeunitId": 139743, - "codeunitName": "APIV1 - Customer Payments E2E", - "method": "TestCustomerAutofillDoesNotOverwrite" - }, - { - "codeunitId": 139743, - "codeunitName": "APIV1 - Customer Payments E2E", - "method": "TestCustomerAutofillWhenGivingInvoiceNumber" - }, - { - "codeunitId": 139743, - "codeunitName": "APIV1 - Customer Payments E2E", - "method": "TestCustomerNoAndIdSync" - }, - { - "codeunitId": 139743, - "codeunitName": "APIV1 - Customer Payments E2E", - "method": "TestDeleteCustomerPayment" - }, - { - "codeunitId": 139743, - "codeunitName": "APIV1 - Customer Payments E2E", - "method": "TestGetCustomerPayment" - }, - { - "codeunitId": 139743, - "codeunitName": "APIV1 - Customer Payments E2E", - "method": "TestModifyCustomerPayment" - }, - { - "codeunitId": 139743, - "codeunitName": "APIV1 - Customer Payments E2E", - "method": "TestModifyCustomerPaymentWithRandomDocNo" - }, - { - "codeunitId": 139744, - "codeunitName": "APIV1 - PDF Document E2E", - "method": "TestGetPDFPurchaseInvoice" - }, - { - "codeunitId": 139744, - "codeunitName": "APIV1 - PDF Document E2E", - "method": "TestGetPDFSalesCreditMemo" - }, - { - "codeunitId": 139744, - "codeunitName": "APIV1 - PDF Document E2E", - "method": "TestGetPDFSalesInvoice" - }, - { - "codeunitId": 139744, - "codeunitName": "APIV1 - PDF Document E2E", - "method": "TestGetPDFSalesQuote" - }, - { - "codeunitId": 139745, - "codeunitName": "APIV1 - Journal Lines E2E", - "method": "TestCreateJournalLineWithDimensionsOnAccountWithDimensionSetup" - }, - { - "codeunitId": 139745, - "codeunitName": "APIV1 - Journal Lines E2E", - "method": "TestCreateJournalLineWithDisabledCopyVATSetup" - }, - { - "codeunitId": 139745, - "codeunitName": "APIV1 - Journal Lines E2E", - "method": "TestCreateJournalLineWithoutDimensionsOnAccountWithDimensionSetup" - }, - { - "codeunitId": 139745, - "codeunitName": "APIV1 - Journal Lines E2E", - "method": "TestCreateJournalLineWithOverridenDefaultDimension" + "codeunitId": 139709, + "codeunitName": "Sales Invoices E2E", + "method": "TestGetInvoicesAppliesDiscountPct" } ] diff --git a/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.DisabledTest.json b/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.DisabledTest.json index 2a73423912e..588bb59f3fd 100644 --- a/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.DisabledTest.json +++ b/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.DisabledTest.json @@ -1,184 +1,9 @@ [ - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestCreateItemWithCategoryCode" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestCreateItemWithCategoryId" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestCreateItemWithTemplate" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestCreateItemWithTemplateIgnoresFieldsSet" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestCreateItemWithUoMCode" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestCreateItemWithUoMId" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestCreateSimpleItem" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestDeleteItem" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestGetItemWithCategory" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestGetRelatedEntities" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestGetSimpleItem" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestModifyItem" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestModifyItemInventory" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestPatchItemWithCategoryCode" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestPatchItemWithCategoryId" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestPatchItemWithGenProdPostingGroup" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestPatchItemWithInventoryPostingGroup" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestUnassignGenProdPostingGroup" - }, - { - "codeunitId": 139800, - "codeunitName": "APIV2 - Items E2E", - "method": "TestUnassignInventoryPostingGroup" - }, - { - "codeunitId": 139801, - "codeunitName": "APIV2 - Accounts E2E", - "method": "TestGetAccounts" - }, - { - "codeunitId": 139804, - "codeunitName": "APIV2 - Payment Terms E2E", - "method": "TestGetPaymentTerms" - }, - { - "codeunitId": 139805, - "codeunitName": "APIV2 - Shipment Methods E2E", - "method": "TestGetShipmentMethods" - }, - { - "codeunitId": 139806, - "codeunitName": "APIV2 - Company Info. E2E", - "method": "TestGetCompanyInformation" - }, { "codeunitId": 139806, "codeunitName": "APIV2 - Company Info. E2E", "method": "TestGetCompanyAndEnvironmentDescriptions" }, - { - "codeunitId": 139806, - "codeunitName": "APIV2 - Company Info. E2E", - "method": "TestModifyCompanyInformation" - }, - { - "codeunitId": 139806, - "codeunitName": "APIV2 - Company Info. E2E", - "method": "TestModifyInvalidExperinceTier" - }, - { - "codeunitId": 139806, - "codeunitName": "APIV2 - Company Info. E2E", - "method": "TestModifyValidExperinceTier" - }, - { - "codeunitId": 139807, - "codeunitName": "APIV2 - Item Categories E2E", - "method": "TestCreateItemCategory" - }, - { - "codeunitId": 139807, - "codeunitName": "APIV2 - Item Categories E2E", - "method": "TestDeleteItemCategory" - }, - { - "codeunitId": 139807, - "codeunitName": "APIV2 - Item Categories E2E", - "method": "TestGetItemCategories" - }, - { - "codeunitId": 139807, - "codeunitName": "APIV2 - Item Categories E2E", - "method": "TestModifyItemCategory" - }, - { - "codeunitId": 139808, - "codeunitName": "APIV2 - Tax Groups E2E", - "method": "TestCreateTaxGroup" - }, - { - "codeunitId": 139808, - "codeunitName": "APIV2 - Tax Groups E2E", - "method": "TestDeleteTaxGroup" - }, - { - "codeunitId": 139808, - "codeunitName": "APIV2 - Tax Groups E2E", - "method": "TestGetTaxGroup" - }, - { - "codeunitId": 139808, - "codeunitName": "APIV2 - Tax Groups E2E", - "method": "TestGetTaxGroups" - }, - { - "codeunitId": 139808, - "codeunitName": "APIV2 - Tax Groups E2E", - "method": "TestModifyTaxGroup" - }, { "codeunitId": 139809, "codeunitName": "APIV2 - Sales Invoices E2E", @@ -189,3399 +14,144 @@ "codeunitName": "APIV2 - Sales Invoices E2E", "method": "TestActionCancelInvoice" }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestActionMakeCorrectiveCreditMemo" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestActionPostAndSendInvoice" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestActionPostInvoice" - }, { "codeunitId": 139809, "codeunitName": "APIV2 - Sales Invoices E2E", "method": "TestActionSendCancelledInvoice" }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestActionSendDraftInvoice" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestActionSendPostedInvoice" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestClearingManualDiscounts" - }, { "codeunitId": 139809, "codeunitName": "APIV2 - Sales Invoices E2E", "method": "TestCreateInvoiceThroughPageAndAPI" }, { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestDeleteInvoice" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestEmptyModifyInvoices" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestGetInvoiceFromPostedOrderCorrectOrderIdAndNo" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestGetInvoices" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestGetInvoicesAppliesDiscountPct" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestGetInvoicesRedistributesDiscountAmt" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestGetSalesInvoiceFromDraftId" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestModifyingWithBlankIdEmptiesTheCodeAndTheId" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestModifyInvoiceNumberForDraftInvoice" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestModifyInvoices" - }, - { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestModifyInvoiceSetManualDiscount" + "codeunitId": 139811, + "codeunitName": "APIV2 - Sales Orders E2E", + "method": "TestCreateOrderThroughPageAndAPI" }, { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestPartialModifyInvoices" + "codeunitId": 139823, + "codeunitName": "APIV2 - Sales Quotes E2E", + "method": "TestActionMakeOrderFromQuote" }, { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestPostInvoices" + "codeunitId": 139823, + "codeunitName": "APIV2 - Sales Quotes E2E", + "method": "TestCreateQuoteThroughPageAndAPI" }, { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestPostInvoiceWithCurrency" + "codeunitId": 139828, + "codeunitName": "APIV2 - Sales Credit Memos E2E", + "method": "TestActionCancelAndSendCorrectiveCreditMemo" }, { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestPostInvoiceWithDates" + "codeunitId": 139828, + "codeunitName": "APIV2 - Sales Credit Memos E2E", + "method": "TestActionCancelCorrectiveCreditMemo" }, { - "codeunitId": 139809, - "codeunitName": "APIV2 - Sales Invoices E2E", - "method": "TestPostInvoiceWithEmail" + "codeunitId": 139828, + "codeunitName": "APIV2 - Sales Credit Memos E2E", + "method": "TestCreateCreditMemoThroughPageAndAPI" }, { - "codeunitId": 139810, - "codeunitName": "APIV2 - Countries/Regions E2E", - "method": "TestCreateCountriesRegions" + "codeunitId": 139834, + "codeunitName": "APIV2 - Sales Inv. Lines E2E", + "method": "TestGettingLinesWithDifferentTypes" }, { - "codeunitId": 139810, - "codeunitName": "APIV2 - Countries/Regions E2E", - "method": "TestDeleteCountriesRegions" + "codeunitId": 139834, + "codeunitName": "APIV2 - Sales Inv. Lines E2E", + "method": "TestPatchingTheIdToItemChangesLineType" }, { - "codeunitId": 139810, - "codeunitName": "APIV2 - Countries/Regions E2E", - "method": "TestGetCountriesRegions" + "codeunitId": 139835, + "codeunitName": "APIV2 - Sales Order Lines E2E", + "method": "TestGettingLinesWithDifferentTypes" }, { - "codeunitId": 139810, - "codeunitName": "APIV2 - Countries/Regions E2E", - "method": "TestModifyCountriesRegions" + "codeunitId": 139835, + "codeunitName": "APIV2 - Sales Order Lines E2E", + "method": "TestPatchingTheIdToItemChangesLineType" }, { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestActionShipAndInvoice" + "codeunitId": 139839, + "codeunitName": "APIV2 - Item Variants E2E", + "method": "TestDeleteInUse" }, { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestClearingManualDiscounts" + "codeunitId": 139843, + "codeunitName": "APIV2 - Vendor Payments E2E", + "method": "TestCreateVendorPaymentWithoutDocNo" }, { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", + "codeunitId": 139851, + "codeunitName": "APIV2 - Purchase Orders E2E", "method": "TestCreateOrderThroughPageAndAPI" }, { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestDeleteOrders" - }, - { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestEmptyModifyOrders" - }, - { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestGetOrders" - }, - { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestGetOrdersAppliesDiscountPct" - }, - { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestGetOrdersRedistributesDiscountAmt" - }, - { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestModifyOrders" - }, - { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestModifyOrderSetManualDiscount" - }, - { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestPartialModifyOrders" - }, - { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestPostOrderForCustomerWithLocationCode" + "codeunitId": 139852, + "codeunitName": "APIV2 - Purch. Order Lines E2E", + "method": "TestGettingLinesWithDifferentTypes" }, { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestPostOrders" + "codeunitId": 139852, + "codeunitName": "APIV2 - Purch. Order Lines E2E", + "method": "TestPatchingTheIdToItemChangesLineType" }, { - "codeunitId": 139811, - "codeunitName": "APIV2 - Sales Orders E2E", - "method": "TestPostOrderWithCurrency" + "codeunitId": 139865, + "codeunitName": "APIV2 - Purch. Cr. Memos E2E", + "method": "TestCreateCreditMemoThroughPageAndAPI" }, { - "codeunitId": 139812, - "codeunitName": "APIV2 - UofM E2E", - "method": "TestDeleteUnitOfMeasure" + "codeunitId": 139867, + "codeunitName": "APIV2 - Apply Vendor Ent. E2E", + "method": "TestApplyVendorEntryAppliesPayment" }, { - "codeunitId": 139812, - "codeunitName": "APIV2 - UofM E2E", - "method": "TestGetUnitOfMeasure" + "codeunitId": 139928, + "codeunitName": "APIV2 - Purch. Ret. Shpts. E2E", + "method": "*" }, { - "codeunitId": 139812, - "codeunitName": "APIV2 - UofM E2E", - "method": "TestInsertUnitOfMeasure" + "codeunitId": 139908, + "codeunitName": "APIV2 - Bl. Purch. Orders E2E", + "method": "*" }, { - "codeunitId": 139812, - "codeunitName": "APIV2 - UofM E2E", - "method": "TestModifyUnitOfMeasure" + "codeunitId": 139927, + "codeunitName": "APIV2 - Sales Ord. Arch. E2E", + "method": "*" }, { - "codeunitId": 139813, - "codeunitName": "APIV2 - Currencies E2E", - "method": "TestCreateCurrency" + "codeunitId": 139909, + "codeunitName": "APIV2 - Bl. Sales Orders E2E", + "method": "*" }, { - "codeunitId": 139813, - "codeunitName": "APIV2 - Currencies E2E", - "method": "TestDeleteCurrency" + "codeunitId": 139924, + "codeunitName": "APIV2 - Sales Ret. Orders E2E", + "method": "*" }, { - "codeunitId": 139813, - "codeunitName": "APIV2 - Currencies E2E", - "method": "TestGetCurrencies" + "codeunitId": 139923, + "codeunitName": "APIV2 - Purch. Ret. Orders E2E", + "method": "*" }, { - "codeunitId": 139813, - "codeunitName": "APIV2 - Currencies E2E", - "method": "TestModifyCurrency" + "codeunitId": 139922, + "codeunitName": "APIV2 - Purch. Quotes E2E", + "method": "*" }, { - "codeunitId": 139814, - "codeunitName": "APIV2 - Payment Methods E2E", - "method": "TestCreatePaymentMethods" - }, - { - "codeunitId": 139814, - "codeunitName": "APIV2 - Payment Methods E2E", - "method": "TestDeletePaymentMethods" - }, - { - "codeunitId": 139814, - "codeunitName": "APIV2 - Payment Methods E2E", - "method": "TestGetPaymentMethods" - }, - { - "codeunitId": 139814, - "codeunitName": "APIV2 - Payment Methods E2E", - "method": "TestModifyPaymentMethods" - }, - { - "codeunitId": 139815, - "codeunitName": "APIV2 - Balance Sheet E2E", - "method": "TestGetBalanceSheetRecords" - }, - { - "codeunitId": 139816, - "codeunitName": "APIV2 - Income Statement E2E", - "method": "TestGetIncomeStatementRecords" - }, - { - "codeunitId": 139817, - "codeunitName": "APIV2 - CashFlow Statement E2E", - "method": "TestGetCashFlowRecords" - }, - { - "codeunitId": 139818, - "codeunitName": "APIV2 - Trial Balance E2E", - "method": "TestGetTrialBalanceRecords" - }, - { - "codeunitId": 139819, - "codeunitName": "APIV2 - Aged AP E2E", - "method": "TestGetAgedAPRecords" - }, - { - "codeunitId": 139820, - "codeunitName": "APIV2 - Aged AR E2E", - "method": "TestGetAgedARRecords" - }, - { - "codeunitId": 139821, - "codeunitName": "APIV2 - Ret. Earnings E2E", - "method": "TestRetainedEarningsStatementRecords" - }, - { - "codeunitId": 139822, - "codeunitName": "APIV2 - Employees E2E", - "method": "TestCreateEmployee" - }, - { - "codeunitId": 139822, - "codeunitName": "APIV2 - Employees E2E", - "method": "TestDeleteEmployee" - }, - { - "codeunitId": 139822, - "codeunitName": "APIV2 - Employees E2E", - "method": "TestGetEmployee" - }, - { - "codeunitId": 139822, - "codeunitName": "APIV2 - Employees E2E", - "method": "TestModifyEmployee" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestActionMakeInvoiceFromQuote" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestActionMakeOrderFromQuote" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestActionSendQuote" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestClearingManualDiscounts" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestCreateQuoteThroughPageAndAPI" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestDeleteQuotes" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestEmptyModifyQuotes" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestGetQuotes" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestGetQuotesAppliesDiscountPct" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestGetQuotesRedistributesDiscountAmt" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestModifyQuotes" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestModifyQuoteSetManualDiscount" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestPartialModifyQuotes" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestPostQuotes" - }, - { - "codeunitId": 139823, - "codeunitName": "APIV2 - Sales Quotes E2E", - "method": "TestPostQuoteWithCurrency" - }, - { - "codeunitId": 139824, - "codeunitName": "APIV2 - Dimensions E2E", - "method": "TestGetDimensions" - }, - { - "codeunitId": 139824, - "codeunitName": "APIV2 - Dimensions E2E", - "method": "TestGetDimensionValues" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestCreateDoesntWorkWithAlreadyExistingCode" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestCreateJournalLineDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestCreatePurchaseCrMemoDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestCreatePurchaseInvoiceDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestCreatePurchaseOrderDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestCreateSalesCrMemoDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestCreateSalesInvoiceDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestCreateSalesOrderDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestCreateSalesQuoteDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestCreateTimeRegEntryDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestDeleteDimensionSetLineOfGenJournalLines" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestDeletePurchaseCrMemoDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestDeletePurchaseInvoiceDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestDeletePurchaseOrderDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestDeleteSalesCrMemoDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestDeleteSalesInvoiceDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestDeleteSalesOrderDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestDeleteSalesQuoteDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestDeleteTimeRegEntryDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestGetDimensionSetLinesOfGLEntry" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestGetDimensionSetLinesOfJournalLines" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestGetDimensionSetLinesOfPurchaseCrMemo" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestGetDimensionSetLinesOfPurchaseInvoice" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestGetDimensionSetLinesOfPurchaseOrder" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestGetDimensionSetLinesOfSalesCrMemo" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestGetDimensionSetLinesOfSalesInvoice" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestGetDimensionSetLinesOfSalesOrder" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestGetDimensionSetLinesOfSalesQuote" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestGetDimensionSetLinesOfTimeRegEntry" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestModifyDimensionSetLineOfGenJournalLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestModifyDimensionSetLineOfSalesOrder" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestModifyDimensionSetLineOfSalesQuote" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestModifyOfDimensionCodeDoesntWork" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestModifyPurchaseCrMemoDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestModifyPurchaseInvoiceDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestModifyPurchaseOrderDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestModifySalesCrMemoDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestModifySalesInvoiceDimensionSetLine" - }, - { - "codeunitId": 139825, - "codeunitName": "APIV2 - Dim. Set Lines E2E", - "method": "TestModifyTimeRegEntryDimensionSetLine" - }, - { - "codeunitId": 139827, - "codeunitName": "APIV2 - Journals E2E", - "method": "TestActionPostGenJournalBatch" - }, - { - "codeunitId": 139827, - "codeunitName": "APIV2 - Journals E2E", - "method": "TestCreateJournal" - }, - { - "codeunitId": 139827, - "codeunitName": "APIV2 - Journals E2E", - "method": "TestDeleteJournal" - }, - { - "codeunitId": 139827, - "codeunitName": "APIV2 - Journals E2E", - "method": "TestGetJournals" - }, - { - "codeunitId": 139827, - "codeunitName": "APIV2 - Journals E2E", - "method": "TestModifyJournal" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestActionCancelAndSendCorrectiveCreditMemo" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestActionCancelCorrectiveCreditMemo" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestActionPostAndSendCreditMemo" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestActionPostCreditMemo" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestActionSendCancelledCreditMemo" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestActionSendPostedCreditMemo" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestClearingManualDiscounts" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestCreateCreditMemoThroughPageAndAPI" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestDeleteCreditMemo" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestEmptyModifyCreditMemos" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestGetCreditMemos" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestGetCreditMemosAppliesDiscountPct" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestGetCreditMemosRedistributesDiscountAmt" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestModifyCreditMemos" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestModifyCreditMemoSetManualDiscount" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestPartialModifyCreditMemos" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestPostCreditMemos" - }, - { - "codeunitId": 139828, - "codeunitName": "APIV2 - Sales Credit Memos E2E", - "method": "TestPostCreditMemoWithCurrency" - }, - { - "codeunitId": 139829, - "codeunitName": "APIV2 - Purchase Invoices E2E", - "method": "TestActionPostPurchaseInvoice" - }, - { - "codeunitId": 139829, - "codeunitName": "APIV2 - Purchase Invoices E2E", - "method": "TestCreateInvoiceThroughPageAndAPI" - }, - { - "codeunitId": 139829, - "codeunitName": "APIV2 - Purchase Invoices E2E", - "method": "TestDeleteInvoice" - }, - { - "codeunitId": 139829, - "codeunitName": "APIV2 - Purchase Invoices E2E", - "method": "TestEmptyModifyInvoices" - }, - { - "codeunitId": 139829, - "codeunitName": "APIV2 - Purchase Invoices E2E", - "method": "TestGetInvoiceFromPostedOrderCorrectOrderIdAndNo" - }, - { - "codeunitId": 139829, - "codeunitName": "APIV2 - Purchase Invoices E2E", - "method": "TestGetInvoices" - }, - { - "codeunitId": 139829, - "codeunitName": "APIV2 - Purchase Invoices E2E", - "method": "TestGetPurchaseInvoiceFromDraftId" - }, - { - "codeunitId": 139829, - "codeunitName": "APIV2 - Purchase Invoices E2E", - "method": "TestModifyInvoices" - }, - { - "codeunitId": 139829, - "codeunitName": "APIV2 - Purchase Invoices E2E", - "method": "TestPartialModifyInvoices" - }, - { - "codeunitId": 139829, - "codeunitName": "APIV2 - Purchase Invoices E2E", - "method": "TestPostInvoices" - }, - { - "codeunitId": 139829, - "codeunitName": "APIV2 - Purchase Invoices E2E", - "method": "TestPostPurchaseInvoiceWithCurrency" - }, - { - "codeunitId": 139830, - "codeunitName": "APIV2 - GLEntries E2E", - "method": "TestGetGLEntries" - }, - { - "codeunitId": 139831, - "codeunitName": "APIV2 - Automation RS Package", - "method": "TestApplyRSPackage" - }, - { - "codeunitId": 139831, - "codeunitName": "APIV2 - Automation RS Package", - "method": "TestCreateRSPackage" - }, - { - "codeunitId": 139831, - "codeunitName": "APIV2 - Automation RS Package", - "method": "TestImportRSPackage" - }, - { - "codeunitId": 139831, - "codeunitName": "APIV2 - Automation RS Package", - "method": "TestImportWrongRSPackage" - }, - { - "codeunitId": 139831, - "codeunitName": "APIV2 - Automation RS Package", - "method": "TestUploadRSPackageFile" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestCreateDraftInvoiceAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestCreateDraftOrderAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestCreateDraftPurchaseCreditMemoAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestCreateDraftPurchaseInvoiceAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestCreateDraftPurchaseOrderAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestCreateGLEntryAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestCreateJournalLineAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestCreatePostedInvoiceAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestCreatePostedPurchaseCreditMemoAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestCreatePostedPurchaseInvoiceAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestCreateQuoteAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestDeleteDraftInvoiceAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestDeleteDraftPurchaseCreditMemoAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestDeleteDraftPurchaseInvoiceAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestDeleteGLEntryAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestDeleteJournalLineAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestDeletePostedInvoiceAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestDeletePostedPurchaseCreditMemoAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestDeletePostedPurchaseInvoiceAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestDeleteQuoteAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetDraftInvoiceAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetDraftInvoiceAttachments" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetDraftOrderAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetDraftOrderAttachments" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetDraftPurchaseCreditMemoAttachments" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetDraftPurchaseInvoiceAttachments" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetDraftPurchaseOrderAttachments" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetGLEntryAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetGLEntryAttachments" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetJournalAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetJournalLineAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetJournalLineAttachments" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetPostedInvoiceAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetPostedInvoiceAttachments" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetPostedPurchaseCreditMemoAttachments" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetPostedPurchaseInvoiceAttachments" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetQuoteAttachment" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestGetQuoteAttachments" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestLinkedAttachmentContentChangeKeepsOtherFieldsUnchanged" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestLinkedAttachmentDocumentIdChangeNotAllowed" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestLinkedAttachmentFileNameChangeKeepsOtherFieldsUnchanged" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestTransferAttachmentFromDraftToPostedInvoice" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestTransferAttachmentFromDraftToPostedPurchaseCreditMemo" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestTransferAttachmentFromDraftToPostedPurchaseInvoice" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestUpdateGenJournalLineAttachmentBinaryContent" - }, - { - "codeunitId": 139833, - "codeunitName": "APIV2 - Attachments E2E", - "method": "TestUpdateGLEntryAttachmentBinaryContent" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestCreateLineThroughPageAndAPI" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestDeleteInvoiceLine" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestDeletingLineKeepsInvoiceDiscountAmt" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestDeletingLineMovesInvoiceDiscountPct" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestDeletingLineRemovesInvoiceDiscountPct" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestGetInvoiceLineDirectly" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestGetInvoiceLines" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestGetInvoiceLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestGetPostedInvoiceLines" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestGettingLinesWithDifferentTypes" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestInsertingLineKeepsInvoiceDiscountAmt" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestInsertingLineUpdatesInvoiceDiscountPct" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestModifyingLineKeepsInvoiceDiscountAmt" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestModifyingLineUpdatesInvoiceDiscountPct" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestModifyInvoiceLine" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestModifyInvoiceLineWithUoM" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestPatchingTheIdToAccountChangesLineType" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestPatchingTheIdToItemChangesLineType" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestPatchingTheTypeBlanksIds" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestPostingBlankLineDefaultsToCommentType" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestPostingCommentLine" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestPostInvoiceLines" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestPostInvoiceLinesWithItemVariant" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestPostInvoiceLinesWithUoM" - }, - { - "codeunitId": 139834, - "codeunitName": "APIV2 - Sales Inv. Lines E2E", - "method": "TestPostInvoiceLineWithSequence" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestCreateLineThroughPageAndAPI" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestCreatingOrderLineWithAssemblyBOM" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestDeleteOrderLine" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestDeletingLineKeepsOrderDiscountAmt" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestDeletingLineMovesOrderDiscountPct" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestDeletingLineRemovesOrderDiscountPct" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestExpandReturnsCorrectDocumentLines" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestExpandReturnsCorrectDocumentLinesWithMultipleDocuments" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestGetOrderLineDirectly" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestGetOrderLines" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestGetOrderLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestGettingLinesWithDifferentTypes" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestInsertingLineKeepsOrderDiscountAmt" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestInsertingLineUpdatesOrderDiscountPct" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestModifyingLineKeepsOrderDiscountAmt" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestModifyingLineUpdatesOrderDiscountPct" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestModifyOrderLines" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestPatchingTheIdToAccountChangesLineType" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestPatchingTheIdToItemChangesLineType" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestPatchingTheTypeBlanksIds" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestPostingBlankLineDefaultsToCommentType" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestPostingCommentLine" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestPostOrderLines" - }, - { - "codeunitId": 139835, - "codeunitName": "APIV2 - Sales Order Lines E2E", - "method": "TestPostOrderLineWithItemVariant" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestDeleteQuoteLine" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestDeletingLineKeepsQuoteDiscountAmt" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestDeletingLineMovesQuoteDiscountPct" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestDeletingLineRemovesQuoteDiscountPct" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestGetQuoteLineDirectly" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestGetQuoteLines" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestGetQuoteLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestInsertingLineKeepsQuoteDiscountAmt" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestInsertingLineUpdatesQuoteDiscountPct" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestModifyingLineKeepsQuoteDiscountAmt" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestModifyingLineUpdatesQuoteDiscountPct" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestModifyQuoteLines" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestPatchingTheTypeBlanksIds" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestPostingBlankLineDefaultsToCommentType" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestPostingCommentLine" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestPostQuoteLines" - }, - { - "codeunitId": 139836, - "codeunitName": "APIV2 - Sales Quote Lines E2E", - "method": "TestPostQuoteLinesWithItemVariant" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestCreateLineThroughPageAndAPI" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestDeleteCreditMemoLine" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestDeletePostedCreditMemoLine" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestDeletingLineKeepsCreditMemoDiscountAmt" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestDeletingLineMovesCreditMemoDiscountPct" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestDeletingLineRemovesCreditMemoDiscountPct" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestGetCreditMemoLineDirectly" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestGetCreditMemoLines" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestGetCreditMemoLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestGetPostedCreditMemoLines" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestGettingLinesWithDifferentTypes" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestInsertingLineKeepsCreditMemoDiscountAmt" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestInsertingLineUpdatesCreditMemoDiscountPct" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestModifyCreditMemoLines" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestModifyingLineKeepsCreditMemoDiscountAmt" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestModifyingLineUpdatesCreditMemoDiscountPct" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestPatchingTheIdToAccountChangesLineType" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestPatchingTheIdToItemChangesLineType" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestPatchingTheTypeBlanksIds" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestPostCreditMemoLines" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestPostCreditMemoLinesWithItemVariant" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestPostCreditMemoLineWithSequence" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestPostingBlankLineDefaultsToCommentType" - }, - { - "codeunitId": 139837, - "codeunitName": "APIV2 - Sales CrMemo Lines E2E", - "method": "TestPostingCommentLine" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestCreateLineThroughPageAndAPI" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestDeleteInvoiceLine" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestDeletingLineRemovesInvoiceDiscountPct" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestGetInvoiceLineDirectly" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestGetInvoiceLines" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestGetInvoiceLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestGetPostedInvoiceLines" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestInsertingLineKeepsInvoiceDiscountAmt" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestInsertingLineUpdatesInvoiceDiscountPct" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestModifyingLineUpdatesInvoiceDiscountPct" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestModifyInvoiceLines" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestPatchingTheTypeBlanksIds" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestPostingBlankLineDefaultsToCommentType" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestPostingCommentLine" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestPostInvoiceLines" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestPostInvoiceLinesWithItemVariant" - }, - { - "codeunitId": 139838, - "codeunitName": "APIV2 - Purchase Inv Lines E2E", - "method": "TestPostInvoiceLineWithSequence" - }, - { - "codeunitId": 139839, - "codeunitName": "APIV2 - Item Variants E2E", - "method": "TestCreateItemVariantWithItemId" - }, - { - "codeunitId": 139839, - "codeunitName": "APIV2 - Item Variants E2E", - "method": "TestCreateItemVariantWithItemNumber" - }, - { - "codeunitId": 139839, - "codeunitName": "APIV2 - Item Variants E2E", - "method": "TestCreateItemVariantWithItemNumberAndItemId" - }, - { - "codeunitId": 139839, - "codeunitName": "APIV2 - Item Variants E2E", - "method": "TestDeleteInUse" - }, - { - "codeunitId": 139839, - "codeunitName": "APIV2 - Item Variants E2E", - "method": "TestDeleteItemVariant" - }, - { - "codeunitId": 139839, - "codeunitName": "APIV2 - Item Variants E2E", - "method": "TestGetItemVariant" - }, - { - "codeunitId": 139839, - "codeunitName": "APIV2 - Item Variants E2E", - "method": "TestModifyItemVariant" - }, - { - "codeunitId": 139839, - "codeunitName": "APIV2 - Item Variants E2E", - "method": "TestModifyItemVariantItemId" - }, - { - "codeunitId": 139839, - "codeunitName": "APIV2 - Item Variants E2E", - "method": "TestModifyItemVariantItemNumber" - }, - { - "codeunitId": 139840, - "codeunitName": "APIV2 - Customer Payments E2E", - "method": "TestAppliesToInvoiceNoAndIdSync" - }, - { - "codeunitId": 139840, - "codeunitName": "APIV2 - Customer Payments E2E", - "method": "TestCreateCustomerPayment" - }, - { - "codeunitId": 139840, - "codeunitName": "APIV2 - Customer Payments E2E", - "method": "TestCreateCustomerPaymentWithInvoiceId" - }, - { - "codeunitId": 139840, - "codeunitName": "APIV2 - Customer Payments E2E", - "method": "TestCreateCustomerPaymentWithoutDocNo" - }, - { - "codeunitId": 139840, - "codeunitName": "APIV2 - Customer Payments E2E", - "method": "TestCustomerAutofillDoesNotOverwrite" - }, - { - "codeunitId": 139840, - "codeunitName": "APIV2 - Customer Payments E2E", - "method": "TestCustomerAutofillWhenGivingInvoiceNumber" - }, - { - "codeunitId": 139840, - "codeunitName": "APIV2 - Customer Payments E2E", - "method": "TestCustomerNoAndIdSync" - }, - { - "codeunitId": 139840, - "codeunitName": "APIV2 - Customer Payments E2E", - "method": "TestDeleteCustomerPayment" - }, - { - "codeunitId": 139840, - "codeunitName": "APIV2 - Customer Payments E2E", - "method": "TestGetCustomerPayment" - }, - { - "codeunitId": 139840, - "codeunitName": "APIV2 - Customer Payments E2E", - "method": "TestModifyCustomerPayment" - }, - { - "codeunitId": 139840, - "codeunitName": "APIV2 - Customer Payments E2E", - "method": "TestModifyCustomerPaymentWithRandomDocNo" - }, - { - "codeunitId": 139841, - "codeunitName": "APIV2 - PDF Document E2E", - "method": "TestGetPDFPurchaseCreditMemo" - }, - { - "codeunitId": 139841, - "codeunitName": "APIV2 - PDF Document E2E", - "method": "TestGetPDFPurchaseInvoice" - }, - { - "codeunitId": 139841, - "codeunitName": "APIV2 - PDF Document E2E", - "method": "TestGetPDFSalesCreditMemo" - }, - { - "codeunitId": 139841, - "codeunitName": "APIV2 - PDF Document E2E", - "method": "TestGetPDFSalesInvoice" - }, - { - "codeunitId": 139841, - "codeunitName": "APIV2 - PDF Document E2E", - "method": "TestGetPDFSalesOrder" - }, - { - "codeunitId": 139841, - "codeunitName": "APIV2 - PDF Document E2E", - "method": "TestGetPDFSalesQuote" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureMetadataContact" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureMetadataContactWithoutPicture" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureMetadataCustomer" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureMetadataCustomerWithoutPicture" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureMetadataEmployee" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureMetadataEmployeeWithoutPicture" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureMetadataItem" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureMetadataVendor" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureMetadataVendorWithoutPicture" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureMetadatItemWithoutPicture" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureValueContact" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureValueContactWithoutPicture" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureValueCustomer" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureValueCustomerWithoutPicture" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureValueEmployee" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureValueEmployeeWithoutPicture" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureValueItem" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureValueItemWithoutPicture" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureValueVendor" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestGetPictureValueVendorWithoutPicture" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureDeleteContact" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureDeleteCustomer" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureDeleteEmployee" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureDeleteItem" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureDeleteVendor" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureReplaceContact" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureReplaceCustomer" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureReplaceEmployee" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureReplaceItem" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureReplaceVendor" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureUploadContact" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureUploadCustomer" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureUploadEmployee" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureUploadItem" - }, - { - "codeunitId": 139842, - "codeunitName": "APIV2 - Pictures E2E", - "method": "TestPictureUploadVendor" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestAppliesToInvoiceNoAndIdSync" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestAppliesToInvoiceNumberEmptyForNonInvoiceApplication" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestCreateVendorPayment" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestCreateVendorPaymentWithInvoiceId" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestCreateVendorPaymentWithoutDocNo" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestDeleteVendorPayment" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestGetVendorPayment" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestGetVendorPaymentWithoutInvoiceApplicationIsVisible" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestModifyVendorPayment" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestModifyVendorPaymentWithRandomDocNo" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestVendorAutofillDoesNotOverwrite" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestVendorAutofillWhenGivingInvoiceNumber" - }, - { - "codeunitId": 139843, - "codeunitName": "APIV2 - Vendor Payments E2E", - "method": "TestVendorNoAndIdSync" - }, - { - "codeunitId": 139847, - "codeunitName": "APIV2 - Sales Shipments E2E", - "method": "TestExpandDimensions" - }, - { - "codeunitId": 139847, - "codeunitName": "APIV2 - Sales Shipments E2E", - "method": "TestGetSalesShipments" - }, - { - "codeunitId": 139848, - "codeunitName": "APIV2 - Sales Ship. Lines E2E", - "method": "TestExpandDimensions" - }, - { - "codeunitId": 139848, - "codeunitName": "APIV2 - Sales Ship. Lines E2E", - "method": "TestGetLineDirectly" - }, - { - "codeunitId": 139848, - "codeunitName": "APIV2 - Sales Ship. Lines E2E", - "method": "TestGetShipmentLines" - }, - { - "codeunitId": 139848, - "codeunitName": "APIV2 - Sales Ship. Lines E2E", - "method": "TestGetShipmentLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139849, - "codeunitName": "APIV2 - Purchase Receipts E2E", - "method": "TestExpandDimensions" - }, - { - "codeunitId": 139849, - "codeunitName": "APIV2 - Purchase Receipts E2E", - "method": "TestGetPurchaseReceipts" - }, - { - "codeunitId": 139850, - "codeunitName": "APIV2 - Purch. Rcpt. Lines E2E", - "method": "TestExpandDimensions" - }, - { - "codeunitId": 139850, - "codeunitName": "APIV2 - Purch. Rcpt. Lines E2E", - "method": "TestGetLineDirectly" - }, - { - "codeunitId": 139850, - "codeunitName": "APIV2 - Purch. Rcpt. Lines E2E", - "method": "TestGetReceiptLines" - }, - { - "codeunitId": 139850, - "codeunitName": "APIV2 - Purch. Rcpt. Lines E2E", - "method": "TestGetReceiptLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestActionRecieveAndInvoice" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestClearingManualDiscounts" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestCreateOrderThroughPageAndAPI" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestDeleteOrders" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestEmptyModifyOrders" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestGetOrders" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestGetOrdersAppliesDiscountPct" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestGetOrdersRedistributesDiscountAmt" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestModifyOrders" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestModifyOrderSetManualDiscount" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestPartialModifyOrders" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestPostOrderForCustomerWithLocationCode" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestPostOrders" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestPostOrdersWithEmptyShippingAddress" - }, - { - "codeunitId": 139851, - "codeunitName": "APIV2 - Purchase Orders E2E", - "method": "TestPostOrderWithCurrency" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestCreateLineThroughPageAndAPI" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestDeleteOrderLine" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestDeletingLineKeepsOrderDiscountAmt" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestDeletingLineMovesOrderDiscountPct" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestDeletingLineRemovesOrderDiscountPct" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestGetOrderLineDirectly" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestGetOrderLines" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestGetOrderLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestGettingLinesWithDifferentTypes" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestInsertingLineKeepsOrderDiscountAmt" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestInsertingLineUpdatesOrderDiscountPct" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestModifyingLineKeepsOrderDiscountAmt" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestModifyingLineUpdatesOrderDiscountPct" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestModifyOrderLines" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestPatchingTheIdToAccountChangesLineType" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestPatchingTheIdToItemChangesLineType" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestPatchingTheTypeBlanksIds" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestPostingBlankLineDefaultsToCommentType" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestPostingCommentLine" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestPostOrderLines" - }, - { - "codeunitId": 139852, - "codeunitName": "APIV2 - Purch. Order Lines E2E", - "method": "TestPostOrderLineWithItemVariant" - }, - { - "codeunitId": 139853, - "codeunitName": "APIV2 - Contacts Inf. E2E", - "method": "TestGetCustomerContactInformation" - }, - { - "codeunitId": 139853, - "codeunitName": "APIV2 - Contacts Inf. E2E", - "method": "TestGetCustomerContactsInformation" - }, - { - "codeunitId": 139853, - "codeunitName": "APIV2 - Contacts Inf. E2E", - "method": "TestGetVendorContactInformation" - }, - { - "codeunitId": 139853, - "codeunitName": "APIV2 - Contacts Inf. E2E", - "method": "TestGetVendorContactsInformation" - }, - { - "codeunitId": 139854, - "codeunitName": "APIV2 - Item Ledg. Entries E2E", - "method": "TestGetItemLedgerEntries" - }, - { - "codeunitId": 139855, - "codeunitName": "APIV2 - Opportunities E2E", - "method": "TestCannotPatchContactNoOfWonOpportunity" - }, - { - "codeunitId": 139855, - "codeunitName": "APIV2 - Opportunities E2E", - "method": "TestCannotPatchSPCodeOfWonOpportunity" - }, - { - "codeunitId": 139855, - "codeunitName": "APIV2 - Opportunities E2E", - "method": "TestCreateOpportunity" - }, - { - "codeunitId": 139855, - "codeunitName": "APIV2 - Opportunities E2E", - "method": "TestCreateOpportunityContactNo" - }, - { - "codeunitId": 139855, - "codeunitName": "APIV2 - Opportunities E2E", - "method": "TestGetOpportunities" - }, - { - "codeunitId": 139855, - "codeunitName": "APIV2 - Opportunities E2E", - "method": "TestPatchOpportunity" - }, - { - "codeunitId": 139856, - "codeunitName": "APIV2 - GenProdPostGroup E2E", - "method": "TestGetGenProdPostGroups" - }, - { - "codeunitId": 139857, - "codeunitName": "APIV2 - InventoryPostGroup E2E", - "method": "TestGetInventoryPostGroups" - }, - { - "codeunitId": 139858, - "codeunitName": "APIV2 Currency Exch. Rate E2E", - "method": "TestGetCurrencyExchangeRates" - }, - { - "codeunitId": 139858, - "codeunitName": "APIV2 Currency Exch. Rate E2E", - "method": "TestSingleGetCurrencyExchangeRate" - }, - { - "codeunitId": 139859, - "codeunitName": "APIV2 - Accounting Periods E2E", - "method": "TestGetAccountingPeriods" - }, - { - "codeunitId": 139860, - "codeunitName": "APIV2 - G/L Setup E2E", - "method": "TestGetGLSetup" - }, - { - "codeunitId": 139861, - "codeunitName": "APIV2JobQueueLogEntriesE2E", - "method": "TestGetJobQueueLogEntry" - }, - { - "codeunitId": 139862, - "codeunitName": "APIV2JobQueueEntriesE2E", - "method": "TestGetCorrespondingJobQueueLogEntryFromExpand" - }, - { - "codeunitId": 139862, - "codeunitName": "APIV2JobQueueEntriesE2E", - "method": "TestGetCorrespondingJobQueueLogEntryFromSubPage" - }, - { - "codeunitId": 139862, - "codeunitName": "APIV2JobQueueEntriesE2E", - "method": "TestGetJobQueueEntry" - }, - { - "codeunitId": 139862, - "codeunitName": "APIV2JobQueueEntriesE2E", - "method": "TestRescheduleJobQueueEntry" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestActionCancelCorrectiveCreditMemo" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestActionPostCreditMemo" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestClearingManualDiscounts" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestCreateCreditMemoThroughPageAndAPI" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestDeleteCreditMemo" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestEmptyModifyCreditMemos" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestGetCreditMemos" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestModifyCreditMemos" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestModifyCreditMemoSetManualDiscount" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestPartialModifyCreditMemos" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestPostCreditMemos" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestPostCreditMemoWithCurrency" - }, - { - "codeunitId": 139865, - "codeunitName": "APIV2 - Purch. Cr. Memos E2E", - "method": "TestPostCreditMemoWithVendorCreditMemoNo" - }, - { - "codeunitId": 139866, - "codeunitName": "APIV2 - Contacts E2E", - "method": "TestDeleteContact" - }, - { - "codeunitId": 139866, - "codeunitName": "APIV2 - Contacts E2E", - "method": "TestGetContactWithAddressAndSpecialCharacters" - }, - { - "codeunitId": 139866, - "codeunitName": "APIV2 - Contacts E2E", - "method": "TestGetSimpleContact" - }, - { - "codeunitId": 139866, - "codeunitName": "APIV2 - Contacts E2E", - "method": "TestModifyContactWithAddress" - }, - { - "codeunitId": 139867, - "codeunitName": "APIV2 - Apply Vendor Ent. E2E", - "method": "TestApplyVendorEntryAppliesPayment" - }, - { - "codeunitId": 139867, - "codeunitName": "APIV2 - Apply Vendor Ent. E2E", - "method": "TestGetApplyVendorEntriesFailsWhenNoVendor" - }, - { - "codeunitId": 139867, - "codeunitName": "APIV2 - Apply Vendor Ent. E2E", - "method": "TestGetApplyVendorEntriesReturnsOpenEntries" - }, - { - "codeunitId": 139868, - "codeunitName": "APIV2 - Locations E2E", - "method": "TestCreateLocation" - }, - { - "codeunitId": 139868, - "codeunitName": "APIV2 - Locations E2E", - "method": "TestCreateSalesOrderLineWithLocation" - }, - { - "codeunitId": 139868, - "codeunitName": "APIV2 - Locations E2E", - "method": "TestDeleteLocation" - }, - { - "codeunitId": 139868, - "codeunitName": "APIV2 - Locations E2E", - "method": "TestGetLocation" - }, - { - "codeunitId": 139868, - "codeunitName": "APIV2 - Locations E2E", - "method": "TestModifyLocationWithAddress" - }, - { - "codeunitId": 139869, - "codeunitName": "APIV2 - Cust. Ret. Reasons E2E", - "method": "TestCreateReasonCode" - }, - { - "codeunitId": 139869, - "codeunitName": "APIV2 - Cust. Ret. Reasons E2E", - "method": "TestDeleteReasonCode" - }, - { - "codeunitId": 139869, - "codeunitName": "APIV2 - Cust. Ret. Reasons E2E", - "method": "TestGetReasonCode" - }, - { - "codeunitId": 139869, - "codeunitName": "APIV2 - Cust. Ret. Reasons E2E", - "method": "TestModifyReasonCode" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestCreateLineThroughPageAndAPI" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestDeleteCreditMemoLine" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestDeletePostedCreditMemoLine" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestGetCreditMemoLineDirectly" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestGetCreditMemoLines" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestGetCreditMemoLinesDirectlyWithDocumentIdFilter" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestGetPostedCreditMemoLines" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestGettingLinesWithDifferentTypes" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestModifyCreditMemoLines" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestPatchingTheIdToAccountChangesLineType" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestPatchingTheIdToItemChangesLineType" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestPatchingTheTypeBlanksIds" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestPostCreditMemoLines" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestPostCreditMemoLinesWithItemVariant" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestPostCreditMemoLineWithSequence" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestPostingBlankLineDefaultsToCommentType" - }, - { - "codeunitId": 139874, - "codeunitName": "APIV2 - Purch. Cr.M. Lines E2E", - "method": "TestPostingCommentLine" - }, - { - "codeunitId": 139882, - "codeunitName": "APIV2 - Salesperson/Purch E2E", - "method": "CreateSalespersonPurchaser" - }, - { - "codeunitId": 139882, - "codeunitName": "APIV2 - Salesperson/Purch E2E", - "method": "DeleteSalespersonPurchaser" - }, - { - "codeunitId": 139882, - "codeunitName": "APIV2 - Salesperson/Purch E2E", - "method": "GetSalespersonPurchaser" - }, - { - "codeunitId": 139882, - "codeunitName": "APIV2 - Salesperson/Purch E2E", - "method": "ModifySalespersonPurchaserJobTitle" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestCreateCustomerAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestCreateDraftInvoiceAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestCreateDraftOrderAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestCreateDraftPurchaseCreditMemoAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestCreateDraftPurchaseInvoiceAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestCreateDraftPurchaseOrderAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestCreatePostedInvoiceAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestCreatePostedPurchaseCreditMemoAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestCreatePostedPurchaseInvoiceAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestCreateQuoteAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestCreateVendorAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestDeleteDraftInvoiceAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestDeleteDraftPurchaseCreditMemoAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestDeleteDraftPurchaseInvoiceAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestDeleteEmployeeAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestDeleteJobAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestDeletePostedInvoiceAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestDeletePostedPurchaseCreditMemoAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestDeletePostedPurchaseInvoiceAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestDeleteQuoteAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestDeleteVendorAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetCustomerAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetDraftInvoiceAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetDraftInvoiceAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetDraftOrderAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetDraftPurchaseCreditMemoAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetDraftPurchaseInvoiceAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetDraftPurchaseOrderAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetEmployeeAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetItemAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetJobAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetPostedInvoiceAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetPostedInvoiceAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetPostedPurchaseCreditMemoAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetPostedPurchaseInvoiceAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetQuoteAttachment" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetQuoteAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestGetVendorAttachments" - }, - { - "codeunitId": 139899, - "codeunitName": "APIV2 - Document Attach. E2E", - "method": "TestUpdateCustomerAttachmentBinaryContent" - }, - { - "codeunitId": 139906, - "codeunitName": "APIV2 - FA Locations E2E", - "method": "TestCreateFALocation" - }, - { - "codeunitId": 139906, - "codeunitName": "APIV2 - FA Locations E2E", - "method": "TestDeleteFALocation" - }, - { - "codeunitId": 139906, - "codeunitName": "APIV2 - FA Locations E2E", - "method": "TestGetFALocation" - }, - { - "codeunitId": 139906, - "codeunitName": "APIV2 - FA Locations E2E", - "method": "TestModifyFALocation" - }, - { - "codeunitId": 139907, - "codeunitName": "APIV2 - Fixed Assets E2E", - "method": "TestCreateFixedAsset" - }, - { - "codeunitId": 139907, - "codeunitName": "APIV2 - Fixed Assets E2E", - "method": "TestDeleteFixedAsset" - }, - { - "codeunitId": 139907, - "codeunitName": "APIV2 - Fixed Assets E2E", - "method": "TestGetFixedAsset" - }, - { - "codeunitId": 139907, - "codeunitName": "APIV2 - Fixed Assets E2E", - "method": "TestModifyFixedAsset" - }, - { - "codeunitId": 139908, - "method": "TestGetBlanketPurchaseOrders", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139908, - "method": "TestGetBlanketPurchaseOrderById", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139908, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139908, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139908, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139908, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139908, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestGetBlanketSalesOrders", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestGetBlanketSalesOrderById", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139917, - "method": "TestGetApprovalEntries", - "codeunitName": "APIV2 - Approval Entries E2E" - }, - { - "codeunitId": 139918, - "method": "TestGetApprovalUserSetup", - "codeunitName": "APIV2 - Approval UserSetup E2E" - }, - { - "codeunitId": 139919, - "method": "TestGetPostedApprovalEntries", - "codeunitName": "APIV2 PstdApprovalEntries E2E" - }, - { - "codeunitId": 139920, - "method": "TestGetApprovalEntries", - "codeunitName": "APIV2 - Workflow Approvers E2E" - }, - { - "codeunitId": 139921, - "method": "TestGetApprovalEntries", - "codeunitName": "APIV2 - Workflows E2E" - }, - { - "codeunitId": 139922, - "method": "TestGetPurchaseQuotes", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139922, - "method": "TestGetPurchaseQuoteById", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139922, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139922, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139922, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139922, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139922, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139923, - "method": "TestGetPurchaseReturnOrders", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139923, - "method": "TestGetPurchaseReturnOrderById", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139923, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139923, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139923, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139923, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139923, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestGetSalesReturnOrders", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestGetSalesReturnOrderById", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestGetAssemblyOrders", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestGetAssemblyOrderById", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestGetTransferOrders", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestGetTransferOrderById", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139927, - "method": "TestGetSalesOrderArchives", - "codeunitName": "APIV2 - Sales Ord. Arch. E2E" - }, - { - "codeunitId": 139927, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Sales Ord. Arch. E2E" - }, - { - "codeunitId": 139927, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Sales Ord. Arch. E2E" - }, - { - "codeunitId": 139927, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Sales Ord. Arch. E2E" - }, - { - "codeunitId": 139928, - "method": "TestGetPurchaseReturnShipments", - "codeunitName": "APIV2 - Purch. Ret. Shpts. E2E" - }, - { - "codeunitId": 139928, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Purch. Ret. Shpts. E2E" - }, - { - "codeunitId": 139928, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Purch. Ret. Shpts. E2E" - }, - { - "codeunitId": 139928, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Purch. Ret. Shpts. E2E" - }, - { - "codeunitId": 139908, - "method": "TestGetBlanketPurchaseOrders", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139908, - "method": "TestGetBlanketPurchaseOrderById", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139908, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139908, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139908, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139908, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139908, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Bl. Purch. Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestGetBlanketSalesOrders", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestGetBlanketSalesOrderById", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139909, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Bl. Sales Orders E2E" - }, - { - "codeunitId": 139917, - "method": "TestGetApprovalEntries", - "codeunitName": "APIV2 - Approval Entries E2E" - }, - { - "codeunitId": 139918, - "method": "TestGetApprovalUserSetup", - "codeunitName": "APIV2 - Approval UserSetup E2E" - }, - { - "codeunitId": 139919, - "method": "TestGetPostedApprovalEntries", - "codeunitName": "APIV2 PstdApprovalEntries E2E" - }, - { - "codeunitId": 139920, - "method": "TestGetApprovalEntries", - "codeunitName": "APIV2 - Workflow Approvers E2E" - }, - { - "codeunitId": 139921, - "method": "TestGetApprovalEntries", - "codeunitName": "APIV2 - Workflows E2E" - }, - { - "codeunitId": 139922, - "method": "TestGetPurchaseQuotes", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139922, - "method": "TestGetPurchaseQuoteById", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139922, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139922, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139922, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139922, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139922, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Purch. Quotes E2E" - }, - { - "codeunitId": 139923, - "method": "TestGetPurchaseReturnOrders", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139923, - "method": "TestGetPurchaseReturnOrderById", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139923, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139923, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139923, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139923, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139923, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Purch. Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestGetSalesReturnOrders", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestGetSalesReturnOrderById", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139924, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Sales Ret. Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestGetAssemblyOrders", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestGetAssemblyOrderById", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139925, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Assembly Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestGetTransferOrders", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestGetTransferOrderById", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139926, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Transfer Orders E2E" + "codeunitId": 139925, + "codeunitName": "APIV2 - Assembly Orders E2E", + "method": "*" }, { "codeunitId": 139926, - "method": "TestExpandLines", - "codeunitName": "APIV2 - Transfer Orders E2E" - }, - { - "codeunitId": 139927, - "method": "TestGetSalesOrderArchives", - "codeunitName": "APIV2 - Sales Ord. Arch. E2E" - }, - { - "codeunitId": 139927, - "method": "TestExpandDimensionSetLines", - "codeunitName": "APIV2 - Sales Ord. Arch. E2E" - }, - { - "codeunitId": 139927, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Sales Ord. Arch. E2E" - }, - { - "codeunitId": 139927, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Sales Ord. Arch. E2E" - }, - { - "codeunitId": 139928, - "method": "TestGetPurchaseReturnShipments", - "codeunitName": "APIV2 - Purch. Ret. Shpts. E2E" - }, - { - "codeunitId": 139928, - "method": "TestExpandAttachments", - "codeunitName": "APIV2 - Purch. Ret. Shpts. E2E" - }, - { - "codeunitId": 139928, - "method": "TestExpandDocumentAttachments", - "codeunitName": "APIV2 - Purch. Ret. Shpts. E2E" - }, - { - "codeunitId": 139928, - "method": "TestExpandPdfDocument", - "codeunitName": "APIV2 - Purch. Ret. Shpts. E2E" + "codeunitName": "APIV2 - Transfer Orders E2E", + "method": "*" } ] diff --git a/src/Layers/APAC/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al b/src/Layers/APAC/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al index 707e6a229cd..0d44ad12a0f 100644 --- a/src/Layers/APAC/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al +++ b/src/Layers/APAC/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al @@ -6479,6 +6479,9 @@ codeunit 134920 "ERM General Journal UT" if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryERMCountryData.UpdateLocalData(); LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateGeneralPostingSetup(); @@ -7574,4 +7577,3 @@ codeunit 134920 "ERM General Journal UT" IsHandled := true; end; } - diff --git a/src/Layers/APAC/Tests/Prepayment/ERMPrepaymentII.Codeunit.al b/src/Layers/APAC/Tests/Prepayment/ERMPrepaymentII.Codeunit.al index be7ad2bb239..514e712534e 100644 --- a/src/Layers/APAC/Tests/Prepayment/ERMPrepaymentII.Codeunit.al +++ b/src/Layers/APAC/Tests/Prepayment/ERMPrepaymentII.Codeunit.al @@ -2694,7 +2694,6 @@ codeunit 134101 "ERM Prepayment II" GenJournalLine.Modify(true); LibraryERM.PostGeneralJnlLine(GenJournalLine); end; - local procedure SelectGenJournalBatch(var GenJournalBatch: Record "Gen. Journal Batch") begin // Select General Journal Batch and clear General Journal Lines to make sure that no line exist before creating diff --git a/src/Layers/CH/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al b/src/Layers/CH/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al index c81c9d60800..31ef248f143 100644 --- a/src/Layers/CH/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al +++ b/src/Layers/CH/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al @@ -6287,6 +6287,9 @@ codeunit 134920 "ERM General Journal UT" if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryERMCountryData.UpdateLocalData(); LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateGeneralPostingSetup(); @@ -7372,4 +7375,3 @@ codeunit 134920 "ERM General Journal UT" IsHandled := true; end; } - diff --git a/src/Layers/CZ/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al b/src/Layers/CZ/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al index 4ed0a80f174..8ec73b78b86 100644 --- a/src/Layers/CZ/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al +++ b/src/Layers/CZ/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al @@ -6483,6 +6483,9 @@ codeunit 134920 "ERM General Journal UT" if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryERMCountryData.UpdateLocalData(); LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateGeneralPostingSetup(); @@ -7578,4 +7581,3 @@ codeunit 134920 "ERM General Journal UT" IsHandled := true; end; } - diff --git a/src/Layers/ES/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al b/src/Layers/ES/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al index 0f00c3467cc..0d3851871a9 100644 --- a/src/Layers/ES/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al +++ b/src/Layers/ES/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al @@ -6512,6 +6512,9 @@ codeunit 134920 "ERM General Journal UT" if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryERMCountryData.UpdateLocalData(); LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateGeneralPostingSetup(); @@ -7607,4 +7610,3 @@ codeunit 134920 "ERM General Journal UT" IsHandled := true; end; } - diff --git a/src/Layers/IT/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al b/src/Layers/IT/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al index 0044783f068..6ca6d94e75b 100644 --- a/src/Layers/IT/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al +++ b/src/Layers/IT/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al @@ -6478,6 +6478,9 @@ codeunit 134920 "ERM General Journal UT" if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryERMCountryData.UpdateLocalData(); LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateGeneralPostingSetup(); @@ -7573,4 +7576,3 @@ codeunit 134920 "ERM General Journal UT" IsHandled := true; end; } - diff --git a/src/Layers/NA/Tests/Prepayment/ERMPrepaymentII.Codeunit.al b/src/Layers/NA/Tests/Prepayment/ERMPrepaymentII.Codeunit.al index e514b0407b0..6cc9605ec93 100644 --- a/src/Layers/NA/Tests/Prepayment/ERMPrepaymentII.Codeunit.al +++ b/src/Layers/NA/Tests/Prepayment/ERMPrepaymentII.Codeunit.al @@ -2632,7 +2632,6 @@ codeunit 134101 "ERM Prepayment II" GenJournalLine.Modify(true); LibraryERM.PostGeneralJnlLine(GenJournalLine); end; - local procedure SelectGenJournalBatch(var GenJournalBatch: Record "Gen. Journal Batch") begin // Select General Journal Batch and clear General Journal Lines to make sure that no line exist before creating diff --git a/src/Layers/W1/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al b/src/Layers/W1/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al index 0bffda864c6..8c7dbafba22 100644 --- a/src/Layers/W1/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al +++ b/src/Layers/W1/Tests/General Journal/ERMGeneralJournalUT.Codeunit.al @@ -6478,6 +6478,9 @@ codeunit 134920 "ERM General Journal UT" if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryERMCountryData.UpdateLocalData(); LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateGeneralPostingSetup(); @@ -7573,4 +7576,3 @@ codeunit 134920 "ERM General Journal UT" IsHandled := true; end; } - diff --git a/src/Layers/W1/Tests/Graph/IRS1099CodeEntityE2ETest.Codeunit.al b/src/Layers/W1/Tests/Graph/IRS1099CodeEntityE2ETest.Codeunit.al index 80c99fa6f1c..68ba57ce87e 100644 --- a/src/Layers/W1/Tests/Graph/IRS1099CodeEntityE2ETest.Codeunit.al +++ b/src/Layers/W1/Tests/Graph/IRS1099CodeEntityE2ETest.Codeunit.al @@ -38,7 +38,9 @@ codeunit 135519 "IRS 1099 Code Entity E2E Test" if IsInitialized then exit; + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + IsInitialized := true; end; } - diff --git a/src/Layers/W1/Tests/Graph/WFWHSubscriptionE2ETests.Codeunit.al b/src/Layers/W1/Tests/Graph/WFWHSubscriptionE2ETests.Codeunit.al index f5b8e0f9859..7892b11735f 100644 --- a/src/Layers/W1/Tests/Graph/WFWHSubscriptionE2ETests.Codeunit.al +++ b/src/Layers/W1/Tests/Graph/WFWHSubscriptionE2ETests.Codeunit.al @@ -54,6 +54,9 @@ codeunit 135528 "WFWH Subscription E2E Tests" if IsInitialized then exit; + + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); IsInitialized := true; Commit(); @@ -1454,4 +1457,3 @@ codeunit 135528 "WFWH Subscription E2E Tests" exit(String); end; } - diff --git a/src/Layers/W1/Tests/Integration/ApiRoutesE2E.Codeunit.al b/src/Layers/W1/Tests/Integration/ApiRoutesE2E.Codeunit.al index 6744a7e1cb5..7149aab8024 100644 --- a/src/Layers/W1/Tests/Integration/ApiRoutesE2E.Codeunit.al +++ b/src/Layers/W1/Tests/Integration/ApiRoutesE2E.Codeunit.al @@ -13,6 +13,12 @@ codeunit 135548 "API Routes E2E" LibraryGraphMgt: Codeunit "Library - Graph Mgt"; ApiRoutesTxt: Label 'apiRoutes', Locked = true; + local procedure Initialize() + begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); + end; + [Test] [Scope('OnPrem')] procedure TestGetApiRoutesMany() @@ -21,6 +27,8 @@ codeunit 135548 "API Routes E2E" ResponseText: Text; begin // [SCENARIO] GET returns many routes in the payload + Initialize(); + // [GIVEN] apiRoutes URI without filters TargetURL := LibraryGraphMgt.CreateTargetURL('', PAGE::"API Routes", ApiRoutesTxt); @@ -39,6 +47,8 @@ codeunit 135548 "API Routes E2E" ResponseText: Text; begin // [SCENARIO] GET returns a single route in the payload + Initialize(); + // [GIVEN] apiRoutes URI for a single route TargetURL := LibraryGraphMgt.CreateTargetURL('''beta''', PAGE::"API Routes", ApiRoutesTxt); @@ -57,6 +67,8 @@ codeunit 135548 "API Routes E2E" ResponseText: Text; begin // [SCENARIO] POST is not allowed for apiRoutes + Initialize(); + // [GIVEN] apiRoutes URI for a single route TargetURL := LibraryGraphMgt.CreateTargetURL('', PAGE::"API Routes", ApiRoutesTxt); @@ -75,6 +87,8 @@ codeunit 135548 "API Routes E2E" ResponseText: Text; begin // [SCENARIO] PATCH is not allowed for apiRoutes + Initialize(); + // [GIVEN] apiRoutes URI for a single route TargetURL := LibraryGraphMgt.CreateTargetURL('''beta''', PAGE::"API Routes", ApiRoutesTxt); @@ -93,6 +107,8 @@ codeunit 135548 "API Routes E2E" ResponseText: Text; begin // [SCENARIO] DELETE is not allowed for apiRoutes + Initialize(); + // [GIVEN] apiRoutes URI for a single route TargetURL := LibraryGraphMgt.CreateTargetURL('''beta''', PAGE::"API Routes", ApiRoutesTxt); @@ -103,4 +119,3 @@ codeunit 135548 "API Routes E2E" Assert.ExpectedError('400 (BadRequest)'); end; } - diff --git a/src/Layers/W1/Tests/Integration/WebhookLogsE2E.Codeunit.al b/src/Layers/W1/Tests/Integration/WebhookLogsE2E.Codeunit.al index a5aad9c9707..5771fde4b9d 100644 --- a/src/Layers/W1/Tests/Integration/WebhookLogsE2E.Codeunit.al +++ b/src/Layers/W1/Tests/Integration/WebhookLogsE2E.Codeunit.al @@ -119,6 +119,8 @@ codeunit 135547 "Webhook Logs E2E" local procedure Initialize() begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); CleanActivityLogs(); AddActivityLog('first', 'red'); AddActivityLog('second', 'yellow'); @@ -153,4 +155,3 @@ codeunit 135547 "Webhook Logs E2E" ActivityLog.DeleteAll(); end; } - diff --git a/src/Layers/W1/Tests/Misc/APITestAuthHTTPTests.Codeunit.al b/src/Layers/W1/Tests/Misc/APITestAuthHTTPTests.Codeunit.al new file mode 100644 index 00000000000..e1575916b41 --- /dev/null +++ b/src/Layers/W1/Tests/Misc/APITestAuthHTTPTests.Codeunit.al @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +#pragma warning disable AA0247 + +codeunit 139496 "API Test Auth HTTP Tests" +{ + // A separate codeunit ends the URL tests' metadata transaction before making HTTP requests. + Subtype = Test; + TestPermissions = Disabled; + + var + Assert: Codeunit Assert; + LibraryGraphMgt: Codeunit "Library - Graph Mgt"; + FirstHttpWebRequestMgt: Codeunit "Http Web Request Mgt."; + SecondHttpWebRequestMgt: Codeunit "Http Web Request Mgt."; + + [Test] + procedure MicrosoftAuthenticationRespectsServerAuthMode() + var + EnvironmentInfo: Codeunit "Environment Information"; + TargetURL: Text; + ResponseCode: Integer; + ExpectedAmbientResponseCode: Integer; + begin + // [SCENARIO] Authentication supplies UserPassword credentials or preserves ambient Windows credentials + Initialize(); + + // [GIVEN] An OnPrem service accepts configured UserPassword credentials or authorized ambient Windows credentials + Assert.IsFalse(EnvironmentInfo.IsSaaSInfrastructure(), 'This HTTP scenario requires an OnPrem test environment.'); + ExpectedAmbientResponseCode := GetExpectedAmbientResponseCode(); + TargetURL := GetUrl(ClientType::ODataV4); + + // [WHEN] The default provider sends a real request to the authenticated OData service document + LibraryGraphMgt.InitializeWebRequestWithURL(FirstHttpWebRequestMgt, TargetURL); + ResponseCode := ExecuteRequest(FirstHttpWebRequestMgt); + + // [THEN] Only a Windows-authenticated service accepts ambient credentials + Assert.AreEqual(ExpectedAmbientResponseCode, ResponseCode, 'The default provider must respect the server authentication mode.'); + + // [WHEN] The Microsoft provider configures a new request to the same endpoint + LibraryGraphMgt.SetAuthenticationProvider(Enum::"API Test Authentication"::"Microsoft Test Environment"); + LibraryGraphMgt.InitializeWebRequestWithURL(SecondHttpWebRequestMgt, TargetURL); + ResponseCode := ExecuteRequest(SecondHttpWebRequestMgt); + + // [THEN] Configured UserPassword credentials or preserved Windows credentials authenticate the request + Assert.AreEqual(200, ResponseCode, 'The Microsoft provider must authenticate without disrupting ambient Windows credentials.'); + + // [WHEN] Authentication is deselected and another request is created + LibraryGraphMgt.SetAuthenticationProvider(Enum::"API Test Authentication"::None); + LibraryGraphMgt.InitializeWebRequestWithURL(FirstHttpWebRequestMgt, TargetURL); + ResponseCode := ExecuteRequest(FirstHttpWebRequestMgt); + + // [THEN] Configured credentials do not leak and ambient Windows authentication remains available + Assert.AreEqual(ExpectedAmbientResponseCode, ResponseCode, 'Deselecting the provider must restore the server authentication mode.'); + end; + + local procedure Initialize() + begin + Clear(LibraryGraphMgt); + Clear(FirstHttpWebRequestMgt); + Clear(SecondHttpWebRequestMgt); + end; + + local procedure GetExpectedAmbientResponseCode(): Integer + var + SecurityGroup: Codeunit "Security Group"; + begin + if SecurityGroup.IsWindowsAuthentication() then + exit(200); + + exit(401); + end; + + local procedure ExecuteRequest(var HttpWebRequestMgt: Codeunit "Http Web Request Mgt.") ResponseCode: Integer + var + TempBlob: Codeunit "Temp Blob"; + HttpStatusCode: DotNet HttpStatusCode; + ResponseHeaders: DotNet NameValueCollection; + ErrorMessage: Text; + ErrorDetails: Text; + Successful: Boolean; + begin + HttpWebRequestMgt.DisableUI(); + HttpWebRequestMgt.SetReturnType('application/json'); + Successful := HttpWebRequestMgt.SendRequestAndReadResponse(TempBlob, ErrorMessage, ErrorDetails, HttpStatusCode, ResponseHeaders); + Assert.IsFalse(IsNull(HttpStatusCode), 'The service must return an HTTP status, not a transport failure.'); + ResponseCode := HttpStatusCode; + Assert.AreEqual(ResponseCode = 200, Successful, 'The response status must agree with the transport result.'); + end; +} diff --git a/src/Layers/W1/Tests/Prepayment/ERMPrepaymentII.Codeunit.al b/src/Layers/W1/Tests/Prepayment/ERMPrepaymentII.Codeunit.al index 27e8b022b74..ac9aa24db4e 100644 --- a/src/Layers/W1/Tests/Prepayment/ERMPrepaymentII.Codeunit.al +++ b/src/Layers/W1/Tests/Prepayment/ERMPrepaymentII.Codeunit.al @@ -2575,7 +2575,6 @@ codeunit 134101 "ERM Prepayment II" GenJournalLine.Modify(true); LibraryERM.PostGeneralJnlLine(GenJournalLine); end; - local procedure SelectGenJournalBatch(var GenJournalBatch: Record "Gen. Journal Batch") begin // Select General Journal Batch and clear General Journal Lines to make sure that no line exist before creating @@ -3145,4 +3144,3 @@ codeunit 134101 "ERM Prepayment II" PurchaseHeader.TestField(Status, ExpectedStatus); end; } - diff --git a/src/Layers/W1/Tests/Report/ReportInboxAPIE2E.Codeunit.al b/src/Layers/W1/Tests/Report/ReportInboxAPIE2E.Codeunit.al index 01905a48000..daa0a0e2ec9 100644 --- a/src/Layers/W1/Tests/Report/ReportInboxAPIE2E.Codeunit.al +++ b/src/Layers/W1/Tests/Report/ReportInboxAPIE2E.Codeunit.al @@ -443,6 +443,8 @@ codeunit 135549 "Report Inbox API E2E" var Company: Record Company; begin + LibraryGraphMgt.SetAuthenticationProvider( + Enum::"API Test Authentication"::"Microsoft Test Environment"); if Company.FindSet() then repeat DeleteEntriesIn(Company.Name, UserId()); diff --git a/src/Layers/W1/Tests/TestLibraries/LibraryGraphDocumentTools.Codeunit.al b/src/Layers/W1/Tests/TestLibraries/LibraryGraphDocumentTools.Codeunit.al index cc281f04818..d89183ab2e0 100644 --- a/src/Layers/W1/Tests/TestLibraries/LibraryGraphDocumentTools.Codeunit.al +++ b/src/Layers/W1/Tests/TestLibraries/LibraryGraphDocumentTools.Codeunit.al @@ -1,10 +1,6 @@ codeunit 130619 "Library - Graph Document Tools" { - trigger OnRun() - begin - end; - var LibraryApplicationArea: Codeunit "Library - Application Area"; LibrarySales: Codeunit "Library - Sales"; @@ -895,4 +891,3 @@ codeunit 130619 "Library - Graph Document Tools" Assert.AreEqual(ExpectedDiscountAmount, ActualInvoiceDiscountAmount, 'Invoice discount amount was not set'); end; } - diff --git a/src/Layers/W1/Tests/TestLibraries/LibraryGraphJournalLines.Codeunit.al b/src/Layers/W1/Tests/TestLibraries/LibraryGraphJournalLines.Codeunit.al index ea303197684..7ec14a67541 100644 --- a/src/Layers/W1/Tests/TestLibraries/LibraryGraphJournalLines.Codeunit.al +++ b/src/Layers/W1/Tests/TestLibraries/LibraryGraphJournalLines.Codeunit.al @@ -1,10 +1,6 @@ codeunit 130622 "Library - Graph Journal Lines" { - trigger OnRun() - begin - end; - var LibraryERM: Codeunit "Library - ERM"; LibrarySales: Codeunit "Library - Sales"; @@ -321,4 +317,3 @@ codeunit 130622 "Library - Graph Journal Lines" Assert.AreEqual(NewDate, GenJournalLine."Posting Date", 'Journal Line ' + PostingDateNameTxt + ' should be changed'); end; } - From 43162697456fffc0199258f5000344d61d5cbcbd Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 5 Oct 2026 16:04:38 +0200 Subject: [PATCH 11/22] Enable remaining Expense HTTP API tests with existing clean isolation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session:3952f078-a881-4da8-ad96-13b727e48a91 --- .../ExpenseCapabilitiesAPITest.Codeunit.al | 2 - .../src/API/ExpenseVATSpecAPITest.Codeunit.al | 1 + .../src/API/TravelRequestsAPITest.Codeunit.al | 6 -- .../Expense_Agent_Tests.DisabledTest.json | 95 ------------------- 4 files changed, 1 insertion(+), 103 deletions(-) diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseCapabilitiesAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseCapabilitiesAPITest.Codeunit.al index ff80ab2eeb8..6dceff107b7 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseCapabilitiesAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseCapabilitiesAPITest.Codeunit.al @@ -103,8 +103,6 @@ codeunit 148318 "Expense Capabilities API Test" TargetURL: Text; ResponseText: Text; begin - // Excluded in BCApps CI until authenticated OData execution in a dedicated test company - // with disabled isolation is available; then remove this method's DisabledTest.json entry. // [SCENARIO] Travel requests are advertised when the supporting APIs are installed. Initialize(); diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al index 2fa5aa46229..a5bfe729b28 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al @@ -11,6 +11,7 @@ codeunit 148348 "Expense VAT Spec. API Test" { Subtype = Test; TestType = IntegrationTest; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; var diff --git a/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al index 15eec688af5..d54e4b2515b 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al @@ -10,12 +10,6 @@ using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.SpendRequest; using Microsoft.HumanResources.Employee; -// These HTTP tests remain excluded in Expense_Agent_Tests.DisabledTest.json until the -// authentication prerequisite provides an authenticated OData endpoint and a dedicated -// test company with committed fixtures and disabled test isolation. -// In-process employee filtering, traveler mapping/navigation, lifecycle, date, and scope coverage -// in "Spend Request Test", and restrictive role coverage in "Expense Permissions Test", remain enabled. -// Only the HTTP scenarios are excluded. codeunit 148347 "Travel Requests API Test" { Subtype = Test; diff --git a/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json b/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json index 8c33fbff153..02c2392f10b 100644 --- a/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json +++ b/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json @@ -1,94 +1,4 @@ [ - { - "codeunitId": 148315, - "codeunitName": "Expense Users API Test", - "method": "EmployeesAPICanFilterExpenseUsers" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "TravelersAPIMapsEmployeeNumberToExpenseUser" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "TravelersAPIRejectsEmployeeWithoutExpenseUser" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "CreateExpenseReportActionRecreatesDeletedReport" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "ApproveTravelRequestActionCreatesExpenseReport" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "TravelRequestsAPINormalizesCurrency" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "TravelRequestDetailsAPINormalizesCurrency" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "TravelRequestsAPIPreservesAndUpdatesDates" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "TravelRequestsAreScopedByEmployeeNumber" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "ExpenseReportAPIExposesLinkedTravelRequest" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "TravelRequestDetailsAPIExposesTypeAndCategory" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "ApproverViewReturnsOnlyAssignedTravelRequests" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "TravelRequestsAPIAllowsOwnerOnInsertAndUnchangedPatch" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "TravelRequestsAPIRejectsLifecycleFieldChanges" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "TravelRequestsAPIRejectsRequestedForChange" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "LegacySpendRequestsAPIAllowsUnchangedOwner" - }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "LegacySpendRequestsAPIRejectsTravelRequestOwnerChange" - }, - { - "codeunitId": 148318, - "codeunitName": "Expense Capabilities API Test", - "method": "TravelRequestCapabilityEnabledViaAPI" - }, { "codeunitId": 148305, "codeunitName": "Expense Test", @@ -133,10 +43,5 @@ "codeunitId": 148319, "codeunitName": "Cancel Posted Exp. Report Test", "method": "CancelMultiLineReportGeneratesNewTransactionNoForReversedLines" - }, - { - "codeunitId": 148348, - "codeunitName": "Expense VAT Spec. API Test", - "method": "AgentVATSpecificationIsInsertedThroughAPI" } ] From 3070e7a45abc0b3a49e946e8dfc030203783e72e Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 5 Oct 2026 21:18:35 +0200 Subject: [PATCH 12/22] Defer general API clean-isolation tests until authenticated uptake Temporarily sequence193 existing methods through DisabledTests selectors:135 APIV1 and58 APIV2. Removed in#11860; no product quarantine or selection logic. AB#646383 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session:3952f078-a881-4da8-ad96-13b727e48a91 --- ..._Tests.ExpenseFirstStage.DisabledTest.json | 137 ++++++++++++++++++ ..._Tests.ExpenseFirstStage.DisabledTest.json | 60 ++++++++ 2 files changed, 197 insertions(+) create mode 100644 src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.ExpenseFirstStage.DisabledTest.json create mode 100644 src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.ExpenseFirstStage.DisabledTest.json diff --git a/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.ExpenseFirstStage.DisabledTest.json b/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.ExpenseFirstStage.DisabledTest.json new file mode 100644 index 00000000000..ba68644423a --- /dev/null +++ b/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.ExpenseFirstStage.DisabledTest.json @@ -0,0 +1,137 @@ +[ + {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestGetSimpleCustomer"}, + {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestGetCustomerWithComplexType"}, + {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestGetCustomerWithAddressAndSpecialCharacters"}, + {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestCreateDetailedCustomer"}, + {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestCreateCustomerWithComplexType"}, + {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestCreateCustomerWithTemplate"}, + {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestDeleteCustomer"}, + {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestModifyCustomer"}, + {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestModifyCustomerWithComplexType"}, + {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestRemoveComplexTypeFromCustomer"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnCustomer"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnVendor"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnItem"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnCustomer"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnVendor"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnItem"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithoutDimensionOnCustomer"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithoutDimensionOnVendor"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithoutDimensionOnItem"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnCustomer"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnVendor"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnItem"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnCustomer"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnVendor"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnItem"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnCustomer"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnVendor"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnItem"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnCustomer"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnVendor"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnItem"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnCustomer"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnVendor"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnItem"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnCustomer"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnVendor"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnItem"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnCustomer"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnVendor"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnItem"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnCustomer"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnVendor"}, + {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnItem"}, + {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestCreateDimensionLine"}, + {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestCreateDimensionLineFailsWithoutParentId"}, + {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestCreateDoesntWorkWithAlreadyExistingCode"}, + {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestGetDimensionLinesOfJournalLines"}, + {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestGetDimensionLinesFailsWithoutFilter"}, + {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestGetDimensionLinesFailsWithNonSupportedEntity"}, + {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestModifyDimensionLine"}, + {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestModifyOfDimensionCodeDoesntWork"}, + {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestDeleteDimensionLine"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithItemId"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithNonExistingItemId"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithItemNumber"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithNonExistingItemNumber"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithItemNumberAndItemId"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithMismatchingItemNumberAndItemId"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithExistingItemNumberAndCode"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestGetItemVariant"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestModifyItemVariant"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestModifyItemVariantItemId"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestModifyItemVariantItemNumber"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestDeleteItemVariant"}, + {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestDeleteInUse"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateSimpleItem"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateItemWithInventoryAmount"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateItemWithCategoryId"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateItemWithCategoryCode"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateItemWithComplexType"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateItemWithTemplate"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateItemWithTemplateIgnoresFieldsSet"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestGetSimpleItem"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestSuccessfulTemplateSelection"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestSuccessfulTemplateSelectionWithSelectionCriteria"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestTempcodeCannotbeblankOrInvalidValue"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestGetItemWithCategory"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestGetItemWithComplexType"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestModifyItem"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestModifyItemInventory"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestModifyItemWithComplexType"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestRemoveComplexTypeFromItemUsingNull"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestRemoveComplexTypeFromItemUsingBlankValue"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestDeleteItem"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestPostItemWithUoMIdCreatesItemUoM"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestPatchItemWithUoMIdCreatesItemUoM"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestPatchItemWithCategoryCode"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestPatchItemWithCategoryId"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestPostItemWithUoMComplexTypeCreatesItemUoM"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestPatchItemWithUoMComplexTypeCreatesItemUoM"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestUoMSync"}, + {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestUoMSyncErrors"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueCustomer"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataCustomer"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueCustomerWithoutPicture"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataCustomerWithoutPicture"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureUploadCustomer"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureReplaceCustomer"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureDeleteCustomer"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueVendor"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataVendor"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueVendorWithoutPicture"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataVendorWithoutPicture"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureUploadVendor"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureReplaceVendor"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureDeleteVendor"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueEmployee"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataEmployee"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueEmployeeWithoutPicture"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataEmployeeWithoutPicture"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureUploadEmployee"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureReplaceEmployee"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureDeleteEmployee"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueItem"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataItem"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueItemWithoutPicture"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadatItemWithoutPicture"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureUploadItem"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureReplaceItem"}, + {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureDeleteItem"}, + {"codeunitId": 139726, "codeunitName": "APIV1 - Tax Areas E2E", "method": "TestVerifyIDandLastDateModified"}, + {"codeunitId": 139726, "codeunitName": "APIV1 - Tax Areas E2E", "method": "TestGetTaxAreas"}, + {"codeunitId": 139726, "codeunitName": "APIV1 - Tax Areas E2E", "method": "TestGetTaxArea"}, + {"codeunitId": 139726, "codeunitName": "APIV1 - Tax Areas E2E", "method": "TestCreateTaxArea"}, + {"codeunitId": 139726, "codeunitName": "APIV1 - Tax Areas E2E", "method": "TestModifyTaxArea"}, + {"codeunitId": 139726, "codeunitName": "APIV1 - Tax Areas E2E", "method": "TestDeleteTaxArea"}, + {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestGetSimpleVendor"}, + {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestGetVendorWithComplexType"}, + {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestCreateSimpleVendor"}, + {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestCreateVendorWithComplexType"}, + {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestCreateVendorWithTemplate"}, + {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestModifyVendor"}, + {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestVendorModifyWithComplexTypes"}, + {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestRemoveComplexTypeFromVendor"}, + {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestDeleteVendor"} +] diff --git a/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.ExpenseFirstStage.DisabledTest.json b/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.ExpenseFirstStage.DisabledTest.json new file mode 100644 index 00000000000..26075df9d71 --- /dev/null +++ b/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.ExpenseFirstStage.DisabledTest.json @@ -0,0 +1,60 @@ +[ + {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestGetSimpleCustomer"}, + {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestGetCustomerWithAddressAndSpecialCharacters"}, + {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestCreateDetailedCustomer"}, + {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestCreateCustomerWithTemplate"}, + {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestDeleteCustomer"}, + {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestModifyCustomer"}, + {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestModifyCustomerWithAddress"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnCustomer"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnVendor"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnItem"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnEmployee"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnCustomer"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnVendor"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnItem"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnEmployee"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnCustomer"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnVendor"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnItem"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnEmployee"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnCustomer"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnVendor"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnItem"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnEmployee"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnCustomer"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnVendor"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnItem"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnEmployee"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnCustomer"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnVendor"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnItem"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnEmployee"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnCustomer"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnVendor"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnItem"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnEmployee"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnCustomer"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnVendor"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnItem"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnEmployee"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnCustomer"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnVendor"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnItem"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnEmployee"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnCustomer"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnVendor"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnItem"}, + {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnEmployee"}, + {"codeunitId": 139826, "codeunitName": "APIV2 - Tax Areas E2E", "method": "TestVerifyIDandLastDateModified"}, + {"codeunitId": 139826, "codeunitName": "APIV2 - Tax Areas E2E", "method": "TestGetTaxAreas"}, + {"codeunitId": 139826, "codeunitName": "APIV2 - Tax Areas E2E", "method": "TestGetTaxArea"}, + {"codeunitId": 139826, "codeunitName": "APIV2 - Tax Areas E2E", "method": "TestCreateTaxArea"}, + {"codeunitId": 139826, "codeunitName": "APIV2 - Tax Areas E2E", "method": "TestModifyTaxArea"}, + {"codeunitId": 139826, "codeunitName": "APIV2 - Tax Areas E2E", "method": "TestDeleteTaxArea"}, + {"codeunitId": 139803, "codeunitName": "APIV2 - Vendors E2E", "method": "TestGetSimpleVendor"}, + {"codeunitId": 139803, "codeunitName": "APIV2 - Vendors E2E", "method": "TestCreateSimpleVendor"}, + {"codeunitId": 139803, "codeunitName": "APIV2 - Vendors E2E", "method": "TestCreateVendorWithTemplate"}, + {"codeunitId": 139803, "codeunitName": "APIV2 - Vendors E2E", "method": "TestModifyVendor"}, + {"codeunitId": 139803, "codeunitName": "APIV2 - Vendors E2E", "method": "TestDeleteVendor"} +] From bd3bbc04e13965f967a26eef435bdbf0cbc09ad7 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 5 Oct 2026 22:49:01 +0200 Subject: [PATCH 13/22] Reuse existing API exclusion manifests for Expense-first sequencing Relocate the same193 temporary method exclusions without changing selection, authentication or tests. Preserve existing manifest order and remove separate stage files. AB#646383 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session:3952f078-a881-4da8-ad96-13b727e48a91 --- .../_Exclude_APIV1__Tests.DisabledTest.json | 675 ++++++++++++++++++ ..._Tests.ExpenseFirstStage.DisabledTest.json | 137 ---- .../_Exclude_APIV2__Tests.DisabledTest.json | 290 ++++++++ ..._Tests.ExpenseFirstStage.DisabledTest.json | 60 -- 4 files changed, 965 insertions(+), 197 deletions(-) delete mode 100644 src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.ExpenseFirstStage.DisabledTest.json delete mode 100644 src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.ExpenseFirstStage.DisabledTest.json diff --git a/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.DisabledTest.json b/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.DisabledTest.json index ad692000a56..a4a9415f3f1 100644 --- a/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.DisabledTest.json +++ b/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.DisabledTest.json @@ -1453,5 +1453,680 @@ "codeunitId": 139745, "codeunitName": "APIV1 - Journal Lines E2E", "method": "TestCreateJournalLineWithOverridenDefaultDimension" + }, + { + "codeunitId": 139702, + "codeunitName": "APIV1 - Customers E2E", + "method": "TestGetSimpleCustomer" + }, + { + "codeunitId": 139702, + "codeunitName": "APIV1 - Customers E2E", + "method": "TestGetCustomerWithComplexType" + }, + { + "codeunitId": 139702, + "codeunitName": "APIV1 - Customers E2E", + "method": "TestGetCustomerWithAddressAndSpecialCharacters" + }, + { + "codeunitId": 139702, + "codeunitName": "APIV1 - Customers E2E", + "method": "TestCreateDetailedCustomer" + }, + { + "codeunitId": 139702, + "codeunitName": "APIV1 - Customers E2E", + "method": "TestCreateCustomerWithComplexType" + }, + { + "codeunitId": 139702, + "codeunitName": "APIV1 - Customers E2E", + "method": "TestCreateCustomerWithTemplate" + }, + { + "codeunitId": 139702, + "codeunitName": "APIV1 - Customers E2E", + "method": "TestDeleteCustomer" + }, + { + "codeunitId": 139702, + "codeunitName": "APIV1 - Customers E2E", + "method": "TestModifyCustomer" + }, + { + "codeunitId": 139702, + "codeunitName": "APIV1 - Customers E2E", + "method": "TestModifyCustomerWithComplexType" + }, + { + "codeunitId": 139702, + "codeunitName": "APIV1 - Customers E2E", + "method": "TestRemoveComplexTypeFromCustomer" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionWithDimensionCodeOnCustomer" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionWithDimensionCodeOnVendor" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionWithDimensionCodeOnItem" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionOnCustomer" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionOnVendor" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionOnItem" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithoutDimensionOnCustomer" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithoutDimensionOnVendor" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithoutDimensionOnItem" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnCustomer" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnVendor" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnItem" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnCustomer" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnVendor" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnItem" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnCustomer" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnVendor" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnItem" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestDeleteDefaultDimensionOnCustomer" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestDeleteDefaultDimensionOnVendor" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestDeleteDefaultDimensionOnItem" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestGetDefaultDimensionOnCustomer" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestGetDefaultDimensionOnVendor" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestGetDefaultDimensionOnItem" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionOnCustomer" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionOnVendor" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionOnItem" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnCustomer" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnVendor" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnItem" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnCustomer" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnVendor" + }, + { + "codeunitId": 139732, + "codeunitName": "APIV1 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnItem" + }, + { + "codeunitId": 139725, + "codeunitName": "APIV1 - Dimension Lines E2E", + "method": "TestCreateDimensionLine" + }, + { + "codeunitId": 139725, + "codeunitName": "APIV1 - Dimension Lines E2E", + "method": "TestCreateDimensionLineFailsWithoutParentId" + }, + { + "codeunitId": 139725, + "codeunitName": "APIV1 - Dimension Lines E2E", + "method": "TestCreateDoesntWorkWithAlreadyExistingCode" + }, + { + "codeunitId": 139725, + "codeunitName": "APIV1 - Dimension Lines E2E", + "method": "TestGetDimensionLinesOfJournalLines" + }, + { + "codeunitId": 139725, + "codeunitName": "APIV1 - Dimension Lines E2E", + "method": "TestGetDimensionLinesFailsWithoutFilter" + }, + { + "codeunitId": 139725, + "codeunitName": "APIV1 - Dimension Lines E2E", + "method": "TestGetDimensionLinesFailsWithNonSupportedEntity" + }, + { + "codeunitId": 139725, + "codeunitName": "APIV1 - Dimension Lines E2E", + "method": "TestModifyDimensionLine" + }, + { + "codeunitId": 139725, + "codeunitName": "APIV1 - Dimension Lines E2E", + "method": "TestModifyOfDimensionCodeDoesntWork" + }, + { + "codeunitId": 139725, + "codeunitName": "APIV1 - Dimension Lines E2E", + "method": "TestDeleteDimensionLine" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestCreateItemVariantWithItemId" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestCreateItemVariantWithNonExistingItemId" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestCreateItemVariantWithItemNumber" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestCreateItemVariantWithNonExistingItemNumber" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestCreateItemVariantWithItemNumberAndItemId" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestCreateItemVariantWithMismatchingItemNumberAndItemId" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestCreateItemVariantWithExistingItemNumberAndCode" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestGetItemVariant" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestModifyItemVariant" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestModifyItemVariantItemId" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestModifyItemVariantItemNumber" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestDeleteItemVariant" + }, + { + "codeunitId": 139739, + "codeunitName": "APIV1 - Item Variants E2E", + "method": "TestDeleteInUse" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestCreateSimpleItem" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestCreateItemWithInventoryAmount" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestCreateItemWithCategoryId" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestCreateItemWithCategoryCode" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestCreateItemWithComplexType" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestCreateItemWithTemplate" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestCreateItemWithTemplateIgnoresFieldsSet" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestGetSimpleItem" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestSuccessfulTemplateSelection" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestSuccessfulTemplateSelectionWithSelectionCriteria" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestTempcodeCannotbeblankOrInvalidValue" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestGetItemWithCategory" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestGetItemWithComplexType" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestModifyItem" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestModifyItemInventory" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestModifyItemWithComplexType" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestRemoveComplexTypeFromItemUsingNull" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestRemoveComplexTypeFromItemUsingBlankValue" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestDeleteItem" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestPostItemWithUoMIdCreatesItemUoM" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestPatchItemWithUoMIdCreatesItemUoM" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestPatchItemWithCategoryCode" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestPatchItemWithCategoryId" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestPostItemWithUoMComplexTypeCreatesItemUoM" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestPatchItemWithUoMComplexTypeCreatesItemUoM" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestUoMSync" + }, + { + "codeunitId": 139700, + "codeunitName": "APIV1 - Items E2E", + "method": "TestUoMSyncErrors" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureValueCustomer" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureMetadataCustomer" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureValueCustomerWithoutPicture" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureMetadataCustomerWithoutPicture" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestPictureUploadCustomer" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestPictureReplaceCustomer" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestPictureDeleteCustomer" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureValueVendor" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureMetadataVendor" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureValueVendorWithoutPicture" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureMetadataVendorWithoutPicture" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestPictureUploadVendor" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestPictureReplaceVendor" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestPictureDeleteVendor" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureValueEmployee" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureMetadataEmployee" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureValueEmployeeWithoutPicture" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureMetadataEmployeeWithoutPicture" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestPictureUploadEmployee" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestPictureReplaceEmployee" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestPictureDeleteEmployee" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureValueItem" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureMetadataItem" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureValueItemWithoutPicture" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestGetPictureMetadatItemWithoutPicture" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestPictureUploadItem" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestPictureReplaceItem" + }, + { + "codeunitId": 139742, + "codeunitName": "APIV1 - Pictures E2E", + "method": "TestPictureDeleteItem" + }, + { + "codeunitId": 139726, + "codeunitName": "APIV1 - Tax Areas E2E", + "method": "TestVerifyIDandLastDateModified" + }, + { + "codeunitId": 139726, + "codeunitName": "APIV1 - Tax Areas E2E", + "method": "TestGetTaxAreas" + }, + { + "codeunitId": 139726, + "codeunitName": "APIV1 - Tax Areas E2E", + "method": "TestGetTaxArea" + }, + { + "codeunitId": 139726, + "codeunitName": "APIV1 - Tax Areas E2E", + "method": "TestCreateTaxArea" + }, + { + "codeunitId": 139726, + "codeunitName": "APIV1 - Tax Areas E2E", + "method": "TestModifyTaxArea" + }, + { + "codeunitId": 139726, + "codeunitName": "APIV1 - Tax Areas E2E", + "method": "TestDeleteTaxArea" + }, + { + "codeunitId": 139703, + "codeunitName": "APIV1 - Vendors E2E", + "method": "TestGetSimpleVendor" + }, + { + "codeunitId": 139703, + "codeunitName": "APIV1 - Vendors E2E", + "method": "TestGetVendorWithComplexType" + }, + { + "codeunitId": 139703, + "codeunitName": "APIV1 - Vendors E2E", + "method": "TestCreateSimpleVendor" + }, + { + "codeunitId": 139703, + "codeunitName": "APIV1 - Vendors E2E", + "method": "TestCreateVendorWithComplexType" + }, + { + "codeunitId": 139703, + "codeunitName": "APIV1 - Vendors E2E", + "method": "TestCreateVendorWithTemplate" + }, + { + "codeunitId": 139703, + "codeunitName": "APIV1 - Vendors E2E", + "method": "TestModifyVendor" + }, + { + "codeunitId": 139703, + "codeunitName": "APIV1 - Vendors E2E", + "method": "TestVendorModifyWithComplexTypes" + }, + { + "codeunitId": 139703, + "codeunitName": "APIV1 - Vendors E2E", + "method": "TestRemoveComplexTypeFromVendor" + }, + { + "codeunitId": 139703, + "codeunitName": "APIV1 - Vendors E2E", + "method": "TestDeleteVendor" } ] diff --git a/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.ExpenseFirstStage.DisabledTest.json b/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.ExpenseFirstStage.DisabledTest.json deleted file mode 100644 index ba68644423a..00000000000 --- a/src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.ExpenseFirstStage.DisabledTest.json +++ /dev/null @@ -1,137 +0,0 @@ -[ - {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestGetSimpleCustomer"}, - {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestGetCustomerWithComplexType"}, - {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestGetCustomerWithAddressAndSpecialCharacters"}, - {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestCreateDetailedCustomer"}, - {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestCreateCustomerWithComplexType"}, - {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestCreateCustomerWithTemplate"}, - {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestDeleteCustomer"}, - {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestModifyCustomer"}, - {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestModifyCustomerWithComplexType"}, - {"codeunitId": 139702, "codeunitName": "APIV1 - Customers E2E", "method": "TestRemoveComplexTypeFromCustomer"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnCustomer"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnVendor"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnItem"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnCustomer"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnVendor"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnItem"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithoutDimensionOnCustomer"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithoutDimensionOnVendor"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithoutDimensionOnItem"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnCustomer"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnVendor"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnItem"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnCustomer"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnVendor"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnItem"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnCustomer"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnVendor"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnItem"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnCustomer"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnVendor"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnItem"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnCustomer"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnVendor"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnItem"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnCustomer"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnVendor"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnItem"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnCustomer"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnVendor"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnItem"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnCustomer"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnVendor"}, - {"codeunitId": 139732, "codeunitName": "APIV1 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnItem"}, - {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestCreateDimensionLine"}, - {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestCreateDimensionLineFailsWithoutParentId"}, - {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestCreateDoesntWorkWithAlreadyExistingCode"}, - {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestGetDimensionLinesOfJournalLines"}, - {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestGetDimensionLinesFailsWithoutFilter"}, - {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestGetDimensionLinesFailsWithNonSupportedEntity"}, - {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestModifyDimensionLine"}, - {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestModifyOfDimensionCodeDoesntWork"}, - {"codeunitId": 139725, "codeunitName": "APIV1 - Dimension Lines E2E", "method": "TestDeleteDimensionLine"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithItemId"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithNonExistingItemId"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithItemNumber"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithNonExistingItemNumber"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithItemNumberAndItemId"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithMismatchingItemNumberAndItemId"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestCreateItemVariantWithExistingItemNumberAndCode"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestGetItemVariant"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestModifyItemVariant"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestModifyItemVariantItemId"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestModifyItemVariantItemNumber"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestDeleteItemVariant"}, - {"codeunitId": 139739, "codeunitName": "APIV1 - Item Variants E2E", "method": "TestDeleteInUse"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateSimpleItem"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateItemWithInventoryAmount"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateItemWithCategoryId"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateItemWithCategoryCode"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateItemWithComplexType"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateItemWithTemplate"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestCreateItemWithTemplateIgnoresFieldsSet"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestGetSimpleItem"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestSuccessfulTemplateSelection"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestSuccessfulTemplateSelectionWithSelectionCriteria"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestTempcodeCannotbeblankOrInvalidValue"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestGetItemWithCategory"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestGetItemWithComplexType"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestModifyItem"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestModifyItemInventory"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestModifyItemWithComplexType"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestRemoveComplexTypeFromItemUsingNull"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestRemoveComplexTypeFromItemUsingBlankValue"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestDeleteItem"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestPostItemWithUoMIdCreatesItemUoM"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestPatchItemWithUoMIdCreatesItemUoM"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestPatchItemWithCategoryCode"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestPatchItemWithCategoryId"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestPostItemWithUoMComplexTypeCreatesItemUoM"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestPatchItemWithUoMComplexTypeCreatesItemUoM"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestUoMSync"}, - {"codeunitId": 139700, "codeunitName": "APIV1 - Items E2E", "method": "TestUoMSyncErrors"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueCustomer"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataCustomer"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueCustomerWithoutPicture"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataCustomerWithoutPicture"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureUploadCustomer"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureReplaceCustomer"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureDeleteCustomer"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueVendor"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataVendor"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueVendorWithoutPicture"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataVendorWithoutPicture"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureUploadVendor"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureReplaceVendor"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureDeleteVendor"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueEmployee"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataEmployee"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueEmployeeWithoutPicture"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataEmployeeWithoutPicture"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureUploadEmployee"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureReplaceEmployee"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureDeleteEmployee"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueItem"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadataItem"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureValueItemWithoutPicture"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestGetPictureMetadatItemWithoutPicture"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureUploadItem"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureReplaceItem"}, - {"codeunitId": 139742, "codeunitName": "APIV1 - Pictures E2E", "method": "TestPictureDeleteItem"}, - {"codeunitId": 139726, "codeunitName": "APIV1 - Tax Areas E2E", "method": "TestVerifyIDandLastDateModified"}, - {"codeunitId": 139726, "codeunitName": "APIV1 - Tax Areas E2E", "method": "TestGetTaxAreas"}, - {"codeunitId": 139726, "codeunitName": "APIV1 - Tax Areas E2E", "method": "TestGetTaxArea"}, - {"codeunitId": 139726, "codeunitName": "APIV1 - Tax Areas E2E", "method": "TestCreateTaxArea"}, - {"codeunitId": 139726, "codeunitName": "APIV1 - Tax Areas E2E", "method": "TestModifyTaxArea"}, - {"codeunitId": 139726, "codeunitName": "APIV1 - Tax Areas E2E", "method": "TestDeleteTaxArea"}, - {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestGetSimpleVendor"}, - {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestGetVendorWithComplexType"}, - {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestCreateSimpleVendor"}, - {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestCreateVendorWithComplexType"}, - {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestCreateVendorWithTemplate"}, - {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestModifyVendor"}, - {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestVendorModifyWithComplexTypes"}, - {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestRemoveComplexTypeFromVendor"}, - {"codeunitId": 139703, "codeunitName": "APIV1 - Vendors E2E", "method": "TestDeleteVendor"} -] diff --git a/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.DisabledTest.json b/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.DisabledTest.json index 2a73423912e..66e7f95e2e0 100644 --- a/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.DisabledTest.json +++ b/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.DisabledTest.json @@ -3583,5 +3583,295 @@ "codeunitId": 139928, "method": "TestExpandPdfDocument", "codeunitName": "APIV2 - Purch. Ret. Shpts. E2E" + }, + { + "codeunitId": 139802, + "codeunitName": "APIV2 - Customers E2E", + "method": "TestGetSimpleCustomer" + }, + { + "codeunitId": 139802, + "codeunitName": "APIV2 - Customers E2E", + "method": "TestGetCustomerWithAddressAndSpecialCharacters" + }, + { + "codeunitId": 139802, + "codeunitName": "APIV2 - Customers E2E", + "method": "TestCreateDetailedCustomer" + }, + { + "codeunitId": 139802, + "codeunitName": "APIV2 - Customers E2E", + "method": "TestCreateCustomerWithTemplate" + }, + { + "codeunitId": 139802, + "codeunitName": "APIV2 - Customers E2E", + "method": "TestDeleteCustomer" + }, + { + "codeunitId": 139802, + "codeunitName": "APIV2 - Customers E2E", + "method": "TestModifyCustomer" + }, + { + "codeunitId": 139802, + "codeunitName": "APIV2 - Customers E2E", + "method": "TestModifyCustomerWithAddress" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionWithDimensionCodeOnCustomer" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionWithDimensionCodeOnVendor" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionWithDimensionCodeOnItem" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionWithDimensionCodeOnEmployee" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionOnCustomer" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionOnVendor" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionOnItem" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionOnEmployee" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnCustomer" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnVendor" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnItem" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnEmployee" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnCustomer" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnVendor" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnItem" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnEmployee" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnCustomer" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnVendor" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnItem" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnEmployee" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestDeleteDefaultDimensionOnCustomer" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestDeleteDefaultDimensionOnVendor" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestDeleteDefaultDimensionOnItem" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestDeleteDefaultDimensionOnEmployee" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestGetDefaultDimensionOnCustomer" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestGetDefaultDimensionOnVendor" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestGetDefaultDimensionOnItem" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestGetDefaultDimensionOnEmployee" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionOnCustomer" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionOnVendor" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionOnItem" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionOnEmployee" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnCustomer" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnVendor" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnItem" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnEmployee" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnCustomer" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnVendor" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnItem" + }, + { + "codeunitId": 139832, + "codeunitName": "APIV2 - Default Dimensions E2E", + "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnEmployee" + }, + { + "codeunitId": 139826, + "codeunitName": "APIV2 - Tax Areas E2E", + "method": "TestVerifyIDandLastDateModified" + }, + { + "codeunitId": 139826, + "codeunitName": "APIV2 - Tax Areas E2E", + "method": "TestGetTaxAreas" + }, + { + "codeunitId": 139826, + "codeunitName": "APIV2 - Tax Areas E2E", + "method": "TestGetTaxArea" + }, + { + "codeunitId": 139826, + "codeunitName": "APIV2 - Tax Areas E2E", + "method": "TestCreateTaxArea" + }, + { + "codeunitId": 139826, + "codeunitName": "APIV2 - Tax Areas E2E", + "method": "TestModifyTaxArea" + }, + { + "codeunitId": 139826, + "codeunitName": "APIV2 - Tax Areas E2E", + "method": "TestDeleteTaxArea" + }, + { + "codeunitId": 139803, + "codeunitName": "APIV2 - Vendors E2E", + "method": "TestGetSimpleVendor" + }, + { + "codeunitId": 139803, + "codeunitName": "APIV2 - Vendors E2E", + "method": "TestCreateSimpleVendor" + }, + { + "codeunitId": 139803, + "codeunitName": "APIV2 - Vendors E2E", + "method": "TestCreateVendorWithTemplate" + }, + { + "codeunitId": 139803, + "codeunitName": "APIV2 - Vendors E2E", + "method": "TestModifyVendor" + }, + { + "codeunitId": 139803, + "codeunitName": "APIV2 - Vendors E2E", + "method": "TestDeleteVendor" } ] diff --git a/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.ExpenseFirstStage.DisabledTest.json b/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.ExpenseFirstStage.DisabledTest.json deleted file mode 100644 index 26075df9d71..00000000000 --- a/src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.ExpenseFirstStage.DisabledTest.json +++ /dev/null @@ -1,60 +0,0 @@ -[ - {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestGetSimpleCustomer"}, - {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestGetCustomerWithAddressAndSpecialCharacters"}, - {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestCreateDetailedCustomer"}, - {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestCreateCustomerWithTemplate"}, - {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestDeleteCustomer"}, - {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestModifyCustomer"}, - {"codeunitId": 139802, "codeunitName": "APIV2 - Customers E2E", "method": "TestModifyCustomerWithAddress"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnCustomer"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnVendor"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnItem"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionWithDimensionCodeOnEmployee"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnCustomer"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnVendor"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnItem"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionOnEmployee"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnCustomer"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnVendor"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnItem"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithMismatchingDimensionsOnEmployee"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnCustomer"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnVendor"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnItem"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionOnEmployee"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnCustomer"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnVendor"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnItem"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestCreateDefaultDimensionFailsWithBlockedDimensionValueOnEmployee"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnCustomer"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnVendor"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnItem"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestDeleteDefaultDimensionOnEmployee"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnCustomer"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnVendor"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnItem"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestGetDefaultDimensionOnEmployee"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnCustomer"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnVendor"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnItem"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionOnEmployee"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnCustomer"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnVendor"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnItem"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWithBlockedValueOnEmployee"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnCustomer"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnVendor"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnItem"}, - {"codeunitId": 139832, "codeunitName": "APIV2 - Default Dimensions E2E", "method": "TestPatchDefaultDimensionFailsWhenChangingDimensionCodeOnEmployee"}, - {"codeunitId": 139826, "codeunitName": "APIV2 - Tax Areas E2E", "method": "TestVerifyIDandLastDateModified"}, - {"codeunitId": 139826, "codeunitName": "APIV2 - Tax Areas E2E", "method": "TestGetTaxAreas"}, - {"codeunitId": 139826, "codeunitName": "APIV2 - Tax Areas E2E", "method": "TestGetTaxArea"}, - {"codeunitId": 139826, "codeunitName": "APIV2 - Tax Areas E2E", "method": "TestCreateTaxArea"}, - {"codeunitId": 139826, "codeunitName": "APIV2 - Tax Areas E2E", "method": "TestModifyTaxArea"}, - {"codeunitId": 139826, "codeunitName": "APIV2 - Tax Areas E2E", "method": "TestDeleteTaxArea"}, - {"codeunitId": 139803, "codeunitName": "APIV2 - Vendors E2E", "method": "TestGetSimpleVendor"}, - {"codeunitId": 139803, "codeunitName": "APIV2 - Vendors E2E", "method": "TestCreateSimpleVendor"}, - {"codeunitId": 139803, "codeunitName": "APIV2 - Vendors E2E", "method": "TestCreateVendorWithTemplate"}, - {"codeunitId": 139803, "codeunitName": "APIV2 - Vendors E2E", "method": "TestModifyVendor"}, - {"codeunitId": 139803, "codeunitName": "APIV2 - Vendors E2E", "method": "TestDeleteVendor"} -] From 0c811291747544c5c243fe28356714ec496bebfa Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Tue, 6 Oct 2026 18:10:01 +0200 Subject: [PATCH 14/22] Add isolated paired SQL database-name reset diagnostic Related to AB#646383; diagnostic only, does not resolve the work item. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .github/AL-Go-Settings.json | 2 +- .github/workflows/CICD.yaml | 469 ++---------------- .github/workflows/PullRequestHandler.yaml | 6 +- build/scripts/NewBcContainer.ps1 | 21 + build/scripts/ParallelTestExecution.psm1 | 49 +- .../SqlResetPilot/ContainerPreflight.ps1 | 34 ++ .../scripts/SqlResetPilot/Finalize.Tests.ps1 | 53 ++ build/scripts/SqlResetPilot/Finalize.ps1 | 28 ++ .../scripts/SqlResetPilot/Lifecycle.Tests.ps1 | 77 +++ build/scripts/SqlResetPilot/Lifecycle.psm1 | 131 +++++ build/scripts/SqlResetPilot/Prepare.ps1 | 53 ++ build/scripts/SqlResetPilot/README.md | 52 ++ 12 files changed, 547 insertions(+), 428 deletions(-) create mode 100644 build/scripts/SqlResetPilot/ContainerPreflight.ps1 create mode 100644 build/scripts/SqlResetPilot/Finalize.Tests.ps1 create mode 100644 build/scripts/SqlResetPilot/Finalize.ps1 create mode 100644 build/scripts/SqlResetPilot/Lifecycle.Tests.ps1 create mode 100644 build/scripts/SqlResetPilot/Lifecycle.psm1 create mode 100644 build/scripts/SqlResetPilot/Prepare.ps1 create mode 100644 build/scripts/SqlResetPilot/README.md diff --git a/.github/AL-Go-Settings.json b/.github/AL-Go-Settings.json index 9e192b255ea..60bb867acb2 100644 --- a/.github/AL-Go-Settings.json +++ b/.github/AL-Go-Settings.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go/91b96c2b294be6f823277dafe6f03350abfb9d23/Actions/.Modules/settings.schema.json", "type": "PTE", "templateUrl": "https://github.com/microsoft/AL-Go-PTE@preview", - "bcContainerHelperVersion": "preview", + "bcContainerHelperVersion": "6.1.19-preview2811389", "runs-on": "windows-latest", "cacheImageName": "", "UsePsSession": true, diff --git a/.github/workflows/CICD.yaml b/.github/workflows/CICD.yaml index 4c650a3d64b..76134472083 100644 --- a/.github/workflows/CICD.yaml +++ b/.github/workflows/CICD.yaml @@ -1,453 +1,86 @@ -name: ' CI/CD' +name: 'SQL reset paired pilot - DO NOT MERGE' on: workflow_dispatch: - push: - paths-ignore: - - '**.md' - - '.github/workflows/*.yaml' - - '!.github/workflows/CICD.yaml' - branches: [ 'main', 'releases/*' ] - -defaults: - run: - shell: powershell permissions: actions: read contents: read id-token: write - pages: read - security-events: write packages: read -env: - workflowDepth: 2 - ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} - ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} - jobs: - Initialization: - needs: [ ] - runs-on: [ windows-latest ] - outputs: - telemetryScopeJson: ${{ steps.init.outputs.telemetryScopeJson }} - environmentsMatrixJson: ${{ steps.DetermineDeploymentEnvironments.outputs.EnvironmentsMatrixJson }} - environmentCount: ${{ steps.DetermineDeploymentEnvironments.outputs.EnvironmentCount }} - deploymentEnvironmentsJson: ${{ steps.DetermineDeploymentEnvironments.outputs.DeploymentEnvironmentsJson }} - generateALDocArtifact: ${{ steps.DetermineDeploymentEnvironments.outputs.GenerateALDocArtifact }} - deployALDocArtifact: ${{ steps.DetermineDeploymentEnvironments.outputs.DeployALDocArtifact }} - deliveryTargetsJson: ${{ steps.DetermineDeliveryTargets.outputs.DeliveryTargetsJson }} - githubRunner: ${{ steps.ReadSettings.outputs.GitHubRunnerJson }} - githubRunnerShell: ${{ steps.ReadSettings.outputs.GitHubRunnerShell }} - projects: ${{ steps.determineProjectsToBuild.outputs.ProjectsJson }} - skippedProjects: ${{ steps.determineProjectsToBuild.outputs.SkippedProjectsJson }} - projectDependenciesJson: ${{ steps.determineProjectsToBuild.outputs.ProjectDependenciesJson }} - buildOrderJson: ${{ steps.determineProjectsToBuild.outputs.BuildOrderJson }} - baselineWorkflowRunId: ${{ steps.determineProjectsToBuild.outputs.BaselineWorkflowRunId }} - baselineWorkflowSHA: ${{ steps.determineProjectsToBuild.outputs.BaselineWorkflowSHA }} - workflowDepth: ${{ steps.DetermineWorkflowDepth.outputs.WorkflowDepth }} - powerPlatformSolutionFolder: ${{ steps.DeterminePowerPlatformSolutionFolder.outputs.powerPlatformSolutionFolder }} - trackALAlertsInGitHub: ${{ steps.SetALCodeAnalysisVar.outputs.trackALAlertsInGitHub }} - steps: - - name: Dump Workflow Information - uses: microsoft/AL-Go/Actions/DumpWorkflowInfo@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - lfs: true - - - name: Initialize the workflow - id: init - uses: microsoft/AL-Go/Actions/WorkflowInitialize@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - - - name: Read settings - id: ReadSettings - uses: microsoft/AL-Go/Actions/ReadSettings@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - get: type,powerPlatformSolutionFolder,useGitSubmodules,trackALAlertsInGitHub - - - name: Set AL Code Analysis Var output - id: SetALCodeAnalysisVar - run: | - Write-Host "trackALAlertsInGitHub environment variable: '$($env:trackALAlertsInGitHub)'" - Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "trackALAlertsInGitHub=$($env:trackALAlertsInGitHub)" - - - name: Read submodules token - id: ReadSubmodulesToken - if: env.useGitSubmodules != 'false' && env.useGitSubmodules != '' - uses: microsoft/AL-Go/Actions/ReadSecrets@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - gitHubSecrets: ${{ toJson(secrets) }} - getSecrets: '-gitSubmodulesToken' - - - name: Checkout Submodules - if: env.useGitSubmodules != 'false' && env.useGitSubmodules != '' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - lfs: true - submodules: ${{ env.useGitSubmodules }} - token: '${{ fromJson(steps.ReadSubmodulesToken.outputs.Secrets).gitSubmodulesToken }}' - - - name: Determine Workflow Depth - id: DetermineWorkflowDepth - run: | - Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "WorkflowDepth=$($env:workflowDepth)" - - - name: Determine Projects To Build - id: determineProjectsToBuild - uses: microsoft/AL-Go/Actions/DetermineProjectsToBuild@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - maxBuildDepth: ${{ env.workflowDepth }} - - - name: Determine PowerPlatform Solution Folder - id: DeterminePowerPlatformSolutionFolder - if: env.type == 'PTE' - run: | - Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "powerPlatformSolutionFolder=$($env:powerPlatformSolutionFolder)" - - - name: Determine Delivery Target Secrets - id: DetermineDeliveryTargetSecrets - uses: microsoft/AL-Go/Actions/DetermineDeliveryTargets@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - projectsJson: '${{ steps.determineProjectsToBuild.outputs.ProjectsJson }}' - checkContextSecrets: 'false' - - - name: Read secrets - id: ReadSecrets - uses: microsoft/AL-Go/Actions/ReadSecrets@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - gitHubSecrets: ${{ toJson(secrets) }} - getSecrets: ${{ steps.DetermineDeliveryTargetSecrets.outputs.ContextSecrets }} - - - name: Determine Delivery Targets - id: DetermineDeliveryTargets - uses: microsoft/AL-Go/Actions/DetermineDeliveryTargets@91b96c2b294be6f823277dafe6f03350abfb9d23 - env: - Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' - with: - shell: powershell - projectsJson: '${{ steps.determineProjectsToBuild.outputs.ProjectsJson }}' - checkContextSecrets: 'true' - - - name: Determine Deployment Environments - id: DetermineDeploymentEnvironments - uses: microsoft/AL-Go/Actions/DetermineDeploymentEnvironments@91b96c2b294be6f823277dafe6f03350abfb9d23 - env: - GITHUB_TOKEN: ${{ github.token }} - with: - shell: powershell - getEnvironments: '*' - type: 'CD' - - CheckForUpdates: - needs: [ Initialization ] - runs-on: [ windows-latest ] - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Read settings - uses: microsoft/AL-Go/Actions/ReadSettings@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - get: templateUrl - - - name: Read secrets - id: ReadSecrets - uses: microsoft/AL-Go/Actions/ReadSecrets@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - gitHubSecrets: ${{ toJson(secrets) }} - getSecrets: 'ghTokenWorkflow' - - - name: Check for updates to AL-Go system files - uses: microsoft/AL-Go/Actions/CheckForUpdates@91b96c2b294be6f823277dafe6f03350abfb9d23 - env: - GITHUB_TOKEN: ${{ github.token }} - with: - shell: powershell - templateUrl: ${{ env.templateUrl }} - token: ${{ fromJson(steps.ReadSecrets.outputs.Secrets).ghTokenWorkflow }} - downloadLatest: true - - Build1: - needs: [ Initialization ] - if: (!failure()) && (!cancelled()) && fromJson(needs.Initialization.outputs.buildOrderJson)[0].projectsCount > 0 + Pilot: + if: github.ref == 'refs/heads/features/646383-sql-reset-pilot' && github.run_attempt == 1 strategy: - matrix: - include: ${{ fromJson(needs.Initialization.outputs.buildOrderJson)[0].buildDimensions }} fail-fast: false - name: Build ${{ matrix.projectName }} (${{ matrix.buildMode }}) - uses: ./.github/workflows/_BuildALGoProject.yaml - secrets: inherit - with: - shell: ${{ matrix.githubRunnerShell }} - runsOn: ${{ matrix.githubRunner }} - project: ${{ matrix.project }} - projectName: ${{ matrix.projectName }} - buildMode: ${{ matrix.buildMode }} - skippedProjectsJson: ${{ needs.Initialization.outputs.skippedProjects }} - projectDependenciesJson: ${{ needs.Initialization.outputs.projectDependenciesJson }} - baselineWorkflowRunId: ${{ needs.Initialization.outputs.baselineWorkflowRunId }} - baselineWorkflowSHA: ${{ needs.Initialization.outputs.baselineWorkflowSHA }} - secrets: 'licenseFileUrl,codeSignCertificateUrl,*codeSignCertificatePassword,keyVaultCertificateUrl,*keyVaultCertificatePassword,keyVaultClientId,gitHubPackagesContext,applicationInsightsConnectionString' - signArtifacts: true - useArtifactCache: true - needsContext: ${{ toJson(needs) }} - - Build: - needs: [ Initialization, Build1 ] - if: (!failure()) && (!cancelled()) && (needs.Build1.result == 'success' || needs.Build1.result == 'skipped') && fromJson(needs.Initialization.outputs.buildOrderJson)[1].projectsCount > 0 - strategy: matrix: - include: ${{ fromJson(needs.Initialization.outputs.buildOrderJson)[1].buildDimensions }} - fail-fast: false - name: Build ${{ matrix.projectName }} (${{ matrix.buildMode }}) - uses: ./.github/workflows/_BuildALGoProject.yaml - secrets: inherit - with: - shell: ${{ matrix.githubRunnerShell }} - runsOn: ${{ matrix.githubRunner }} - project: ${{ matrix.project }} - projectName: ${{ matrix.projectName }} - buildMode: ${{ matrix.buildMode }} - skippedProjectsJson: ${{ needs.Initialization.outputs.skippedProjects }} - projectDependenciesJson: ${{ needs.Initialization.outputs.projectDependenciesJson }} - baselineWorkflowRunId: ${{ needs.Initialization.outputs.baselineWorkflowRunId }} - baselineWorkflowSHA: ${{ needs.Initialization.outputs.baselineWorkflowSHA }} - secrets: 'licenseFileUrl,codeSignCertificateUrl,*codeSignCertificatePassword,keyVaultCertificateUrl,*keyVaultCertificatePassword,keyVaultClientId,gitHubPackagesContext,applicationInsightsConnectionString' - signArtifacts: true - useArtifactCache: true - needsContext: ${{ toJson(needs) }} - - CodeAnalysisUpload: - needs: [ Initialization, Build ] - if: (!cancelled()) && (needs.Initialization.outputs.trackALAlertsInGitHub == 'True') - runs-on: [ windows-latest ] - name: Code Analysis Processing - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Download artifacts - ErrorLogs - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - env: - ACTIONS_RUNNER_DISABLE_NODE_MAGLEV: 1 - if: (success() || failure()) - with: - pattern: '*-*ErrorLogs-*' - path: '${{ github.workspace }}/ErrorLogs/' - merge-multiple: true - - - name: Process AL Code Analysis Logs - id: ProcessALCodeAnalysisLogs - if: (success() || failure()) - uses: microsoft/AL-Go/Actions/ProcessALCodeAnalysisLogs@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - - - name: Upload SARIF file to GitHub - uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 - if: (success() || failure()) && (hashFiles(format('{0}/ErrorLogs/output.sarif.json',github.workspace)) != '') - with: - sarif_file: '${{ github.workspace }}/ErrorLogs/output.sarif.json' - category: "ALCodeAnalysis" - - DeployALDoc: - needs: [ Initialization, Build ] - if: (!cancelled()) && (needs.Build.result == 'success' || needs.Build.result == 'skipped') && needs.Initialization.outputs.generateALDocArtifact == 1 && github.ref_name == 'main' - runs-on: [ windows-latest ] - name: Deploy Reference Documentation - permissions: - contents: read - actions: read - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Download artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - env: - ACTIONS_RUNNER_DISABLE_NODE_MAGLEV: 1 - with: - path: '.artifacts' - - - name: Read settings - uses: microsoft/AL-Go/Actions/ReadSettings@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - - - name: Determine ArtifactUrl - id: determineArtifactUrl - uses: microsoft/AL-Go/Actions/DetermineArtifactUrl@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - - - name: Setup Pages - if: needs.Initialization.outputs.deployALDocArtifact == 1 - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - - - name: Build Reference Documentation - uses: microsoft/AL-Go/Actions/BuildReferenceDocumentation@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: powershell - artifacts: '.artifacts' - artifactUrl: ${{ env.artifact }} - - - name: Upload pages artifact - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 - with: - path: ".aldoc/_site/" - - - name: Deploy to GitHub Pages - if: needs.Initialization.outputs.deployALDocArtifact == 1 - id: deployment - uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 - - Deploy: - needs: [ Initialization, Build1, Build ] - if: (!cancelled()) && (needs.Build1.result == 'success' || needs.Build1.result == 'skipped') && (needs.Build.result == 'success' || needs.Build.result == 'skipped') && needs.Initialization.outputs.environmentCount > 0 - strategy: ${{ fromJson(needs.Initialization.outputs.environmentsMatrixJson) }} - runs-on: ${{ fromJson(matrix.os) }} - name: Deploy to ${{ matrix.environment }} + arm: [control, fresh] + runs-on: GitHub-BCAppsPL + timeout-minutes: 120 defaults: run: - shell: ${{ matrix.shell }} - environment: - name: ${{ matrix.environment }} - url: ${{ steps.Deploy.outputs.environmentUrl }} + shell: powershell env: - ALGoEnvSettings: ${{ vars.ALGoEnvironmentSettings }} - ALGoEnvName: ${{ matrix.environment }} + ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} + ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} + BC_SQL_PILOT_ARM: ${{ matrix.arm }} + BuildMode: IntegrationTests steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Download artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - env: - ACTIONS_RUNNER_DISABLE_NODE_MAGLEV: 1 - with: - path: '.artifacts' - - - name: Read settings - uses: microsoft/AL-Go/Actions/ReadSettings@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: ${{ matrix.shell }} - get: type,powerPlatformSolutionFolder - - - name: EnvName - id: envName - run: | - $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 - $envName = '${{ matrix.environment }}'.split(' ')[0] - Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "envName=$envName" - - - name: Read secrets - id: ReadSecrets - uses: microsoft/AL-Go/Actions/ReadSecrets@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: ${{ matrix.shell }} - gitHubSecrets: ${{ toJson(secrets) }} - getSecrets: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext' - - - name: Deploy to Business Central - id: Deploy - uses: microsoft/AL-Go/Actions/Deploy@91b96c2b294be6f823277dafe6f03350abfb9d23 - env: - Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' - with: - shell: ${{ matrix.shell }} - environmentName: ${{ matrix.environment }} - artifactsFolder: '.artifacts' - type: 'CD' - deploymentEnvironmentsJson: ${{ needs.Initialization.outputs.deploymentEnvironmentsJson }} - - - name: Deploy to Power Platform - if: env.type == 'PTE' && env.powerPlatformSolutionFolder != '' - uses: microsoft/AL-Go/Actions/DeployPowerPlatform@91b96c2b294be6f823277dafe6f03350abfb9d23 - env: - Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' + - name: Checkout experiment + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: - shell: powershell - environmentName: ${{ matrix.environment }} - artifactsFolder: '.artifacts' - deploymentEnvironmentsJson: ${{ needs.Initialization.outputs.deploymentEnvironmentsJson }} - - Deliver: - needs: [ Initialization, Build1, Build ] - if: (!cancelled()) && (needs.Build1.result == 'success' || needs.Build1.result == 'skipped') && (needs.Build.result == 'success' || needs.Build.result == 'skipped') && needs.Initialization.outputs.deliveryTargetsJson != '[]' - strategy: - matrix: - deliveryTarget: ${{ fromJson(needs.Initialization.outputs.deliveryTargetsJson) }} - fail-fast: false - runs-on: [ windows-latest ] - name: Deliver to ${{ matrix.deliveryTarget }} - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + lfs: true + fetch-depth: 2 - - name: Download artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - name: Validate provenance and download exact compiled PR2 packages + id: packages env: - ACTIONS_RUNNER_DISABLE_NODE_MAGLEV: 1 - with: - path: '.artifacts' + GH_TOKEN: ${{ github.token }} + run: | + & .\build\scripts\SqlResetPilot\Prepare.ps1 - - name: Read settings + - name: Read pinned W1 Integration settings uses: microsoft/AL-Go/Actions/ReadSettings@91b96c2b294be6f823277dafe6f03350abfb9d23 with: shell: powershell + project: build/projects/Test Apps W1 + buildMode: IntegrationTests - - name: Read secrets - id: ReadSecrets + - name: Read normal disposable CI license and package secrets + id: secrets uses: microsoft/AL-Go/Actions/ReadSecrets@91b96c2b294be6f823277dafe6f03350abfb9d23 with: shell: powershell gitHubSecrets: ${{ toJson(secrets) }} - getSecrets: '${{ matrix.deliveryTarget }}Context' + getSecrets: 'licenseFileUrl,gitHubPackagesContext,applicationInsightsConnectionString,appDependencySecrets' - - name: Deliver - uses: microsoft/AL-Go/Actions/Deliver@91b96c2b294be6f823277dafe6f03350abfb9d23 + - name: Execute original pipeline with bounded clean-test prefix + uses: microsoft/AL-Go/Actions/RunPipeline@91b96c2b294be6f823277dafe6f03350abfb9d23 env: - Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' + Secrets: '${{ steps.secrets.outputs.Secrets }}' with: shell: powershell - type: 'CD' - projects: ${{ needs.Initialization.outputs.projects }} - deliveryTarget: ${{ matrix.deliveryTarget }} - artifacts: '.artifacts' - - PostProcess: - needs: [ Initialization, Build1, Build, Deploy, Deliver, DeployALDoc, CodeAnalysisUpload ] - if: (!cancelled()) - runs-on: [ windows-latest ] - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + project: build/projects/Test Apps W1 + buildMode: IntegrationTests + artifact: https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/sandbox/30.0.55683.0/w1 + installAppsJson: ${{ steps.packages.outputs.apps }} + installTestAppsJson: ${{ steps.packages.outputs.tests }} - - name: Finalize the workflow - id: PostProcess - uses: microsoft/AL-Go/Actions/WorkflowPostProcess@91b96c2b294be6f823277dafe6f03350abfb9d23 - env: - GITHUB_TOKEN: ${{ github.token }} - with: - shell: powershell - telemetryScopeJson: ${{ needs.Initialization.outputs.telemetryScopeJson }} - currentJobContext: ${{ toJson(job) }} + - name: Export diagnostics and tear down only owned disposable container + if: always() + run: | + & .\build\scripts\SqlResetPilot\Finalize.ps1 + + - name: Preserve raw outcomes (including failures) + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: sql-reset-pilot-${{ matrix.arm }}-${{ github.run_id }} + retention-days: 7 + if-no-files-found: error + path: | + sql-reset-pilot-output/** + build/projects/Test Apps W1/TestResults*.xml + build/projects/Test Apps W1/ContainerEventLog.evtx + build/projects/Test Apps W1/BuildOutput.txt diff --git a/.github/workflows/PullRequestHandler.yaml b/.github/workflows/PullRequestHandler.yaml index 5bf5c05d665..bfafea2b708 100644 --- a/.github/workflows/PullRequestHandler.yaml +++ b/.github/workflows/PullRequestHandler.yaml @@ -35,7 +35,7 @@ jobs: Initialization: needs: [ PregateCheck ] - if: (!failure() && !cancelled()) + if: (!failure() && !cancelled()) && github.head_ref != 'features/646383-sql-reset-pilot' runs-on: [ windows-latest ] outputs: projects: ${{ steps.determineProjectsToBuild.outputs.ProjectsJson }} @@ -93,7 +93,7 @@ jobs: Build1: needs: [ Initialization, CustomJob-VerifyAppChanges ] - if: (!failure()) && (!cancelled()) && fromJson(needs.Initialization.outputs.buildOrderJson)[0].projectsCount > 0 + if: (!failure()) && (!cancelled()) && needs.Initialization.result == 'success' && fromJson(needs.Initialization.outputs.buildOrderJson)[0].projectsCount > 0 strategy: matrix: include: ${{ fromJson(needs.Initialization.outputs.buildOrderJson)[0].buildDimensions }} @@ -119,7 +119,7 @@ jobs: Build: needs: [ Initialization, Build1, CustomJob-VerifyAppChanges ] - if: (!failure()) && (!cancelled()) && (needs.Build1.result == 'success' || needs.Build1.result == 'skipped') && fromJson(needs.Initialization.outputs.buildOrderJson)[1].projectsCount > 0 + if: (!failure()) && (!cancelled()) && needs.Initialization.result == 'success' && (needs.Build1.result == 'success' || needs.Build1.result == 'skipped') && fromJson(needs.Initialization.outputs.buildOrderJson)[1].projectsCount > 0 strategy: matrix: include: ${{ fromJson(needs.Initialization.outputs.buildOrderJson)[1].buildDimensions }} diff --git a/build/scripts/NewBcContainer.ps1 b/build/scripts/NewBcContainer.ps1 index 78871ab947b..974459b5f5d 100644 --- a/build/scripts/NewBcContainer.ps1 +++ b/build/scripts/NewBcContainer.ps1 @@ -22,7 +22,28 @@ if ($platformVersion) { $parameters.platformArtifactUrl = "$platformUrl/platform" } +if ($env:BC_SQL_PILOT_ARM) { + & (Join-Path $PSScriptRoot 'SqlResetPilot\ContainerPreflight.ps1') -Parameters $parameters +} New-BcContainer @parameters +if ($env:BC_SQL_PILOT_ARM) { + $runtime = Invoke-ScriptInBcContainer -containerName $parameters.ContainerName -scriptblock { + $commands = @('Get-NAVTenant', 'Dismount-NAVTenant', 'Mount-NAVTenant', 'Test-NAVDatabase', 'Copy-NAVDatabase', 'Remove-NAVDatabase') + @{ + commands = @($commands | ForEach-Object { + $command = Get-Command $_ -ErrorAction Stop + @{ name = $command.Name; version = [string]$command.Version; parameters = @($command.Parameters.Keys) } + }) + nst = @(Get-Process 'Microsoft.Dynamics.Nav.Server' | ForEach-Object { + @{ pid = $_.Id; version = $_.FileVersion } + }) + } + } + $runtime | ConvertTo-Json -Depth 6 | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT 'runtime-preflight.json') + if (@($runtime.nst).Count -ne 1 -or $runtime.nst[0].version -notmatch '^30\.0\.55665\.0(?:\s|$)') { + throw 'Installed NST is not the predeclared 30.0.55665.0 runtime.' + } +} if ($parameters.auth -in @('UserPassword', 'NavUserPassword')) { if (-not $parameters.credential) { diff --git a/build/scripts/ParallelTestExecution.psm1 b/build/scripts/ParallelTestExecution.psm1 index f8ed562ccee..4adfc6a0b21 100644 --- a/build/scripts/ParallelTestExecution.psm1 +++ b/build/scripts/ParallelTestExecution.psm1 @@ -13,6 +13,9 @@ if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) { } } Import-Module (Join-Path $PSScriptRoot "ALAppBuild.psm1" -Resolve) +if ($env:BC_SQL_PILOT_ARM) { + Import-Module (Join-Path $PSScriptRoot 'SqlResetPilot\Lifecycle.psm1') +} <# .SYNOPSIS @@ -376,9 +379,12 @@ function New-BcTestTenantTemplate { [string]$SourceDatabaseName ) - $result = @(Invoke-ScriptInBcContainer -containerName $ContainerName -useSession $false -scriptblock { Param($sourceDatabaseName) + $result = @(Invoke-ScriptInBcContainer -containerName $ContainerName -useSession $false -scriptblock { Param($sourceDatabaseName, $sqlPilot) $templateDatabaseName = "$sourceDatabaseName-test-template" - $maxAttempts = 3 + if ($sqlPilot -and (Test-NAVDatabase -DatabaseName $templateDatabaseName)) { + throw 'Pilot template already exists; it must never be overwritten.' + } + $maxAttempts = if ($sqlPilot) { 1 } else { 3 } $retryDelaySeconds = 5 for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { try { @@ -398,7 +404,7 @@ function New-BcTestTenantTemplate { } } $templateDatabaseName - } -argumentList $SourceDatabaseName) + } -argumentList $SourceDatabaseName, ([bool]$env:BC_SQL_PILOT_ARM)) if ($result.Count -eq 0) { throw "Creating the clean test tenant template returned no database name." @@ -430,6 +436,12 @@ function Reset-BcTestTenant { [string]$TemplateDatabaseName ) + if ($env:BC_SQL_PILOT_ARM) { + Invoke-SqlPilotReset -ContainerName $ContainerName -Tenant $Tenant -DatabaseName $TenantDatabaseName ` + -TemplateDatabaseName $TemplateDatabaseName + return + } + Invoke-ScriptInBcContainer -containerName $ContainerName -useSession $false -scriptblock { Param($tenant, $tenantDatabaseName, $templateDatabaseName) @@ -635,13 +647,16 @@ function Receive-TestJobResult { $outcome = 'Passed' if ($Job.State -eq 'Failed' -or $Job.State -eq 'Stopped') { - if ((Test-TransientTestFailure $output) -and -not $Retried.ContainsKey($Entry.appName)) { + if (-not $env:BC_SQL_PILOT_ARM -and (Test-TransientTestFailure $output) -and -not $Retried.ContainsKey($Entry.appName)) { $outcome = 'Transient' } else { $outcome = 'Failed' } } + if ($env:BC_SQL_PILOT_ARM) { + $output | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT "worker-$($Job.Id).log") -Encoding UTF8 + } Remove-Job -Job $Job -Force return [PSCustomObject]@{ @@ -836,6 +851,9 @@ function Invoke-RequiredDisabledTestExecution { -ScriptPath $ScriptPath -TestType $TestType -State $state -Verb $dispatch.Verb } $null = Wait-ForAllTestJobs -state $state + if ($env:BC_SQL_PILOT_ARM -and $state.hasFailures) { + break + } } return (-not $state.hasFailures) @@ -1327,6 +1345,10 @@ function Invoke-ParallelTestExecution { } $tenantInfo = @(Get-AvailableBcTenantInfo -containerName $parameters.containerName) + if ($env:BC_SQL_PILOT_ARM) { + Initialize-SqlResetPilot -ContainerName $parameters.containerName -TenantInfo $tenantInfo ` + -Arm $env:BC_SQL_PILOT_ARM -RunId $env:GITHUB_RUN_ID -OutputDirectory $env:BC_SQL_PILOT_OUTPUT + } $tenants = @($tenantInfo | ForEach-Object { $_.Id }) Write-Host "Available tenants: $($tenants -join ', ')" @@ -1384,6 +1406,11 @@ function Invoke-ParallelTestExecution { -AppNamesToTest $appNamesToTest -AppIdByName $appIdByName ) } + if ($env:BC_SQL_PILOT_ARM) { + $requiredDisabledWorkItems = @(Select-SqlPilotPrefix -WorkItems $requiredDisabledWorkItems) + $requiredDisabledWorkItems | ConvertTo-Json -Depth 5 | + Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT 'ordered-prefix.json') -Encoding UTF8 + } $templateDatabaseName = "" try { if ($requiredDisabledWorkItems.Count -gt 0) { @@ -1417,13 +1444,23 @@ function Invoke-ParallelTestExecution { } } finally { - foreach ($cleanTenant in $cleanTenantInfo) { + foreach ($cleanTenant in @($cleanTenantInfo | Where-Object { -not $env:BC_SQL_PILOT_ARM })) { Reset-BcTestTenant -ContainerName $parameters.containerName -Tenant $cleanTenant.Id ` -TenantDatabaseName $cleanTenant.DatabaseName -TemplateDatabaseName $templateDatabaseName } } } + if ($env:BC_SQL_PILOT_ARM) { + Merge-TenantTestResults -parameters $parameters -tenants $tenants -workItems $requiredDisabledWorkItems -rerunSuffixes @() + $state.finalResult = -not $state.hasFailures + $state.completed = $true + $state | ConvertTo-Json -Depth 5 | Set-Content $stateFile -Force + @{ completedUtc = [DateTime]::UtcNow.ToString('o'); passed = $state.finalResult; selectedCodeunits = $requiredDisabledWorkItems.Count } | + ConvertTo-Json | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT 'outcome.json') -Encoding UTF8 + return $state.finalResult + } + # Single dispatch loop, FIFO. TestConfiguration.json lists the smallest app first (a cheap # serial warmup) and the rest longest-first (LPT, keeps the tail short). The retry cap lives in # Receive-TestJobResult: an app already in $state.retried is classified as Failed on a re-fail. @@ -1521,7 +1558,7 @@ function Invoke-ParallelTestExecution { return $allPassed } finally { - if ($templateDatabaseName) { + if ($templateDatabaseName -and -not $env:BC_SQL_PILOT_ARM) { Remove-BcTestTenantTemplate -ContainerName $parameters.containerName ` -TemplateDatabaseName $templateDatabaseName } diff --git a/build/scripts/SqlResetPilot/ContainerPreflight.ps1 b/build/scripts/SqlResetPilot/ContainerPreflight.ps1 new file mode 100644 index 00000000000..d15e6d33df4 --- /dev/null +++ b/build/scripts/SqlResetPilot/ContainerPreflight.ps1 @@ -0,0 +1,34 @@ +param([hashtable]$Parameters) +$ErrorActionPreference = 'Stop' +if ($env:GITHUB_REF -ne 'refs/heads/features/646383-sql-reset-pilot' -or + $env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or + $env:BC_SQL_PILOT_ARM -notin @('control', 'fresh') -or -not $env:BC_SQL_PILOT_OUTPUT) { + throw 'Refusing SQL pilot outside its disposable manually dispatched CI job.' +} +$expectedName = "bcbuildprojectsTestAppsW1$($env:GITHUB_RUN_ID)" +if ($Parameters.ContainerName -ne $expectedName -or (Test-BcContainer -containerName $expectedName)) { + throw 'Pilot container name is unexpected or already exists. No replacement is authorized.' +} +$module = Get-Module BcContainerHelper -ErrorAction Stop +$helperVersion = & $module { $bcContainerHelperVersion } +if ($helperVersion -ne '6.1.19-preview2811389') { throw "Wrong BCH version: $helperVersion" } +$settings = $env:Settings | ConvertFrom-Json +if ($settings.country -ne 'w1' -or $settings.testType -ne 'IntegrationTest' -or + $settings.numberOfTenantsForTesting -ne 4 -or $settings.companyName -ne 'My Company' -or + -not $settings.enableCleanTestCodeunitExecution -or $settings.enableTaskScheduler) { + throw 'Effective W1 Integration settings differ from the predeclared experiment.' +} +if (-not (Get-Command New-BcContainer).Parameters.ContainsKey('useGenericImage')) { + throw 'Exact BCH does not expose the supported generic-image parameter.' +} +$Parameters.useGenericImage = 'mcr.microsoft.com/businesscentral@sha256:c899d12093ad7bbdbfd08ccc0e6294e0f98c682c7e35db4ecfbca345fb068492' +if ($Parameters.platformArtifactUrl -ne 'https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/platform/30.0.55665.0/platform') { + throw 'Platform artifact is not the pinned source platform.' +} +@{ + container = $expectedName; runId = $env:GITHUB_RUN_ID; arm = $env:BC_SQL_PILOT_ARM + helperVersion = $helperVersion; image = $Parameters.useGenericImage + imageGenericTag = '1.0.2.128'; platform = '30.0.55665.0' + hostOS = [Environment]::OSVersion.Version.ToString() + registeredUtc = [DateTime]::UtcNow.ToString('o') +} | ConvertTo-Json | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT 'container-ownership.json') -Encoding UTF8 diff --git a/build/scripts/SqlResetPilot/Finalize.Tests.ps1 b/build/scripts/SqlResetPilot/Finalize.Tests.ps1 new file mode 100644 index 00000000000..deca7127d20 --- /dev/null +++ b/build/scripts/SqlResetPilot/Finalize.Tests.ps1 @@ -0,0 +1,53 @@ +function global:Test-BcContainer { param($containerName) throw 'Live container call forbidden.' } +function global:Get-BcContainerEventLog { param($containerName, [switch]$doNotOpen) throw 'Live container call forbidden.' } +function global:Remove-BcContainer { param($containerName) throw 'Live container call forbidden.' } + +Describe 'SQL pilot final cleanup ownership' { + BeforeEach { + $script:oldWorkspace = $env:GITHUB_WORKSPACE + $script:oldRun = $env:GITHUB_RUN_ID + $script:oldArm = $env:BC_SQL_PILOT_ARM + $script:oldHelper = $env:BcContainerHelperPath + $env:GITHUB_WORKSPACE = $PSScriptRoot + $env:GITHUB_RUN_ID = '123' + $env:BC_SQL_PILOT_ARM = 'fresh' + $env:BcContainerHelperPath = Join-Path $PSScriptRoot 'BcContainerHelper.ps1' + Mock Import-Module {} + Mock Test-Path { $true } + Mock Get-Content { '{"runId":"123","arm":"fresh","container":"bcbuildprojectsTestAppsW1123"}' } + Mock Get-ChildItem {} + Mock Copy-Item {} + Mock Set-Content {} + $global:sqlPilotTestContainerExists = $true + Mock Test-BcContainer { $global:sqlPilotTestContainerExists } + Mock Get-BcContainerEventLog { 'events.evtx' } + Mock Remove-BcContainer { $global:sqlPilotTestContainerExists = $false } + } + AfterEach { + $env:GITHUB_WORKSPACE = $script:oldWorkspace + $env:GITHUB_RUN_ID = $script:oldRun + $env:BC_SQL_PILOT_ARM = $script:oldArm + $env:BcContainerHelperPath = $script:oldHelper + Remove-Variable sqlPilotTestContainerExists -Scope Global + } + It 'exports before disposing only the registered container' { + & (Join-Path $PSScriptRoot 'Finalize.ps1') + Assert-MockCalled Get-BcContainerEventLog -Times 1 -Exactly -Scope It + Assert-MockCalled Remove-BcContainer -Times 1 -Exactly -Scope It -ParameterFilter { $containerName -eq 'bcbuildprojectsTestAppsW1123' } + } + It 'still tears down if exporting fails and keeps that failure visible' { + Mock Get-BcContainerEventLog { throw 'event export failed' } + { & (Join-Path $PSScriptRoot 'Finalize.ps1') } | Should -Throw + Assert-MockCalled Remove-BcContainer -Times 1 -Exactly -Scope It + } + It 'refuses deletion if ownership was not recorded' { + Mock Test-Path { $false } + & (Join-Path $PSScriptRoot 'Finalize.ps1') + Assert-MockCalled Remove-BcContainer -Times 0 -Exactly -Scope It + } + It 'refuses deletion when run, arm or exact name differs' { + Mock Get-Content { '{"runId":"999","arm":"fresh","container":"shared"}' } + { & (Join-Path $PSScriptRoot 'Finalize.ps1') } | Should -Throw + Assert-MockCalled Remove-BcContainer -Times 0 -Exactly -Scope It + } +} diff --git a/build/scripts/SqlResetPilot/Finalize.ps1 b/build/scripts/SqlResetPilot/Finalize.ps1 new file mode 100644 index 00000000000..41a667bd01d --- /dev/null +++ b/build/scripts/SqlResetPilot/Finalize.ps1 @@ -0,0 +1,28 @@ +$ErrorActionPreference = 'Stop' +$output = Join-Path $env:GITHUB_WORKSPACE 'sql-reset-pilot-output' +$ownershipPath = Join-Path $output 'container-ownership.json' +if (-not (Test-Path $ownershipPath)) { + Write-Host 'No owned container was registered; no container deletion is authorized.' + return +} +$ownership = Get-Content $ownershipPath -Raw | ConvertFrom-Json +if ($ownership.runId -ne $env:GITHUB_RUN_ID -or $ownership.arm -ne $env:BC_SQL_PILOT_ARM -or + $ownership.container -ne "bcbuildprojectsTestAppsW1$($env:GITHUB_RUN_ID)") { + throw 'Cleanup ownership mismatch.' +} +Import-Module (Join-Path (Split-Path $env:BcContainerHelperPath -Parent) 'BcContainerHelper.psm1') -ErrorAction Stop +try { + if (Test-BcContainer -containerName $ownership.container) { + $events = Get-BcContainerEventLog -containerName $ownership.container -doNotOpen + Copy-Item $events (Join-Path $output 'final-container.evtx') + } + $project = Join-Path $env:GITHUB_WORKSPACE 'build\projects\Test Apps W1' + Get-ChildItem $project -Filter 'TestResults*.xml' -File | Copy-Item -Destination $output +} finally { + if (Test-BcContainer -containerName $ownership.container) { + Remove-BcContainer -containerName $ownership.container + } + @{ completedUtc = [DateTime]::UtcNow.ToString('o'); containerRemaining = (Test-BcContainer -containerName $ownership.container) } | + ConvertTo-Json | Set-Content (Join-Path $output 'cleanup.json') -Encoding UTF8 +} +if (Test-BcContainer -containerName $ownership.container) { throw 'Owned container teardown did not complete.' } diff --git a/build/scripts/SqlResetPilot/Lifecycle.Tests.ps1 b/build/scripts/SqlResetPilot/Lifecycle.Tests.ps1 new file mode 100644 index 00000000000..da023382fb4 --- /dev/null +++ b/build/scripts/SqlResetPilot/Lifecycle.Tests.ps1 @@ -0,0 +1,77 @@ +Import-Module (Join-Path $PSScriptRoot 'Lifecycle.psm1') -Force +function global:Invoke-ScriptInBcContainer { + param($containerName, $useSession, $scriptblock, $argumentList) + throw 'No live container calls are permitted in unit tests.' +} + +Describe 'SQL reset pilot ownership and mapping' { + BeforeEach { + $script:tenants = @('default','tenant2','tenant3','tenant4') | ForEach-Object { + [PSCustomObject]@{ Id = $_; DatabaseName = $_ } + } + Initialize-SqlResetPilot 'owned' $script:tenants 'fresh' '123456789' $PSScriptRoot + } + It 'uses a fresh database target and updates the original caller mapping for the next cycle' { + $first = Get-SqlResetPlan 'owned' 'tenant2' 'tenant2' 'default' + $first.Destination | Should -Be 'tenant2_r123456789_g1' + Complete-SqlResetPlan $first + $script:tenants[1].DatabaseName | Should -Be $first.Destination + $second = Get-SqlResetPlan 'owned' 'tenant2' $script:tenants[1].DatabaseName 'default-test-template' + $second.Previous | Should -Be $first.Destination + $second.Destination | Should -Be 'tenant2_r123456789_g2' + } + It 'retains the original SQL target in control while increasing its observation label' { + Initialize-SqlResetPilot 'owned' $script:tenants 'control' '123456789' $PSScriptRoot + $first = Get-SqlResetPlan 'owned' 'tenant3' 'tenant3' 'default-test-template' + Complete-SqlResetPlan $first + $second = Get-SqlResetPlan 'owned' 'tenant3' 'tenant3' 'default-test-template' + $second.Destination | Should -Be 'tenant3' + $second.Generation | Should -Be 2 + } + It 'rejects stale mappings after a successful rename' { + Complete-SqlResetPlan (Get-SqlResetPlan 'owned' 'tenant2' 'tenant2' 'default') + { Get-SqlResetPlan 'owned' 'tenant2' 'tenant2' 'default' } | Should -Throw + } + It 'never accepts the primary or template as a deletion target' { + { Get-SqlResetPlan 'owned' 'default' 'default' 'default-test-template' } | Should -Throw + { Get-SqlResetPlan 'owned' 'tenant2' 'default-test-template' 'default' } | Should -Throw + { Get-SqlResetPlan 'owned' 'tenant2' 'app' 'default' } | Should -Throw + } + It 'rejects unowned containers, workers and source databases' { + { Get-SqlResetPlan 'shared' 'tenant2' 'tenant2' 'default' } | Should -Throw + { Get-SqlResetPlan 'owned' 'tenant5' 'tenant5' 'default' } | Should -Throw + { Get-SqlResetPlan 'owned' 'tenant2' 'tenant2' 'shared' } | Should -Throw + } + It 'rejects unsafe run identities and unexpected original mappings' { + { Initialize-SqlResetPilot 'owned' $script:tenants 'fresh' '1;drop' $PSScriptRoot } | Should -Throw + $script:tenants[1].DatabaseName = 'default' + { Initialize-SqlResetPilot 'owned' $script:tenants 'fresh' '123' $PSScriptRoot } | Should -Throw + } + It 'preserves the seven original three-worker batches and stops after the target batch' { + $ids = @(139700,139702,139703,139706,139725,139726,139732,139739,139742,139745,139802,139803,139806,139826,139832,139854,139972,139780,148315,148318,148343,148344) + $items = @($ids | ForEach-Object { [PSCustomObject]@{ CodeunitId = $_ } }) + $prefix = @(Select-SqlPilotPrefix $items) + $prefix.Count | Should -Be 21 + $prefix[18].CodeunitId | Should -Be 148315 + $prefix[20].CodeunitId | Should -Be 148343 + $items[0].CodeunitId = 1 + { Select-SqlPilotPrefix $items } | Should -Throw + } + It 'advances mapping only after the container operation succeeds' { + Mock Invoke-ScriptInBcContainer -ModuleName Lifecycle { @{ state = 'Operational' } } + Mock Add-Content -ModuleName Lifecycle {} + Invoke-SqlPilotReset 'owned' 'tenant2' 'tenant2' 'default' + $script:tenants[1].DatabaseName | Should -Be 'tenant2_r123456789_g1' + Assert-MockCalled Invoke-ScriptInBcContainer -ModuleName Lifecycle -Times 1 -Exactly -Scope It -ParameterFilter { + $containerName -eq 'owned' -and $argumentList.Previous -eq 'tenant2' -and + $argumentList.Destination -eq 'tenant2_r123456789_g1' + } + } + It 'leaves mapping unchanged and propagates a failed restore without retrying' { + Mock Invoke-ScriptInBcContainer -ModuleName Lifecycle { throw 'copy failed' } + Mock Add-Content -ModuleName Lifecycle {} + { Invoke-SqlPilotReset 'owned' 'tenant2' 'tenant2' 'default' } | Should -Throw + $script:tenants[1].DatabaseName | Should -Be 'tenant2' + Assert-MockCalled Invoke-ScriptInBcContainer -ModuleName Lifecycle -Times 1 -Exactly -Scope It + } +} diff --git a/build/scripts/SqlResetPilot/Lifecycle.psm1 b/build/scripts/SqlResetPilot/Lifecycle.psm1 new file mode 100644 index 00000000000..180d63232b6 --- /dev/null +++ b/build/scripts/SqlResetPilot/Lifecycle.psm1 @@ -0,0 +1,131 @@ +Set-StrictMode -Version 2 +$script:pilot = $null + +function Initialize-SqlResetPilot { + param([string]$ContainerName, [array]$TenantInfo, [string]$Arm, [string]$RunId, [string]$OutputDirectory) + if ($Arm -notin @('control', 'fresh') -or $RunId -notmatch '^\d{1,20}$') { throw 'Invalid pilot identity.' } + if ($TenantInfo.Count -ne 4 -or @($TenantInfo.Id | Sort-Object -Unique).Count -ne 4) { + throw 'Pilot requires exactly default and three distinct workers.' + } + $workers = @{} + foreach ($id in @('tenant2', 'tenant3', 'tenant4')) { + $info = @($TenantInfo | Where-Object Id -eq $id) + if ($info.Count -ne 1 -or $info[0].DatabaseName -ne $id) { throw 'Unexpected initial owned worker mapping.' } + $workers[$id] = @{ Info = $info[0]; Generation = 0; Owned = @($id); Used = @($id) } + } + $source = @($TenantInfo | Where-Object Id -eq 'default') + if ($source.Count -ne 1 -or $source[0].DatabaseName -ne 'default') { throw 'Unexpected protected source mapping.' } + $script:pilot = @{ + Container = $ContainerName; Arm = $Arm; RunId = $RunId; Workers = $workers + OutputDirectory = $OutputDirectory + } +} + +function Get-SqlResetPlan { + param([string]$ContainerName, [string]$Tenant, [string]$DatabaseName, [string]$TemplateDatabaseName) + if (-not $script:pilot -or $ContainerName -ne $script:pilot.Container -or + -not $script:pilot.Workers.ContainsKey($Tenant)) { throw 'Unowned container or tenant.' } + $worker = $script:pilot.Workers[$Tenant] + if ($DatabaseName -ne $worker.Info.DatabaseName -or $DatabaseName -notin $worker.Owned -or + $TemplateDatabaseName -notin @('default', 'default-test-template') -or + $DatabaseName -in @('default', 'default-test-template', 'app') -or + $DatabaseName -eq $TemplateDatabaseName) { throw 'Unsafe or stale database mapping.' } + $generation = $worker.Generation + 1 + $destination = if ($script:pilot.Arm -eq 'fresh') { "${Tenant}_r$($script:pilot.RunId)_g$generation" } else { $DatabaseName } + if ($destination -notmatch '^tenant[234](?:_r\d{1,20}_g\d+)?$' -or $destination.Length -gt 64 -or + ($script:pilot.Arm -eq 'fresh' -and $destination -in $worker.Used)) { throw 'Unsafe or reused destination.' } + [PSCustomObject]@{ + Tenant = $Tenant; Previous = $DatabaseName; Destination = $destination + Template = $TemplateDatabaseName; Generation = $generation + } +} + +function Complete-SqlResetPlan { + param($Plan) + $worker = $script:pilot.Workers[$Plan.Tenant] + $worker.Info.DatabaseName = $Plan.Destination + $worker.Generation = $Plan.Generation + $worker.Owned = @($worker.Owned | Where-Object { $_ -ne $Plan.Previous }) + @($Plan.Destination) + $worker.Used += $Plan.Destination +} + +function Invoke-SqlPilotReset { + param([string]$ContainerName, [string]$Tenant, [string]$DatabaseName, [string]$TemplateDatabaseName) + $plan = Get-SqlResetPlan $ContainerName $Tenant $DatabaseName $TemplateDatabaseName + $trace = Join-Path $script:pilot.OutputDirectory 'reset-timeline.jsonl' + @{ phase = 'reset-start'; utc = [DateTime]::UtcNow.ToString('o'); arm = $script:pilot.Arm; plan = $plan } | + ConvertTo-Json -Depth 5 -Compress | Add-Content $trace + try { + $result = Invoke-ScriptInBcContainer -containerName $ContainerName -useSession $false -scriptblock { + param($plan) + $ErrorActionPreference = 'Stop' + $stopwatch = [Diagnostics.Stopwatch]::StartNew() + $mounted = Get-NAVTenant -ServerInstance $ServerInstance -Tenant $plan.Tenant + $localServers = @('.', 'localhost', $env:COMPUTERNAME, '.\SQLEXPRESS', 'localhost\SQLEXPRESS', "$env:COMPUTERNAME\SQLEXPRESS") + if ($mounted.DatabaseName -ne $plan.Previous -or $mounted.DatabaseServer -notin $localServers) { + throw 'Actual mounted mapping differs from the owned local SQL mapping.' + } + if ($plan.Destination -ne $plan.Previous -and (Test-NAVDatabase -DatabaseName $plan.Destination)) { + throw 'Fresh database destination already exists; refusing to overwrite it.' + } + function Read-PilotIdentity($name) { + $connection = New-Object System.Data.SqlClient.SqlConnection 'Server=.\SQLEXPRESS;Database=master;Integrated Security=True;TrustServerCertificate=True' + try { + $connection.Open() + $command = $connection.CreateCommand() + $command.CommandText = 'SELECT name, database_id, create_date, service_broker_guid FROM sys.databases WHERE name = @name' + $null = $command.Parameters.AddWithValue('@name', $name) + $reader = $command.ExecuteReader() + if (-not $reader.Read()) { throw 'Owned SQL database identity is missing.' } + @{ name = [string]$reader[0]; databaseId = [int]$reader[1]; created = [string]$reader[2]; serviceBrokerGuid = [string]$reader[3] } + } finally { $connection.Dispose() } + } + $before = Read-PilotIdentity $plan.Previous + $started = [DateTime]::UtcNow.ToString('o') + Dismount-NAVTenant -ServerInstance $ServerInstance -Tenant $plan.Tenant -Force | Out-Null + if (Test-NAVDatabase -DatabaseName $plan.Previous) { + Remove-NAVDatabase -DatabaseName $plan.Previous | Out-Null + } + Copy-NAVDatabase -SourceDatabaseName $plan.Template -DestinationDatabaseName $plan.Destination -DatabaseServer '.' | Out-Null + Mount-NAVTenant -ServerInstance $ServerInstance -Id $plan.Tenant -DatabaseServer '.' ` + -DatabaseName $plan.Destination -OverwriteTenantIdInDatabase -Force | Out-Null + while ((Get-NAVTenant -ServerInstance $ServerInstance -Tenant $plan.Tenant).State -eq 'Mounting') { + if ($stopwatch.Elapsed.TotalSeconds -ge 300) { throw 'Tenant mount exceeded baseline 300-second bound.' } + Start-Sleep -Milliseconds 250 + } + $mounted = Get-NAVTenant -ServerInstance $ServerInstance -Tenant $plan.Tenant + if ($mounted.State -notin @('Operational', 'OperationalWithWarnings') -or $mounted.DatabaseName -ne $plan.Destination) { + throw 'Mounted state or mapping differs from the expected new generation.' + } + @{ + startedUtc = $started; completedUtc = [DateTime]::UtcNow.ToString('o') + before = $before; after = (Read-PilotIdentity $plan.Destination) + databaseServer = [string]$mounted.DatabaseServer; state = [string]$mounted.State + detailedState = [string]$mounted.DetailedState; forceRefresh = $false + nst = @(Get-Process 'Microsoft.Dynamics.Nav.Server' | ForEach-Object { + @{ pid = $_.Id; version = $_.FileVersion } + }) + } + } -argumentList $plan + Complete-SqlResetPlan $plan + @{ phase = 'reset-complete'; utc = [DateTime]::UtcNow.ToString('o'); plan = $plan; observation = $result } | + ConvertTo-Json -Depth 8 -Compress | Add-Content $trace + } catch { + # Do not log exception text: SQL/provider diagnostics can contain connection information. + @{ phase = 'reset-failed'; utc = [DateTime]::UtcNow.ToString('o'); plan = $plan; errorType = $_.Exception.GetType().FullName } | + ConvertTo-Json -Depth 5 -Compress | Add-Content $trace + throw + } +} + +function Select-SqlPilotPrefix { + param([array]$WorkItems) + $ids = @(139700,139702,139703,139706,139725,139726,139732,139739,139742,139745,139802,139803,139806,139826,139832,139854,139972,139780,148315,148318,148343) + if ($WorkItems.Count -lt $ids.Count) { throw 'Discovery returned an incomplete pilot prefix.' } + for ($i = 0; $i -lt $ids.Count; $i++) { + if ([int]$WorkItems[$i].CodeunitId -ne $ids[$i]) { throw "Original W1 discovery order differs at position $i." } + } + @($WorkItems | Select-Object -First $ids.Count) +} + +Export-ModuleMember -Function Initialize-SqlResetPilot, Get-SqlResetPlan, Complete-SqlResetPlan, Invoke-SqlPilotReset, Select-SqlPilotPrefix diff --git a/build/scripts/SqlResetPilot/Prepare.ps1 b/build/scripts/SqlResetPilot/Prepare.ps1 new file mode 100644 index 00000000000..50556207828 --- /dev/null +++ b/build/scripts/SqlResetPilot/Prepare.ps1 @@ -0,0 +1,53 @@ +$ErrorActionPreference = 'Stop' +$base = 'c4953dceffe02a017adad34973e1955017bf5d20' +if ($env:GITHUB_REPOSITORY -ne 'microsoft/BCApps' -or + $env:GITHUB_REF -ne 'refs/heads/features/646383-sql-reset-pilot' -or + $env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or + $env:BC_SQL_PILOT_ARM -notin @('control', 'fresh')) { + throw 'This diagnostic is restricted to its explicitly dispatched disposable CI branch, attempt one.' +} +git merge-base --is-ancestor $base HEAD +if ($LASTEXITCODE -ne 0) { throw 'PR2 base is not an ancestor.' } +git diff --exit-code $base HEAD -- src +if ($LASTEXITCODE -ne 0) { throw 'AL/package source differs from the pinned compiled PR2 source.' } + +$output = Join-Path $env:GITHUB_WORKSPACE 'sql-reset-pilot-output' +New-Item -ItemType Directory $output -Force | Out-Null +"BC_SQL_PILOT_OUTPUT=$output" | Add-Content $env:GITHUB_ENV +@{ phase = 'package-preflight'; arm = $env:BC_SQL_PILOT_ARM; run = $env:GITHUB_RUN_ID; utc = [DateTime]::UtcNow.ToString('o') } | + ConvertTo-Json | Set-Content (Join-Path $output 'start.json') -Encoding UTF8 +$headers = @{ Authorization = "Bearer $env:GH_TOKEN"; Accept = 'application/vnd.github+json' } +$api = 'https://api.github.com/repos/microsoft/BCApps' +$artifacts = @( + @{ Kind = 'apps'; Id = 11374170356; Digest = '683343673e6efeb7699734b250d2ee5004d2d83fe08e57f324c4c645c09612db' }, + @{ Kind = 'tests'; Id = 11374145359; Digest = 'f75ce06920437328e5d79a5512eddcc2425624bb696c339c108478c83554171d' } +) +$manifest = @() +foreach ($artifact in $artifacts) { + $metadata = Invoke-RestMethod "$api/actions/artifacts/$($artifact.Id)" -Headers $headers + if ($metadata.expired -or $metadata.workflow_run.id -ne 37372848860 -or + $metadata.workflow_run.head_sha -ne $base -or $metadata.digest -ne "sha256:$($artifact.Digest)") { + throw 'Exact artifact provenance/digest check failed; no moving-build fallback is permitted.' + } + $destination = Join-Path $env:GITHUB_WORKSPACE "sql-reset-pilot-packages\$($artifact.Kind)" + New-Item -ItemType Directory $destination -Force | Out-Null + $zip = "$destination.zip" + Invoke-WebRequest "$api/actions/artifacts/$($artifact.Id)/zip" -Headers $headers -OutFile $zip -UseBasicParsing + if ((Get-FileHash $zip -Algorithm SHA256).Hash -ne $artifact.Digest) { throw 'Artifact archive hash mismatch.' } + Expand-Archive $zip $destination + $files = @(Get-ChildItem $destination -Filter '*.app' -Recurse -File | Sort-Object FullName) + if ($files.Count -eq 0) { throw 'Pinned artifact contains no app packages.' } + $list = Join-Path $destination 'packages.json' + ConvertTo-Json -InputObject @($files.FullName) | Set-Content $list -Encoding UTF8 + "$($artifact.Kind)=$list" | Add-Content $env:GITHUB_OUTPUT + $manifest += @{ + artifactId = $artifact.Id; sourceRun = 37372848860; sourceHead = $base + kind = $artifact.Kind; archiveSha256 = $artifact.Digest + files = @($files | ForEach-Object { @{ name = $_.Name; sha256 = (Get-FileHash $_.FullName).Hash } }) + } +} +@{ + experimentHead = $env:GITHUB_SHA; run = $env:GITHUB_RUN_ID; arm = $env:BC_SQL_PILOT_ARM + budgetMinutes = 120; startedUtc = [DateTime]::UtcNow.ToString('o') + artifacts = $manifest +} | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $output 'provenance.json') -Encoding UTF8 diff --git a/build/scripts/SqlResetPilot/README.md b/build/scripts/SqlResetPilot/README.md new file mode 100644 index 00000000000..f43cd35b66c --- /dev/null +++ b/build/scripts/SqlResetPilot/README.md @@ -0,0 +1,52 @@ +# Diagnostic only — do not merge + +One manually dispatched paired W1 Integration pilot on top of PR #11860, +`c4953dceffe02a017adad34973e1955017bf5d20`. Related context: AB#646383; +this experiment does **not** resolve that work item and is outside the native +merge group. No auto-merge, queue entry, automatic rerun or main-branch change. + +The branch-only `CICD.yaml` replaces the normal CI matrix with two fresh +`GitHub-BCAppsPL` containers, capped at **120 minutes per arm** (240 runner-minutes). +PR project discovery is disabled for this experiment branch to avoid a full-country +build. Dispatch the existing registered `CICD.yaml` with this branch as its ref. +Do not dispatch a second pair without new authorization. + +Both arms use the exact same PR2 app/test archive IDs and SHA256 digests, +BCH `6.1.19-preview2811389`, NST `30.0.55665.0`, W1 application artifact +`30.0.55683.0`, image digest, original license provisioning, auth files, +test assertions and three-worker scheduling. No AL source is changed or compiled +against a moving head. The image digest resolves the original generic +`1.0.2.128` LTSC2025-dev image; the historic log did not record its digest. +The pilot records the runner OS; compare both manifests before interpretation. + +The original discovery runs unchanged. Its ordered clean-test prefix must match +the 21 codeunits captured in W1 job 112207627851, run 37372848860. Seven batches +end with Expense Users, Capabilities and Activity Log. No ordinary-test phase, +HTTP warmup, auth mutation, extra sleep, force-refresh or pool clearing is added. +The existing mount-state polling remains. Copy failures and test failures are +terminal; baseline copy/transient retries are deliberately disabled in both arms. +A failed batch drains its three workers and prevents later batches. + +Control restores worker databases under `tenant2/3/4`. Treatment preserves those +logical IDs/URLs, but uses `tenantN_rRUN_gGENERATION` database names. Both arms +read the same SQL identity fields before and after reset. This administrative +probe has a possible timing effect and does not reveal the NST internal pool +generation. A fresh SQL name does not guarantee a fresh database ID or GUID. + +Deletion is allowed only for a tracked worker's previously mounted local database, +after dismount. A stale/foreign mapping or pre-existing fresh target fails closed. +The caller's tenant objects are updated after each successful mount, including +the discovery restore. Default/application/template databases are not deleted by +the pilot. The immutable template is retained until whole-container teardown. +The ordinary disposable pipeline teardown plus the final ownership-checked +fallback clean up the complete container, including partial failed copies. +Runner loss/hard cancellation may prevent exports/finally; that is inconclusive. + +Artifacts contain package hashes, effective identities, ordered worklist, +reset UTC/SQL identity timeline, worker output, JUnit and raw EVTX. +No credentials, access keys or authorization headers are added to diagnostics. +Missing artifacts, setup drift, timeout or no reproduction mean **inconclusive**. +Control-only reproduction is a name-reuse signal, not proof of a production fix. + +Local validation (Pester 5, no NST required): +`Invoke-Pester .\build\scripts\SqlResetPilot` From ae6513a3a3b8902513e69af929331c17272a221d Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Tue, 6 Oct 2026 18:33:11 +0200 Subject: [PATCH 15/22] Match SQL pilot action shells to baseline PowerShell 7 Preserve failed preparation as inconclusive; one replacement pair authorized. Related to AB#646383, diagnostic only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .github/workflows/CICD.yaml | 24 +++++++++++++++++++----- build/scripts/SqlResetPilot/README.md | 22 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/.github/workflows/CICD.yaml b/.github/workflows/CICD.yaml index 76134472083..a6a72e3733d 100644 --- a/.github/workflows/CICD.yaml +++ b/.github/workflows/CICD.yaml @@ -20,7 +20,7 @@ jobs: timeout-minutes: 120 defaults: run: - shell: powershell + shell: pwsh env: ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} @@ -31,7 +31,21 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: lfs: true - fetch-depth: 2 + fetch-depth: 3 + + - name: Verify baseline PowerShell 7 host + run: | + if ($PSVersionTable.PSEdition -ne 'Core' -or $PSVersionTable.PSVersion.Major -ne 7) { + throw 'The pinned baseline executes on PowerShell 7; this pilot must use the same shell.' + } + $output = Join-Path $env:GITHUB_WORKSPACE 'sql-reset-pilot-output' + New-Item -ItemType Directory $output -Force | Out-Null + @{ + edition = $PSVersionTable.PSEdition + version = $PSVersionTable.PSVersion.ToString() + executable = (Get-Process -Id $PID).Path + utc = [DateTime]::UtcNow.ToString('o') + } | ConvertTo-Json | Tee-Object -FilePath (Join-Path $output 'shell-preflight.json') - name: Validate provenance and download exact compiled PR2 packages id: packages @@ -43,7 +57,7 @@ jobs: - name: Read pinned W1 Integration settings uses: microsoft/AL-Go/Actions/ReadSettings@91b96c2b294be6f823277dafe6f03350abfb9d23 with: - shell: powershell + shell: pwsh project: build/projects/Test Apps W1 buildMode: IntegrationTests @@ -51,7 +65,7 @@ jobs: id: secrets uses: microsoft/AL-Go/Actions/ReadSecrets@91b96c2b294be6f823277dafe6f03350abfb9d23 with: - shell: powershell + shell: pwsh gitHubSecrets: ${{ toJson(secrets) }} getSecrets: 'licenseFileUrl,gitHubPackagesContext,applicationInsightsConnectionString,appDependencySecrets' @@ -60,7 +74,7 @@ jobs: env: Secrets: '${{ steps.secrets.outputs.Secrets }}' with: - shell: powershell + shell: pwsh project: build/projects/Test Apps W1 buildMode: IntegrationTests artifact: https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/sandbox/30.0.55683.0/w1 diff --git a/build/scripts/SqlResetPilot/README.md b/build/scripts/SqlResetPilot/README.md index f43cd35b66c..56ad191a661 100644 --- a/build/scripts/SqlResetPilot/README.md +++ b/build/scripts/SqlResetPilot/README.md @@ -11,6 +11,28 @@ PR project discovery is disabled for this experiment branch to avoid a full-coun build. Dispatch the existing registered `CICD.yaml` with this branch as its ref. Do not dispatch a second pair without new authorization. +## Authorized preparation correction + +The initial pair, run `37493608127`, never created a container or ran AL tests. +It incorrectly selected Windows PowerShell 5.1 while original W1 job +`112207627851` used `pwsh` 7. `Get-PlatformVersions` therefore failed parsing an +HTTP response because the Internet Explorer engine was unavailable. This is an +experiment preparation/configuration bug, **not a network failure or SQL result**. +The failed pair remains inconclusive and its logs/artifacts are retained. + +One replacement pair is explicitly authorized after correcting the job shell +**and all AL-Go composite action shell inputs** to `pwsh`. `Invoke-AlGoAction` +invokes its supplied scriptblock in that shell; it does not select the shell. +A preflight records and requires PowerShell 7 before package preparation. +Checkout includes the unchanged PR2 ancestor through the two diagnostic commits. +No PlatformHelper, vendor action, parsing workaround or shell policy is changed. + +The failed preparation consumed 13m16s (control) plus 14m23s (fresh), totaling +27m39s of runner time. The replacement remains capped at 120 minutes per arm +(240 additional runner-minutes; 267m39s cumulative worst-case including the failed +preparation). Record actual old/new preparation time separately. No further +dispatch or failed-job rerun is authorized after the replacement. + Both arms use the exact same PR2 app/test archive IDs and SHA256 digests, BCH `6.1.19-preview2811389`, NST `30.0.55665.0`, W1 application artifact `30.0.55683.0`, image digest, original license provisioning, auth files, From d7dd4dddcd49da397ee42de1677d1b7d44622149 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Wed, 7 Oct 2026 11:26:30 +0200 Subject: [PATCH 16/22] Allow bounded control-only SQL reset diagnostic repetitions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .github/workflows/CICD.yaml | 15 ++++++++-- build/scripts/SqlResetPilot/README.md | 40 +++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/.github/workflows/CICD.yaml b/.github/workflows/CICD.yaml index a6a72e3733d..1dd4b8052ee 100644 --- a/.github/workflows/CICD.yaml +++ b/.github/workflows/CICD.yaml @@ -1,7 +1,16 @@ -name: 'SQL reset paired pilot - DO NOT MERGE' +name: 'SQL reset pilot - DO NOT MERGE' on: workflow_dispatch: + inputs: + arm: + description: 'Control only by default; paired requires separate authorization' + required: true + type: choice + default: control + options: + - control + - paired permissions: actions: read @@ -15,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - arm: [control, fresh] + arm: ${{ fromJSON(inputs.arm == 'paired' && '["control","fresh"]' || '["control"]') }} runs-on: GitHub-BCAppsPL timeout-minutes: 120 defaults: @@ -31,7 +40,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: lfs: true - fetch-depth: 3 + fetch-depth: 0 - name: Verify baseline PowerShell 7 host run: | diff --git a/build/scripts/SqlResetPilot/README.md b/build/scripts/SqlResetPilot/README.md index 56ad191a661..b127afef724 100644 --- a/build/scripts/SqlResetPilot/README.md +++ b/build/scripts/SqlResetPilot/README.md @@ -1,12 +1,14 @@ # Diagnostic only — do not merge -One manually dispatched paired W1 Integration pilot on top of PR #11860, +A manually dispatched W1 Integration diagnostic on top of PR #11860, `c4953dceffe02a017adad34973e1955017bf5d20`. Related context: AB#646383; this experiment does **not** resolve that work item and is outside the native merge group. No auto-merge, queue entry, automatic rerun or main-branch change. -The branch-only `CICD.yaml` replaces the normal CI matrix with two fresh -`GitHub-BCAppsPL` containers, capped at **120 minutes per arm** (240 runner-minutes). +The branch-only `CICD.yaml` replaces the normal CI matrix with disposable +`GitHub-BCAppsPL` containers, capped at **120 minutes per arm**. +Its `arm` choice defaults to `control` (one job); `paired` explicitly selects +control and fresh (two jobs, 240 runner-minutes) and requires separate authorization. PR project discovery is disabled for this experiment branch to avoid a full-country build. Dispatch the existing registered `CICD.yaml` with this branch as its ref. Do not dispatch a second pair without new authorization. @@ -24,14 +26,40 @@ One replacement pair is explicitly authorized after correcting the job shell **and all AL-Go composite action shell inputs** to `pwsh`. `Invoke-AlGoAction` invokes its supplied scriptblock in that shell; it does not select the shell. A preflight records and requires PowerShell 7 before package preparation. -Checkout includes the unchanged PR2 ancestor through the two diagnostic commits. +Checkout now fetches full history so further diagnostic commits cannot hide the +unchanged PR2 ancestor from the provenance guard. No PlatformHelper, vendor action, parsing workaround or shell policy is changed. The failed preparation consumed 13m16s (control) plus 14m23s (fresh), totaling 27m39s of runner time. The replacement remains capped at 120 minutes per arm (240 additional runner-minutes; 267m39s cumulative worst-case including the failed -preparation). Record actual old/new preparation time separately. No further -dispatch or failed-job rerun is authorized after the replacement. +preparation). Record actual old/new preparation time separately. That authorization +allowed no further dispatch or failed-job rerun after the replacement. + +## October 7 control-only repetitions + +Replacement paired run `37496613531` completed successfully for both arms. +Its control baseline recorded 234 passed cases and 19 skipped cases across +20 codeunits with executed cases in the selected 21-codeunit, seven-batch prefix. +This did not reproduce the error and does not establish that name reuse is safe. + +New authorization permits **exactly three additional independent control-only +dispatches**, each with `arm=control`, attempt one, capped at 120 minutes: +**360 additional runner-minutes maximum**. No treatment, automatic retry, +replacement dispatch, or failed-job rerun is authorized. Each run owns a distinct +container named `bcbuildprojectsTestAppsW1`; no workflow concurrency group +cancels or serializes these independent trials. + +These controls use the same original PR2 compiled app/test artifacts from run +`37372848860`, head `c4953dceffe02a017adad34973e1955017bf5d20`, not rebuilt tests. +They repeat only the existing first seven clean batches (21 selected codeunits), +not the ordinary lane or the full 192-codeunit cohort. Compare against 234 passes +and 19 skips if a trial reaches completion; failures can stop the prefix early. +Password-file authentication, reused `tenant2/3/4` database names, three-worker +scheduling, runtime/package pins, reset logic and diagnostic reads are unchanged. +Check artifact availability before dispatch; expired artifacts have no fallback. +Launch verification is not a test result: inspect final logs, JUnit, provenance +and cleanup artifacts separately before drawing conclusions. Both arms use the exact same PR2 app/test archive IDs and SHA256 digests, BCH `6.1.19-preview2811389`, NST `30.0.55665.0`, W1 application artifact From fcc1776c6b165199dd66e4227675b1cc31da7a8f Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Wed, 7 Oct 2026 12:32:14 +0200 Subject: [PATCH 17/22] Run isolated W1 and DE API warmup diagnostic trials Preserve pinned PR2 packages and reused worker databases; run the original first-app warmup before clean execution, then one authenticated companies probe per restored worker. No retries or production source changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .github/workflows/CICD.yaml | 37 ++-- .github/workflows/PullRequestHandler.yaml | 2 +- build/scripts/ParallelTestExecution.psm1 | 59 +++++- .../SqlResetPilot/CompaniesProbe.Tests.ps1 | 76 ++++++++ .../SqlResetPilot/ContainerPreflight.ps1 | 13 +- .../scripts/SqlResetPilot/Finalize.Tests.ps1 | 25 ++- build/scripts/SqlResetPilot/Finalize.ps1 | 6 +- .../scripts/SqlResetPilot/Lifecycle.Tests.ps1 | 4 +- build/scripts/SqlResetPilot/Lifecycle.psm1 | 72 ++++++- .../scripts/SqlResetPilot/Preflight.Tests.ps1 | 70 +++++++ build/scripts/SqlResetPilot/Prepare.ps1 | 20 +- build/scripts/SqlResetPilot/README.md | 180 ++++++++---------- build/scripts/SqlResetPilot/Warmup.Tests.ps1 | 169 ++++++++++++++++ 13 files changed, 590 insertions(+), 143 deletions(-) create mode 100644 build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 create mode 100644 build/scripts/SqlResetPilot/Preflight.Tests.ps1 create mode 100644 build/scripts/SqlResetPilot/Warmup.Tests.ps1 diff --git a/.github/workflows/CICD.yaml b/.github/workflows/CICD.yaml index 1dd4b8052ee..3db952d48d6 100644 --- a/.github/workflows/CICD.yaml +++ b/.github/workflows/CICD.yaml @@ -1,16 +1,7 @@ -name: 'SQL reset pilot - DO NOT MERGE' +name: 'SQL API warmup experiment - DO NOT MERGE' on: workflow_dispatch: - inputs: - arm: - description: 'Control only by default; paired requires separate authorization' - required: true - type: choice - default: control - options: - - control - - paired permissions: actions: read @@ -20,11 +11,13 @@ permissions: jobs: Pilot: - if: github.ref == 'refs/heads/features/646383-sql-reset-pilot' && github.run_attempt == 1 + if: github.ref == 'refs/heads/features/646383-sql-api-warmup-experiment' && github.run_attempt == 1 strategy: fail-fast: false + max-parallel: 5 matrix: - arm: ${{ fromJSON(inputs.arm == 'paired' && '["control","fresh"]' || '["control"]') }} + country: [W1, DE] + trial: [1, 2, 3, 4, 5] runs-on: GitHub-BCAppsPL timeout-minutes: 120 defaults: @@ -33,7 +26,9 @@ jobs: env: ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} - BC_SQL_PILOT_ARM: ${{ matrix.arm }} + BC_SQL_PILOT_ARM: control + BC_SQL_PILOT_COUNTRY: ${{ matrix.country }} + BC_SQL_PILOT_TRIAL: ${{ matrix.trial }} BuildMode: IntegrationTests steps: - name: Checkout experiment @@ -63,11 +58,11 @@ jobs: run: | & .\build\scripts\SqlResetPilot\Prepare.ps1 - - name: Read pinned W1 Integration settings + - name: Read pinned country Integration settings uses: microsoft/AL-Go/Actions/ReadSettings@91b96c2b294be6f823277dafe6f03350abfb9d23 with: shell: pwsh - project: build/projects/Test Apps W1 + project: build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }} buildMode: IntegrationTests - name: Read normal disposable CI license and package secrets @@ -84,9 +79,9 @@ jobs: Secrets: '${{ steps.secrets.outputs.Secrets }}' with: shell: pwsh - project: build/projects/Test Apps W1 + project: build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }} buildMode: IntegrationTests - artifact: https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/sandbox/30.0.55683.0/w1 + artifact: ${{ steps.packages.outputs.artifact }} installAppsJson: ${{ steps.packages.outputs.apps }} installTestAppsJson: ${{ steps.packages.outputs.tests }} @@ -99,11 +94,11 @@ jobs: if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: sql-reset-pilot-${{ matrix.arm }}-${{ github.run_id }} + name: sql-api-warmup-${{ matrix.country }}-${{ matrix.trial }}-${{ github.run_id }} retention-days: 7 if-no-files-found: error path: | sql-reset-pilot-output/** - build/projects/Test Apps W1/TestResults*.xml - build/projects/Test Apps W1/ContainerEventLog.evtx - build/projects/Test Apps W1/BuildOutput.txt + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}/TestResults*.xml + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}/ContainerEventLog.evtx + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}/BuildOutput.txt diff --git a/.github/workflows/PullRequestHandler.yaml b/.github/workflows/PullRequestHandler.yaml index bfafea2b708..142d13c1cfd 100644 --- a/.github/workflows/PullRequestHandler.yaml +++ b/.github/workflows/PullRequestHandler.yaml @@ -35,7 +35,7 @@ jobs: Initialization: needs: [ PregateCheck ] - if: (!failure() && !cancelled()) && github.head_ref != 'features/646383-sql-reset-pilot' + if: (!failure() && !cancelled()) && github.head_ref != 'features/646383-sql-api-warmup-experiment' runs-on: [ windows-latest ] outputs: projects: ${{ steps.determineProjectsToBuild.outputs.ProjectsJson }} diff --git a/build/scripts/ParallelTestExecution.psm1 b/build/scripts/ParallelTestExecution.psm1 index 4adfc6a0b21..a5a532273bc 100644 --- a/build/scripts/ParallelTestExecution.psm1 +++ b/build/scripts/ParallelTestExecution.psm1 @@ -845,13 +845,20 @@ function Invoke-RequiredDisabledTestExecution { Reset-BcTestTenant -ContainerName $Parameters.containerName -Tenant $dispatch.TenantInfo.Id ` -TenantDatabaseName $dispatch.TenantInfo.DatabaseName -TemplateDatabaseName $TemplateDatabaseName } + if ($env:BC_SQL_PILOT_ARM) { + foreach ($dispatch in $batch) { + Invoke-SqlPilotCompaniesProbe -ContainerName $Parameters.containerName -Tenant $dispatch.TenantInfo.Id ` + -Credential $Parameters.credential -CompanyName $Parameters.companyName + } + } foreach ($dispatch in $batch) { Start-RequiredDisabledDispatch -Parameters $Parameters -WorkItem $dispatch.WorkItem ` -TenantInfo $dispatch.TenantInfo ` -ScriptPath $ScriptPath -TestType $TestType -State $state -Verb $dispatch.Verb } $null = Wait-ForAllTestJobs -state $state - if ($env:BC_SQL_PILOT_ARM -and $state.hasFailures) { + if ($env:BC_SQL_PILOT_ARM -and ($state.hasFailures -or $state.transient.Count -gt 0)) { + $state.hasFailures = $true break } } @@ -878,6 +885,10 @@ function Register-TestJobOutcome { $State ) + if ($env:BC_SQL_PILOT_ARM -and $Result.Outcome -in @('Failed', 'Transient')) { + $State.hasFailures = $true + return + } switch ($Result.Outcome) { 'Transient' { Write-Host "Transient platform race for '$($Result.AppName)' on '$($Result.Tenant)'. Queued for one retry." @@ -1254,6 +1265,46 @@ function Invoke-WarmupDispatch { return @($Pending | Select-Object -Skip 1) } +function Invoke-SqlPilotWarmup { + param( + [hashtable]$Parameters, [string[]]$AppNames, [hashtable]$AppIdByName, + [string[]]$Tenants, [string]$ScriptPath, [string]$TestType, [string[]]$CleanTenantAppNames + ) + if ($Parameters.tenant -ne 'default' -or $Tenants[0] -ne 'default' -or + $AppNames.Count -le 1 -or -not $AppIdByName[$AppNames[0]]) { + throw 'Original first-app warmup cannot be guaranteed on the default tenant.' + } + $warmupDirectory = Join-Path $env:BC_SQL_PILOT_OUTPUT 'warmup' + New-Item -ItemType Directory $warmupDirectory -Force | Out-Null + $warmupParameters = $Parameters.Clone() + foreach ($key in @('XUnitResultFileName', 'JUnitResultFileName')) { + if ($warmupParameters[$key]) { + $warmupParameters[$key] = Join-Path $warmupDirectory ([IO.Path]::GetFileName($warmupParameters[$key])) + } + } + $warmupState = [PSCustomObject]@{ + jobs = @(); hasFailures = $false; transient = @(); retried = @{}; retryTenant = @{} + rerun = @(); rerunDone = @{}; rerunBudget = 0; tenantCount = $Tenants.Count + } + $observation = @{ + phase = 'warmup'; app = $AppNames[0]; tenant = 'default' + startedUtc = [DateTime]::UtcNow.ToString('o'); passed = $false; retries = 0 + } + try { + $remaining = @(Invoke-WarmupDispatch -Parameters $warmupParameters -Pending $AppNames -AppIdByName $AppIdByName ` + -Tenants $Tenants -ScriptPath $ScriptPath -TestType $TestType -State $warmupState ` + -CleanTenantAppNames $CleanTenantAppNames) + if ($remaining.Count -ne ($AppNames.Count - 1) -or $warmupState.hasFailures -or + $warmupState.transient.Count -gt 0 -or $warmupState.rerun.Count -gt 0 -or $warmupState.jobs.Count -gt 0) { + throw 'First-app warmup failed or did not complete; no retry or clean-lane dispatch is permitted.' + } + $observation.passed = $true + } finally { + $observation.completedUtc = [DateTime]::UtcNow.ToString('o') + $observation | ConvertTo-Json | Set-Content (Join-Path $warmupDirectory 'outcome.json') -Encoding UTF8 + } +} + <# .SYNOPSIS Deletes the result file written by a rerun, so it cannot take part in the merge. @@ -1434,6 +1485,12 @@ function Invoke-ParallelTestExecution { } $state | ConvertTo-Json -Depth 5 | Set-Content $stateFile -Force + if ($env:BC_SQL_PILOT_ARM) { + if (-not $templateDatabaseName) { throw 'Pristine template must be frozen before warmup.' } + Invoke-SqlPilotWarmup -Parameters $parameters -AppNames $appNamesToTest -AppIdByName $appIdByName ` + -Tenants $tenants -ScriptPath $scriptPath -TestType $testType -CleanTenantAppNames $cleanTenantAppNames + } + if ($requiredDisabledWorkItems.Count -gt 0) { try { $requiredDisabledPassed = Invoke-RequiredDisabledTestExecution -Parameters $parameters ` diff --git a/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 b/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 new file mode 100644 index 00000000000..6fdcbcf79af --- /dev/null +++ b/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 @@ -0,0 +1,76 @@ +BeforeAll { + function global:Get-BcContainerServerConfiguration { param($ContainerName) $null = $ContainerName; throw 'Live call forbidden.' } + function global:Get-BcContainerIpAddress { param($containerName) $null = $containerName; throw 'Live call forbidden.' } + Import-Module (Join-Path $PSScriptRoot 'Lifecycle.psm1') -Force +} + +Describe 'External authenticated companies probe' { + BeforeEach { + $script:tenants = @('default','tenant2','tenant3','tenant4') | ForEach-Object { + [PSCustomObject]@{ Id = $_; DatabaseName = $_ } + } + $password = [Security.SecureString]::new() + foreach ($character in 'test-only-marker'.ToCharArray()) { $password.AppendChar($character) } + $script:credential = [PSCredential]::new('fixture', $password) + Initialize-SqlResetPilot 'owned' $script:tenants 'control' '123' $TestDrive + Complete-SqlResetPlan (Get-SqlResetPlan 'owned' 'tenant2' 'tenant2' 'default-test-template') + Mock Get-BcContainerServerConfiguration -ModuleName Lifecycle { + @{ ServerInstance = 'BC'; ODataServicesPort = 7048; ODataServicesSSLEnabled = 'false'; ClientServicesCredentialType = 'NavUserPassword' } + } + Mock Get-BcContainerIpAddress -ModuleName Lifecycle { '172.16.1.2' } + Mock Invoke-WebRequest -ModuleName Lifecycle { + @{ StatusCode = 200; Headers = @{ 'request-id' = @('server-correlation') }; Content = '{"value":[{"name":"My Company","id":"company-id"}]}' } + } + Mock Add-Content -ModuleName Lifecycle {} + } + It 'sends exactly one explicitly tenant-scoped preauthenticated HTTP request with retries disabled' { + Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' + Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { + $Uri -eq 'http://172.16.1.2:7048/BC/api/v2.0/companies?tenant=tenant2' -and + $Authentication -eq 'Basic' -and $Credential.UserName -eq 'fixture' -and + $MaximumRetryCount -eq 0 -and $MaximumRedirection -eq 0 -and $TimeoutSec -eq 60 -and $SkipHttpErrorCheck + } + Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { + $Value -match '"status":200' -and $Value -match '"passed":true' -and + $Value -match 'server-correlation' -and $Value -notmatch 'fixture|test-only-marker' + } + } + It 'treats HTTP failures as terminal without logging response content' { + Mock Invoke-WebRequest -ModuleName Lifecycle { + @{ StatusCode = 500; Headers = @{}; Content = 'sensitive-response-marker' } + } + { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | + Should -Throw '*No retry*' + Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly + Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { + $Value -match '"status":500' -and $Value -notmatch 'sensitive-response-marker|test-only-marker' + } + } + It 'rejects invalid JSON without another request' { + Mock Invoke-WebRequest -ModuleName Lifecycle { @{ StatusCode = 200; Headers = @{}; Content = 'invalid-json' } } + { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw + Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly + } + It 'rejects missing expected company without another request' { + Mock Invoke-WebRequest -ModuleName Lifecycle { + @{ StatusCode = 200; Headers = @{}; Content = '{"value":[{"name":"Other","id":"id"}]}' } + } + { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw + Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly + } + It 'redacts transport exception text and never retries' { + Mock Invoke-WebRequest -ModuleName Lifecycle { throw 'sensitive-exception-marker' } + { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | + Should -Throw '*sanitized companies-probes*' + Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly + Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { + $Value -notmatch 'sensitive-exception-marker|test-only-marker' + } + } + It 'refuses a foreign container, default tenant, or worker not yet restored' { + { Invoke-SqlPilotCompaniesProbe 'foreign' 'tenant2' $script:credential 'My Company' } | Should -Throw + { Invoke-SqlPilotCompaniesProbe 'owned' 'default' $script:credential 'My Company' } | Should -Throw + { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant3' $script:credential 'My Company' } | Should -Throw + Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 0 + } +} diff --git a/build/scripts/SqlResetPilot/ContainerPreflight.ps1 b/build/scripts/SqlResetPilot/ContainerPreflight.ps1 index d15e6d33df4..25e895d1da9 100644 --- a/build/scripts/SqlResetPilot/ContainerPreflight.ps1 +++ b/build/scripts/SqlResetPilot/ContainerPreflight.ps1 @@ -1,11 +1,13 @@ param([hashtable]$Parameters) $ErrorActionPreference = 'Stop' -if ($env:GITHUB_REF -ne 'refs/heads/features/646383-sql-reset-pilot' -or +if ($env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-warmup-experiment' -or $env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or - $env:BC_SQL_PILOT_ARM -notin @('control', 'fresh') -or -not $env:BC_SQL_PILOT_OUTPUT) { + $env:BC_SQL_PILOT_ARM -ne 'control' -or -not $env:BC_SQL_PILOT_OUTPUT -or + $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^[1-5]$' -or + $env:GITHUB_RUN_ID -notmatch '^\d{1,20}$') { throw 'Refusing SQL pilot outside its disposable manually dispatched CI job.' } -$expectedName = "bcbuildprojectsTestAppsW1$($env:GITHUB_RUN_ID)" +$expectedName = "bcbuildprojectsTestApps$($env:BC_SQL_PILOT_COUNTRY)Trial$($env:BC_SQL_PILOT_TRIAL)$($env:GITHUB_RUN_ID)" if ($Parameters.ContainerName -ne $expectedName -or (Test-BcContainer -containerName $expectedName)) { throw 'Pilot container name is unexpected or already exists. No replacement is authorized.' } @@ -13,10 +15,10 @@ $module = Get-Module BcContainerHelper -ErrorAction Stop $helperVersion = & $module { $bcContainerHelperVersion } if ($helperVersion -ne '6.1.19-preview2811389') { throw "Wrong BCH version: $helperVersion" } $settings = $env:Settings | ConvertFrom-Json -if ($settings.country -ne 'w1' -or $settings.testType -ne 'IntegrationTest' -or +if ($settings.country -ne $env:BC_SQL_PILOT_COUNTRY -or $settings.testType -ne 'IntegrationTest' -or $settings.numberOfTenantsForTesting -ne 4 -or $settings.companyName -ne 'My Company' -or -not $settings.enableCleanTestCodeunitExecution -or $settings.enableTaskScheduler) { - throw 'Effective W1 Integration settings differ from the predeclared experiment.' + throw 'Effective country Integration settings differ from the predeclared experiment.' } if (-not (Get-Command New-BcContainer).Parameters.ContainsKey('useGenericImage')) { throw 'Exact BCH does not expose the supported generic-image parameter.' @@ -27,6 +29,7 @@ if ($Parameters.platformArtifactUrl -ne 'https://bcinsider-fvh2ekdjecfjd6gk.b02. } @{ container = $expectedName; runId = $env:GITHUB_RUN_ID; arm = $env:BC_SQL_PILOT_ARM + country = $env:BC_SQL_PILOT_COUNTRY; trial = $env:BC_SQL_PILOT_TRIAL helperVersion = $helperVersion; image = $Parameters.useGenericImage imageGenericTag = '1.0.2.128'; platform = '30.0.55665.0' hostOS = [Environment]::OSVersion.Version.ToString() diff --git a/build/scripts/SqlResetPilot/Finalize.Tests.ps1 b/build/scripts/SqlResetPilot/Finalize.Tests.ps1 index deca7127d20..7bcda67c576 100644 --- a/build/scripts/SqlResetPilot/Finalize.Tests.ps1 +++ b/build/scripts/SqlResetPilot/Finalize.Tests.ps1 @@ -8,13 +8,17 @@ Describe 'SQL pilot final cleanup ownership' { $script:oldRun = $env:GITHUB_RUN_ID $script:oldArm = $env:BC_SQL_PILOT_ARM $script:oldHelper = $env:BcContainerHelperPath + $script:oldCountry = $env:BC_SQL_PILOT_COUNTRY + $script:oldTrial = $env:BC_SQL_PILOT_TRIAL $env:GITHUB_WORKSPACE = $PSScriptRoot $env:GITHUB_RUN_ID = '123' - $env:BC_SQL_PILOT_ARM = 'fresh' + $env:BC_SQL_PILOT_ARM = 'control' + $env:BC_SQL_PILOT_COUNTRY = 'DE' + $env:BC_SQL_PILOT_TRIAL = '2' $env:BcContainerHelperPath = Join-Path $PSScriptRoot 'BcContainerHelper.ps1' Mock Import-Module {} Mock Test-Path { $true } - Mock Get-Content { '{"runId":"123","arm":"fresh","container":"bcbuildprojectsTestAppsW1123"}' } + Mock Get-Content { '{"runId":"123","arm":"control","country":"DE","trial":"2","container":"bcbuildprojectsTestAppsDETrial2123"}' } Mock Get-ChildItem {} Mock Copy-Item {} Mock Set-Content {} @@ -28,26 +32,33 @@ Describe 'SQL pilot final cleanup ownership' { $env:GITHUB_RUN_ID = $script:oldRun $env:BC_SQL_PILOT_ARM = $script:oldArm $env:BcContainerHelperPath = $script:oldHelper + $env:BC_SQL_PILOT_COUNTRY = $script:oldCountry + $env:BC_SQL_PILOT_TRIAL = $script:oldTrial Remove-Variable sqlPilotTestContainerExists -Scope Global } It 'exports before disposing only the registered container' { & (Join-Path $PSScriptRoot 'Finalize.ps1') - Assert-MockCalled Get-BcContainerEventLog -Times 1 -Exactly -Scope It - Assert-MockCalled Remove-BcContainer -Times 1 -Exactly -Scope It -ParameterFilter { $containerName -eq 'bcbuildprojectsTestAppsW1123' } + Should -Invoke Get-BcContainerEventLog -Times 1 -Exactly -Scope It + Should -Invoke Remove-BcContainer -Times 1 -Exactly -Scope It -ParameterFilter { $containerName -eq 'bcbuildprojectsTestAppsDETrial2123' } } It 'still tears down if exporting fails and keeps that failure visible' { Mock Get-BcContainerEventLog { throw 'event export failed' } { & (Join-Path $PSScriptRoot 'Finalize.ps1') } | Should -Throw - Assert-MockCalled Remove-BcContainer -Times 1 -Exactly -Scope It + Should -Invoke Remove-BcContainer -Times 1 -Exactly -Scope It } It 'refuses deletion if ownership was not recorded' { Mock Test-Path { $false } & (Join-Path $PSScriptRoot 'Finalize.ps1') - Assert-MockCalled Remove-BcContainer -Times 0 -Exactly -Scope It + Should -Invoke Remove-BcContainer -Times 0 -Exactly -Scope It } It 'refuses deletion when run, arm or exact name differs' { Mock Get-Content { '{"runId":"999","arm":"fresh","container":"shared"}' } { & (Join-Path $PSScriptRoot 'Finalize.ps1') } | Should -Throw - Assert-MockCalled Remove-BcContainer -Times 0 -Exactly -Scope It + Should -Invoke Remove-BcContainer -Times 0 -Exactly -Scope It + } + It 'refuses deletion of another trial in the same country and run' { + $env:BC_SQL_PILOT_TRIAL = '3' + { & (Join-Path $PSScriptRoot 'Finalize.ps1') } | Should -Throw + Should -Invoke Remove-BcContainer -Times 0 -Exactly -Scope It } } diff --git a/build/scripts/SqlResetPilot/Finalize.ps1 b/build/scripts/SqlResetPilot/Finalize.ps1 index 41a667bd01d..cc6a802b9a0 100644 --- a/build/scripts/SqlResetPilot/Finalize.ps1 +++ b/build/scripts/SqlResetPilot/Finalize.ps1 @@ -7,7 +7,9 @@ if (-not (Test-Path $ownershipPath)) { } $ownership = Get-Content $ownershipPath -Raw | ConvertFrom-Json if ($ownership.runId -ne $env:GITHUB_RUN_ID -or $ownership.arm -ne $env:BC_SQL_PILOT_ARM -or - $ownership.container -ne "bcbuildprojectsTestAppsW1$($env:GITHUB_RUN_ID)") { + $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^[1-5]$' -or + $ownership.country -ne $env:BC_SQL_PILOT_COUNTRY -or $ownership.trial -ne $env:BC_SQL_PILOT_TRIAL -or + $ownership.container -ne "bcbuildprojectsTestApps$($env:BC_SQL_PILOT_COUNTRY)Trial$($env:BC_SQL_PILOT_TRIAL)$($env:GITHUB_RUN_ID)") { throw 'Cleanup ownership mismatch.' } Import-Module (Join-Path (Split-Path $env:BcContainerHelperPath -Parent) 'BcContainerHelper.psm1') -ErrorAction Stop @@ -16,7 +18,7 @@ try { $events = Get-BcContainerEventLog -containerName $ownership.container -doNotOpen Copy-Item $events (Join-Path $output 'final-container.evtx') } - $project = Join-Path $env:GITHUB_WORKSPACE 'build\projects\Test Apps W1' + $project = Join-Path $env:GITHUB_WORKSPACE "build\projects\Test Apps $($env:BC_SQL_PILOT_COUNTRY) Trial$($env:BC_SQL_PILOT_TRIAL)" Get-ChildItem $project -Filter 'TestResults*.xml' -File | Copy-Item -Destination $output } finally { if (Test-BcContainer -containerName $ownership.container) { diff --git a/build/scripts/SqlResetPilot/Lifecycle.Tests.ps1 b/build/scripts/SqlResetPilot/Lifecycle.Tests.ps1 index da023382fb4..caff235310a 100644 --- a/build/scripts/SqlResetPilot/Lifecycle.Tests.ps1 +++ b/build/scripts/SqlResetPilot/Lifecycle.Tests.ps1 @@ -62,7 +62,7 @@ Describe 'SQL reset pilot ownership and mapping' { Mock Add-Content -ModuleName Lifecycle {} Invoke-SqlPilotReset 'owned' 'tenant2' 'tenant2' 'default' $script:tenants[1].DatabaseName | Should -Be 'tenant2_r123456789_g1' - Assert-MockCalled Invoke-ScriptInBcContainer -ModuleName Lifecycle -Times 1 -Exactly -Scope It -ParameterFilter { + Should -Invoke Invoke-ScriptInBcContainer -ModuleName Lifecycle -Times 1 -Exactly -Scope It -ParameterFilter { $containerName -eq 'owned' -and $argumentList.Previous -eq 'tenant2' -and $argumentList.Destination -eq 'tenant2_r123456789_g1' } @@ -72,6 +72,6 @@ Describe 'SQL reset pilot ownership and mapping' { Mock Add-Content -ModuleName Lifecycle {} { Invoke-SqlPilotReset 'owned' 'tenant2' 'tenant2' 'default' } | Should -Throw $script:tenants[1].DatabaseName | Should -Be 'tenant2' - Assert-MockCalled Invoke-ScriptInBcContainer -ModuleName Lifecycle -Times 1 -Exactly -Scope It + Should -Invoke Invoke-ScriptInBcContainer -ModuleName Lifecycle -Times 1 -Exactly -Scope It } } diff --git a/build/scripts/SqlResetPilot/Lifecycle.psm1 b/build/scripts/SqlResetPilot/Lifecycle.psm1 index 180d63232b6..0acbc839510 100644 --- a/build/scripts/SqlResetPilot/Lifecycle.psm1 +++ b/build/scripts/SqlResetPilot/Lifecycle.psm1 @@ -128,4 +128,74 @@ function Select-SqlPilotPrefix { @($WorkItems | Select-Object -First $ids.Count) } -Export-ModuleMember -Function Initialize-SqlResetPilot, Get-SqlResetPlan, Complete-SqlResetPlan, Invoke-SqlPilotReset, Select-SqlPilotPrefix +<# +.SYNOPSIS + Makes one authenticated companies request from the host after an owned worker restore. +.DESCRIPTION + Uses the disposable container's existing password credential and private API endpoint. + No retries, redirects, response bodies, passwords or authorization headers are logged. +#> +function Invoke-SqlPilotCompaniesProbe { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingAllowUnencryptedAuthentication', '', + Justification = 'Preserves the existing disposable-container Basic authentication over its private Docker address.')] + [CmdletBinding()] + param( + [string]$ContainerName, [string]$Tenant, + [Parameter(Mandatory)][PSCredential]$Credential, + [Parameter(Mandatory)][string]$CompanyName + ) + if (-not $script:pilot -or $ContainerName -ne $script:pilot.Container -or + -not $script:pilot.Workers.ContainsKey($Tenant) -or + $script:pilot.Workers[$Tenant].Generation -lt 1) { + throw 'Companies probe requires an owned, freshly restored worker.' + } + $trace = Join-Path $script:pilot.OutputDirectory 'companies-probes.jsonl' + $observation = @{ + tenant = $Tenant; generation = $script:pilot.Workers[$Tenant].Generation + startedUtc = [DateTime]::UtcNow.ToString('o'); status = $null; passed = $false + clientRequestId = [guid]::NewGuid().ToString(); serverRequestId = $null + uri = $null; retries = 0 + } + $clock = [Diagnostics.Stopwatch]::StartNew() + try { + # Match BCH's direct container API address, without its request wrapper or auth negotiation. + $config = Get-BcContainerServerConfiguration -ContainerName $ContainerName + $ip = Get-BcContainerIpAddress -containerName $ContainerName + $parsedIp = $null + if (-not [Net.IPAddress]::TryParse($ip, [ref]$parsedIp) -or + $config.ServerInstance -notmatch '^[a-zA-Z0-9_-]+$' -or + $config.ClientServicesCredentialType -notin @('NavUserPassword', 'UserPassword')) { + throw 'Unexpected container API configuration.' + } + $scheme = if ($config.ODataServicesSSLEnabled -eq 'true') { 'https' } else { 'http' } + $uri = [UriBuilder]::new($scheme, $ip, [int]$config.ODataServicesPort, + "$($config.ServerInstance)/api/v2.0/companies", "?tenant=$Tenant").Uri.AbsoluteUri + $observation.uri = $uri + $response = Invoke-WebRequest -Uri $uri -Method Get -Authentication Basic -Credential $Credential ` + -AllowUnencryptedAuthentication -SkipCertificateCheck -SkipHttpErrorCheck -MaximumRedirection 0 ` + -MaximumRetryCount 0 -TimeoutSec 60 -ErrorAction Stop ` + -Headers @{ Accept = 'application/json'; 'client-request-id' = $observation.clientRequestId } + $observation.status = [int]$response.StatusCode + foreach ($key in @('request-id', 'x-ms-request-id')) { + $value = [string]($response.Headers[$key] | Select-Object -First 1) + if ($value -match '^[a-zA-Z0-9._:-]{1,128}$') { $observation.serverRequestId = $value; break } + } + if ($response.StatusCode -ne 200) { throw 'Companies probe returned non-200 status.' } + $body = $response.Content | ConvertFrom-Json -ErrorAction Stop + if (-not $body.PSObject.Properties['value'] -or + @($body.value | Where-Object { $_.name -eq $CompanyName -and $_.id }).Count -ne 1) { + throw 'Companies probe did not return exactly one expected company.' + } + $observation.passed = $true + } catch { + $observation.errorType = $_.Exception.GetType().FullName + # HTTP exception details can contain authentication material; never emit the original error. + throw "Companies probe failed for $Tenant; see sanitized companies-probes.jsonl. No retry was attempted." + } finally { + $observation.completedUtc = [DateTime]::UtcNow.ToString('o') + $observation.elapsedMilliseconds = $clock.ElapsedMilliseconds + $observation | ConvertTo-Json -Compress | Add-Content $trace + } +} + +Export-ModuleMember -Function Initialize-SqlResetPilot, Get-SqlResetPlan, Complete-SqlResetPlan, Invoke-SqlPilotReset, Select-SqlPilotPrefix, Invoke-SqlPilotCompaniesProbe diff --git a/build/scripts/SqlResetPilot/Preflight.Tests.ps1 b/build/scripts/SqlResetPilot/Preflight.Tests.ps1 new file mode 100644 index 00000000000..110f3d90abf --- /dev/null +++ b/build/scripts/SqlResetPilot/Preflight.Tests.ps1 @@ -0,0 +1,70 @@ +BeforeAll { + function global:Test-BcContainer { param($containerName) $null = $containerName; throw 'Live call forbidden.' } + function global:New-BcContainer { param($useGenericImage) $null = $useGenericImage; throw 'Live call forbidden.' } +} +Describe 'Experiment country and trial container ownership' { + BeforeEach { + $script:saved = @{} + foreach ($name in @('GITHUB_REF','GITHUB_EVENT_NAME','GITHUB_RUN_ATTEMPT','GITHUB_RUN_ID', + 'BC_SQL_PILOT_ARM','BC_SQL_PILOT_COUNTRY','BC_SQL_PILOT_TRIAL','BC_SQL_PILOT_OUTPUT','Settings')) { + $script:saved[$name] = [Environment]::GetEnvironmentVariable($name) + } + $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-warmup-experiment' + $env:GITHUB_EVENT_NAME = 'workflow_dispatch' + $env:GITHUB_RUN_ATTEMPT = '1' + $env:GITHUB_RUN_ID = '123' + $env:BC_SQL_PILOT_ARM = 'control' + $env:BC_SQL_PILOT_COUNTRY = 'DE' + $env:BC_SQL_PILOT_TRIAL = '2' + $env:BC_SQL_PILOT_OUTPUT = $TestDrive + $env:Settings = @{ + country = 'DE'; testType = 'IntegrationTest'; numberOfTenantsForTesting = 4 + companyName = 'My Company'; enableCleanTestCodeunitExecution = $true; enableTaskScheduler = $false + } | ConvertTo-Json + $script:parameters = @{ + ContainerName = 'bcbuildprojectsTestAppsDETrial2123' + platformArtifactUrl = 'https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/platform/30.0.55665.0/platform' + } + Mock Get-Module { { '6.1.19-preview2811389' } } -ParameterFilter { $Name -eq 'BcContainerHelper' } + Mock Test-BcContainer { $false } + } + AfterEach { + foreach ($name in $script:saved.Keys) { [Environment]::SetEnvironmentVariable($name, $script:saved[$name]) } + } + It 'pins image and registers the exact country-trial-run container' { + & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters + $record = Get-Content (Join-Path $TestDrive 'container-ownership.json') -Raw | ConvertFrom-Json + $record.country | Should -Be 'DE' + $record.trial | Should -Be '2' + $record.container | Should -Be 'bcbuildprojectsTestAppsDETrial2123' + $script:parameters.useGenericImage | Should -Be 'mcr.microsoft.com/businesscentral@sha256:c899d12093ad7bbdbfd08ccc0e6294e0f98c682c7e35db4ecfbca345fb068492' + } + It 'refuses a preexisting container rather than replacing it' { + Mock Test-BcContainer { $true } + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw '*already exists*' + } + It 'rejects original non-trial container names' { + $script:parameters.ContainerName = 'bcbuildprojectsTestAppsDE123' + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw + } + It 'rejects another trial or country mapping' { + $env:BC_SQL_PILOT_TRIAL = '3' + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw + $env:BC_SQL_PILOT_TRIAL = '2' + $env:BC_SQL_PILOT_COUNTRY = 'W1' + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw + } + It 'rejects reruns and fresh-name treatment' { + $env:GITHUB_RUN_ATTEMPT = '2' + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw + $env:GITHUB_RUN_ATTEMPT = '1' + $env:BC_SQL_PILOT_ARM = 'fresh' + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw + } + It 'rejects effective country drift' { + $settings = $env:Settings | ConvertFrom-Json + $settings.country = 'W1' + $env:Settings = $settings | ConvertTo-Json + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw '*settings differ*' + } +} diff --git a/build/scripts/SqlResetPilot/Prepare.ps1 b/build/scripts/SqlResetPilot/Prepare.ps1 index 50556207828..7d72e288605 100644 --- a/build/scripts/SqlResetPilot/Prepare.ps1 +++ b/build/scripts/SqlResetPilot/Prepare.ps1 @@ -1,9 +1,10 @@ $ErrorActionPreference = 'Stop' $base = 'c4953dceffe02a017adad34973e1955017bf5d20' if ($env:GITHUB_REPOSITORY -ne 'microsoft/BCApps' -or - $env:GITHUB_REF -ne 'refs/heads/features/646383-sql-reset-pilot' -or + $env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-warmup-experiment' -or $env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or - $env:BC_SQL_PILOT_ARM -notin @('control', 'fresh')) { + $env:BC_SQL_PILOT_ARM -ne 'control' -or $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or + $env:BC_SQL_PILOT_TRIAL -notmatch '^[1-5]$' -or $env:GITHUB_RUN_ID -notmatch '^\d{1,20}$') { throw 'This diagnostic is restricted to its explicitly dispatched disposable CI branch, attempt one.' } git merge-base --is-ancestor $base HEAD @@ -11,6 +12,13 @@ if ($LASTEXITCODE -ne 0) { throw 'PR2 base is not an ancestor.' } git diff --exit-code $base HEAD -- src if ($LASTEXITCODE -ne 0) { throw 'AL/package source differs from the pinned compiled PR2 source.' } +$project = "build\projects\Test Apps $($env:BC_SQL_PILOT_COUNTRY)" +$trialProject = "$project Trial$($env:BC_SQL_PILOT_TRIAL)" +if (Test-Path $trialProject) { throw 'Trial project must not already exist.' } +# AL-Go derives its container name from the project path. Preserve wrapper depth and country settings. +Copy-Item $project $trialProject -Recurse +"artifact=https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/sandbox/30.0.55683.0/$($env:BC_SQL_PILOT_COUNTRY.ToLowerInvariant())" | + Add-Content $env:GITHUB_OUTPUT $output = Join-Path $env:GITHUB_WORKSPACE 'sql-reset-pilot-output' New-Item -ItemType Directory $output -Force | Out-Null "BC_SQL_PILOT_OUTPUT=$output" | Add-Content $env:GITHUB_ENV @@ -22,6 +30,12 @@ $artifacts = @( @{ Kind = 'apps'; Id = 11374170356; Digest = '683343673e6efeb7699734b250d2ee5004d2d83fe08e57f324c4c645c09612db' }, @{ Kind = 'tests'; Id = 11374145359; Digest = 'f75ce06920437328e5d79a5512eddcc2425624bb696c339c108478c83554171d' } ) +if ($env:BC_SQL_PILOT_COUNTRY -eq 'DE') { + $artifacts = @( + @{ Kind = 'apps'; Id = 11376415128; Digest = '409af00bb5e6922224626f94a4d4f5625f4a9bf13b9398e84cc101a41a313dd5' }, + @{ Kind = 'tests'; Id = 11375174896; Digest = '4e1edde77089e0b586679b168f11123a8181a99c63c3a82b42323503340a5d3b' } + ) +} $manifest = @() foreach ($artifact in $artifacts) { $metadata = Invoke-RestMethod "$api/actions/artifacts/$($artifact.Id)" -Headers $headers @@ -48,6 +62,8 @@ foreach ($artifact in $artifacts) { } @{ experimentHead = $env:GITHUB_SHA; run = $env:GITHUB_RUN_ID; arm = $env:BC_SQL_PILOT_ARM + country = $env:BC_SQL_PILOT_COUNTRY; trial = $env:BC_SQL_PILOT_TRIAL; project = $trialProject + warmup = 'original-first-app-before-clean-lane'; companiesProbe = 'once-per-restored-worker'; retries = 0 budgetMinutes = 120; startedUtc = [DateTime]::UtcNow.ToString('o') artifacts = $manifest } | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $output 'provenance.json') -Encoding UTF8 diff --git a/build/scripts/SqlResetPilot/README.md b/build/scripts/SqlResetPilot/README.md index b127afef724..daa33ab95a5 100644 --- a/build/scripts/SqlResetPilot/README.md +++ b/build/scripts/SqlResetPilot/README.md @@ -1,102 +1,80 @@ -# Diagnostic only — do not merge - -A manually dispatched W1 Integration diagnostic on top of PR #11860, -`c4953dceffe02a017adad34973e1955017bf5d20`. Related context: AB#646383; -this experiment does **not** resolve that work item and is outside the native -merge group. No auto-merge, queue entry, automatic rerun or main-branch change. - -The branch-only `CICD.yaml` replaces the normal CI matrix with disposable -`GitHub-BCAppsPL` containers, capped at **120 minutes per arm**. -Its `arm` choice defaults to `control` (one job); `paired` explicitly selects -control and fresh (two jobs, 240 runner-minutes) and requires separate authorization. -PR project discovery is disabled for this experiment branch to avoid a full-country -build. Dispatch the existing registered `CICD.yaml` with this branch as its ref. -Do not dispatch a second pair without new authorization. - -## Authorized preparation correction - -The initial pair, run `37493608127`, never created a container or ran AL tests. -It incorrectly selected Windows PowerShell 5.1 while original W1 job -`112207627851` used `pwsh` 7. `Get-PlatformVersions` therefore failed parsing an -HTTP response because the Internet Explorer engine was unavailable. This is an -experiment preparation/configuration bug, **not a network failure or SQL result**. -The failed pair remains inconclusive and its logs/artifacts are retained. - -One replacement pair is explicitly authorized after correcting the job shell -**and all AL-Go composite action shell inputs** to `pwsh`. `Invoke-AlGoAction` -invokes its supplied scriptblock in that shell; it does not select the shell. -A preflight records and requires PowerShell 7 before package preparation. -Checkout now fetches full history so further diagnostic commits cannot hide the -unchanged PR2 ancestor from the provenance guard. -No PlatformHelper, vendor action, parsing workaround or shell policy is changed. - -The failed preparation consumed 13m16s (control) plus 14m23s (fresh), totaling -27m39s of runner time. The replacement remains capped at 120 minutes per arm -(240 additional runner-minutes; 267m39s cumulative worst-case including the failed -preparation). Record actual old/new preparation time separately. That authorization -allowed no further dispatch or failed-job rerun after the replacement. - -## October 7 control-only repetitions - -Replacement paired run `37496613531` completed successfully for both arms. -Its control baseline recorded 234 passed cases and 19 skipped cases across -20 codeunits with executed cases in the selected 21-codeunit, seven-batch prefix. -This did not reproduce the error and does not establish that name reuse is safe. - -New authorization permits **exactly three additional independent control-only -dispatches**, each with `arm=control`, attempt one, capped at 120 minutes: -**360 additional runner-minutes maximum**. No treatment, automatic retry, -replacement dispatch, or failed-job rerun is authorized. Each run owns a distinct -container named `bcbuildprojectsTestAppsW1`; no workflow concurrency group -cancels or serializes these independent trials. - -These controls use the same original PR2 compiled app/test artifacts from run -`37372848860`, head `c4953dceffe02a017adad34973e1955017bf5d20`, not rebuilt tests. -They repeat only the existing first seven clean batches (21 selected codeunits), -not the ordinary lane or the full 192-codeunit cohort. Compare against 234 passes -and 19 skips if a trial reaches completion; failures can stop the prefix early. -Password-file authentication, reused `tenant2/3/4` database names, three-worker -scheduling, runtime/package pins, reset logic and diagnostic reads are unchanged. -Check artifact availability before dispatch; expired artifacts have no fallback. -Launch verification is not a test result: inspect final logs, JUnit, provenance -and cleanup artifacts separately before drawing conclusions. - -Both arms use the exact same PR2 app/test archive IDs and SHA256 digests, -BCH `6.1.19-preview2811389`, NST `30.0.55665.0`, W1 application artifact -`30.0.55683.0`, image digest, original license provisioning, auth files, -test assertions and three-worker scheduling. No AL source is changed or compiled -against a moving head. The image digest resolves the original generic -`1.0.2.128` LTSC2025-dev image; the historic log did not record its digest. -The pilot records the runner OS; compare both manifests before interpretation. - -The original discovery runs unchanged. Its ordered clean-test prefix must match -the 21 codeunits captured in W1 job 112207627851, run 37372848860. Seven batches -end with Expense Users, Capabilities and Activity Log. No ordinary-test phase, -HTTP warmup, auth mutation, extra sleep, force-refresh or pool clearing is added. -The existing mount-state polling remains. Copy failures and test failures are -terminal; baseline copy/transient retries are deliberately disabled in both arms. -A failed batch drains its three workers and prevents later batches. - -Control restores worker databases under `tenant2/3/4`. Treatment preserves those -logical IDs/URLs, but uses `tenantN_rRUN_gGENERATION` database names. Both arms -read the same SQL identity fields before and after reset. This administrative -probe has a possible timing effect and does not reveal the NST internal pool -generation. A fresh SQL name does not guarantee a fresh database ID or GUID. - -Deletion is allowed only for a tracked worker's previously mounted local database, -after dismount. A stale/foreign mapping or pre-existing fresh target fails closed. -The caller's tenant objects are updated after each successful mount, including -the discovery restore. Default/application/template databases are not deleted by -the pilot. The immutable template is retained until whole-container teardown. -The ordinary disposable pipeline teardown plus the final ownership-checked -fallback clean up the complete container, including partial failed copies. -Runner loss/hard cancellation may prevent exports/finally; that is inconclusive. - -Artifacts contain package hashes, effective identities, ordered worklist, -reset UTC/SQL identity timeline, worker output, JUnit and raw EVTX. -No credentials, access keys or authorization headers are added to diagnostics. -Missing artifacts, setup drift, timeout or no reproduction mean **inconclusive**. -Control-only reproduction is a name-reuse signal, not proof of a production fix. - -Local validation (Pester 5, no NST required): +# API warmup experiment — diagnostic only, do not merge + +One manual `CICD.yaml` dispatch on `features/646383-sql-api-warmup-experiment` +runs **W1 and DE, five independent trials each**. Maximum parallelism is five; +each trial is capped at 120 minutes (1,200 runner-minutes total maximum). +Only attempt one is allowed. No automatic rerun, replacement dispatch, PR, +merge queue, local/shared NST operation, or fresh-database-name treatment. + +## Fixed baseline + +The branch starts from SQL reset pilot `d7dd4dddcd49da397ee42de1677d1b7d44622149`. +AL source remains exactly PR2 `c4953dceffe02a017adad34973e1955017bf5d20`. +Both countries consume their own unchanged compiled packages from run +`37372848860`; this does not claim country package binaries are identical. +`Prepare.ps1` verifies the source ancestry/diff, artifact run/head/digests, +downloaded archive SHA256 and per-package hashes. Missing or expired artifacts +fail closed; there is no moving-build fallback. + +| Country | App artifact | Test artifact | +| --- | --- | --- | +| W1 | 11374170356 | 11374145359 | +| DE | 11376415128 | 11375174896 | + +The pinned runtime is NST **30.0.55665.0**, application **30.0.55683.0**, +BCH **6.1.19-preview2811389**, AL-Go +`91b96c2b294be6f823277dafe6f03350abfb9d23`, and generic image +`sha256:c899d12093ad7bbdbfd08ccc0e6294e0f98c682c7e35db4ecfbca345fb068492`. +The image matches the pilot; historic source logs did not record its digest. +PowerShell 7, four tenants, original password-file authentication and company +`My Company` are retained. + +## Experiment order and failure policy + +1. Original discovery runs on a worker, which is then restored from default. +2. The immutable pristine template is frozen **before** app warmup fixtures. +3. The original `Invoke-WarmupDispatch` runs the ordinary first test app alone + on default, with the original `SkipAutomaticDisabledPass` behavior. + Results live under `sql-reset-pilot-output/warmup`, outside prefix coverage. + Failure, a transient queue, a skipped warmup or an incomplete job stops the trial. +4. For each of seven clean batches, restore all three workers under their + original `tenant2/3/4` database names, then probe all three, then dispatch. +5. Each probe makes **one external host HTTP GET** to + `/BC/api/v2.0/companies?tenant=tenantN`, using the actual server instance, + port/IP and existing container credential. Basic auth is sent preemptively. + No redirects or HTTP retries; timeout is 60 seconds. A non-200 response, + invalid JSON or missing expected company stops the trial before dispatch. +6. Test failures drain the current three-worker batch and stop later batches. + No test, transient, warmup or restore retries are enabled. Existing bounded + mount-state polling and ordinary scheduler waits are unchanged. + +The cohort remains the same ordered **21-codeunit prefix**, ending with Expense +Users (148315), Capabilities (148318), and Activity Log (148343). The previous W1 +control yielded **253 cases: 234 passed, 19 skipped**, across 20 codeunits with +cases. This is a quick cohort, **not all API paths or the full suite**. Warmup +counts must be reported separately. Discovery-order drift fails closed. + +## Isolation, evidence and interpretation + +At runtime only, the selected project descriptor is copied to +`build/projects/Test Apps Trial` at the same directory depth. +AL-Go derives a unique container name: +`bcbuildprojectsTestAppsTrial`. +Country settings and wrapper targets stay unchanged. Preflight refuses an +existing container; final cleanup checks country, trial, run, arm and exact +registered name before removing only that owned disposable container. + +Artifacts preserve provenance, shell/runtime identity, worklist, SQL reset +timeline, warmup outcome/results, worker output, prefix JUnit and EVTX. +`companies-probes.jsonl` records sanitized URI, tenant, generation, HTTP status, +UTC start/end, duration and safe client/server correlation IDs. No password, +authorization header, response body or raw HTTP exception is added to logs. +Runner loss or cancellation can prevent final export/cleanup and is inconclusive. + +This experiment combines two warmup interventions; it cannot distinguish their +individual effects. A pass/non-reproduction is not proof of a production fix or +SQL-name reuse safety. Report every trial, including preflight/probe failures, +timeouts and missing evidence. Do not launch another matrix without authorization. + +Local validation requires Pester 5+ (validated with 6.1.0), not an NST: `Invoke-Pester .\build\scripts\SqlResetPilot` diff --git a/build/scripts/SqlResetPilot/Warmup.Tests.ps1 b/build/scripts/SqlResetPilot/Warmup.Tests.ps1 new file mode 100644 index 00000000000..331f49cd232 --- /dev/null +++ b/build/scripts/SqlResetPilot/Warmup.Tests.ps1 @@ -0,0 +1,169 @@ +$discoveryArm = $env:BC_SQL_PILOT_ARM +try { + $env:BC_SQL_PILOT_ARM = 'control' + Import-Module (Join-Path $PSScriptRoot '..\ParallelTestExecution.psm1') -Force +} finally { + $env:BC_SQL_PILOT_ARM = $discoveryArm +} +BeforeAll { + $script:oldArm = $env:BC_SQL_PILOT_ARM + $script:oldOutput = $env:BC_SQL_PILOT_OUTPUT + $script:oldRun = $env:GITHUB_RUN_ID + $script:oldRunnerTemp = $env:RUNNER_TEMP + $env:BC_SQL_PILOT_ARM = 'control' + $env:BC_SQL_PILOT_OUTPUT = $TestDrive + $env:GITHUB_RUN_ID = '123' + $env:RUNNER_TEMP = $TestDrive + function global:Get-BcContainerAppInfo { + param($containerName, $tenant, [switch]$tenantSpecificProperties) + $null = $containerName, $tenant, $tenantSpecificProperties + throw 'Live container call forbidden.' + } +} +AfterAll { + $env:BC_SQL_PILOT_ARM = $script:oldArm + $env:BC_SQL_PILOT_OUTPUT = $script:oldOutput + $env:GITHUB_RUN_ID = $script:oldRun + $env:RUNNER_TEMP = $script:oldRunnerTemp +} + +Describe 'Diagnostic first-app warmup' { + InModuleScope ParallelTestExecution { + BeforeEach { + $script:parameters = @{ + containerName = 'owned'; tenant = 'default'; companyName = 'My Company' + JUnitResultFileName = (Join-Path $TestDrive 'TestResults.xml') + } + Mock Invoke-WarmupDispatch { @($Pending | Select-Object -Skip 1) } + } + It 'calls the original warmup once on default and keeps results outside prefix coverage' { + Invoke-SqlPilotWarmup -Parameters $script:parameters -AppNames @('First', 'Second') ` + -AppIdByName @{ First = 'id' } -Tenants @('default','tenant2','tenant3','tenant4') ` + -ScriptPath 'runner.ps1' -TestType 'IntegrationTest' -CleanTenantAppNames @('First','Second') + Should -Invoke Invoke-WarmupDispatch -Times 1 -Exactly -ParameterFilter { + $Parameters.tenant -eq 'default' -and $Tenants[0] -eq 'default' -and + $Parameters.JUnitResultFileName -like '*\warmup\TestResults.xml' -and + $State.rerunBudget -eq 0 -and $CleanTenantAppNames -contains 'First' + } + $script:parameters.JUnitResultFileName | Should -Not -Match '\\warmup\\' + } + It 'stops on warmup failure without retrying' { + Mock Invoke-WarmupDispatch { $State.hasFailures = $true; @($Pending | Select-Object -Skip 1) } + { Invoke-SqlPilotWarmup $script:parameters @('First','Second') @{ First = 'id' } ` + @('default','tenant2') 'runner.ps1' 'IntegrationTest' @('First') } | Should -Throw '*warmup failed*' + Should -Invoke Invoke-WarmupDispatch -Times 1 -Exactly + } + It 'stops on any transient queue without retrying' { + Mock Invoke-WarmupDispatch { $State.transient = @('failed'); @($Pending | Select-Object -Skip 1) } + { Invoke-SqlPilotWarmup $script:parameters @('First','Second') @{ First = 'id' } ` + @('default','tenant2') 'runner.ps1' 'IntegrationTest' @('First') } | Should -Throw '*warmup failed*' + Should -Invoke Invoke-WarmupDispatch -Times 1 -Exactly + } + It 'rejects a skipped warmup or a nondefault primary tenant' { + { Invoke-SqlPilotWarmup $script:parameters @('First','Second') @{} ` + @('default','tenant2') 'runner.ps1' 'IntegrationTest' @('First') } | Should -Throw + $script:parameters.tenant = 'tenant2' + { Invoke-SqlPilotWarmup $script:parameters @('First','Second') @{ First = 'id' } ` + @('tenant2','default') 'runner.ps1' 'IntegrationTest' @('First') } | Should -Throw + Should -Invoke Invoke-WarmupDispatch -Times 0 + } + It 'freezes the template before warmup and only then starts the clean lane' { + $script:order = [Collections.Generic.List[string]]::new() + Mock Test-Path { $false } + Mock Set-Content {} + Mock Get-AvailableBcTenantInfo { + @('default','tenant2','tenant3','tenant4') | ForEach-Object { + [PSCustomObject]@{ Id = $_; DatabaseName = $_ } + } + } + Mock Get-BcContainerAppInfo { + [PSCustomObject]@{ IsInstalled = $true; Name = 'First'; AppId = 'id' } + } + Mock Get-ALGoSetting { $true } + Mock Get-AppRerunBudget { 0 } + Mock Get-RequiredDisabledWorkItems { [PSCustomObject]@{ CodeunitId = 139700 } } + Mock Select-SqlPilotPrefix { $WorkItems } + Mock Reset-BcTestTenant { $script:order.Add('discovery-reset') } + Mock New-BcTestTenantTemplate { $script:order.Add('template'); 'default-test-template' } + Mock Invoke-SqlPilotWarmup { $script:order.Add('warmup') } + Mock Invoke-RequiredDisabledTestExecution { $script:order.Add('clean'); $true } + Mock Merge-TenantTestResults {} + Invoke-ParallelTestExecution $script:parameters 'runner.ps1' 'IntegrationTest' @('First','Second') | + Should -BeTrue + ($script:order -join ',') | Should -Be 'discovery-reset,template,warmup,clean' + } + It 'never dispatches clean tests when warmup fails' { + $script:order = [Collections.Generic.List[string]]::new() + Mock Test-Path { $false } + Mock Set-Content {} + Mock Get-AvailableBcTenantInfo { + @('default','tenant2','tenant3','tenant4') | ForEach-Object { + [PSCustomObject]@{ Id = $_; DatabaseName = $_ } + } + } + Mock Get-BcContainerAppInfo { [PSCustomObject]@{ IsInstalled = $true; Name = 'First'; AppId = 'id' } } + Mock Get-ALGoSetting { $true } + Mock Get-AppRerunBudget { 0 } + Mock Get-RequiredDisabledWorkItems { [PSCustomObject]@{ CodeunitId = 139700 } } + Mock Select-SqlPilotPrefix { $WorkItems } + Mock Reset-BcTestTenant {} + Mock New-BcTestTenantTemplate { 'default-test-template' } + Mock Invoke-SqlPilotWarmup { throw 'warmup failed' } + Mock Invoke-RequiredDisabledTestExecution { $true } + { Invoke-ParallelTestExecution $script:parameters 'runner.ps1' 'IntegrationTest' @('First','Second') } | + Should -Throw '*warmup failed*' + Should -Invoke Invoke-RequiredDisabledTestExecution -Times 0 + } + It 'makes failures terminal even if the ordinary rerun budget is positive' { + foreach ($outcome in @('Failed', 'Transient')) { + $state = [PSCustomObject]@{ + hasFailures = $false; transient = @(); rerun = @() + rerunBudget = 2; tenantCount = 4; rerunDone = @{} + } + Register-TestJobOutcome -Result ([PSCustomObject]@{ Outcome = $outcome }) -State $state + $state.hasFailures | Should -BeTrue + $state.transient.Count | Should -Be 0 + $state.rerun.Count | Should -Be 0 + } + } + } +} + +Describe 'Companies probe batch barrier' { + InModuleScope ParallelTestExecution { + BeforeEach { + $script:order = [Collections.Generic.List[string]]::new() + $script:items = @(1..6 | ForEach-Object { [PSCustomObject]@{ Key = "CU$_" } }) + $script:tenants = @('tenant2','tenant3','tenant4') | ForEach-Object { + [PSCustomObject]@{ Id = $_; DatabaseName = $_ } + } + $script:parameters = @{ containerName = 'owned'; companyName = 'My Company'; credential = [PSCredential]::Empty } + Mock Reset-BcTestTenant { $script:order.Add("reset:$Tenant") } + Mock Invoke-SqlPilotCompaniesProbe { $script:order.Add("probe:$Tenant") } + Mock Start-RequiredDisabledDispatch { $script:order.Add("dispatch:$($TenantInfo.Id)") } + Mock Wait-ForAllTestJobs {} + } + It 'probes each fresh worker once after every reset and before any dispatch in each batch' { + Invoke-RequiredDisabledTestExecution $script:parameters $script:items $script:tenants ` + 'default-test-template' 'runner.ps1' 'IntegrationTest' | Should -BeTrue + $batch = 'reset:tenant2,reset:tenant3,reset:tenant4,probe:tenant2,probe:tenant3,probe:tenant4,dispatch:tenant2,dispatch:tenant3,dispatch:tenant4' + ($script:order -join ',') | Should -Be "$batch,$batch" + Should -Invoke Invoke-SqlPilotCompaniesProbe -Times 6 -Exactly + } + It 'stops without dispatch or retry when a probe fails' { + Mock Invoke-SqlPilotCompaniesProbe { throw 'probe failed' } + { Invoke-RequiredDisabledTestExecution $script:parameters $script:items $script:tenants ` + 'default-test-template' 'runner.ps1' 'IntegrationTest' } | Should -Throw '*probe failed*' + Should -Invoke Reset-BcTestTenant -Times 3 -Exactly + Should -Invoke Invoke-SqlPilotCompaniesProbe -Times 1 -Exactly + Should -Invoke Start-RequiredDisabledDispatch -Times 0 + } + It 'does not execute another batch or retry after a failed test batch' { + Mock Wait-ForAllTestJobs { $state.hasFailures = $true } + Invoke-RequiredDisabledTestExecution $script:parameters $script:items $script:tenants ` + 'default-test-template' 'runner.ps1' 'IntegrationTest' | Should -BeFalse + Should -Invoke Start-RequiredDisabledDispatch -Times 3 -Exactly + Should -Invoke Invoke-SqlPilotCompaniesProbe -Times 3 -Exactly + } + } +} From b234da1e60992cd7e16a6e376dfaaecd7cad0d65 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Wed, 7 Oct 2026 12:43:55 +0200 Subject: [PATCH 18/22] Allow bounded companies readiness probe retries only Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .../SqlResetPilot/CompaniesProbe.Tests.ps1 | 97 ++++++++++++++++-- build/scripts/SqlResetPilot/Lifecycle.psm1 | 98 +++++++++++++++---- build/scripts/SqlResetPilot/Prepare.ps1 | 4 +- build/scripts/SqlResetPilot/README.md | 16 ++- 4 files changed, 183 insertions(+), 32 deletions(-) diff --git a/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 b/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 index 6fdcbcf79af..9ede785e853 100644 --- a/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 +++ b/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 @@ -22,30 +22,101 @@ Describe 'External authenticated companies probe' { @{ StatusCode = 200; Headers = @{ 'request-id' = @('server-correlation') }; Content = '{"value":[{"name":"My Company","id":"company-id"}]}' } } Mock Add-Content -ModuleName Lifecycle {} + Mock Start-Sleep -ModuleName Lifecycle {} + $script:requestNumber = 0 } - It 'sends exactly one explicitly tenant-scoped preauthenticated HTTP request with retries disabled' { + It 'records first-try success with explicit tenant auth and hidden HTTP retries disabled' { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { $Uri -eq 'http://172.16.1.2:7048/BC/api/v2.0/companies?tenant=tenant2' -and $Authentication -eq 'Basic' -and $Credential.UserName -eq 'fixture' -and - $MaximumRetryCount -eq 0 -and $MaximumRedirection -eq 0 -and $TimeoutSec -eq 60 -and $SkipHttpErrorCheck + $MaximumRetryCount -eq 0 -and $MaximumRedirection -eq 0 -and $TimeoutSec -eq 20 -and + $OperationTimeoutSeconds -eq 20 -and $SkipHttpErrorCheck } Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { + $Value -match '"recordType":"attempt"' -and $Value -match '"status":200' -and $Value -match '"passed":true' -and $Value -match 'server-correlation' -and $Value -notmatch 'fixture|test-only-marker' } + Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { + $Value -match '"outcome":"first-try-success"' -and $Value -match '"retries":0' -and + $Value -match '"attempts":1' -and $Value -match '"recovered":false' + } + Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 0 } - It 'treats HTTP failures as terminal without logging response content' { + It 'bounds transient HTTP to three attempts and records each failure safely' -ForEach @( + @{ Code = 500 }, @{ Code = 502 }, @{ Code = 503 }, @{ Code = 504 } + ) { Mock Invoke-WebRequest -ModuleName Lifecycle { - @{ StatusCode = 500; Headers = @{}; Content = 'sensitive-response-marker' } + @{ StatusCode = $Code; Headers = @{}; Content = 'sensitive-response-marker' } } { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | - Should -Throw '*No retry*' + Should -Throw '*after 3 attempt*' + Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 3 -Exactly + Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 2 -Exactly -ParameterFilter { $Seconds -eq 2 } + Should -Invoke Add-Content -ModuleName Lifecycle -Times 3 -Exactly -ParameterFilter { + $Value -match '"recordType":"attempt"' -and $Value -match '"retryable":true' -and + $Value -match '"startedUtc":' -and $Value -match '"completedUtc":' -and + $Value -match '"elapsedMilliseconds":' -and $Value -match '"errorCategory":"HttpStatus"' -and + $Value -notmatch 'sensitive-response-marker|test-only-marker' + } + Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { + $Value -match '"outcome":"failed"' -and $Value -match '"attempts":3' -and $Value -match '"retries":2' + } + } + It 'does not retry permanent HTTP ' -ForEach @( + @{ Code = 301 }, @{ Code = 400 }, @{ Code = 401 }, @{ Code = 403 }, + @{ Code = 404 }, @{ Code = 408 }, @{ Code = 429 }, @{ Code = 501 } + ) { + Mock Invoke-WebRequest -ModuleName Lifecycle { @{ StatusCode = $Code; Headers = @{}; Content = '' } } + { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw '*after 1 attempt*' Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly + Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 0 + } + It 'records recovered success on the third attempt and does not issue a fourth' { + Mock Invoke-WebRequest -ModuleName Lifecycle { + $script:requestNumber++ + if ($script:requestNumber -lt 3) { return @{ StatusCode = 503; Headers = @{}; Content = '' } } + @{ StatusCode = 200; Headers = @{}; Content = '{"value":[{"name":"My Company","id":"company-id"}]}' } + } + Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' + Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 3 -Exactly + Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 2 -Exactly + Should -Invoke Add-Content -ModuleName Lifecycle -Times 3 -Exactly -ParameterFilter { $Value -match '"recordType":"attempt"' } Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { - $Value -match '"status":500' -and $Value -notmatch 'sensitive-response-marker|test-only-marker' + $Value -match '"outcome":"recovered"' -and $Value -match '"recovered":true' -and + $Value -match '"attempts":3' -and $Value -match '"retries":2' } } + It 'retries typed transport failure and records recovery' -ForEach @( + @{ Kind = 'timeout'; Failure = [TimeoutException]::new('sensitive-exception-marker') } + @{ Kind = 'cancelled request timeout'; Failure = [Threading.Tasks.TaskCanceledException]::new('sensitive-exception-marker') } + @{ Kind = 'connection'; Failure = [Net.Http.HttpRequestException]::new('sensitive-exception-marker', + [Net.Sockets.SocketException]::new(10061)) } + @{ Kind = 'web timeout'; Failure = [Net.WebException]::new('sensitive-exception-marker', [Net.WebExceptionStatus]::Timeout) } + ) { + Mock Invoke-WebRequest -ModuleName Lifecycle { + $script:requestNumber++ + if ($script:requestNumber -eq 1) { throw $Failure } + @{ StatusCode = 200; Headers = @{}; Content = '{"value":[{"name":"My Company","id":"company-id"}]}' } + } + Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' + Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 2 -Exactly + Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { $Seconds -eq 2 } + Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { + $Value -match '"errorCategory":"ConnectionOrTimeout"' -and $Value -match '"status":null' -and + $Value -match '"errorType":' -and $Value -notmatch 'sensitive-exception-marker|test-only-marker' + } + Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { + $Value -match '"outcome":"recovered"' -and $Value -match '"attempts":2' + } + } + It 'stops repeated transport timeouts after three attempts' { + Mock Invoke-WebRequest -ModuleName Lifecycle { throw [TimeoutException]::new('sensitive-exception-marker') } + { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw '*after 3 attempt*' + Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 3 -Exactly + Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 2 -Exactly + } It 'rejects invalid JSON without another request' { Mock Invoke-WebRequest -ModuleName Lifecycle { @{ StatusCode = 200; Headers = @{}; Content = 'invalid-json' } } { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw @@ -58,14 +129,24 @@ Describe 'External authenticated companies probe' { { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly } - It 'redacts transport exception text and never retries' { + It 'redacts unknown exception text without retrying permanent failures' { Mock Invoke-WebRequest -ModuleName Lifecycle { throw 'sensitive-exception-marker' } { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw '*sanitized companies-probes*' Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { - $Value -notmatch 'sensitive-exception-marker|test-only-marker' + $Value -match '"recordType":"attempt"' -and $Value -notmatch 'sensitive-exception-marker|test-only-marker' } + Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 0 + } + It 'does not retry TLS authentication errors' { + Mock Invoke-WebRequest -ModuleName Lifecycle { + throw [Net.Http.HttpRequestException]::new('sensitive-exception-marker', + [Security.Authentication.AuthenticationException]::new('sensitive-exception-marker')) + } + { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw '*after 1 attempt*' + Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly + Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 0 } It 'refuses a foreign container, default tenant, or worker not yet restored' { { Invoke-SqlPilotCompaniesProbe 'foreign' 'tenant2' $script:credential 'My Company' } | Should -Throw diff --git a/build/scripts/SqlResetPilot/Lifecycle.psm1 b/build/scripts/SqlResetPilot/Lifecycle.psm1 index 0acbc839510..8301134c2b2 100644 --- a/build/scripts/SqlResetPilot/Lifecycle.psm1 +++ b/build/scripts/SqlResetPilot/Lifecycle.psm1 @@ -130,10 +130,11 @@ function Select-SqlPilotPrefix { <# .SYNOPSIS - Makes one authenticated companies request from the host after an owned worker restore. + Probes companies from the host after an owned worker restore, with bounded readiness retries. .DESCRIPTION Uses the disposable container's existing password credential and private API endpoint. - No retries, redirects, response bodies, passwords or authorization headers are logged. + Only transient HTTP and connection/timeout failures may retry, up to three attempts. + No redirects, response bodies, passwords or authorization headers are logged. #> function Invoke-SqlPilotCompaniesProbe { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingAllowUnencryptedAuthentication', '', @@ -154,7 +155,8 @@ function Invoke-SqlPilotCompaniesProbe { tenant = $Tenant; generation = $script:pilot.Workers[$Tenant].Generation startedUtc = [DateTime]::UtcNow.ToString('o'); status = $null; passed = $false clientRequestId = [guid]::NewGuid().ToString(); serverRequestId = $null - uri = $null; retries = 0 + uri = $null; retries = 0; attempts = 0; recordType = 'outcome' + outcome = 'failed'; recovered = $false; maximumAttempts = 3 } $clock = [Diagnostics.Stopwatch]::StartNew() try { @@ -171,26 +173,84 @@ function Invoke-SqlPilotCompaniesProbe { $uri = [UriBuilder]::new($scheme, $ip, [int]$config.ODataServicesPort, "$($config.ServerInstance)/api/v2.0/companies", "?tenant=$Tenant").Uri.AbsoluteUri $observation.uri = $uri - $response = Invoke-WebRequest -Uri $uri -Method Get -Authentication Basic -Credential $Credential ` - -AllowUnencryptedAuthentication -SkipCertificateCheck -SkipHttpErrorCheck -MaximumRedirection 0 ` - -MaximumRetryCount 0 -TimeoutSec 60 -ErrorAction Stop ` - -Headers @{ Accept = 'application/json'; 'client-request-id' = $observation.clientRequestId } - $observation.status = [int]$response.StatusCode - foreach ($key in @('request-id', 'x-ms-request-id')) { - $value = [string]($response.Headers[$key] | Select-Object -First 1) - if ($value -match '^[a-zA-Z0-9._:-]{1,128}$') { $observation.serverRequestId = $value; break } - } - if ($response.StatusCode -ne 200) { throw 'Companies probe returned non-200 status.' } - $body = $response.Content | ConvertFrom-Json -ErrorAction Stop - if (-not $body.PSObject.Properties['value'] -or - @($body.value | Where-Object { $_.name -eq $CompanyName -and $_.id }).Count -ne 1) { - throw 'Companies probe did not return exactly one expected company.' + for ($attemptNumber = 1; $attemptNumber -le 3; $attemptNumber++) { + $attempt = @{ + recordType = 'attempt'; probeId = $observation.clientRequestId + tenant = $Tenant; generation = $observation.generation; uri = $uri + attempt = $attemptNumber; startedUtc = [DateTime]::UtcNow.ToString('o') + status = $null; passed = $false; retryable = $false + clientRequestId = [guid]::NewGuid().ToString(); serverRequestId = $null + } + $attemptClock = [Diagnostics.Stopwatch]::StartNew() + try { + $response = Invoke-WebRequest -Uri $uri -Method Get -Authentication Basic -Credential $Credential ` + -AllowUnencryptedAuthentication -SkipCertificateCheck -SkipHttpErrorCheck -MaximumRedirection 0 ` + -MaximumRetryCount 0 -TimeoutSec 20 -OperationTimeoutSeconds 20 -ErrorAction Stop ` + -Headers @{ Accept = 'application/json'; 'client-request-id' = $attempt.clientRequestId } + $attempt.status = [int]$response.StatusCode + foreach ($key in @('request-id', 'x-ms-request-id')) { + $value = [string]($response.Headers[$key] | Select-Object -First 1) + if ($value -match '^[a-zA-Z0-9._:-]{1,128}$') { $attempt.serverRequestId = $value; break } + } + if ($attempt.status -ne 200) { + $attempt.errorCategory = 'HttpStatus' + $attempt.retryable = $attempt.status -in @(500, 502, 503, 504) + } else { + $body = $response.Content | ConvertFrom-Json -ErrorAction Stop + if (-not $body.PSObject.Properties['value'] -or + @($body.value | Where-Object { $_.name -eq $CompanyName -and $_.id }).Count -ne 1) { + throw 'Companies probe did not return exactly one expected company.' + } + $attempt.passed = $true + } + } catch { + $attempt.errorType = $_.Exception.GetType().FullName + $attempt.errorCategory = 'PermanentFailure' + # Inspect types, never exception messages (which may contain authentication material). + if ($null -eq $attempt.status) { + $exception = $_.Exception + while ($null -ne $exception) { + if ($exception -is [TimeoutException] -or + $exception -is [OperationCanceledException] -or + ($exception -is [Net.Http.HttpRequestException] -and + $exception.PSObject.Properties['HttpRequestError'] -and + $exception.HttpRequestError -in @('ConnectionError', 'NameResolutionError', 'ResponseEnded')) -or + ($exception -is [Net.Sockets.SocketException] -and + $exception.SocketErrorCode -in @('ConnectionRefused', 'ConnectionReset', 'ConnectionAborted', + 'HostNotFound', 'HostUnreachable', 'NetworkDown', 'NetworkReset', 'NetworkUnreachable', 'TimedOut', 'TryAgain')) -or + ($exception -is [Net.WebException] -and + $exception.Status -in @('Timeout', 'ConnectFailure', 'ConnectionClosed', 'KeepAliveFailure', + 'ReceiveFailure', 'SendFailure', 'NameResolutionFailure', 'ProxyNameResolutionFailure'))) { + $attempt.retryable = $true + $attempt.errorCategory = 'ConnectionOrTimeout' + break + } + $exception = $exception.InnerException + } + } + } finally { + $attempt.completedUtc = [DateTime]::UtcNow.ToString('o') + $attempt.elapsedMilliseconds = $attemptClock.ElapsedMilliseconds + $attempt | ConvertTo-Json -Compress | Add-Content $trace + $observation.attempts = $attemptNumber + $observation.retries = $attemptNumber - 1 + $observation.status = $attempt.status + $observation.serverRequestId = $attempt.serverRequestId + } + if ($attempt.passed) { + $observation.passed = $true + $observation.recovered = $attemptNumber -gt 1 + $observation.outcome = if ($observation.recovered) { 'recovered' } else { 'first-try-success' } + break + } + if (-not $attempt.retryable -or $attemptNumber -eq 3) { break } + Start-Sleep -Seconds 2 } - $observation.passed = $true + if (-not $observation.passed) { throw 'Companies probe exhausted eligible attempts or encountered a permanent failure.' } } catch { $observation.errorType = $_.Exception.GetType().FullName # HTTP exception details can contain authentication material; never emit the original error. - throw "Companies probe failed for $Tenant; see sanitized companies-probes.jsonl. No retry was attempted." + throw "Companies probe failed for $Tenant after $($observation.attempts) attempt(s); see sanitized companies-probes.jsonl." } finally { $observation.completedUtc = [DateTime]::UtcNow.ToString('o') $observation.elapsedMilliseconds = $clock.ElapsedMilliseconds diff --git a/build/scripts/SqlResetPilot/Prepare.ps1 b/build/scripts/SqlResetPilot/Prepare.ps1 index 7d72e288605..3d72bc76f33 100644 --- a/build/scripts/SqlResetPilot/Prepare.ps1 +++ b/build/scripts/SqlResetPilot/Prepare.ps1 @@ -63,7 +63,9 @@ foreach ($artifact in $artifacts) { @{ experimentHead = $env:GITHUB_SHA; run = $env:GITHUB_RUN_ID; arm = $env:BC_SQL_PILOT_ARM country = $env:BC_SQL_PILOT_COUNTRY; trial = $env:BC_SQL_PILOT_TRIAL; project = $trialProject - warmup = 'original-first-app-before-clean-lane'; companiesProbe = 'once-per-restored-worker'; retries = 0 + warmup = 'original-first-app-before-clean-lane'; companiesProbe = 'per-restored-worker-readiness-retries' + companiesProbeMaximumAttempts = 3; companiesProbeTimeoutSeconds = 20; companiesProbeRetryDelaySeconds = 2 + warmupRetries = 0; testRetries = 0; schedulerRetries = 0; ciRetries = 0 budgetMinutes = 120; startedUtc = [DateTime]::UtcNow.ToString('o') artifacts = $manifest } | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $output 'provenance.json') -Encoding UTF8 diff --git a/build/scripts/SqlResetPilot/README.md b/build/scripts/SqlResetPilot/README.md index daa33ab95a5..8bda7a0d945 100644 --- a/build/scripts/SqlResetPilot/README.md +++ b/build/scripts/SqlResetPilot/README.md @@ -39,11 +39,14 @@ PowerShell 7, four tenants, original password-file authentication and company Failure, a transient queue, a skipped warmup or an incomplete job stops the trial. 4. For each of seven clean batches, restore all three workers under their original `tenant2/3/4` database names, then probe all three, then dispatch. -5. Each probe makes **one external host HTTP GET** to +5. Each probe makes **up to three external host HTTP GET attempts** to `/BC/api/v2.0/companies?tenant=tenantN`, using the actual server instance, port/IP and existing container credential. Basic auth is sent preemptively. - No redirects or HTTP retries; timeout is 60 seconds. A non-200 response, - invalid JSON or missing expected company stops the trial before dispatch. + Each attempt has 20-second connection and read timeouts; eligible attempts are separated by two + seconds. Only HTTP 500/502/503/504 and transport connection/timeout failures + may retry. Authentication failures, other HTTP statuses (including all 4xx), + invalid JSON and missing expected company are terminal. Redirects and hidden + HTTP-client retries remain disabled. Exhaustion stops the trial before dispatch. 6. Test failures drain the current three-worker batch and stop later batches. No test, transient, warmup or restore retries are enabled. Existing bounded mount-state polling and ordinary scheduler waits are unchanged. @@ -67,7 +70,12 @@ registered name before removing only that owned disposable container. Artifacts preserve provenance, shell/runtime identity, worklist, SQL reset timeline, warmup outcome/results, worker output, prefix JUnit and EVTX. `companies-probes.jsonl` records sanitized URI, tenant, generation, HTTP status, -UTC start/end, duration and safe client/server correlation IDs. No password, +UTC start/end, duration and safe client/server correlation IDs **for every attempt**. +Separate outcome records distinguish `first-try-success`, `recovered` and `failed`, +and record actual attempt/retry counts. Report this treatment as **companies-probe +retry allowed**, not blanket no retries; warmup, AL tests, scheduler and CI still +have zero retries. The earlier run `37608087700` used the previous no-probe-retry +commit and cannot acquire this change in flight. No password, authorization header, response body or raw HTTP exception is added to logs. Runner loss or cancellation can prevent final export/cleanup and is inconclusive. From 8a7ebc7d665c0a812c2b3b4902c63df570959954 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Wed, 7 Oct 2026 12:47:20 +0200 Subject: [PATCH 19/22] Preserve nested clean test results separately from warmup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .../scripts/SqlResetPilot/Finalize.Tests.ps1 | 25 +++++++++++++++++++ build/scripts/SqlResetPilot/Finalize.ps1 | 13 +++++++++- build/scripts/SqlResetPilot/Lifecycle.psm1 | 2 +- build/scripts/SqlResetPilot/README.md | 7 +++++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/build/scripts/SqlResetPilot/Finalize.Tests.ps1 b/build/scripts/SqlResetPilot/Finalize.Tests.ps1 index 7bcda67c576..72502044d1c 100644 --- a/build/scripts/SqlResetPilot/Finalize.Tests.ps1 +++ b/build/scripts/SqlResetPilot/Finalize.Tests.ps1 @@ -20,6 +20,7 @@ Describe 'SQL pilot final cleanup ownership' { Mock Test-Path { $true } Mock Get-Content { '{"runId":"123","arm":"control","country":"DE","trial":"2","container":"bcbuildprojectsTestAppsDETrial2123"}' } Mock Get-ChildItem {} + Mock New-Item {} Mock Copy-Item {} Mock Set-Content {} $global:sqlPilotTestContainerExists = $true @@ -46,6 +47,30 @@ Describe 'SQL pilot final cleanup ownership' { { & (Join-Path $PSScriptRoot 'Finalize.ps1') } | Should -Throw Should -Invoke Remove-BcContainer -Times 1 -Exactly -Scope It } + It 'collects root and nested runner results separately from warmup' { + Mock Get-ChildItem { [PSCustomObject]@{ FullName = 'TestResults.xml' } } + & (Join-Path $PSScriptRoot 'Finalize.ps1') + Should -Invoke Get-ChildItem -Times 1 -Exactly -Scope It -ParameterFilter { + $Path -like '*Test Apps DE Trial2\.buildartifacts' -and $Filter -eq 'TestResults*.xml' -and $File + } + Should -Invoke Copy-Item -Times 1 -Exactly -Scope It -ParameterFilter { + $Destination -like '*clean-results\buildartifacts' + } + Should -Invoke Copy-Item -Times 1 -Exactly -Scope It -ParameterFilter { + $Destination -like '*clean-results\project-root' + } + Should -Invoke Copy-Item -Times 0 -Exactly -Scope It -ParameterFilter { + $Destination -like '*sql-reset-pilot-output\warmup*' + } + } + It 'does not fail cleanup when nested runner results are absent' { + Mock Test-Path { $false } -ParameterFilter { $Path -like '*\.buildartifacts' } + & (Join-Path $PSScriptRoot 'Finalize.ps1') + Should -Invoke Get-ChildItem -Times 0 -Exactly -Scope It -ParameterFilter { + $Path -like '*\.buildartifacts' + } + Should -Invoke Remove-BcContainer -Times 1 -Exactly -Scope It + } It 'refuses deletion if ownership was not recorded' { Mock Test-Path { $false } & (Join-Path $PSScriptRoot 'Finalize.ps1') diff --git a/build/scripts/SqlResetPilot/Finalize.ps1 b/build/scripts/SqlResetPilot/Finalize.ps1 index cc6a802b9a0..fc5c99261a7 100644 --- a/build/scripts/SqlResetPilot/Finalize.ps1 +++ b/build/scripts/SqlResetPilot/Finalize.ps1 @@ -19,7 +19,18 @@ try { Copy-Item $events (Join-Path $output 'final-container.evtx') } $project = Join-Path $env:GITHUB_WORKSPACE "build\projects\Test Apps $($env:BC_SQL_PILOT_COUNTRY) Trial$($env:BC_SQL_PILOT_TRIAL)" - Get-ChildItem $project -Filter 'TestResults*.xml' -File | Copy-Item -Destination $output + $resultLocations = @{ + 'project-root' = $project + 'buildartifacts' = Join-Path $project '.buildartifacts' + } + foreach ($label in $resultLocations.Keys) { + if (Test-Path $resultLocations[$label]) { + $destination = Join-Path $output "clean-results\$label" + New-Item -ItemType Directory -Path $destination -Force | Out-Null + Get-ChildItem $resultLocations[$label] -Filter 'TestResults*.xml' -File | + Copy-Item -Destination $destination + } + } } finally { if (Test-BcContainer -containerName $ownership.container) { Remove-BcContainer -containerName $ownership.container diff --git a/build/scripts/SqlResetPilot/Lifecycle.psm1 b/build/scripts/SqlResetPilot/Lifecycle.psm1 index 8301134c2b2..f59cf3d3036 100644 --- a/build/scripts/SqlResetPilot/Lifecycle.psm1 +++ b/build/scripts/SqlResetPilot/Lifecycle.psm1 @@ -123,7 +123,7 @@ function Select-SqlPilotPrefix { $ids = @(139700,139702,139703,139706,139725,139726,139732,139739,139742,139745,139802,139803,139806,139826,139832,139854,139972,139780,148315,148318,148343) if ($WorkItems.Count -lt $ids.Count) { throw 'Discovery returned an incomplete pilot prefix.' } for ($i = 0; $i -lt $ids.Count; $i++) { - if ([int]$WorkItems[$i].CodeunitId -ne $ids[$i]) { throw "Original W1 discovery order differs at position $i." } + if ([int]$WorkItems[$i].CodeunitId -ne $ids[$i]) { throw "Pinned 21-codeunit discovery order differs at position $i; do not reorder or reduce the cohort." } } @($WorkItems | Select-Object -First $ids.Count) } diff --git a/build/scripts/SqlResetPilot/README.md b/build/scripts/SqlResetPilot/README.md index 8bda7a0d945..e28eb693e3f 100644 --- a/build/scripts/SqlResetPilot/README.md +++ b/build/scripts/SqlResetPilot/README.md @@ -55,7 +55,9 @@ The cohort remains the same ordered **21-codeunit prefix**, ending with Expense Users (148315), Capabilities (148318), and Activity Log (148343). The previous W1 control yielded **253 cases: 234 passed, 19 skipped**, across 20 codeunits with cases. This is a quick cohort, **not all API paths or the full suite**. Warmup -counts must be reported separately. Discovery-order drift fails closed. +counts must be reported separately. Discovery-order drift fails closed for both +countries; a DE-specific prefix requires evidence from the original DE baseline, +must include all three Expense codeunits, and must be recorded in full. ## Isolation, evidence and interpretation @@ -69,6 +71,9 @@ registered name before removing only that owned disposable container. Artifacts preserve provenance, shell/runtime identity, worklist, SQL reset timeline, warmup outcome/results, worker output, prefix JUnit and EVTX. +Finalization collects project-root `TestResults*.xml` into `clean-results/project-root` +and `.buildartifacts/TestResults*.xml` into `clean-results/buildartifacts`, preserving +both without overwrites. Warmup results remain separately labeled under `warmup`. `companies-probes.jsonl` records sanitized URI, tenant, generation, HTTP status, UTC start/end, duration and safe client/server correlation IDs **for every attempt**. Separate outcome records distinguish `first-try-success`, `recovered` and `failed`, From fbd7ec46c636ee5f9d940cb81e5abbc1df90f055 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Wed, 7 Oct 2026 15:32:23 +0200 Subject: [PATCH 20/22] Add isolated method-exclusion and evidence-gated SQL retry experiments AB#646383. Diagnostic only: twenty manual W1/DE trials, maximum five parallel; immutable source and runtime pins. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .github/workflows/CICD.yaml | 18 +- .github/workflows/PullRequestHandler.yaml | 2 +- build/scripts/ParallelTestExecution.psm1 | 124 +++++- build/scripts/SqlApiTestRetry.psm1 | 201 +++++++++ .../SqlResetPilot/CompaniesProbe.Tests.ps1 | 12 + .../SqlResetPilot/ContainerPreflight.ps1 | 6 +- .../scripts/SqlResetPilot/Finalize.Tests.ps1 | 9 +- build/scripts/SqlResetPilot/Finalize.ps1 | 5 +- build/scripts/SqlResetPilot/Lifecycle.psm1 | 7 +- .../scripts/SqlResetPilot/Preflight.Tests.ps1 | 10 +- build/scripts/SqlResetPilot/Prepare.ps1 | 14 +- build/scripts/SqlResetPilot/README.md | 174 ++++---- build/scripts/SqlResetPilot/TwoArm.Tests.ps1 | 164 ++++++++ build/scripts/tests/SqlApiTestRetry.Test.ps1 | 385 ++++++++++++++++++ 14 files changed, 1017 insertions(+), 114 deletions(-) create mode 100644 build/scripts/SqlApiTestRetry.psm1 create mode 100644 build/scripts/SqlResetPilot/TwoArm.Tests.ps1 create mode 100644 build/scripts/tests/SqlApiTestRetry.Test.ps1 diff --git a/.github/workflows/CICD.yaml b/.github/workflows/CICD.yaml index 3db952d48d6..0c7873932b6 100644 --- a/.github/workflows/CICD.yaml +++ b/.github/workflows/CICD.yaml @@ -1,4 +1,4 @@ -name: 'SQL API warmup experiment - DO NOT MERGE' +name: 'SQL API two-arm experiment - DO NOT MERGE' on: workflow_dispatch: @@ -11,13 +11,14 @@ permissions: jobs: Pilot: - if: github.ref == 'refs/heads/features/646383-sql-api-warmup-experiment' && github.run_attempt == 1 + if: github.ref == 'refs/heads/features/646383-sql-api-two-arm-experiment' && github.run_attempt == 1 strategy: fail-fast: false max-parallel: 5 matrix: country: [W1, DE] trial: [1, 2, 3, 4, 5] + experiment: [A, B] runs-on: GitHub-BCAppsPL timeout-minutes: 120 defaults: @@ -27,6 +28,7 @@ jobs: ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} BC_SQL_PILOT_ARM: control + BC_SQL_API_EXPERIMENT: ${{ matrix.experiment }} BC_SQL_PILOT_COUNTRY: ${{ matrix.country }} BC_SQL_PILOT_TRIAL: ${{ matrix.trial }} BuildMode: IntegrationTests @@ -62,7 +64,7 @@ jobs: uses: microsoft/AL-Go/Actions/ReadSettings@91b96c2b294be6f823277dafe6f03350abfb9d23 with: shell: pwsh - project: build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }} + project: build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }} buildMode: IntegrationTests - name: Read normal disposable CI license and package secrets @@ -79,7 +81,7 @@ jobs: Secrets: '${{ steps.secrets.outputs.Secrets }}' with: shell: pwsh - project: build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }} + project: build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }} buildMode: IntegrationTests artifact: ${{ steps.packages.outputs.artifact }} installAppsJson: ${{ steps.packages.outputs.apps }} @@ -94,11 +96,11 @@ jobs: if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: sql-api-warmup-${{ matrix.country }}-${{ matrix.trial }}-${{ github.run_id }} + name: sql-api-two-arm-${{ matrix.experiment }}-${{ matrix.country }}-${{ matrix.trial }}-${{ github.run_id }} retention-days: 7 if-no-files-found: error path: | sql-reset-pilot-output/** - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}/TestResults*.xml - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}/ContainerEventLog.evtx - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}/BuildOutput.txt + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/TestResults*.xml + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/ContainerEventLog.evtx + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/BuildOutput.txt diff --git a/.github/workflows/PullRequestHandler.yaml b/.github/workflows/PullRequestHandler.yaml index 142d13c1cfd..599330b4467 100644 --- a/.github/workflows/PullRequestHandler.yaml +++ b/.github/workflows/PullRequestHandler.yaml @@ -35,7 +35,7 @@ jobs: Initialization: needs: [ PregateCheck ] - if: (!failure() && !cancelled()) && github.head_ref != 'features/646383-sql-api-warmup-experiment' + if: (!failure() && !cancelled()) && github.head_ref != 'features/646383-sql-api-warmup-experiment' && github.head_ref != 'features/646383-sql-api-two-arm-experiment' runs-on: [ windows-latest ] outputs: projects: ${{ steps.determineProjectsToBuild.outputs.ProjectsJson }} diff --git a/build/scripts/ParallelTestExecution.psm1 b/build/scripts/ParallelTestExecution.psm1 index a5a532273bc..6e43f662b06 100644 --- a/build/scripts/ParallelTestExecution.psm1 +++ b/build/scripts/ParallelTestExecution.psm1 @@ -13,6 +13,7 @@ if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) { } } Import-Module (Join-Path $PSScriptRoot "ALAppBuild.psm1" -Resolve) +Import-Module (Join-Path $PSScriptRoot 'SqlApiTestRetry.psm1' -Resolve) if ($env:BC_SQL_PILOT_ARM) { Import-Module (Join-Path $PSScriptRoot 'SqlResetPilot\Lifecycle.psm1') } @@ -47,9 +48,26 @@ function Get-DisabledTestsForApp { } } + if ((Test-SqlApiExperiment) -and $env:BC_SQL_API_EXPERIMENT -eq 'A') { + $disabledTests += [PSCustomObject]@{ codeunitId = 148318; method = 'CapabilitiesProjectsEnabledViaAPI' } + } return @($disabledTests) } +function Test-SqlApiExperiment { + return ($env:GITHUB_REPOSITORY -eq 'microsoft/BCApps' -and + $env:GITHUB_REF -eq 'refs/heads/features/646383-sql-api-two-arm-experiment' -and + $env:GITHUB_EVENT_NAME -eq 'workflow_dispatch' -and $env:GITHUB_RUN_ATTEMPT -eq '1' -and + $env:BC_SQL_PILOT_ARM -eq 'control' -and $env:BC_SQL_API_EXPERIMENT -in @('A', 'B')) +} + +function Get-TestJobResultFileName { + param([string]$Path, [string]$Suffix) + $dir = [System.IO.Path]::GetDirectoryName($Path) + if (-not $dir) { $dir = '.' } + Join-Path $dir "$([System.IO.Path]::GetFileNameWithoutExtension($Path))-$Suffix$([System.IO.Path]::GetExtension($Path))" +} + function Get-ParametersForCommand { param( [Parameter(Mandatory=$true)] @@ -646,14 +664,57 @@ function Receive-TestJobResult { $output = $sb.ToString() $outcome = 'Passed' + $sqlRetryEvidence = $null if ($Job.State -eq 'Failed' -or $Job.State -eq 'Stopped') { if (-not $env:BC_SQL_PILOT_ARM -and (Test-TransientTestFailure $output) -and -not $Retried.ContainsKey($Entry.appName)) { $outcome = 'Transient' } else { $outcome = 'Failed' + if ($Job.State -eq 'Failed' -and -not $Retried.ContainsKey($Entry.appName) -and + $Entry.PSObject.Properties['SqlRetryContext'] -and $Entry.SqlRetryContext -and + $env:BC_SQL_API_EXPERIMENT -eq 'B' -and (Test-SqlApiExperiment)) { + $sqlRetryEvidence = Get-SqlApiTestRetryEvidence -Context $Entry.SqlRetryContext + if ($sqlRetryEvidence) { $outcome = 'SqlPoolRetry' } + } } } + if ($Entry.PSObject.Properties['SqlRetryContext'] -and $Entry.SqlRetryContext) { + $context = $Entry.SqlRetryContext + if (-not (Test-SqlApiExperimentSelection -Context $context -Experiment $env:BC_SQL_API_EXPERIMENT)) { + $outcome = 'Failed' + $sqlRetryEvidence = $null + } + if ($Entry.SqlRetryExpectedTests.Count -gt 0 -and $outcome -eq 'Passed') { + if (-not (Test-SqlApiRetryResult -Context $context -ExpectedTests $Entry.SqlRetryExpectedTests)) { + $outcome = 'Failed' + } + } + $attemptDirectory = Join-Path $env:BC_SQL_PILOT_OUTPUT "test-attempts\$($context.Tenant)-$($context.CodeunitId)-$($context.Attempt)" + New-Item -ItemType Directory -Path $attemptDirectory -Force -ErrorAction Stop | Out-Null + foreach ($key in $context.ResultFiles.Keys) { + $path = $context.ResultFiles[$key] + if (Test-Path -LiteralPath $path) { + Copy-Item -LiteralPath $path -Destination (Join-Path $attemptDirectory $key) -ErrorAction Stop + } + } + @{ + codeunitId = $context.CodeunitId; tenant = $context.Tenant; attempt = $context.Attempt + outcome = $outcome; jobState = [string]$Job.State; startedUtc = $context.StartedUtc.ToString('o') + completedUtc = [datetime]::UtcNow.ToString('o'); experiment = $env:BC_SQL_API_EXPERIMENT + retryEligible = [bool]$sqlRetryEvidence; expectedCases = $context.TestCount + } | ConvertTo-Json | Set-Content (Join-Path $attemptDirectory 'outcome.json') -ErrorAction Stop + if ($context.Attempt -eq 2 -and $outcome -eq 'Passed') { + foreach ($key in $context.ResultFiles.Keys) { + $first = Join-Path $env:BC_SQL_PILOT_OUTPUT "test-attempts\$($context.Tenant)-$($context.CodeunitId)-1\$key" + if (-not (Test-Path $first) -or -not (Test-Path $context.ResultFiles[$key])) { + throw 'Cannot merge retry without both original and retry result files.' + } + Merge-TestResultFiles -targetFile $context.FinalResultFiles[$key] ` + -sourceFiles @($first, $context.ResultFiles[$key]) + } + } + } if ($env:BC_SQL_PILOT_ARM) { $output | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT "worker-$($Job.Id).log") -Encoding UTF8 } @@ -664,6 +725,7 @@ function Receive-TestJobResult { AppName = $Entry.appName Tenant = $Entry.tenant JobState = $Job.State + SqlRetryEvidence = $sqlRetryEvidence } } @@ -755,13 +817,44 @@ function Start-RequiredDisabledDispatch { } } + $sqlRetryContext = $null + $sqlRetryExpectedTests = @() + $fileSuffix = $TenantInfo.Id + if (Test-SqlApiExperiment) { + if (-not $Parameters.JUnitResultFileName) { throw 'Two-arm experiment requires JUnit evidence.' } + $attempt = 1 + $finalFiles = @{} + if ($State.sqlRetryEvidence.ContainsKey($WorkItem.Key)) { + $attempt = 2 + $fileSuffix = "$($TenantInfo.Id)-sqlretry-$($WorkItem.CodeunitId)" + $sqlRetryExpectedTests = @($State.sqlRetryEvidence[$WorkItem.Key].TestCases) + # BCH ReRun selects only failing methods. This experiment retries the entire codeunit. + $codeunitParameters.Remove('ReRun') | Out-Null + } + $files = @{} + foreach ($key in $appendKeys.Keys) { + if ($Parameters.ContainsKey($key) -and $Parameters[$key]) { + $files[$key] = Get-TestJobResultFileName $Parameters[$key] $fileSuffix + $finalFiles[$key] = Get-TestJobResultFileName $Parameters[$key] $TenantInfo.Id + if ($attempt -eq 2 -and (Test-Path $files[$key])) { throw 'Retry result file already exists.' } + } + } + $sqlRetryContext = [PSCustomObject]@{ + ContainerName = $Parameters.containerName; Tenant = $TenantInfo.Id + CodeunitId = $WorkItem.CodeunitId; TestCount = $WorkItem.TestCount + StartedUtc = [datetime]::UtcNow; ResultFiles = $files; FinalResultFiles = $finalFiles + Attempt = $attempt; OutputDirectory = $env:BC_SQL_PILOT_OUTPUT + } + } $job = Start-TestJob -parameters $codeunitParameters -tenant $TenantInfo.Id -scriptPath $ScriptPath ` - -testType $TestType -skipAutomaticDisabledPass + -testType $TestType -skipAutomaticDisabledPass -fileSuffix $fileSuffix $State.jobs = @($State.jobs) + @( [PSCustomObject]@{ jobId = $job.Id tenant = $TenantInfo.Id appName = $WorkItem.Key + SqlRetryContext = $sqlRetryContext + SqlRetryExpectedTests = $sqlRetryExpectedTests } ) Start-Sleep -Seconds 1 @@ -798,6 +891,7 @@ function Invoke-RequiredDisabledTestExecution { transient = @() retried = @{} retryTenant = @{} + sqlRetryEvidence = @{} } $pending = @($WorkItems) @@ -857,7 +951,7 @@ function Invoke-RequiredDisabledTestExecution { -ScriptPath $ScriptPath -TestType $TestType -State $state -Verb $dispatch.Verb } $null = Wait-ForAllTestJobs -state $state - if ($env:BC_SQL_PILOT_ARM -and ($state.hasFailures -or $state.transient.Count -gt 0)) { + if ($env:BC_SQL_PILOT_ARM -and $state.hasFailures) { $state.hasFailures = $true break } @@ -885,6 +979,16 @@ function Register-TestJobOutcome { $State ) + if ($Result.Outcome -eq 'SqlPoolRetry') { + if (-not (Test-SqlApiExperiment) -or $env:BC_SQL_API_EXPERIMENT -ne 'B' -or + -not $Result.SqlRetryEvidence -or $State.retried.ContainsKey($Result.AppName)) { + $State.hasFailures = $true + return + } + $State.sqlRetryEvidence[$Result.AppName] = $Result.SqlRetryEvidence + $State.transient = @($State.transient) + @([PSCustomObject]@{ Key = $Result.AppName; Tenant = $Result.Tenant }) + return + } if ($env:BC_SQL_PILOT_ARM -and $Result.Outcome -in @('Failed', 'Transient')) { $State.hasFailures = $true return @@ -1104,6 +1208,9 @@ function Wait-ForAllTestJobs { $result = Receive-TestJobResult -Entry $entry -Job $pendingJob -Retried $state.retried Register-TestJobOutcome -Result $result -State $state + } elseif (Test-SqlApiExperiment) { + $state.hasFailures = $true + Write-Host "::warning::Expected experiment worker job $($entry.jobId) is missing; failing closed." } } # All jobs in $state.jobs have been received and removed; clear the list so any later @@ -1510,6 +1617,19 @@ function Invoke-ParallelTestExecution { if ($env:BC_SQL_PILOT_ARM) { Merge-TenantTestResults -parameters $parameters -tenants $tenants -workItems $requiredDisabledWorkItems -rerunSuffixes @() + if ((Test-SqlApiExperiment) -and (Test-Path $parameters.JUnitResultFileName)) { + [xml]$finalXml = Get-Content $parameters.JUnitResultFileName -Raw + $finalCases = @($finalXml.SelectNodes('/testsuites/testsuite/testcase')) + @{ + total = $finalCases.Count + failed = @($finalCases | Where-Object { $_.SelectSingleNode('failure | error') }).Count + skipped = @($finalCases | Where-Object { $_.SelectSingleNode('skipped') }).Count + passed = @($finalCases | Where-Object { -not $_.SelectSingleNode('failure | error | skipped') }).Count + suites = $finalXml.SelectNodes('/testsuites/testsuite').Count + experiment = $env:BC_SQL_API_EXPERIMENT + firstAttemptsAndRetries = 'See test-attempts; final XML replaces only validated recovered CUs.' + } | ConvertTo-Json | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT 'final-counts.json') + } $state.finalResult = -not $state.hasFailures $state.completed = $true $state | ConvertTo-Json -Depth 5 | Set-Content $stateFile -Force diff --git a/build/scripts/SqlApiTestRetry.psm1 b/build/scripts/SqlApiTestRetry.psm1 new file mode 100644 index 00000000000..c548b5cd693 --- /dev/null +++ b/build/scripts/SqlApiTestRetry.psm1 @@ -0,0 +1,201 @@ +<# +.SYNOPSIS + Correlates clean-codeunit HTTP failures with NST SQL-pool exceptions. +.DESCRIPTION + This is a manual diagnostic experiment, not a generic HTTP retry or a pool-lifecycle fix. + Every failed case must have a request CorrelationId matching an NST ClientSessionId, + a NullReferenceException in AcquireSqlConnectionFromPool, and the observed OData + metadata stack, within this dispatch's time window. Missing evidence fails closed. + The scheduler permits only one retry, restores the tenant first, and reruns the entire + codeunit. Original results and matching events are retained separately from final results. + The scheduler enables this only on the exact manual experiment branch, arm B, attempt one. + SqlApiRetryEvidence JSON files under the diagnostic output are uploaded with raw outcomes, + including on successful runs, so recovered failures remain visible without duplicating cases. +#> +function Get-SqlApiRetrySuite { + param($Context) + + [xml]$document = Get-Content -LiteralPath $Context.ResultFiles.JUnitResultFileName -Raw -ErrorAction Stop + $suites = @($document.SelectNodes('/testsuites/testsuite') | Where-Object { + ($_.GetAttribute('name') -split ' ', 2)[0] -eq [string]$Context.CodeunitId + }) + if ($suites.Count -ne 1) { + throw "Expected exactly one JUnit suite for codeunit $($Context.CodeunitId), found $($suites.Count)." + } + return ,$suites[0] +} + +function Get-SqlApiRetryEvents { + param( + [string]$ContainerName, + [datetime]$StartedUtc, + [datetime]$FinishedUtc + ) + + return @(Invoke-ScriptInBcContainer -containerName $ContainerName -scriptblock { + param($start, $end) + try { + $events = @(Get-WinEvent -FilterHashtable @{ + LogName = 'Application' + ProviderName = 'MicrosoftDynamicsNavServer*' + Id = 701 + StartTime = [datetime]::Parse($start).ToUniversalTime() + EndTime = [datetime]::Parse($end).ToUniversalTime() + } -MaxEvents 1000 -ErrorAction Stop) + } catch { + if ($_.FullyQualifiedErrorId -like 'NoMatchingEventsFound*') { + return + } + throw + } + foreach ($eventRecord in $events) { + $eventRecord.ToXml() + } + } -argumentList $StartedUtc.ToString('o'), $FinishedUtc.ToString('o')) +} + +<# +.SYNOPSIS + Persists original JUnit and correlated NST events before authorizing one clean-codeunit retry. +#> +function Get-SqlApiTestRetryEvidence { + param($Context) + + try { + $suite = Get-SqlApiRetrySuite -Context $Context + $cases = @($suite.SelectNodes('testcase')) + if ($cases.Count -ne $Context.TestCount -or $cases.Count -eq 0) { + throw "Incomplete JUnit codeunit: expected $($Context.TestCount) cases, found $($cases.Count)." + } + $failures = @($suite.SelectNodes('testcase/failure')) + if ($failures.Count -eq 0 -or $suite.SelectNodes('.//error').Count -gt 0) { + return + } + $requestIds = @() + foreach ($failure in $failures) { + $message = $failure.GetAttribute('message') + if ($message -notmatch 'GET request failed\. Response code is 500 \(InternalServerError\)' -or + $message -notmatch 'Object reference not set to an instance of an object\.' -or + $message -notmatch 'CorrelationId:\s*([0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})(?![0-9a-f-])') { + return + } + $requestIds += $Matches[1] + } + # Refuse malformed/partial results rather than overlooking an unrepresented failure. + foreach ($case in $cases) { + if ($case.GetAttribute('result') -eq 'Fail' -and $case.SelectNodes('failure').Count -ne 1) { + return + } + } + + $finishedUtc = [datetime]::UtcNow + $events = @(Get-SqlApiRetryEvents -ContainerName $Context.ContainerName ` + -StartedUtc $Context.StartedUtc -FinishedUtc $finishedUtc) + $matchedEvents = @() + foreach ($requestId in $requestIds) { + $matchesForRequest = @() + foreach ($eventText in $events) { + [xml]$eventXml = $eventText + $ns = [System.Xml.XmlNamespaceManager]::new($eventXml.NameTable) + $ns.AddNamespace('e', 'http://schemas.microsoft.com/win/2004/08/events/event') + $system = $eventXml.SelectSingleNode('/e:Event/e:System', $ns) + $provider = $system.SelectSingleNode('e:Provider', $ns).GetAttribute('Name') + $eventId = $system.SelectSingleNode('e:EventID', $ns).InnerText + $time = [datetime]::Parse($system.SelectSingleNode('e:TimeCreated', $ns).GetAttribute('SystemTime')).ToUniversalTime() + $text = ($eventXml.SelectNodes('/e:Event/e:EventData/e:Data', $ns) | + ForEach-Object { $_.InnerText }) -join "`n" + if ($provider -match '^MicrosoftDynamicsNavServer\$' -and $eventId -eq '701' -and + $time -ge $Context.StartedUtc -and $time -le $finishedUtc -and + $text -match "(?m)^ClientSessionId:\s*$([regex]::Escape($requestId))\s*$" -and + $text -match 'RootException: NullReferenceException' -and + $text -match 'at Microsoft\.Dynamics\.Nav\.Runtime\.NavSqlConnectionScope\.AcquireSqlConnectionFromPool\(' -and + $text -match 'at Microsoft\.Dynamics\.Nav\.XmlMetadata\.MetadataProvider\.GetRelativeHelpUrl\(' -and + $text -match 'at Microsoft\.Dynamics\.Nav\.Service\.OData\.V4\.PageDataProvider\.GetNavRecordDataAsync\(') { + $matchesForRequest += [string]$eventText + } + } + if ($matchesForRequest.Count -eq 0) { + Write-Host "::warning::SQL API retry not eligible: no matching NST event for request '$requestId'." + return + } + $matchedEvents += $matchesForRequest + } + $evidence = [PSCustomObject]@{ + ContainerName = $Context.ContainerName + Tenant = $Context.Tenant + CodeunitId = $Context.CodeunitId + StartedUtc = $Context.StartedUtc.ToString('o') + FinishedUtc = $finishedUtc.ToString('o') + RequestIds = @($requestIds | Select-Object -Unique) + OriginalSuite = $suite.OuterXml + Events = @($matchedEvents | Select-Object -Unique) + TestCases = @($cases | ForEach-Object { + [PSCustomObject]@{ Name = $_.GetAttribute('name'); Skipped = ($null -ne $_.SelectSingleNode('skipped')) } + }) + } + $folder = Join-Path ([System.IO.Path]::GetDirectoryName($Context.ResultFiles.JUnitResultFileName)) 'SqlApiRetryEvidence' + if ($Context.PSObject.Properties['OutputDirectory'] -and $Context.OutputDirectory) { + $folder = Join-Path $Context.OutputDirectory 'SqlApiRetryEvidence' + } + $null = New-Item -ItemType Directory -Path $folder -Force -ErrorAction Stop + $evidenceFile = Join-Path $folder "$($Context.Tenant)-$($Context.CodeunitId)-$([guid]::NewGuid().ToString('N')).json" + $evidence | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $evidenceFile -Encoding utf8 -ErrorAction Stop + Write-Host "::warning::SQL API pool failure confirmed for codeunit $($Context.CodeunitId) on '$($Context.Tenant)'; requests: $($evidence.RequestIds -join ', '). Retrying once after a tenant reset. Original evidence: $evidenceFile" + return $evidence + } catch { + Write-Host "::warning::SQL API retry evidence unavailable; preserving test failure: $($_.Exception.Message)" + return + } +} + +<# +.SYNOPSIS + Requires a SQL retry to preserve the original test names and skip status without failures. +#> +function Test-SqlApiRetryResult { + param( + $Context, + [array]$ExpectedTests + ) + + try { + $suite = Get-SqlApiRetrySuite -Context $Context + $cases = @($suite.SelectNodes('testcase')) + if ($cases.Count -ne $ExpectedTests.Count -or $suite.SelectNodes('.//failure | .//error').Count -gt 0) { + throw 'The retry did not produce a complete, passing codeunit.' + } + foreach ($expected in $ExpectedTests) { + $actual = @($cases | Where-Object { $_.GetAttribute('name') -ceq $expected.Name }) + if ($actual.Count -ne 1 -or + ($null -ne $actual[0].SelectSingleNode('skipped')) -ne $expected.Skipped -or + $actual[0].GetAttribute('result') -eq 'Fail') { + throw "Retry result missing, duplicated, failed or changed skip status for '$($expected.Name)'." + } + } + return $true + } catch { + Write-Host "::warning::SQL API retry remains failed: $($_.Exception.Message)" + return $false + } +} + +function Test-SqlApiExperimentSelection { + param($Context, [ValidateSet('A', 'B')][string]$Experiment) + if ([string]$Context.CodeunitId -ne '148318') { return $true } + try { + $suite = Get-SqlApiRetrySuite -Context $Context + $target = @($suite.SelectNodes('testcase') | Where-Object { + ($_.GetAttribute('name') -split '\.')[-1] -ceq 'CapabilitiesProjectsEnabledViaAPI' + }) + if ($target.Count -ne 1) { throw 'Expected exactly one target method in Capabilities results.' } + if (($null -ne $target[0].SelectSingleNode('skipped')) -ne ($Experiment -eq 'A')) { + throw 'Target method skip status differs from the experiment arm.' + } + return $true + } catch { + Write-Host "::warning::Experiment selection verification failed: $($_.Exception.Message)" + return $false + } +} + +Export-ModuleMember -Function Get-SqlApiTestRetryEvidence, Test-SqlApiRetryResult, Test-SqlApiExperimentSelection diff --git a/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 b/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 index 9ede785e853..d2ed24c07d6 100644 --- a/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 +++ b/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 @@ -44,6 +44,18 @@ Describe 'External authenticated companies probe' { } Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 0 } + It 'uses exactly one attempt in experiment arm even for HTTP500' -ForEach @( + @{ Arm = 'A' }, @{ Arm = 'B' } + ) { + $old = $env:BC_SQL_API_EXPERIMENT + try { + $env:BC_SQL_API_EXPERIMENT = $Arm + Mock Invoke-WebRequest -ModuleName Lifecycle { @{ StatusCode = 500; Headers = @{}; Content = '' } } + { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw '*after 1 attempt*' + Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly + Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 0 + } finally { $env:BC_SQL_API_EXPERIMENT = $old } + } It 'bounds transient HTTP to three attempts and records each failure safely' -ForEach @( @{ Code = 500 }, @{ Code = 502 }, @{ Code = 503 }, @{ Code = 504 } ) { diff --git a/build/scripts/SqlResetPilot/ContainerPreflight.ps1 b/build/scripts/SqlResetPilot/ContainerPreflight.ps1 index 25e895d1da9..4ab3eebc661 100644 --- a/build/scripts/SqlResetPilot/ContainerPreflight.ps1 +++ b/build/scripts/SqlResetPilot/ContainerPreflight.ps1 @@ -1,13 +1,14 @@ param([hashtable]$Parameters) $ErrorActionPreference = 'Stop' -if ($env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-warmup-experiment' -or +if ($env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-two-arm-experiment' -or + $env:BC_SQL_API_EXPERIMENT -notin @('A', 'B') -or $env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or $env:BC_SQL_PILOT_ARM -ne 'control' -or -not $env:BC_SQL_PILOT_OUTPUT -or $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^[1-5]$' -or $env:GITHUB_RUN_ID -notmatch '^\d{1,20}$') { throw 'Refusing SQL pilot outside its disposable manually dispatched CI job.' } -$expectedName = "bcbuildprojectsTestApps$($env:BC_SQL_PILOT_COUNTRY)Trial$($env:BC_SQL_PILOT_TRIAL)$($env:GITHUB_RUN_ID)" +$expectedName = "bcbuildprojectsTestApps$($env:BC_SQL_PILOT_COUNTRY)Trial$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)$($env:GITHUB_RUN_ID)" if ($Parameters.ContainerName -ne $expectedName -or (Test-BcContainer -containerName $expectedName)) { throw 'Pilot container name is unexpected or already exists. No replacement is authorized.' } @@ -30,6 +31,7 @@ if ($Parameters.platformArtifactUrl -ne 'https://bcinsider-fvh2ekdjecfjd6gk.b02. @{ container = $expectedName; runId = $env:GITHUB_RUN_ID; arm = $env:BC_SQL_PILOT_ARM country = $env:BC_SQL_PILOT_COUNTRY; trial = $env:BC_SQL_PILOT_TRIAL + experiment = $env:BC_SQL_API_EXPERIMENT helperVersion = $helperVersion; image = $Parameters.useGenericImage imageGenericTag = '1.0.2.128'; platform = '30.0.55665.0' hostOS = [Environment]::OSVersion.Version.ToString() diff --git a/build/scripts/SqlResetPilot/Finalize.Tests.ps1 b/build/scripts/SqlResetPilot/Finalize.Tests.ps1 index 72502044d1c..412ac9ac889 100644 --- a/build/scripts/SqlResetPilot/Finalize.Tests.ps1 +++ b/build/scripts/SqlResetPilot/Finalize.Tests.ps1 @@ -10,15 +10,17 @@ Describe 'SQL pilot final cleanup ownership' { $script:oldHelper = $env:BcContainerHelperPath $script:oldCountry = $env:BC_SQL_PILOT_COUNTRY $script:oldTrial = $env:BC_SQL_PILOT_TRIAL + $script:oldExperiment = $env:BC_SQL_API_EXPERIMENT $env:GITHUB_WORKSPACE = $PSScriptRoot $env:GITHUB_RUN_ID = '123' $env:BC_SQL_PILOT_ARM = 'control' $env:BC_SQL_PILOT_COUNTRY = 'DE' $env:BC_SQL_PILOT_TRIAL = '2' + $env:BC_SQL_API_EXPERIMENT = 'B' $env:BcContainerHelperPath = Join-Path $PSScriptRoot 'BcContainerHelper.ps1' Mock Import-Module {} Mock Test-Path { $true } - Mock Get-Content { '{"runId":"123","arm":"control","country":"DE","trial":"2","container":"bcbuildprojectsTestAppsDETrial2123"}' } + Mock Get-Content { '{"runId":"123","arm":"control","country":"DE","trial":"2","experiment":"B","container":"bcbuildprojectsTestAppsDETrial2B123"}' } Mock Get-ChildItem {} Mock New-Item {} Mock Copy-Item {} @@ -35,12 +37,13 @@ Describe 'SQL pilot final cleanup ownership' { $env:BcContainerHelperPath = $script:oldHelper $env:BC_SQL_PILOT_COUNTRY = $script:oldCountry $env:BC_SQL_PILOT_TRIAL = $script:oldTrial + $env:BC_SQL_API_EXPERIMENT = $script:oldExperiment Remove-Variable sqlPilotTestContainerExists -Scope Global } It 'exports before disposing only the registered container' { & (Join-Path $PSScriptRoot 'Finalize.ps1') Should -Invoke Get-BcContainerEventLog -Times 1 -Exactly -Scope It - Should -Invoke Remove-BcContainer -Times 1 -Exactly -Scope It -ParameterFilter { $containerName -eq 'bcbuildprojectsTestAppsDETrial2123' } + Should -Invoke Remove-BcContainer -Times 1 -Exactly -Scope It -ParameterFilter { $containerName -eq 'bcbuildprojectsTestAppsDETrial2B123' } } It 'still tears down if exporting fails and keeps that failure visible' { Mock Get-BcContainerEventLog { throw 'event export failed' } @@ -51,7 +54,7 @@ Describe 'SQL pilot final cleanup ownership' { Mock Get-ChildItem { [PSCustomObject]@{ FullName = 'TestResults.xml' } } & (Join-Path $PSScriptRoot 'Finalize.ps1') Should -Invoke Get-ChildItem -Times 1 -Exactly -Scope It -ParameterFilter { - $Path -like '*Test Apps DE Trial2\.buildartifacts' -and $Filter -eq 'TestResults*.xml' -and $File + $Path -like '*Test Apps DE Trial2B\.buildartifacts' -and $Filter -eq 'TestResults*.xml' -and $File } Should -Invoke Copy-Item -Times 1 -Exactly -Scope It -ParameterFilter { $Destination -like '*clean-results\buildartifacts' diff --git a/build/scripts/SqlResetPilot/Finalize.ps1 b/build/scripts/SqlResetPilot/Finalize.ps1 index fc5c99261a7..ec5e15bf5d0 100644 --- a/build/scripts/SqlResetPilot/Finalize.ps1 +++ b/build/scripts/SqlResetPilot/Finalize.ps1 @@ -7,9 +7,10 @@ if (-not (Test-Path $ownershipPath)) { } $ownership = Get-Content $ownershipPath -Raw | ConvertFrom-Json if ($ownership.runId -ne $env:GITHUB_RUN_ID -or $ownership.arm -ne $env:BC_SQL_PILOT_ARM -or + $env:BC_SQL_API_EXPERIMENT -notin @('A', 'B') -or $ownership.experiment -ne $env:BC_SQL_API_EXPERIMENT -or $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^[1-5]$' -or $ownership.country -ne $env:BC_SQL_PILOT_COUNTRY -or $ownership.trial -ne $env:BC_SQL_PILOT_TRIAL -or - $ownership.container -ne "bcbuildprojectsTestApps$($env:BC_SQL_PILOT_COUNTRY)Trial$($env:BC_SQL_PILOT_TRIAL)$($env:GITHUB_RUN_ID)") { + $ownership.container -ne "bcbuildprojectsTestApps$($env:BC_SQL_PILOT_COUNTRY)Trial$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)$($env:GITHUB_RUN_ID)") { throw 'Cleanup ownership mismatch.' } Import-Module (Join-Path (Split-Path $env:BcContainerHelperPath -Parent) 'BcContainerHelper.psm1') -ErrorAction Stop @@ -18,7 +19,7 @@ try { $events = Get-BcContainerEventLog -containerName $ownership.container -doNotOpen Copy-Item $events (Join-Path $output 'final-container.evtx') } - $project = Join-Path $env:GITHUB_WORKSPACE "build\projects\Test Apps $($env:BC_SQL_PILOT_COUNTRY) Trial$($env:BC_SQL_PILOT_TRIAL)" + $project = Join-Path $env:GITHUB_WORKSPACE "build\projects\Test Apps $($env:BC_SQL_PILOT_COUNTRY) Trial$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)" $resultLocations = @{ 'project-root' = $project 'buildartifacts' = Join-Path $project '.buildartifacts' diff --git a/build/scripts/SqlResetPilot/Lifecycle.psm1 b/build/scripts/SqlResetPilot/Lifecycle.psm1 index f59cf3d3036..db975202184 100644 --- a/build/scripts/SqlResetPilot/Lifecycle.psm1 +++ b/build/scripts/SqlResetPilot/Lifecycle.psm1 @@ -150,13 +150,14 @@ function Invoke-SqlPilotCompaniesProbe { $script:pilot.Workers[$Tenant].Generation -lt 1) { throw 'Companies probe requires an owned, freshly restored worker.' } + $maximumAttempts = if ($env:BC_SQL_API_EXPERIMENT -in @('A', 'B')) { 1 } else { 3 } $trace = Join-Path $script:pilot.OutputDirectory 'companies-probes.jsonl' $observation = @{ tenant = $Tenant; generation = $script:pilot.Workers[$Tenant].Generation startedUtc = [DateTime]::UtcNow.ToString('o'); status = $null; passed = $false clientRequestId = [guid]::NewGuid().ToString(); serverRequestId = $null uri = $null; retries = 0; attempts = 0; recordType = 'outcome' - outcome = 'failed'; recovered = $false; maximumAttempts = 3 + outcome = 'failed'; recovered = $false; maximumAttempts = $maximumAttempts } $clock = [Diagnostics.Stopwatch]::StartNew() try { @@ -173,7 +174,7 @@ function Invoke-SqlPilotCompaniesProbe { $uri = [UriBuilder]::new($scheme, $ip, [int]$config.ODataServicesPort, "$($config.ServerInstance)/api/v2.0/companies", "?tenant=$Tenant").Uri.AbsoluteUri $observation.uri = $uri - for ($attemptNumber = 1; $attemptNumber -le 3; $attemptNumber++) { + for ($attemptNumber = 1; $attemptNumber -le $maximumAttempts; $attemptNumber++) { $attempt = @{ recordType = 'attempt'; probeId = $observation.clientRequestId tenant = $Tenant; generation = $observation.generation; uri = $uri @@ -243,7 +244,7 @@ function Invoke-SqlPilotCompaniesProbe { $observation.outcome = if ($observation.recovered) { 'recovered' } else { 'first-try-success' } break } - if (-not $attempt.retryable -or $attemptNumber -eq 3) { break } + if (-not $attempt.retryable -or $attemptNumber -eq $maximumAttempts) { break } Start-Sleep -Seconds 2 } if (-not $observation.passed) { throw 'Companies probe exhausted eligible attempts or encountered a permanent failure.' } diff --git a/build/scripts/SqlResetPilot/Preflight.Tests.ps1 b/build/scripts/SqlResetPilot/Preflight.Tests.ps1 index 110f3d90abf..0b8e806fce1 100644 --- a/build/scripts/SqlResetPilot/Preflight.Tests.ps1 +++ b/build/scripts/SqlResetPilot/Preflight.Tests.ps1 @@ -6,10 +6,11 @@ Describe 'Experiment country and trial container ownership' { BeforeEach { $script:saved = @{} foreach ($name in @('GITHUB_REF','GITHUB_EVENT_NAME','GITHUB_RUN_ATTEMPT','GITHUB_RUN_ID', - 'BC_SQL_PILOT_ARM','BC_SQL_PILOT_COUNTRY','BC_SQL_PILOT_TRIAL','BC_SQL_PILOT_OUTPUT','Settings')) { + 'BC_SQL_PILOT_ARM','BC_SQL_PILOT_COUNTRY','BC_SQL_PILOT_TRIAL','BC_SQL_PILOT_OUTPUT','BC_SQL_API_EXPERIMENT','Settings')) { $script:saved[$name] = [Environment]::GetEnvironmentVariable($name) } - $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-warmup-experiment' + $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-two-arm-experiment' + $env:BC_SQL_API_EXPERIMENT = 'B' $env:GITHUB_EVENT_NAME = 'workflow_dispatch' $env:GITHUB_RUN_ATTEMPT = '1' $env:GITHUB_RUN_ID = '123' @@ -22,7 +23,7 @@ Describe 'Experiment country and trial container ownership' { companyName = 'My Company'; enableCleanTestCodeunitExecution = $true; enableTaskScheduler = $false } | ConvertTo-Json $script:parameters = @{ - ContainerName = 'bcbuildprojectsTestAppsDETrial2123' + ContainerName = 'bcbuildprojectsTestAppsDETrial2B123' platformArtifactUrl = 'https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/platform/30.0.55665.0/platform' } Mock Get-Module { { '6.1.19-preview2811389' } } -ParameterFilter { $Name -eq 'BcContainerHelper' } @@ -36,7 +37,8 @@ Describe 'Experiment country and trial container ownership' { $record = Get-Content (Join-Path $TestDrive 'container-ownership.json') -Raw | ConvertFrom-Json $record.country | Should -Be 'DE' $record.trial | Should -Be '2' - $record.container | Should -Be 'bcbuildprojectsTestAppsDETrial2123' + $record.container | Should -Be 'bcbuildprojectsTestAppsDETrial2B123' + $record.experiment | Should -Be 'B' $script:parameters.useGenericImage | Should -Be 'mcr.microsoft.com/businesscentral@sha256:c899d12093ad7bbdbfd08ccc0e6294e0f98c682c7e35db4ecfbca345fb068492' } It 'refuses a preexisting container rather than replacing it' { diff --git a/build/scripts/SqlResetPilot/Prepare.ps1 b/build/scripts/SqlResetPilot/Prepare.ps1 index 3d72bc76f33..ccdeac21bca 100644 --- a/build/scripts/SqlResetPilot/Prepare.ps1 +++ b/build/scripts/SqlResetPilot/Prepare.ps1 @@ -1,7 +1,8 @@ $ErrorActionPreference = 'Stop' $base = 'c4953dceffe02a017adad34973e1955017bf5d20' if ($env:GITHUB_REPOSITORY -ne 'microsoft/BCApps' -or - $env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-warmup-experiment' -or + $env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-two-arm-experiment' -or + $env:BC_SQL_API_EXPERIMENT -notin @('A', 'B') -or $env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or $env:BC_SQL_PILOT_ARM -ne 'control' -or $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^[1-5]$' -or $env:GITHUB_RUN_ID -notmatch '^\d{1,20}$') { @@ -13,7 +14,7 @@ git diff --exit-code $base HEAD -- src if ($LASTEXITCODE -ne 0) { throw 'AL/package source differs from the pinned compiled PR2 source.' } $project = "build\projects\Test Apps $($env:BC_SQL_PILOT_COUNTRY)" -$trialProject = "$project Trial$($env:BC_SQL_PILOT_TRIAL)" +$trialProject = "$project Trial$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)" if (Test-Path $trialProject) { throw 'Trial project must not already exist.' } # AL-Go derives its container name from the project path. Preserve wrapper depth and country settings. Copy-Item $project $trialProject -Recurse @@ -62,10 +63,13 @@ foreach ($artifact in $artifacts) { } @{ experimentHead = $env:GITHUB_SHA; run = $env:GITHUB_RUN_ID; arm = $env:BC_SQL_PILOT_ARM + experiment = $env:BC_SQL_API_EXPERIMENT; baseline = '8a7ebc7d665c0a812c2b3b4902c63df570959954' country = $env:BC_SQL_PILOT_COUNTRY; trial = $env:BC_SQL_PILOT_TRIAL; project = $trialProject - warmup = 'original-first-app-before-clean-lane'; companiesProbe = 'per-restored-worker-readiness-retries' - companiesProbeMaximumAttempts = 3; companiesProbeTimeoutSeconds = 20; companiesProbeRetryDelaySeconds = 2 - warmupRetries = 0; testRetries = 0; schedulerRetries = 0; ciRetries = 0 + warmup = 'original-first-app-before-clean-lane'; companiesProbe = 'per-restored-worker-single-attempt' + companiesProbeMaximumAttempts = 1; companiesProbeTimeoutSeconds = 20; companiesProbeRetryDelaySeconds = 0 + additionalDisabledTest = $(if ($env:BC_SQL_API_EXPERIMENT -eq 'A') { '148318.CapabilitiesProjectsEnabledViaAPI' } else { $null }) + warmupRetries = 0; maximumEvidenceGatedRetriesPerCodeunit = $(if ($env:BC_SQL_API_EXPERIMENT -eq 'B') { 1 } else { 0 }) + genericTestRetries = 0; schedulerRetries = 0; ciRetries = 0 budgetMinutes = 120; startedUtc = [DateTime]::UtcNow.ToString('o') artifacts = $manifest } | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $output 'provenance.json') -Encoding UTF8 diff --git a/build/scripts/SqlResetPilot/README.md b/build/scripts/SqlResetPilot/README.md index e28eb693e3f..7a4a52293ec 100644 --- a/build/scripts/SqlResetPilot/README.md +++ b/build/scripts/SqlResetPilot/README.md @@ -1,93 +1,99 @@ -# API warmup experiment — diagnostic only, do not merge +# SQL API two-arm diagnostic — AB#646383 — DO NOT MERGE -One manual `CICD.yaml` dispatch on `features/646383-sql-api-warmup-experiment` -runs **W1 and DE, five independent trials each**. Maximum parallelism is five; -each trial is capped at 120 minutes (1,200 runner-minutes total maximum). -Only attempt one is allowed. No automatic rerun, replacement dispatch, PR, -merge queue, local/shared NST operation, or fresh-database-name treatment. +One manual `CICD.yaml` dispatch on `features/646383-sql-api-two-arm-experiment` +runs **A/B × W1/DE × five trials = 20 independent disposable containers**. +Maximum parallelism is **five globally**, timeout 120 minutes per cell (2,400 +runner-minutes maximum). Exact branch and run-attempt-one guards reject reruns. +The exact experiment PR head is excluded from automatic PR Initialization; +there is no full-country pipeline, job retry, replacement run, or local NST work. -## Fixed baseline +## Shared immutable baseline -The branch starts from SQL reset pilot `d7dd4dddcd49da397ee42de1677d1b7d44622149`. -AL source remains exactly PR2 `c4953dceffe02a017adad34973e1955017bf5d20`. -Both countries consume their own unchanged compiled packages from run -`37372848860`; this does not claim country package binaries are identical. -`Prepare.ps1` verifies the source ancestry/diff, artifact run/head/digests, -downloaded archive SHA256 and per-package hashes. Missing or expired artifacts -fail closed; there is no moving-build fallback. +Both arms start at warmup head `8a7ebc7d665c0a812c2b3b4902c63df570959954`. +AL/package source remains **c4953dceffe02a017adad34973e1955017bf5d20**. +`Prepare.ps1` verifies ancestry, no source diff, exact source run **37372848860**, +artifact metadata/digests, ZIP SHA256 and package hashes. Missing/expired +artifacts fail closed; there is no rebuild or moving-package fallback. | Country | App artifact | Test artifact | | --- | --- | --- | | W1 | 11374170356 | 11374145359 | | DE | 11376415128 | 11375174896 | -The pinned runtime is NST **30.0.55665.0**, application **30.0.55683.0**, -BCH **6.1.19-preview2811389**, AL-Go -`91b96c2b294be6f823277dafe6f03350abfb9d23`, and generic image -`sha256:c899d12093ad7bbdbfd08ccc0e6294e0f98c682c7e35db4ecfbca345fb068492`. -The image matches the pilot; historic source logs did not record its digest. -PowerShell 7, four tenants, original password-file authentication and company -`My Company` are retained. - -## Experiment order and failure policy - -1. Original discovery runs on a worker, which is then restored from default. -2. The immutable pristine template is frozen **before** app warmup fixtures. -3. The original `Invoke-WarmupDispatch` runs the ordinary first test app alone - on default, with the original `SkipAutomaticDisabledPass` behavior. - Results live under `sql-reset-pilot-output/warmup`, outside prefix coverage. - Failure, a transient queue, a skipped warmup or an incomplete job stops the trial. -4. For each of seven clean batches, restore all three workers under their - original `tenant2/3/4` database names, then probe all three, then dispatch. -5. Each probe makes **up to three external host HTTP GET attempts** to - `/BC/api/v2.0/companies?tenant=tenantN`, using the actual server instance, - port/IP and existing container credential. Basic auth is sent preemptively. - Each attempt has 20-second connection and read timeouts; eligible attempts are separated by two - seconds. Only HTTP 500/502/503/504 and transport connection/timeout failures - may retry. Authentication failures, other HTTP statuses (including all 4xx), - invalid JSON and missing expected company are terminal. Redirects and hidden - HTTP-client retries remain disabled. Exhaustion stops the trial before dispatch. -6. Test failures drain the current three-worker batch and stop later batches. - No test, transient, warmup or restore retries are enabled. Existing bounded - mount-state polling and ordinary scheduler waits are unchanged. - -The cohort remains the same ordered **21-codeunit prefix**, ending with Expense -Users (148315), Capabilities (148318), and Activity Log (148343). The previous W1 -control yielded **253 cases: 234 passed, 19 skipped**, across 20 codeunits with -cases. This is a quick cohort, **not all API paths or the full suite**. Warmup -counts must be reported separately. Discovery-order drift fails closed for both -countries; a DE-specific prefix requires evidence from the original DE baseline, -must include all three Expense codeunits, and must be recorded in full. - -## Isolation, evidence and interpretation - -At runtime only, the selected project descriptor is copied to -`build/projects/Test Apps Trial` at the same directory depth. -AL-Go derives a unique container name: -`bcbuildprojectsTestAppsTrial`. -Country settings and wrapper targets stay unchanged. Preflight refuses an -existing container; final cleanup checks country, trial, run, arm and exact -registered name before removing only that owned disposable container. - -Artifacts preserve provenance, shell/runtime identity, worklist, SQL reset -timeline, warmup outcome/results, worker output, prefix JUnit and EVTX. -Finalization collects project-root `TestResults*.xml` into `clean-results/project-root` -and `.buildartifacts/TestResults*.xml` into `clean-results/buildartifacts`, preserving -both without overwrites. Warmup results remain separately labeled under `warmup`. -`companies-probes.jsonl` records sanitized URI, tenant, generation, HTTP status, -UTC start/end, duration and safe client/server correlation IDs **for every attempt**. -Separate outcome records distinguish `first-try-success`, `recovered` and `failed`, -and record actual attempt/retry counts. Report this treatment as **companies-probe -retry allowed**, not blanket no retries; warmup, AL tests, scheduler and CI still -have zero retries. The earlier run `37608087700` used the previous no-probe-retry -commit and cannot acquire this change in flight. No password, -authorization header, response body or raw HTTP exception is added to logs. -Runner loss or cancellation can prevent final export/cleanup and is inconclusive. - -This experiment combines two warmup interventions; it cannot distinguish their -individual effects. A pass/non-reproduction is not proof of a production fix or -SQL-name reuse safety. Report every trial, including preflight/probe failures, -timeouts and missing evidence. Do not launch another matrix without authorization. - -Local validation requires Pester 5+ (validated with 6.1.0), not an NST: -`Invoke-Pester .\build\scripts\SqlResetPilot` +NST **30.0.55665.0**, application **30.0.55683.0**, BCH +**6.1.19-preview2811389**, AL-Go `91b96c2b294be6f823277dafe6f03350abfb9d23`, +generic image `sha256:c899d12093ad7bbdbfd08ccc0e6294e0f98c682c7e35db4ecfbca345fb068492`, +PowerShell 7, four tenants, existing password-file authentication and `My Company` +are unchanged. No auth/provider changes or AL assertion changes. + +Both arms freeze the pristine template before ordinary first-app warmup, then +restore each clean batch under the original tenant database names, probe every +restored worker, and only then dispatch. Companies probes use **one attempt**, +20-second connect/read timeouts, no hidden HTTP retries or redirects. This +disables the optional three-attempt readiness behavior added after the original +warmup run `37608087700` at `fcc1776c6b165199dd66e4227675b1cc31da7a8f`. +The newer baseline's nested `.buildartifacts` collection is retained. + +## Treatments + +**A — exclude one method:** the existing `Get-DisabledTestsForApp` runner config +adds only `{codeunitId:148318, method:CapabilitiesProjectsEnabledViaAPI}`. +No source or package is edited; the whole Capabilities codeunit stays selected. +All existing baseline disabled tests remain unchanged. No test retries. + +**B — all baseline-enabled tests remain enabled:** no additional disabled entry. +At most **one whole-codeunit retry**, and only after every failed JUnit case has: + +* The observed GET HTTP500/null-reference response and a valid CorrelationId. +* An NST Application event 701 from `MicrosoftDynamicsNavServer$...`, obtained + from this container during this dispatch's UTC time window. +* Matching `ClientSessionId`, `RootException: NullReferenceException`, and frames + `NavSqlConnectionScope.AcquireSqlConnectionFromPool`, + `MetadataProvider.GetRelativeHelpUrl`, and `PageDataProvider.GetNavRecordDataAsync`. + +Generic HTTP500, generic NRE, real/mixed assertions, stale/unmatched events, +partial XML, stopped jobs, missing evidence or persistence errors never qualify. +No automatic polling/wait for evidence. This conservative additional observed +metadata-stack requirement can reject other SQL-pool shapes. + +The entire current batch drains before a retry. The retry uses the same worker +after another pristine-template restore and fresh single-attempt companies probe. +`ReRun` is explicitly removed: BCH must run **every method**, not just failures. +Retry files use a unique suffix. Recovery requires the exact original test names, +count and skip states with no failures/errors. Only verified recovery replaces +that CU in the final merged result; original and retry files remain separately +preserved. A failed/incomplete retry stays failed and cannot retry again. +Any unrelated terminal batch failure stops later batches (including queued retries). + +## Cohort and interpretation + +The exact ordered 21-CU prefix remains: +139700,139702,139703,139706,139725,139726,139732,139739,139742,139745, +139802,139803,139806,139826,139832,139854,139972,139780,148315,148318,148343. +Order drift fails closed. This is not the full API suite. +Previous complete W1 coverage was 253 cases (234 passing, 19 skipped). +A should turn only the target method into an additional skip; B retains baseline +coverage. Actual counts and any missing/incomplete coverage must be reported per +trial; do not count recovered first failures as first-attempt passes. + +Containers derive from `Test Apps Trial` at unchanged +descriptor depth. Preflight refuses existing containers; cleanup verifies +country/trial/experiment/run/exact name before deleting only the owned container. + +Artifacts preserve provenance, pins, ordered worklist, SQL reset timeline, +single-attempt probe records, worker logs, EVTX, separately labeled warmup, +project-root and nested final XML, plus: +* `test-attempts/--1/`: original result snapshot and outcome. +* `test-attempts/--2/`: retry result snapshot and outcome. +* `SqlApiRetryEvidence/`: original CU XML and correlated NST events. + +Per-CU snapshots may include previously completed suites on that worker; count +only the outcome's CU when aggregating first attempts. Final merged XML contains +one result per CU, with recovery replacing rather than duplicating cases. +Runner loss/cancellation can prevent final export and is **inconclusive**. +Passes are non-reproductions, not proof of a production fix or SQL-name reuse safety. + +Local validation uses Pester 5+ and PSScriptAnalyzer, with all live container +operations mocked. Tests: `build\scripts\SqlResetPilot`, +`build\scripts\tests\SqlApiTestRetry.Test.ps1`, and +`build\scripts\tests\ParallelTestExecution.Test.ps1`. diff --git a/build/scripts/SqlResetPilot/TwoArm.Tests.ps1 b/build/scripts/SqlResetPilot/TwoArm.Tests.ps1 new file mode 100644 index 00000000000..4e2a19610a1 --- /dev/null +++ b/build/scripts/SqlResetPilot/TwoArm.Tests.ps1 @@ -0,0 +1,164 @@ +$discoveryArm = $env:BC_SQL_PILOT_ARM +try { + $env:BC_SQL_PILOT_ARM = 'control' + Import-Module (Join-Path $PSScriptRoot '..\ParallelTestExecution.psm1') -Force +} finally { + $env:BC_SQL_PILOT_ARM = $discoveryArm +} +Describe 'Exact manual two-arm experiment isolation' { + BeforeEach { + $script:saved = @{} + foreach ($key in @('GITHUB_REPOSITORY','GITHUB_REF','GITHUB_EVENT_NAME','GITHUB_RUN_ATTEMPT', + 'BC_SQL_PILOT_ARM','BC_SQL_API_EXPERIMENT','BC_SQL_PILOT_OUTPUT')) { + $script:saved[$key] = [Environment]::GetEnvironmentVariable($key) + } + $env:GITHUB_REPOSITORY = 'microsoft/BCApps' + $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-two-arm-experiment' + $env:GITHUB_EVENT_NAME = 'workflow_dispatch' + $env:GITHUB_RUN_ATTEMPT = '1' + $env:BC_SQL_PILOT_ARM = 'control' + $env:BC_SQL_API_EXPERIMENT = 'B' + $env:BC_SQL_PILOT_OUTPUT = $TestDrive + } + AfterEach { + foreach ($key in $script:saved.Keys) { [Environment]::SetEnvironmentVariable($key, $script:saved[$key]) } + } + InModuleScope ParallelTestExecution { + Context 'Runner and scheduler hooks' { + BeforeEach { + Mock Get-ChildItem { @() } + Mock Start-Sleep {} + Mock Start-TestJob { [PSCustomObject]@{ Id = 1 } } + $script:state = [PSCustomObject]@{ + jobs = @(); hasFailures = $false; transient = @(); retried = @{}; retryTenant = @{}; sqlRetryEvidence = @{} + } + $script:item = [PSCustomObject]@{ + Key = 'Test App::148318'; AppName = 'Test App'; AppId = 'id' + CodeunitId = '148318'; CodeunitName = 'Capabilities'; TestCount = 3 + } + $script:tenant = [PSCustomObject]@{ Id = 'tenant2'; DatabaseName = 'tenant2' } + $script:parameters = @{ + containerName = 'owned'; JUnitResultFileName = (Join-Path $TestDrive 'TestResults.xml') + credential = [PSCredential]::Empty; companyName = 'My Company' + } + } + It 'adds exactly the one named method only in A and does not mutate baseline config' { + $env:BC_SQL_API_EXPERIMENT = 'A' + $disabled = @(Get-DisabledTestsForApp 'Test App') + $disabled.Count | Should -Be 1 + $disabled[0].codeunitId | Should -Be 148318 + $disabled[0].method | Should -BeExactly 'CapabilitiesProjectsEnabledViaAPI' + $env:BC_SQL_API_EXPERIMENT = 'B' + @(Get-DisabledTestsForApp 'Test App').Count | Should -Be 0 + } + It 'cannot activate on ' -ForEach @( + @{ Kind = 'PR'; Key = 'GITHUB_EVENT_NAME'; Value = 'pull_request' } + @{ Kind = 'another branch'; Key = 'GITHUB_REF'; Value = 'refs/heads/main' } + @{ Kind = 'rerun'; Key = 'GITHUB_RUN_ATTEMPT'; Value = '2' } + @{ Kind = 'other repository'; Key = 'GITHUB_REPOSITORY'; Value = 'other/BCApps' } + @{ Kind = 'fresh SQL'; Key = 'BC_SQL_PILOT_ARM'; Value = 'fresh' } + ) { + [Environment]::SetEnvironmentVariable($Key, $Value) + Test-SqlApiExperiment | Should -BeFalse + } + It 'queues only a confirmed B failure and never a generic transient' { + $result = [PSCustomObject]@{ Outcome = 'SqlPoolRetry'; AppName = $script:item.Key; Tenant = 'tenant2'; SqlRetryEvidence = @{ TestCases = @('one') } } + Register-TestJobOutcome $result $script:state + $script:state.transient.Count | Should -Be 1 + $script:state.hasFailures | Should -BeFalse + $script:state.retried[$script:item.Key] = $true + Register-TestJobOutcome $result $script:state + $script:state.transient.Count | Should -Be 1 + $script:state.hasFailures | Should -BeTrue + } + It 'never queues SQL retry in A' { + $env:BC_SQL_API_EXPERIMENT = 'A' + Register-TestJobOutcome ([PSCustomObject]@{ Outcome = 'SqlPoolRetry' }) $script:state + $script:state.hasFailures | Should -BeTrue + $script:state.transient.Count | Should -Be 0 + } + It 'retries the full CU without BCH ReRun and writes to a separate result file' { + $script:state.sqlRetryEvidence[$script:item.Key] = @{ TestCases = @(@{ Name = 'one'; Skipped = $false }) } + Start-RequiredDisabledDispatch $script:parameters $script:item $script:tenant 'runner.ps1' 'IntegrationTest' $script:state 'Re-dispatching' + Should -Invoke Start-TestJob -Times 1 -Exactly -ParameterFilter { + -not $parameters.ContainsKey('ReRun') -and $parameters.testCodeunit -eq '148318' -and + $fileSuffix -eq 'tenant2-sqlretry-148318' -and $skipAutomaticDisabledPass + } + $script:state.jobs[0].SqlRetryContext.Attempt | Should -Be 2 + $script:state.jobs[0].SqlRetryContext.ResultFiles.JUnitResultFileName | Should -Match 'tenant2-sqlretry-148318' + } + It 'restores and probes again before a once-only retry and drains failures' { + $script:order = [Collections.Generic.List[string]]::new() + $script:waits = 0 + Mock Reset-BcTestTenant { $script:order.Add('restore') } + Mock Invoke-SqlPilotCompaniesProbe { $script:order.Add('probe') } + Mock Start-RequiredDisabledDispatch { $script:order.Add("dispatch:$Verb") } + Mock Wait-ForAllTestJobs { + $script:waits++ + if ($script:waits -eq 1) { + $state.sqlRetryEvidence[$script:item.Key] = @{ TestCases = @('one') } + $state.transient = @([PSCustomObject]@{ Key = $script:item.Key; Tenant = 'tenant2' }) + } else { $state.hasFailures = $true } + } + Invoke-RequiredDisabledTestExecution $script:parameters @($script:item) @($script:tenant) ` + 'default-test-template' 'runner.ps1' 'IntegrationTest' | Should -BeFalse + ($script:order -join ',') | Should -Be 'restore,probe,dispatch:Dispatching,restore,probe,dispatch:Re-dispatching' + $script:waits | Should -Be 2 + } + It 'preserves first failure and separate full retry, merging only validated recovery' { + Mock Receive-Job {} -RemoveParameterType Job + Mock Remove-Job {} -RemoveParameterType Job + Mock Get-SqlApiTestRetryEvidence { @{ TestCases = @(@{ Name = 'CapabilitiesProjectsEnabledViaAPI'; Skipped = $false }) } } + $originalPath = Join-Path $TestDrive 'TestResults-tenant2.xml' + $retryPath = Join-Path $TestDrive 'TestResults-tenant2-sqlretry-148318.xml' + $original = '' + $passed = '' + $original | Set-Content $originalPath + $context = [PSCustomObject]@{ + CodeunitId = 148318; Tenant = 'tenant2'; StartedUtc = [datetime]::UtcNow + TestCount = 1; Attempt = 1; ResultFiles = @{ JUnitResultFileName = $originalPath } + FinalResultFiles = @{ JUnitResultFileName = $originalPath }; OutputDirectory = $TestDrive + } + $entry = [PSCustomObject]@{ appName = $script:item.Key; tenant = 'tenant2'; SqlRetryContext = $context; SqlRetryExpectedTests = @() } + $firstResult = Receive-TestJobResult $entry @{ Id = 91; State = 'Failed' } @{} + $firstResult.Outcome | Should -Be 'SqlPoolRetry' + $firstSaved = Join-Path $TestDrive 'test-attempts\tenant2-148318-1\JUnitResultFileName' + (Get-Content $firstSaved -Raw).Trim() | Should -Be $original + $passed | Set-Content $retryPath + $context.Attempt = 2 + $context.ResultFiles = @{ JUnitResultFileName = $retryPath } + $entry.SqlRetryExpectedTests = $firstResult.SqlRetryEvidence.TestCases + (Receive-TestJobResult $entry @{ Id = 92; State = 'Completed' } @{ $script:item.Key = $true }).Outcome | Should -Be 'Passed' + ([xml](Get-Content $originalPath -Raw)).SelectNodes('//failure').Count | Should -Be 0 + (Get-Content $firstSaved -Raw).Trim() | Should -Be $original + Test-Path (Join-Path $TestDrive 'test-attempts\tenant2-148318-2\JUnitResultFileName') | Should -BeTrue + Should -Invoke Get-SqlApiTestRetryEvidence -Times 1 -Exactly + } + It 'does not classify stopped or already-retried failures as eligible' { + Mock Receive-Job {} -RemoveParameterType Job + Mock Remove-Job {} -RemoveParameterType Job + Mock Get-SqlApiTestRetryEvidence { throw 'Evidence classifier must not be called.' } + Mock Test-SqlApiExperimentSelection { $true } + $context = [PSCustomObject]@{ + CodeunitId = 148318; Tenant = 'tenant2'; StartedUtc = [datetime]::UtcNow + TestCount = 1; Attempt = 2; ResultFiles = @{} + } + $entry = [PSCustomObject]@{ appName = $script:item.Key; tenant = 'tenant2'; SqlRetryContext = $context; SqlRetryExpectedTests = @() } + foreach ($jobState in @('Stopped','Failed')) { + (Receive-TestJobResult $entry @{ Id = 91; State = $jobState } @{ $script:item.Key = $true }).Outcome | Should -Be 'Failed' + } + Should -Invoke Get-SqlApiTestRetryEvidence -Times 0 + } + It 'rejects silently reenabled A and inherited B exclusions in actual JUnit' { + $path = Join-Path $TestDrive 'selection.xml' + $context = [PSCustomObject]@{ CodeunitId = 148318; ResultFiles = @{ JUnitResultFileName = $path } } + '' | Set-Content $path + Test-SqlApiExperimentSelection $context 'A' | Should -BeTrue + Test-SqlApiExperimentSelection $context 'B' | Should -BeFalse + '' | Set-Content $path + Test-SqlApiExperimentSelection $context 'A' | Should -BeFalse + Test-SqlApiExperimentSelection $context 'B' | Should -BeTrue + } + } + } +} diff --git a/build/scripts/tests/SqlApiTestRetry.Test.ps1 b/build/scripts/tests/SqlApiTestRetry.Test.ps1 new file mode 100644 index 00000000000..d1c7f33f53f --- /dev/null +++ b/build/scripts/tests/SqlApiTestRetry.Test.ps1 @@ -0,0 +1,385 @@ +Describe 'SQL API bounded retry evidence and result validation' { + BeforeAll { + Import-Module (Join-Path $PSScriptRoot '..\SqlApiTestRetry.psm1') -Force + $script:createdContainerStub = -not (Get-Command Invoke-ScriptInBcContainer -ListImported -ErrorAction SilentlyContinue) + if ($script:createdContainerStub) { + function global:Invoke-ScriptInBcContainer { + param([string]$containerName, [scriptblock]$scriptblock, [object[]]$argumentList) + $null = $containerName, $scriptblock, $argumentList + throw 'Container execution must be mocked.' + } + } + function New-FailureMessage { + param([string]$CorrelationId = $script:requestId) + "Assert.Fail failed. GET request failed. Response code is 500 (InternalServerError), expected code is 200. Error code: Unknown. Error message: Object reference not set to an instance of an object. CorrelationId: $CorrelationId." + } + function New-CaseXml { + param([string]$Name, [string]$Message, [switch]$Skipped, [string]$Result = 'Pass') + $body = '' + if ($Message) { + $body = '' + $Result = 'Fail' + } + if ($Skipped) { $body += '' } + '' + $body + '' + } + function Write-Suite { + param([string[]]$Cases = $script:originalCases) + '' + + ($Cases -join '') + '' | + Set-Content -LiteralPath $script:context.ResultFiles.JUnitResultFileName -Encoding utf8 + } + function New-NstEvent { + param( + [string]$CorrelationId = $script:requestId, + [datetime]$Time = $script:eventTime, + [string]$Provider = 'MicrosoftDynamicsNavServer$BC', + [string]$EventId = '701', + [string]$Stack = "RootException: NullReferenceException`nat Microsoft.Dynamics.Nav.Runtime.NavSqlConnectionScope.AcquireSqlConnectionFromPool(...)`nat Microsoft.Dynamics.Nav.XmlMetadata.MetadataProvider.GetRelativeHelpUrl(...)`nat Microsoft.Dynamics.Nav.Service.OData.V4.PageDataProvider.GetNavRecordDataAsync(...)" + ) + $text = [System.Security.SecurityElement]::Escape("ClientSessionId: $CorrelationId`n$Stack") + '' + $EventId + '' + $text + '' + } + } + + AfterAll { + if ($script:createdContainerStub) { Remove-Item function:global:Invoke-ScriptInBcContainer } + } + + BeforeEach { + $script:fixtureRoot = Join-Path $PSScriptRoot ('.sql-api-retry-fixture-' + [guid]::NewGuid().ToString('N')) + $null = New-Item -ItemType Directory -Path $script:fixtureRoot + $script:requestId = '11111111-2222-4333-8444-555555555555' + $script:eventTime = [datetime]::UtcNow.AddSeconds(-10) + $script:context = [pscustomobject]@{ + ContainerName = 'synthetic-container' + Tenant = 'synthetic-tenant' + CodeunitId = 70001 + TestCount = 3 + StartedUtc = $script:eventTime.AddSeconds(-10) + ResultFiles = [pscustomobject]@{ JUnitResultFileName = Join-Path $script:fixtureRoot 'results.xml' } + } + $script:expectedTests = @( + [pscustomobject]@{ Name = 'Synthetic API Codeunit.Read Item'; Skipped = $false } + [pscustomobject]@{ Name = 'Synthetic API Codeunit.Read Customer'; Skipped = $false } + [pscustomobject]@{ Name = 'Synthetic API Codeunit.Disabled Case'; Skipped = $true } + ) + $script:originalCases = @( + New-CaseXml -Name $script:expectedTests[0].Name -Message (New-FailureMessage) + New-CaseXml -Name $script:expectedTests[1].Name + New-CaseXml -Name $script:expectedTests[2].Name -Skipped + ) + $script:passingCases = @( + New-CaseXml -Name $script:expectedTests[0].Name + New-CaseXml -Name $script:expectedTests[1].Name + New-CaseXml -Name $script:expectedTests[2].Name -Skipped + ) + Write-Suite + Mock -ModuleName SqlApiTestRetry Write-Host {} + Mock -ModuleName SqlApiTestRetry Invoke-ScriptInBcContainer { throw 'Live container execution is forbidden.' } + } + + AfterEach { + Remove-Item -LiteralPath $script:fixtureRoot -Recurse -Force + } + + Context 'Evidence classification and preservation' { + BeforeEach { + $script:events = @(New-NstEvent) + Mock -ModuleName SqlApiTestRetry Get-SqlApiRetryEvents { $script:events } + } + + It 'recognizes the observed namespaced event and failure attribute shapes without changing results' { + $before = [System.IO.File]::ReadAllBytes($script:context.ResultFiles.JUnitResultFileName) + $evidence = Get-SqlApiTestRetryEvidence -Context $script:context + $evidence | Should -Not -BeNullOrEmpty + $evidence.RequestIds | Should -Be @($script:requestId) + $evidence.Events | Should -Be $script:events + $evidence.TestCases.Name | Should -Be $script:expectedTests.Name + $evidence.TestCases.Skipped | Should -Be @($false, $false, $true) + [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($script:context.ResultFiles.JUnitResultFileName)) | + Should -Be ([Convert]::ToBase64String($before)) + Should -Invoke -ModuleName SqlApiTestRetry Get-SqlApiRetryEvents -Times 1 -Exactly -ParameterFilter { + $ContainerName -eq 'synthetic-container' -and $StartedUtc -eq $script:context.StartedUtc -and + $FinishedUtc -ge $script:eventTime -and $FinishedUtc -le [datetime]::UtcNow + } + Should -Invoke -ModuleName SqlApiTestRetry Invoke-ScriptInBcContainer -Times 0 + } + + It 'persists the first full suite and events before the result file is replaced by the rerun' { + $original = ([xml](Get-Content -LiteralPath $script:context.ResultFiles.JUnitResultFileName -Raw)).testsuites.testsuite.OuterXml + $evidence = Get-SqlApiTestRetryEvidence -Context $script:context + $files = @(Get-ChildItem -LiteralPath (Join-Path $script:fixtureRoot 'SqlApiRetryEvidence') -Filter '*.json') + $files.Count | Should -Be 1 + $savedBeforeRerun = Get-Content -LiteralPath $files[0].FullName -Raw + ($savedBeforeRerun | ConvertFrom-Json).OriginalSuite | Should -Be $original + Write-Suite -Cases $script:passingCases + Test-SqlApiRetryResult -Context $script:context -ExpectedTests $evidence.TestCases | Should -BeTrue + Get-Content -LiteralPath $files[0].FullName -Raw | Should -Be $savedBeforeRerun + $persisted = $savedBeforeRerun | ConvertFrom-Json + $persisted.Events | Should -Be $script:events + $persisted.RequestIds | Should -Be @($script:requestId) + $persisted.Tenant | Should -Be 'synthetic-tenant' + $persisted.CodeunitId | Should -Be 70001 + $persisted.TestCases.Name | Should -Be $script:expectedTests.Name + } + + It 'rejects without querying events' -ForEach @( + @{ Kind = 'generic 500'; Message = 'Assert.Fail failed. GET request failed. Response code is 500 (InternalServerError), expected code is 200.' } + @{ Kind = 'authentication 401'; Message = 'Assert.Fail failed. GET request failed. Response code is 401 (Unauthorized), expected code is 200.' } + @{ Kind = 'real assertion'; Message = 'Assert.AreEqual failed. Expected quantity 5, actual quantity 4.' } + ) { + $script:originalCases[0] = New-CaseXml -Name $script:expectedTests[0].Name -Message $Message + Write-Suite + Get-SqlApiTestRetryEvidence -Context $script:context | Should -BeNullOrEmpty + Should -Invoke -ModuleName SqlApiTestRetry Get-SqlApiRetryEvents -Times 0 + Test-Path (Join-Path $script:fixtureRoot 'SqlApiRetryEvidence') | Should -BeFalse + } + + It 'rejects an eligible HTTP failure mixed with a genuine assertion failure' { + $script:originalCases[1] = New-CaseXml -Name $script:expectedTests[1].Name -Message 'Assert.AreEqual failed. Expected true, actual false.' + Write-Suite + Get-SqlApiTestRetryEvidence -Context $script:context | Should -BeNullOrEmpty + Should -Invoke -ModuleName SqlApiTestRetry Get-SqlApiRetryEvents -Times 0 + } + + It 'rejects 401 even when the null-reference text and correlation match' { + $message = (New-FailureMessage).Replace('500 (InternalServerError)', '401 (Unauthorized)') + $script:originalCases[0] = New-CaseXml -Name $script:expectedTests[0].Name -Message $message + Write-Suite + Get-SqlApiTestRetryEvidence -Context $script:context | Should -BeNullOrEmpty + Should -Invoke -ModuleName SqlApiTestRetry Get-SqlApiRetryEvents -Times 0 + } + + It 'requires matching events for every failed request' { + $secondRequest = '66666666-7777-4888-8999-aaaaaaaaaaaa' + $script:originalCases[1] = New-CaseXml -Name $script:expectedTests[1].Name -Message (New-FailureMessage -CorrelationId $secondRequest) + Write-Suite + Get-SqlApiTestRetryEvidence -Context $script:context | Should -BeNullOrEmpty + $script:events += New-NstEvent -CorrelationId $secondRequest + $evidence = Get-SqlApiTestRetryEvidence -Context $script:context + $evidence.RequestIds | Should -Be @($script:requestId, $secondRequest) + $evidence.Events.Count | Should -Be 2 + } + + It 'rejects correlation in the failure message' -ForEach @( + @{ Kind = 'missing'; Correlation = '' } + @{ Kind = 'malformed'; Correlation = 'not-a-guid' } + @{ Kind = 'overlong'; Correlation = '11111111-2222-4333-8444-5555555555556' } + ) { + $script:originalCases[0] = New-CaseXml -Name $script:expectedTests[0].Name -Message (New-FailureMessage -CorrelationId $Correlation) + Write-Suite + Get-SqlApiTestRetryEvidence -Context $script:context | Should -BeNullOrEmpty + Should -Invoke -ModuleName SqlApiTestRetry Get-SqlApiRetryEvents -Times 0 + } + + It 'rejects event evidence with ' -ForEach @( + @{ Kind = 'wrong correlation' } + @{ Kind = 'missing correlation' } + @{ Kind = 'stale time' } + @{ Kind = 'future time' } + @{ Kind = 'wrong provider' } + @{ Kind = 'wrong event ID' } + @{ Kind = 'wrong exception' } + @{ Kind = 'wrong pool frame' } + @{ Kind = 'wrong metadata frame' } + @{ Kind = 'wrong OData frame' } + ) { + switch ($Kind) { + 'wrong correlation' { $script:events = @(New-NstEvent -CorrelationId '66666666-7777-4888-8999-aaaaaaaaaaaa') } + 'missing correlation' { $script:events = @((New-NstEvent) -replace 'ClientSessionId:', 'OtherSessionId:') } + 'stale time' { $script:events = @(New-NstEvent -Time $script:context.StartedUtc.AddTicks(-1)) } + 'future time' { $script:events = @(New-NstEvent -Time ([datetime]::UtcNow.AddHours(1))) } + 'wrong provider' { $script:events = @(New-NstEvent -Provider 'UnrelatedProvider') } + 'wrong event ID' { $script:events = @(New-NstEvent -EventId '702') } + 'wrong exception' { $script:events = @((New-NstEvent) -replace 'NullReferenceException', 'InvalidOperationException') } + 'wrong pool frame' { $script:events = @((New-NstEvent) -replace 'AcquireSqlConnectionFromPool', 'OpenConnection') } + 'wrong metadata frame' { $script:events = @((New-NstEvent) -replace 'GetRelativeHelpUrl', 'ReadMetadata') } + 'wrong OData frame' { $script:events = @((New-NstEvent) -replace 'GetNavRecordDataAsync', 'ReadSomethingElse') } + } + Get-SqlApiTestRetryEvidence -Context $script:context | Should -BeNullOrEmpty + Test-Path (Join-Path $script:fixtureRoot 'SqlApiRetryEvidence') | Should -BeFalse + } + + It 'accepts an event exactly at the dispatch start boundary' { + $script:events = @(New-NstEvent -Time $script:context.StartedUtc) + Get-SqlApiTestRetryEvidence -Context $script:context | Should -Not -BeNullOrEmpty + } + + It 'fails closed with a warning for ' -ForEach @( + @{ Kind = 'missing result file' } + @{ Kind = 'malformed result XML' } + @{ Kind = 'wrong suite' } + @{ Kind = 'missing test count' } + @{ Kind = 'partial result count' } + @{ Kind = 'malformed event XML' } + @{ Kind = 'missing event system' } + @{ Kind = 'event collection access error' } + @{ Kind = 'evidence persistence failure' } + ) { + switch ($Kind) { + 'missing result file' { Remove-Item -LiteralPath $script:context.ResultFiles.JUnitResultFileName } + 'malformed result XML' { '' | Set-Content -LiteralPath $script:context.ResultFiles.JUnitResultFileName } + 'wrong suite' { + (Get-Content -LiteralPath $script:context.ResultFiles.JUnitResultFileName -Raw).Replace('70001', '70002') | + Set-Content -LiteralPath $script:context.ResultFiles.JUnitResultFileName + } + 'missing test count' { $script:context.PSObject.Properties.Remove('TestCount') } + 'partial result count' { $script:context.TestCount = 4 } + 'malformed event XML' { $script:events = @('') } + 'missing event system' { $script:events = @('') } + 'event collection access error' { Mock -ModuleName SqlApiTestRetry Get-SqlApiRetryEvents { throw 'Synthetic access denied' } } + 'evidence persistence failure' { Mock -ModuleName SqlApiTestRetry Set-Content { throw 'Synthetic write denied' } } + } + Get-SqlApiTestRetryEvidence -Context $script:context | Should -BeNullOrEmpty + Should -Invoke -ModuleName SqlApiTestRetry Write-Host -Times 1 -Exactly -ParameterFilter { + $Object -like '::warning::*evidence unavailable*preserving test failure*' + } + } + + It 'fails closed with a warning when event collection returns no events' { + $script:events = @() + Get-SqlApiTestRetryEvidence -Context $script:context | Should -BeNullOrEmpty + Should -Invoke -ModuleName SqlApiTestRetry Write-Host -Times 1 -Exactly -ParameterFilter { + $Object -like '::warning::*no matching NST event*' + } + } + + It 'does not retry an error element or an unrepresented failed case' -ForEach @( + @{ CaseXml = '' } + @{ CaseXml = '' } + ) { + $script:originalCases[1] = $CaseXml + Write-Suite + Get-SqlApiTestRetryEvidence -Context $script:context | Should -BeNullOrEmpty + Should -Invoke -ModuleName SqlApiTestRetry Get-SqlApiRetryEvents -Times 0 + } + + It 'does not retry an already passing suite' { + Write-Suite -Cases $script:passingCases + Get-SqlApiTestRetryEvidence -Context $script:context | Should -BeNullOrEmpty + Should -Invoke -ModuleName SqlApiTestRetry Get-SqlApiRetryEvents -Times 0 + } + } + + Context 'Rerun result shape' { + It 'accepts all original full names and skip states in any order without mutating results' { + Write-Suite -Cases @($script:passingCases[2], $script:passingCases[0], $script:passingCases[1]) + $before = Get-Content -LiteralPath $script:context.ResultFiles.JUnitResultFileName -Raw + Test-SqlApiRetryResult -Context $script:context -ExpectedTests $script:expectedTests | Should -BeTrue + Get-Content -LiteralPath $script:context.ResultFiles.JUnitResultFileName -Raw | Should -Be $before + } + + It 'rejects rerun with ' -ForEach @( + @{ Kind = 'new skipped case' } + @{ Kind = 'removed original skip' } + @{ Kind = 'empty suite' } + @{ Kind = 'duplicate testcase' } + @{ Kind = 'missing testcase' } + @{ Kind = 'extra testcase' } + @{ Kind = 'renamed full testcase' } + @{ Kind = 'empty testcase name' } + @{ Kind = 'changed case in name' } + @{ Kind = 'failure element' } + @{ Kind = 'error element' } + @{ Kind = 'failed result without failure element' } + @{ Kind = 'missing result file' } + @{ Kind = 'malformed result XML' } + ) { + switch ($Kind) { + 'new skipped case' { $script:passingCases[0] = New-CaseXml -Name $script:expectedTests[0].Name -Skipped } + 'removed original skip' { $script:passingCases[2] = New-CaseXml -Name $script:expectedTests[2].Name } + 'empty suite' { $script:passingCases = @() } + 'duplicate testcase' { $script:passingCases[1] = $script:passingCases[0] } + 'missing testcase' { $script:passingCases = @($script:passingCases[0], $script:passingCases[2]) } + 'extra testcase' { $script:passingCases += New-CaseXml -Name 'Synthetic API Codeunit.Extra Case' } + 'renamed full testcase' { $script:passingCases[0] = New-CaseXml -Name 'Read Item' } + 'empty testcase name' { $script:passingCases[0] = New-CaseXml -Name '' } + 'changed case in name' { $script:passingCases[0] = New-CaseXml -Name $script:expectedTests[0].Name.ToLowerInvariant() } + 'failure element' { $script:passingCases[0] = $script:originalCases[0] } + 'error element' { $script:passingCases[0] = $script:passingCases[0].Replace('', '') } + 'failed result without failure element' { $script:passingCases[0] = New-CaseXml -Name $script:expectedTests[0].Name -Result 'Fail' } + } + Write-Suite -Cases $script:passingCases + if ($Kind -eq 'missing result file') { Remove-Item -LiteralPath $script:context.ResultFiles.JUnitResultFileName } + if ($Kind -eq 'malformed result XML') { '' | Set-Content -LiteralPath $script:context.ResultFiles.JUnitResultFileName } + Test-SqlApiRetryResult -Context $script:context -ExpectedTests $script:expectedTests | Should -BeFalse + Should -Invoke -ModuleName SqlApiTestRetry Write-Host -Times 1 -Exactly -ParameterFilter { + $Object -like '::warning::SQL API retry remains failed:*' + } + } + } + + Context 'Bounded container event collection' { + BeforeEach { + Mock -ModuleName SqlApiTestRetry Invoke-ScriptInBcContainer { + param($containerName, $scriptblock, $argumentList) + $null = $containerName + & $scriptblock @argumentList + } + Mock -ModuleName SqlApiTestRetry Get-WinEvent { @() } + } + + It 'bounds the application query by NST provider, event ID, dispatch time and maximum event count' { + $start = $script:context.StartedUtc + $end = $script:eventTime + $null = & (Get-Module SqlApiTestRetry) { + param($start, $end) + Get-SqlApiRetryEvents -ContainerName 'synthetic-container' -StartedUtc $start -FinishedUtc $end + } $start $end + Should -Invoke -ModuleName SqlApiTestRetry Invoke-ScriptInBcContainer -Times 1 -Exactly -ParameterFilter { + $containerName -eq 'synthetic-container' -and $argumentList.Count -eq 2 -and + $argumentList[0] -eq $script:context.StartedUtc.ToString('o') -and $argumentList[1] -eq $script:eventTime.ToString('o') + } + Should -Invoke -ModuleName SqlApiTestRetry Get-WinEvent -Times 1 -Exactly -ParameterFilter { + $FilterHashtable.LogName -eq 'Application' -and + $FilterHashtable.ProviderName -eq 'MicrosoftDynamicsNavServer*' -and + $FilterHashtable.Id -eq 701 -and + $FilterHashtable.StartTime -eq $script:context.StartedUtc -and + $FilterHashtable.EndTime -eq $script:eventTime -and + $MaxEvents -eq 1000 -and $ErrorAction -eq 'Stop' + } + } + + It 'returns original XML from collected events' { + $script:collectedXml = New-NstEvent + Mock -ModuleName SqlApiTestRetry Get-WinEvent { + $eventRecord = [pscustomobject]@{ Xml = $script:collectedXml } + $eventRecord | Add-Member ScriptMethod ToXml { $this.Xml } + $eventRecord + } + $actual = & (Get-Module SqlApiTestRetry) { + param($context) + Get-SqlApiRetryEvents -ContainerName $context.ContainerName -StartedUtc $context.StartedUtc -FinishedUtc ([datetime]::UtcNow) + } $script:context + $actual | Should -Be $script:collectedXml + } + + It 'treats only NoMatchingEventsFound as an empty collection' { + Mock -ModuleName SqlApiTestRetry Get-WinEvent { + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Synthetic no matching events'), + 'NoMatchingEventsFound,Microsoft.PowerShell.Commands.GetWinEventCommand', + [System.Management.Automation.ErrorCategory]::ObjectNotFound, $null) + throw $errorRecord + } + $actual = & (Get-Module SqlApiTestRetry) { + param($context) + Get-SqlApiRetryEvents -ContainerName $context.ContainerName -StartedUtc $context.StartedUtc -FinishedUtc ([datetime]::UtcNow) + } $script:context + $actual | Should -BeNullOrEmpty + } + + It 'propagates event-log access errors instead of reporting no matches' { + Mock -ModuleName SqlApiTestRetry Get-WinEvent { throw [System.UnauthorizedAccessException]::new('Synthetic event log access denied') } + { + & (Get-Module SqlApiTestRetry) { + param($context) + Get-SqlApiRetryEvents -ContainerName $context.ContainerName -StartedUtc $context.StartedUtc -FinishedUtc ([datetime]::UtcNow) + } $script:context + } | Should -Throw '*Synthetic event log access denied*' + } + } +} From 0227094059f2f26f3fcb1b7f85a3285c17e78b8e Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Wed, 7 Oct 2026 16:09:07 +0200 Subject: [PATCH 21/22] Schedule balanced 300-trial SQL API comparison (AB#646383) Diagnostic only; do not merge. Compare control, identical original warmup, and correlated once-per-CU SQL retry. Keep all tests enabled, pinned source/packages, single 60-second probes and six-container global scheduling. Preserve source ZIP snapshots and first-attempt evidence; communication replacements remain explicit and separately accounted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .github/workflows/CICD.yaml | 291 ++++++++++++------ .github/workflows/PullRequestHandler.yaml | 2 +- .github/workflows/SqlApiComparisonBatch.yaml | 128 ++++++++ build/scripts/ParallelTestExecution.psm1 | 47 ++- build/scripts/SqlApiTestRetry.psm1 | 11 +- .../SqlResetPilot/CompaniesProbe.Tests.ps1 | 109 +------ .../SqlResetPilot/Comparison.Tests.ps1 | 123 ++++++++ build/scripts/SqlResetPilot/Comparison.psm1 | 102 ++++++ ...m.Tests.ps1 => ComparisonRunner.Tests.ps1} | 65 ++-- .../SqlResetPilot/ContainerPreflight.ps1 | 7 +- .../scripts/SqlResetPilot/Finalize.Tests.ps1 | 8 +- build/scripts/SqlResetPilot/Finalize.ps1 | 6 +- build/scripts/SqlResetPilot/Lifecycle.psm1 | 101 ++---- .../scripts/SqlResetPilot/PlanComparison.ps1 | 88 ++++++ .../scripts/SqlResetPilot/Preflight.Tests.ps1 | 31 +- build/scripts/SqlResetPilot/Prepare.ps1 | 36 ++- build/scripts/SqlResetPilot/README.md | 210 +++++++------ .../scripts/SqlResetPilot/Snapshot.Tests.ps1 | 86 ++++++ 18 files changed, 1023 insertions(+), 428 deletions(-) create mode 100644 .github/workflows/SqlApiComparisonBatch.yaml create mode 100644 build/scripts/SqlResetPilot/Comparison.Tests.ps1 create mode 100644 build/scripts/SqlResetPilot/Comparison.psm1 rename build/scripts/SqlResetPilot/{TwoArm.Tests.ps1 => ComparisonRunner.Tests.ps1} (79%) create mode 100644 build/scripts/SqlResetPilot/PlanComparison.ps1 create mode 100644 build/scripts/SqlResetPilot/Snapshot.Tests.ps1 diff --git a/.github/workflows/CICD.yaml b/.github/workflows/CICD.yaml index 0c7873932b6..c295adb0d6a 100644 --- a/.github/workflows/CICD.yaml +++ b/.github/workflows/CICD.yaml @@ -1,106 +1,225 @@ -name: 'SQL API two-arm experiment - DO NOT MERGE' +name: 'SQL API 300 trial comparison - DO NOT MERGE' +run-name: SQL API 300 comparison - ${{ inputs.mode }} on: workflow_dispatch: + inputs: + mode: + description: originals schedules all 300; replacement schedules one proven communication-invalid cell + type: choice + options: [originals, replacement] + default: originals + required: true + replacementArm: + description: Replacement only - control, warmup, or retry + type: string + replacementCountry: + description: Replacement only - W1 or DE + type: string + replacementTrial: + description: Replacement only - original index 1 through 50 + type: string + originalRun: + description: Replacement only - this comparison's original run ID + type: string + originalJob: + description: Replacement only - communication-failed original trial job ID + type: string permissions: actions: read + checks: read contents: read id-token: write packages: read +# Serialize this comparison and its explicit replacements, never unrelated workflows. +concurrency: + group: sql-api-646383-300-trial-comparison + cancel-in-progress: false + jobs: - Pilot: - if: github.ref == 'refs/heads/features/646383-sql-api-two-arm-experiment' && github.run_attempt == 1 - strategy: - fail-fast: false - max-parallel: 5 - matrix: - country: [W1, DE] - trial: [1, 2, 3, 4, 5] - experiment: [A, B] - runs-on: GitHub-BCAppsPL - timeout-minutes: 120 - defaults: - run: - shell: pwsh - env: - ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} - ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} - BC_SQL_PILOT_ARM: control - BC_SQL_API_EXPERIMENT: ${{ matrix.experiment }} - BC_SQL_PILOT_COUNTRY: ${{ matrix.country }} - BC_SQL_PILOT_TRIAL: ${{ matrix.trial }} - BuildMode: IntegrationTests + Plan: + if: github.repository == 'microsoft/BCApps' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/features/646383-sql-api-300-trial-comparison' && github.run_attempt == 1 + runs-on: windows-latest + timeout-minutes: 10 + outputs: + mode: ${{ steps.plan.outputs.mode }} + replacement: ${{ steps.plan.outputs.replacement }} + w1Package: ${{ steps.w1.outputs.artifact-id || steps.plan.outputs.w1Package }} + dePackage: ${{ steps.de.outputs.artifact-id || steps.plan.outputs.dePackage }} + packageRun: ${{ steps.plan.outputs.packageRun }} + batch01: ${{ steps.plan.outputs.batch01 }} + batch02: ${{ steps.plan.outputs.batch02 }} + batch03: ${{ steps.plan.outputs.batch03 }} + batch04: ${{ steps.plan.outputs.batch04 }} + batch05: ${{ steps.plan.outputs.batch05 }} + batch06: ${{ steps.plan.outputs.batch06 }} + batch07: ${{ steps.plan.outputs.batch07 }} + batch08: ${{ steps.plan.outputs.batch08 }} + batch09: ${{ steps.plan.outputs.batch09 }} + batch10: ${{ steps.plan.outputs.batch10 }} steps: - - name: Checkout experiment - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - name: Validate immutable artifacts and exact trial manifest + id: plan + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + COMPARISON_MODE: ${{ inputs.mode }} + REPLACEMENT_ARM: ${{ inputs.replacementArm }} + REPLACEMENT_COUNTRY: ${{ inputs.replacementCountry }} + REPLACEMENT_TRIAL: ${{ inputs.replacementTrial }} + ORIGINAL_RUN: ${{ inputs.originalRun }} + ORIGINAL_JOB: ${{ inputs.originalJob }} + run: .\build\scripts\SqlResetPilot\PlanComparison.ps1 + - name: Preserve exact W1 source ZIPs beyond original expiry + id: w1 + if: inputs.mode == 'originals' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: sql-api-comparison-packages-W1-${{ github.run_id }} + retention-days: 14 + compression-level: 0 + if-no-files-found: error + include-hidden-files: true + path: .sql-api-comparison-plan/W1/* + - name: Preserve exact DE source ZIPs beyond original expiry + id: de + if: inputs.mode == 'originals' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: sql-api-comparison-packages-DE-${{ github.run_id }} + retention-days: 14 + compression-level: 0 + if-no-files-found: error + include-hidden-files: true + path: .sql-api-comparison-plan/DE/* + - name: Preserve scheduling and package provenance + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: - lfs: true - fetch-depth: 0 + name: sql-api-comparison-manifest-${{ github.run_id }} + retention-days: 14 + if-no-files-found: error + include-hidden-files: true + path: .sql-api-comparison-plan/*.json - - name: Verify baseline PowerShell 7 host - run: | - if ($PSVersionTable.PSEdition -ne 'Core' -or $PSVersionTable.PSVersion.Major -ne 7) { - throw 'The pinned baseline executes on PowerShell 7; this pilot must use the same shell.' - } - $output = Join-Path $env:GITHUB_WORKSPACE 'sql-reset-pilot-output' - New-Item -ItemType Directory $output -Force | Out-Null - @{ - edition = $PSVersionTable.PSEdition - version = $PSVersionTable.PSVersion.ToString() - executable = (Get-Process -Id $PID).Path - utc = [DateTime]::UtcNow.ToString('o') - } | ConvertTo-Json | Tee-Object -FilePath (Join-Path $output 'shell-preflight.json') + Batch01: + needs: Plan + uses: ./.github/workflows/SqlApiComparisonBatch.yaml + secrets: inherit + with: + matrix: ${{ needs.Plan.outputs.batch01 }} + replacement: ${{ needs.Plan.outputs.replacement }} + w1Package: ${{ needs.Plan.outputs.w1Package }} + dePackage: ${{ needs.Plan.outputs.dePackage }} + packageRun: ${{ needs.Plan.outputs.packageRun }} - - name: Validate provenance and download exact compiled PR2 packages - id: packages - env: - GH_TOKEN: ${{ github.token }} - run: | - & .\build\scripts\SqlResetPilot\Prepare.ps1 + Batch02: + needs: [Plan, Batch01] + if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + uses: ./.github/workflows/SqlApiComparisonBatch.yaml + secrets: inherit + with: + matrix: ${{ needs.Plan.outputs.batch02 }} + replacement: ${{ needs.Plan.outputs.replacement }} + w1Package: ${{ needs.Plan.outputs.w1Package }} + dePackage: ${{ needs.Plan.outputs.dePackage }} + packageRun: ${{ needs.Plan.outputs.packageRun }} - - name: Read pinned country Integration settings - uses: microsoft/AL-Go/Actions/ReadSettings@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: pwsh - project: build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }} - buildMode: IntegrationTests + Batch03: + needs: [Plan, Batch02] + if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + uses: ./.github/workflows/SqlApiComparisonBatch.yaml + secrets: inherit + with: + matrix: ${{ needs.Plan.outputs.batch03 }} + replacement: ${{ needs.Plan.outputs.replacement }} + w1Package: ${{ needs.Plan.outputs.w1Package }} + dePackage: ${{ needs.Plan.outputs.dePackage }} + packageRun: ${{ needs.Plan.outputs.packageRun }} - - name: Read normal disposable CI license and package secrets - id: secrets - uses: microsoft/AL-Go/Actions/ReadSecrets@91b96c2b294be6f823277dafe6f03350abfb9d23 - with: - shell: pwsh - gitHubSecrets: ${{ toJson(secrets) }} - getSecrets: 'licenseFileUrl,gitHubPackagesContext,applicationInsightsConnectionString,appDependencySecrets' + Batch04: + needs: [Plan, Batch03] + if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + uses: ./.github/workflows/SqlApiComparisonBatch.yaml + secrets: inherit + with: + matrix: ${{ needs.Plan.outputs.batch04 }} + replacement: ${{ needs.Plan.outputs.replacement }} + w1Package: ${{ needs.Plan.outputs.w1Package }} + dePackage: ${{ needs.Plan.outputs.dePackage }} + packageRun: ${{ needs.Plan.outputs.packageRun }} - - name: Execute original pipeline with bounded clean-test prefix - uses: microsoft/AL-Go/Actions/RunPipeline@91b96c2b294be6f823277dafe6f03350abfb9d23 - env: - Secrets: '${{ steps.secrets.outputs.Secrets }}' - with: - shell: pwsh - project: build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }} - buildMode: IntegrationTests - artifact: ${{ steps.packages.outputs.artifact }} - installAppsJson: ${{ steps.packages.outputs.apps }} - installTestAppsJson: ${{ steps.packages.outputs.tests }} + Batch05: + needs: [Plan, Batch04] + if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + uses: ./.github/workflows/SqlApiComparisonBatch.yaml + secrets: inherit + with: + matrix: ${{ needs.Plan.outputs.batch05 }} + replacement: ${{ needs.Plan.outputs.replacement }} + w1Package: ${{ needs.Plan.outputs.w1Package }} + dePackage: ${{ needs.Plan.outputs.dePackage }} + packageRun: ${{ needs.Plan.outputs.packageRun }} - - name: Export diagnostics and tear down only owned disposable container - if: always() - run: | - & .\build\scripts\SqlResetPilot\Finalize.ps1 + Batch06: + needs: [Plan, Batch05] + if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + uses: ./.github/workflows/SqlApiComparisonBatch.yaml + secrets: inherit + with: + matrix: ${{ needs.Plan.outputs.batch06 }} + replacement: ${{ needs.Plan.outputs.replacement }} + w1Package: ${{ needs.Plan.outputs.w1Package }} + dePackage: ${{ needs.Plan.outputs.dePackage }} + packageRun: ${{ needs.Plan.outputs.packageRun }} - - name: Preserve raw outcomes (including failures) - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: sql-api-two-arm-${{ matrix.experiment }}-${{ matrix.country }}-${{ matrix.trial }}-${{ github.run_id }} - retention-days: 7 - if-no-files-found: error - path: | - sql-reset-pilot-output/** - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/TestResults*.xml - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/ContainerEventLog.evtx - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/BuildOutput.txt + Batch07: + needs: [Plan, Batch06] + if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + uses: ./.github/workflows/SqlApiComparisonBatch.yaml + secrets: inherit + with: + matrix: ${{ needs.Plan.outputs.batch07 }} + replacement: ${{ needs.Plan.outputs.replacement }} + w1Package: ${{ needs.Plan.outputs.w1Package }} + dePackage: ${{ needs.Plan.outputs.dePackage }} + packageRun: ${{ needs.Plan.outputs.packageRun }} + + Batch08: + needs: [Plan, Batch07] + if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + uses: ./.github/workflows/SqlApiComparisonBatch.yaml + secrets: inherit + with: + matrix: ${{ needs.Plan.outputs.batch08 }} + replacement: ${{ needs.Plan.outputs.replacement }} + w1Package: ${{ needs.Plan.outputs.w1Package }} + dePackage: ${{ needs.Plan.outputs.dePackage }} + packageRun: ${{ needs.Plan.outputs.packageRun }} + + Batch09: + needs: [Plan, Batch08] + if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + uses: ./.github/workflows/SqlApiComparisonBatch.yaml + secrets: inherit + with: + matrix: ${{ needs.Plan.outputs.batch09 }} + replacement: ${{ needs.Plan.outputs.replacement }} + w1Package: ${{ needs.Plan.outputs.w1Package }} + dePackage: ${{ needs.Plan.outputs.dePackage }} + packageRun: ${{ needs.Plan.outputs.packageRun }} + + Batch10: + needs: [Plan, Batch09] + if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + uses: ./.github/workflows/SqlApiComparisonBatch.yaml + secrets: inherit + with: + matrix: ${{ needs.Plan.outputs.batch10 }} + replacement: ${{ needs.Plan.outputs.replacement }} + w1Package: ${{ needs.Plan.outputs.w1Package }} + dePackage: ${{ needs.Plan.outputs.dePackage }} + packageRun: ${{ needs.Plan.outputs.packageRun }} diff --git a/.github/workflows/PullRequestHandler.yaml b/.github/workflows/PullRequestHandler.yaml index 599330b4467..ddba89afff4 100644 --- a/.github/workflows/PullRequestHandler.yaml +++ b/.github/workflows/PullRequestHandler.yaml @@ -35,7 +35,7 @@ jobs: Initialization: needs: [ PregateCheck ] - if: (!failure() && !cancelled()) && github.head_ref != 'features/646383-sql-api-warmup-experiment' && github.head_ref != 'features/646383-sql-api-two-arm-experiment' + if: (!failure() && !cancelled()) && github.head_ref != 'features/646383-sql-api-warmup-experiment' && github.head_ref != 'features/646383-sql-api-two-arm-experiment' && github.head_ref != 'features/646383-sql-api-300-trial-comparison' runs-on: [ windows-latest ] outputs: projects: ${{ steps.determineProjectsToBuild.outputs.ProjectsJson }} diff --git a/.github/workflows/SqlApiComparisonBatch.yaml b/.github/workflows/SqlApiComparisonBatch.yaml new file mode 100644 index 00000000000..dbd1ef17622 --- /dev/null +++ b/.github/workflows/SqlApiComparisonBatch.yaml @@ -0,0 +1,128 @@ +name: 'SQL API comparison batch - DO NOT MERGE' + +on: + workflow_call: + inputs: + matrix: + required: true + type: string + replacement: + required: true + type: string + w1Package: + required: true + type: string + dePackage: + required: true + type: string + packageRun: + required: true + type: string + +permissions: + actions: read + contents: read + id-token: write + packages: read + +jobs: + Pilot: + name: Trial ${{ matrix.experiment }}/${{ matrix.country }}/${{ matrix.trial }} + if: github.repository == 'microsoft/BCApps' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/features/646383-sql-api-300-trial-comparison' && github.run_attempt == 1 + strategy: + fail-fast: false + max-parallel: 6 + matrix: ${{ fromJSON(inputs.matrix) }} + runs-on: GitHub-BCAppsPL + timeout-minutes: 120 + defaults: + run: + shell: pwsh + env: + ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} + ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} + BC_SQL_PILOT_ARM: control + BC_SQL_API_EXPERIMENT: ${{ matrix.experiment }} + BC_SQL_PILOT_COUNTRY: ${{ matrix.country }} + BC_SQL_PILOT_TRIAL: ${{ matrix.trial }} + BC_SQL_COMPARISON_REPLACEMENT: ${{ inputs.replacement }} + BC_SQL_COMPARISON_PACKAGE_ARTIFACT: ${{ matrix.country == 'W1' && inputs.w1Package || inputs.dePackage }} + BC_SQL_COMPARISON_PACKAGE_RUN: ${{ inputs.packageRun }} + BuildMode: IntegrationTests + steps: + - name: Checkout experiment + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + lfs: true + fetch-depth: 0 + + - name: Verify baseline PowerShell 7 host + run: | + if ($PSVersionTable.PSEdition -ne 'Core' -or $PSVersionTable.PSVersion.Major -ne 7) { + throw 'The pinned baseline executes on PowerShell 7; this pilot must use the same shell.' + } + $output = Join-Path $env:GITHUB_WORKSPACE 'sql-reset-pilot-output' + New-Item -ItemType Directory $output -Force | Out-Null + @{ + edition = $PSVersionTable.PSEdition + version = $PSVersionTable.PSVersion.ToString() + executable = (Get-Process -Id $PID).Path + utc = [DateTime]::UtcNow.ToString('o') + } | ConvertTo-Json | Tee-Object -FilePath (Join-Path $output 'shell-preflight.json') + + - name: Validate provenance and download exact compiled PR2 packages + id: packages + env: + GH_TOKEN: ${{ github.token }} + run: | + & .\build\scripts\SqlResetPilot\Prepare.ps1 + + - name: Read pinned country Integration settings + uses: microsoft/AL-Go/Actions/ReadSettings@91b96c2b294be6f823277dafe6f03350abfb9d23 + with: + shell: pwsh + project: build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }} + buildMode: IntegrationTests + + - name: Read normal disposable CI license and package secrets + id: secrets + uses: microsoft/AL-Go/Actions/ReadSecrets@91b96c2b294be6f823277dafe6f03350abfb9d23 + with: + shell: pwsh + gitHubSecrets: ${{ toJson(secrets) }} + getSecrets: 'licenseFileUrl,gitHubPackagesContext,applicationInsightsConnectionString,appDependencySecrets' + + - name: Execute original pipeline with bounded clean-test prefix + uses: microsoft/AL-Go/Actions/RunPipeline@91b96c2b294be6f823277dafe6f03350abfb9d23 + env: + Secrets: '${{ steps.secrets.outputs.Secrets }}' + with: + shell: pwsh + project: build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }} + buildMode: IntegrationTests + artifact: ${{ steps.packages.outputs.artifact }} + installAppsJson: ${{ steps.packages.outputs.apps }} + installTestAppsJson: ${{ steps.packages.outputs.tests }} + + - name: Export diagnostics and tear down only owned disposable container + if: always() + run: | + & .\build\scripts\SqlResetPilot\Finalize.ps1 + + - name: Preserve raw outcomes (including failures) + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: sql-api-comparison-${{ matrix.experiment }}-${{ matrix.country }}-${{ matrix.trial }}-${{ github.run_id }} + retention-days: 14 + include-hidden-files: true + if-no-files-found: error + path: | + sql-reset-pilot-output/** + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/TestResults*.xml + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/ContainerEventLog.evtx + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/BuildOutput.txt + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/.buildartifacts/**/*.xml + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/.buildartifacts/**/*.log + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/.buildartifacts/**/*.evtx + build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/.buildartifacts/**/BuildOutput.txt diff --git a/build/scripts/ParallelTestExecution.psm1 b/build/scripts/ParallelTestExecution.psm1 index 6e43f662b06..db246b6f5d7 100644 --- a/build/scripts/ParallelTestExecution.psm1 +++ b/build/scripts/ParallelTestExecution.psm1 @@ -48,17 +48,19 @@ function Get-DisabledTestsForApp { } } - if ((Test-SqlApiExperiment) -and $env:BC_SQL_API_EXPERIMENT -eq 'A') { - $disabledTests += [PSCustomObject]@{ codeunitId = 148318; method = 'CapabilitiesProjectsEnabledViaAPI' } - } return @($disabledTests) } function Test-SqlApiExperiment { return ($env:GITHUB_REPOSITORY -eq 'microsoft/BCApps' -and - $env:GITHUB_REF -eq 'refs/heads/features/646383-sql-api-two-arm-experiment' -and + $env:GITHUB_REF -eq 'refs/heads/features/646383-sql-api-300-trial-comparison' -and $env:GITHUB_EVENT_NAME -eq 'workflow_dispatch' -and $env:GITHUB_RUN_ATTEMPT -eq '1' -and - $env:BC_SQL_PILOT_ARM -eq 'control' -and $env:BC_SQL_API_EXPERIMENT -in @('A', 'B')) + $env:BC_SQL_PILOT_ARM -eq 'control' -and $env:BC_SQL_API_EXPERIMENT -in @('control', 'warmup', 'retry')) +} + +function Test-SqlApiWarmupEnabled { + return ($env:BC_SQL_PILOT_ARM -and + (-not (Test-SqlApiExperiment) -or $env:BC_SQL_API_EXPERIMENT -ne 'control')) } function Get-TestJobResultFileName { @@ -672,9 +674,9 @@ function Receive-TestJobResult { $outcome = 'Failed' if ($Job.State -eq 'Failed' -and -not $Retried.ContainsKey($Entry.appName) -and $Entry.PSObject.Properties['SqlRetryContext'] -and $Entry.SqlRetryContext -and - $env:BC_SQL_API_EXPERIMENT -eq 'B' -and (Test-SqlApiExperiment)) { + (Test-SqlApiExperiment)) { $sqlRetryEvidence = Get-SqlApiTestRetryEvidence -Context $Entry.SqlRetryContext - if ($sqlRetryEvidence) { $outcome = 'SqlPoolRetry' } + if ($sqlRetryEvidence -and $env:BC_SQL_API_EXPERIMENT -eq 'retry') { $outcome = 'SqlPoolRetry' } } } } @@ -702,7 +704,8 @@ function Receive-TestJobResult { codeunitId = $context.CodeunitId; tenant = $context.Tenant; attempt = $context.Attempt outcome = $outcome; jobState = [string]$Job.State; startedUtc = $context.StartedUtc.ToString('o') completedUtc = [datetime]::UtcNow.ToString('o'); experiment = $env:BC_SQL_API_EXPERIMENT - retryEligible = [bool]$sqlRetryEvidence; expectedCases = $context.TestCount + sqlPoolFailureConfirmed = [bool]$sqlRetryEvidence + retryEligible = ($outcome -eq 'SqlPoolRetry'); expectedCases = $context.TestCount } | ConvertTo-Json | Set-Content (Join-Path $attemptDirectory 'outcome.json') -ErrorAction Stop if ($context.Attempt -eq 2 -and $outcome -eq 'Passed') { foreach ($key in $context.ResultFiles.Keys) { @@ -821,7 +824,7 @@ function Start-RequiredDisabledDispatch { $sqlRetryExpectedTests = @() $fileSuffix = $TenantInfo.Id if (Test-SqlApiExperiment) { - if (-not $Parameters.JUnitResultFileName) { throw 'Two-arm experiment requires JUnit evidence.' } + if (-not $Parameters.JUnitResultFileName) { throw 'Comparison requires JUnit evidence.' } $attempt = 1 $finalFiles = @{} if ($State.sqlRetryEvidence.ContainsKey($WorkItem.Key)) { @@ -939,7 +942,7 @@ function Invoke-RequiredDisabledTestExecution { Reset-BcTestTenant -ContainerName $Parameters.containerName -Tenant $dispatch.TenantInfo.Id ` -TenantDatabaseName $dispatch.TenantInfo.DatabaseName -TemplateDatabaseName $TemplateDatabaseName } - if ($env:BC_SQL_PILOT_ARM) { + if (Test-SqlApiWarmupEnabled) { foreach ($dispatch in $batch) { Invoke-SqlPilotCompaniesProbe -ContainerName $Parameters.containerName -Tenant $dispatch.TenantInfo.Id ` -Credential $Parameters.credential -CompanyName $Parameters.companyName @@ -980,7 +983,7 @@ function Register-TestJobOutcome { ) if ($Result.Outcome -eq 'SqlPoolRetry') { - if (-not (Test-SqlApiExperiment) -or $env:BC_SQL_API_EXPERIMENT -ne 'B' -or + if (-not (Test-SqlApiExperiment) -or $env:BC_SQL_API_EXPERIMENT -ne 'retry' -or -not $Result.SqlRetryEvidence -or $State.retried.ContainsKey($Result.AppName)) { $State.hasFailures = $true return @@ -1592,7 +1595,7 @@ function Invoke-ParallelTestExecution { } $state | ConvertTo-Json -Depth 5 | Set-Content $stateFile -Force - if ($env:BC_SQL_PILOT_ARM) { + if (Test-SqlApiWarmupEnabled) { if (-not $templateDatabaseName) { throw 'Pristine template must be frozen before warmup.' } Invoke-SqlPilotWarmup -Parameters $parameters -AppNames $appNamesToTest -AppIdByName $appIdByName ` -Tenants $tenants -ScriptPath $scriptPath -TestType $testType -CleanTenantAppNames $cleanTenantAppNames @@ -1620,15 +1623,31 @@ function Invoke-ParallelTestExecution { if ((Test-SqlApiExperiment) -and (Test-Path $parameters.JUnitResultFileName)) { [xml]$finalXml = Get-Content $parameters.JUnitResultFileName -Raw $finalCases = @($finalXml.SelectNodes('/testsuites/testsuite/testcase')) + $skippedCount = @($finalCases | Where-Object { $_.SelectSingleNode('skipped') }).Count + $completeCohort = $finalCases.Count -eq 253 -and $skippedCount -eq 19 -and + $finalXml.SelectNodes('/testsuites/testsuite').Count -eq 21 + $failedCount = @($finalCases | Where-Object { + $_.SelectSingleNode('failure | error') -or $_.GetAttribute('result') -eq 'Fail' + }).Count + if (-not $completeCohort -or $failedCount -gt 0) { $state.hasFailures = $true } + $attempts = @(Get-ChildItem (Join-Path $env:BC_SQL_PILOT_OUTPUT 'test-attempts') -Filter outcome.json -Recurse -File | + ForEach-Object { Get-Content $_.FullName -Raw | ConvertFrom-Json }) @{ total = $finalCases.Count - failed = @($finalCases | Where-Object { $_.SelectSingleNode('failure | error') }).Count - skipped = @($finalCases | Where-Object { $_.SelectSingleNode('skipped') }).Count + failed = $failedCount + skipped = $skippedCount passed = @($finalCases | Where-Object { -not $_.SelectSingleNode('failure | error | skipped') }).Count suites = $finalXml.SelectNodes('/testsuites/testsuite').Count experiment = $env:BC_SQL_API_EXPERIMENT + completeCohort = $completeCohort + firstAttemptSqlFailures = @($attempts | Where-Object { $_.attempt -eq 1 -and $_.sqlPoolFailureConfirmed }).Count + firstAttemptFailedCodeunits = @($attempts | Where-Object { $_.attempt -eq 1 -and $_.outcome -ne 'Passed' }).Count + retryRecoveries = @($attempts | Where-Object { $_.attempt -eq 2 -and $_.outcome -eq 'Passed' }).Count + failedRetries = @($attempts | Where-Object { $_.attempt -eq 2 -and $_.outcome -ne 'Passed' }).Count firstAttemptsAndRetries = 'See test-attempts; final XML replaces only validated recovered CUs.' } | ConvertTo-Json | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT 'final-counts.json') + } elseif (Test-SqlApiExperiment) { + $state.hasFailures = $true } $state.finalResult = -not $state.hasFailures $state.completed = $true diff --git a/build/scripts/SqlApiTestRetry.psm1 b/build/scripts/SqlApiTestRetry.psm1 index c548b5cd693..76e1a2614c6 100644 --- a/build/scripts/SqlApiTestRetry.psm1 +++ b/build/scripts/SqlApiTestRetry.psm1 @@ -8,7 +8,8 @@ metadata stack, within this dispatch's time window. Missing evidence fails closed. The scheduler permits only one retry, restores the tenant first, and reruns the entire codeunit. Original results and matching events are retained separately from final results. - The scheduler enables this only on the exact manual experiment branch, arm B, attempt one. + The scheduler enables retry only on the exact manual comparison branch, retry arm, attempt one. + All arms classify and preserve matching first-attempt evidence without authorizing other retries. SqlApiRetryEvidence JSON files under the diagnostic output are uploaded with raw outcomes, including on successful runs, so recovered failures remain visible without duplicating cases. #> @@ -140,7 +141,7 @@ function Get-SqlApiTestRetryEvidence { $null = New-Item -ItemType Directory -Path $folder -Force -ErrorAction Stop $evidenceFile = Join-Path $folder "$($Context.Tenant)-$($Context.CodeunitId)-$([guid]::NewGuid().ToString('N')).json" $evidence | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $evidenceFile -Encoding utf8 -ErrorAction Stop - Write-Host "::warning::SQL API pool failure confirmed for codeunit $($Context.CodeunitId) on '$($Context.Tenant)'; requests: $($evidence.RequestIds -join ', '). Retrying once after a tenant reset. Original evidence: $evidenceFile" + Write-Host "::warning::SQL API pool failure confirmed for codeunit $($Context.CodeunitId) on '$($Context.Tenant)'; requests: $($evidence.RequestIds -join ', '). Original evidence: $evidenceFile. Only the retry arm may authorize a single clean retry." return $evidence } catch { Write-Host "::warning::SQL API retry evidence unavailable; preserving test failure: $($_.Exception.Message)" @@ -180,7 +181,7 @@ function Test-SqlApiRetryResult { } function Test-SqlApiExperimentSelection { - param($Context, [ValidateSet('A', 'B')][string]$Experiment) + param($Context, [ValidateSet('control', 'warmup', 'retry')][string]$Experiment) if ([string]$Context.CodeunitId -ne '148318') { return $true } try { $suite = Get-SqlApiRetrySuite -Context $Context @@ -188,8 +189,8 @@ function Test-SqlApiExperimentSelection { ($_.GetAttribute('name') -split '\.')[-1] -ceq 'CapabilitiesProjectsEnabledViaAPI' }) if ($target.Count -ne 1) { throw 'Expected exactly one target method in Capabilities results.' } - if (($null -ne $target[0].SelectSingleNode('skipped')) -ne ($Experiment -eq 'A')) { - throw 'Target method skip status differs from the experiment arm.' + if ($null -ne $target[0].SelectSingleNode('skipped')) { + throw "Target method must remain enabled in $Experiment." } return $true } catch { diff --git a/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 b/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 index d2ed24c07d6..6fdcbcf79af 100644 --- a/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 +++ b/build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 @@ -22,113 +22,30 @@ Describe 'External authenticated companies probe' { @{ StatusCode = 200; Headers = @{ 'request-id' = @('server-correlation') }; Content = '{"value":[{"name":"My Company","id":"company-id"}]}' } } Mock Add-Content -ModuleName Lifecycle {} - Mock Start-Sleep -ModuleName Lifecycle {} - $script:requestNumber = 0 } - It 'records first-try success with explicit tenant auth and hidden HTTP retries disabled' { + It 'sends exactly one explicitly tenant-scoped preauthenticated HTTP request with retries disabled' { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { $Uri -eq 'http://172.16.1.2:7048/BC/api/v2.0/companies?tenant=tenant2' -and $Authentication -eq 'Basic' -and $Credential.UserName -eq 'fixture' -and - $MaximumRetryCount -eq 0 -and $MaximumRedirection -eq 0 -and $TimeoutSec -eq 20 -and - $OperationTimeoutSeconds -eq 20 -and $SkipHttpErrorCheck + $MaximumRetryCount -eq 0 -and $MaximumRedirection -eq 0 -and $TimeoutSec -eq 60 -and $SkipHttpErrorCheck } Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { - $Value -match '"recordType":"attempt"' -and $Value -match '"status":200' -and $Value -match '"passed":true' -and $Value -match 'server-correlation' -and $Value -notmatch 'fixture|test-only-marker' } - Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { - $Value -match '"outcome":"first-try-success"' -and $Value -match '"retries":0' -and - $Value -match '"attempts":1' -and $Value -match '"recovered":false' - } - Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 0 - } - It 'uses exactly one attempt in experiment arm even for HTTP500' -ForEach @( - @{ Arm = 'A' }, @{ Arm = 'B' } - ) { - $old = $env:BC_SQL_API_EXPERIMENT - try { - $env:BC_SQL_API_EXPERIMENT = $Arm - Mock Invoke-WebRequest -ModuleName Lifecycle { @{ StatusCode = 500; Headers = @{}; Content = '' } } - { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw '*after 1 attempt*' - Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly - Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 0 - } finally { $env:BC_SQL_API_EXPERIMENT = $old } } - It 'bounds transient HTTP to three attempts and records each failure safely' -ForEach @( - @{ Code = 500 }, @{ Code = 502 }, @{ Code = 503 }, @{ Code = 504 } - ) { + It 'treats HTTP failures as terminal without logging response content' { Mock Invoke-WebRequest -ModuleName Lifecycle { - @{ StatusCode = $Code; Headers = @{}; Content = 'sensitive-response-marker' } + @{ StatusCode = 500; Headers = @{}; Content = 'sensitive-response-marker' } } { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | - Should -Throw '*after 3 attempt*' - Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 3 -Exactly - Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 2 -Exactly -ParameterFilter { $Seconds -eq 2 } - Should -Invoke Add-Content -ModuleName Lifecycle -Times 3 -Exactly -ParameterFilter { - $Value -match '"recordType":"attempt"' -and $Value -match '"retryable":true' -and - $Value -match '"startedUtc":' -and $Value -match '"completedUtc":' -and - $Value -match '"elapsedMilliseconds":' -and $Value -match '"errorCategory":"HttpStatus"' -and - $Value -notmatch 'sensitive-response-marker|test-only-marker' - } - Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { - $Value -match '"outcome":"failed"' -and $Value -match '"attempts":3' -and $Value -match '"retries":2' - } - } - It 'does not retry permanent HTTP ' -ForEach @( - @{ Code = 301 }, @{ Code = 400 }, @{ Code = 401 }, @{ Code = 403 }, - @{ Code = 404 }, @{ Code = 408 }, @{ Code = 429 }, @{ Code = 501 } - ) { - Mock Invoke-WebRequest -ModuleName Lifecycle { @{ StatusCode = $Code; Headers = @{}; Content = '' } } - { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw '*after 1 attempt*' + Should -Throw '*No retry*' Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly - Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 0 - } - It 'records recovered success on the third attempt and does not issue a fourth' { - Mock Invoke-WebRequest -ModuleName Lifecycle { - $script:requestNumber++ - if ($script:requestNumber -lt 3) { return @{ StatusCode = 503; Headers = @{}; Content = '' } } - @{ StatusCode = 200; Headers = @{}; Content = '{"value":[{"name":"My Company","id":"company-id"}]}' } - } - Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' - Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 3 -Exactly - Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 2 -Exactly - Should -Invoke Add-Content -ModuleName Lifecycle -Times 3 -Exactly -ParameterFilter { $Value -match '"recordType":"attempt"' } - Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { - $Value -match '"outcome":"recovered"' -and $Value -match '"recovered":true' -and - $Value -match '"attempts":3' -and $Value -match '"retries":2' - } - } - It 'retries typed transport failure and records recovery' -ForEach @( - @{ Kind = 'timeout'; Failure = [TimeoutException]::new('sensitive-exception-marker') } - @{ Kind = 'cancelled request timeout'; Failure = [Threading.Tasks.TaskCanceledException]::new('sensitive-exception-marker') } - @{ Kind = 'connection'; Failure = [Net.Http.HttpRequestException]::new('sensitive-exception-marker', - [Net.Sockets.SocketException]::new(10061)) } - @{ Kind = 'web timeout'; Failure = [Net.WebException]::new('sensitive-exception-marker', [Net.WebExceptionStatus]::Timeout) } - ) { - Mock Invoke-WebRequest -ModuleName Lifecycle { - $script:requestNumber++ - if ($script:requestNumber -eq 1) { throw $Failure } - @{ StatusCode = 200; Headers = @{}; Content = '{"value":[{"name":"My Company","id":"company-id"}]}' } - } - Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' - Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 2 -Exactly - Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { $Seconds -eq 2 } - Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { - $Value -match '"errorCategory":"ConnectionOrTimeout"' -and $Value -match '"status":null' -and - $Value -match '"errorType":' -and $Value -notmatch 'sensitive-exception-marker|test-only-marker' - } Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { - $Value -match '"outcome":"recovered"' -and $Value -match '"attempts":2' + $Value -match '"status":500' -and $Value -notmatch 'sensitive-response-marker|test-only-marker' } } - It 'stops repeated transport timeouts after three attempts' { - Mock Invoke-WebRequest -ModuleName Lifecycle { throw [TimeoutException]::new('sensitive-exception-marker') } - { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw '*after 3 attempt*' - Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 3 -Exactly - Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 2 -Exactly - } It 'rejects invalid JSON without another request' { Mock Invoke-WebRequest -ModuleName Lifecycle { @{ StatusCode = 200; Headers = @{}; Content = 'invalid-json' } } { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw @@ -141,24 +58,14 @@ Describe 'External authenticated companies probe' { { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly } - It 'redacts unknown exception text without retrying permanent failures' { + It 'redacts transport exception text and never retries' { Mock Invoke-WebRequest -ModuleName Lifecycle { throw 'sensitive-exception-marker' } { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw '*sanitized companies-probes*' Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly Should -Invoke Add-Content -ModuleName Lifecycle -Times 1 -Exactly -ParameterFilter { - $Value -match '"recordType":"attempt"' -and $Value -notmatch 'sensitive-exception-marker|test-only-marker' - } - Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 0 - } - It 'does not retry TLS authentication errors' { - Mock Invoke-WebRequest -ModuleName Lifecycle { - throw [Net.Http.HttpRequestException]::new('sensitive-exception-marker', - [Security.Authentication.AuthenticationException]::new('sensitive-exception-marker')) + $Value -notmatch 'sensitive-exception-marker|test-only-marker' } - { Invoke-SqlPilotCompaniesProbe 'owned' 'tenant2' $script:credential 'My Company' } | Should -Throw '*after 1 attempt*' - Should -Invoke Invoke-WebRequest -ModuleName Lifecycle -Times 1 -Exactly - Should -Invoke Start-Sleep -ModuleName Lifecycle -Times 0 } It 'refuses a foreign container, default tenant, or worker not yet restored' { { Invoke-SqlPilotCompaniesProbe 'foreign' 'tenant2' $script:credential 'My Company' } | Should -Throw diff --git a/build/scripts/SqlResetPilot/Comparison.Tests.ps1 b/build/scripts/SqlResetPilot/Comparison.Tests.ps1 new file mode 100644 index 00000000000..34263a8dc2e --- /dev/null +++ b/build/scripts/SqlResetPilot/Comparison.Tests.ps1 @@ -0,0 +1,123 @@ +BeforeAll { + Import-Module (Join-Path $PSScriptRoot 'Comparison.psm1') -Force +} + +Describe 'Exact balanced 300-cell comparison manifest' { + BeforeAll { + $script:cells = @(Get-SqlComparisonCell) + } + It 'contains exactly 300 unique identities and all 50 indices per arm and country' { + $script:cells.Count | Should -Be 300 + @($script:cells | ForEach-Object { "$($_.experiment)/$($_.country)/$($_.trial)" } | + Sort-Object -Unique).Count | Should -Be 300 + foreach ($arm in @('control', 'warmup', 'retry')) { + foreach ($country in @('W1', 'DE')) { + $indices = @($script:cells | Where-Object { $_.experiment -eq $arm -and $_.country -eq $country } | + Select-Object -ExpandProperty trial | Sort-Object) + ($indices -join ',') | Should -Be ((1..50) -join ',') + } + } + } + It 'splits into ten matrix-safe balanced batches and six-cell index blocks' { + foreach ($batch in 0..9) { + $selected = @($script:cells | Select-Object -Skip ($batch * 30) -First 30) + $selected.Count | Should -Be 30 + foreach ($group in @($selected | Group-Object experiment, country)) { + $group.Count | Should -Be 5 + } + } + foreach ($block in 0..49) { + $selected = @($script:cells | Select-Object -Skip ($block * 6) -First 6) + @($selected.trial | Sort-Object -Unique).Count | Should -Be 1 + @($selected | Group-Object experiment, country).Count | Should -Be 6 + } + } + It 'generates exactly one explicit replacement and no other cells' { + $cells = @(Get-SqlComparisonCell -Mode replacement -Arm retry -Country DE -Trial 50) + $cells.Count | Should -Be 1 + $cells[0].experiment | Should -Be 'retry' + $cells[0].country | Should -Be 'DE' + $cells[0].trial | Should -Be 50 + } + It 'rejects invalid replacement identities and mixed original selection' { + foreach ($trial in @('0', '51', '01', '-1', '1,2', '')) { + { Get-SqlComparisonCell -Mode replacement -Arm retry -Country DE -Trial $trial } | Should -Throw + } + { Get-SqlComparisonCell -Mode replacement -Arm A -Country DE -Trial 1 } | Should -Throw + { Get-SqlComparisonCell -Mode replacement -Arm retry -Country US -Trial 1 } | Should -Throw + { Get-SqlComparisonCell -Mode originals -Arm control } | Should -Throw + } + It 'preserves original probe implementation and compiled source byte-for-byte' { + $root = Resolve-Path (Join-Path $PSScriptRoot '..\..\..') + $diff = git -C $root diff fcc1776c6b165199dd66e4227675b1cc31da7a8f -- ` + build/scripts/SqlResetPilot/Lifecycle.psm1 build/scripts/SqlResetPilot/CompaniesProbe.Tests.ps1 ` + build/scripts/RunTestsInBcContainer.ps1 .github/AL-Go-Settings.json build/projects src + $LASTEXITCODE | Should -Be 0 + $diff | Should -BeNullOrEmpty + } + It 'keeps exact immutable artifact pairs consistent with runtime preparation' { + $prepare = Get-Content (Join-Path $PSScriptRoot 'Prepare.ps1') -Raw + $pins = @(Get-SqlComparisonArtifact) + $pins.Count | Should -Be 4 + foreach ($pin in $pins) { + $prepare | Should -Match ([regex]::Escape($pin.id)) + $prepare | Should -Match ([regex]::Escape($pin.digest)) + } + } +} + +Describe 'Only authoritative original communication failures qualify for replacement' { + BeforeEach { + $script:sha = '1111111111111111111111111111111111111111' + $script:run = [PSCustomObject]@{ + id = 100; head_sha = $script:sha; head_branch = 'features/646383-sql-api-300-trial-comparison' + event = 'workflow_dispatch'; run_attempt = 1; path = '.github/workflows/CICD.yaml' + display_title = 'SQL API 300 comparison - originals' + } + + $script:job = [PSCustomObject]@{ + id = 200; run_id = 100; conclusion = 'failure'; status = 'completed' + name = 'Batch01 / Trial warmup/DE/3' + } + $script:annotations = @([PSCustomObject]@{ + annotation_level = 'failure' + message = 'The self-hosted runner: isolated-runner lost communication with the server.' + }) + $script:cell = [PSCustomObject]@{ experiment = 'warmup'; country = 'DE'; trial = 3 } + } + It 'preserves the exact original run, job, revision and annotation' { + $record = Assert-SqlComparisonReplacement $script:run $script:job $script:annotations $script:cell $script:sha + $record.runId | Should -Be '100' + $record.jobId | Should -Be '200' + $record.trialIdentity | Should -Be 'warmup/DE/3' + $record.workflowSha | Should -Be $script:sha + $record.annotation.Count | Should -Be 1 + } + It 'rejects SQL or ordinary test failures even if caller requests replacement' { + $script:annotations[0].message = 'GET request failed: 500. AcquireSqlConnectionFromPool NullReferenceException' + { Assert-SqlComparisonReplacement $script:run $script:job $script:annotations $script:cell $script:sha } | Should -Throw + { Assert-SqlComparisonReplacement $script:run $script:job @() $script:cell $script:sha } | Should -Throw + } + It 'rejects original run drift: ' -ForEach @( + @{ Property = 'head_sha'; Value = '2222222222222222222222222222222222222222' } + @{ Property = 'head_branch'; Value = 'features/646383-sql-api-de3-communication-replay' } + @{ Property = 'event'; Value = 'pull_request' } + @{ Property = 'run_attempt'; Value = 2 } + @{ Property = 'path'; Value = '.github/workflows/Other.yaml' } + @{ Property = 'display_title'; Value = 'SQL API 300 comparison - replacement' } + ) { + $script:run.$Property = $Value + { Assert-SqlComparisonReplacement $script:run $script:job $script:annotations $script:cell $script:sha } | Should -Throw + } + It 'rejects wrong, skipped, cancelled or successful jobs: /' -ForEach @( + @{ Property = 'run_id'; Value = 101 } + @{ Property = 'name'; Value = 'Batch01 / Trial retry/DE/3' } + @{ Property = 'conclusion'; Value = 'success' } + @{ Property = 'conclusion'; Value = 'cancelled' } + @{ Property = 'conclusion'; Value = 'skipped' } + @{ Property = 'status'; Value = 'in_progress' } + ) { + $script:job.$Property = $Value + { Assert-SqlComparisonReplacement $script:run $script:job $script:annotations $script:cell $script:sha } | Should -Throw + } +} diff --git a/build/scripts/SqlResetPilot/Comparison.psm1 b/build/scripts/SqlResetPilot/Comparison.psm1 new file mode 100644 index 00000000000..ef5334bc600 --- /dev/null +++ b/build/scripts/SqlResetPilot/Comparison.psm1 @@ -0,0 +1,102 @@ +function Get-SqlComparisonCell { + param([ValidateSet('originals', 'replacement')][string]$Mode = 'originals', + [string]$Arm, [string]$Country, [string]$Trial) + if ($Mode -eq 'replacement') { + if ($Arm -notin @('control', 'warmup', 'retry') -or $Country -notin @('W1', 'DE') -or + $Trial -notmatch '^(?:[1-9]|[1-4][0-9]|50)$') { throw 'Invalid replacement identity.' } + return [PSCustomObject]@{ experiment = $Arm; country = $Country; trial = [int]$Trial } + } + if ($Arm -or $Country -or $Trial) { throw 'Original schedule cannot contain replacement selection.' } + foreach ($index in 1..50) { + # Rotate queue order across indices without changing balanced six-cell blocks. + $countries = if ($index % 2) { @('W1', 'DE') } else { @('DE', 'W1') } + $arms = @('control', 'warmup', 'retry') + foreach ($location in $countries) { + foreach ($offset in 0..2) { + [PSCustomObject]@{ + experiment = $arms[($index - 1 + $offset) % 3] + country = $location + trial = $index + } + } + } + } +} + +function Assert-SqlComparisonReplacement { + param($Run, $Job, [array]$Annotations, $Cell, [string]$ExpectedSha) + $identity = "$($Cell.experiment)/$($Cell.country)/$($Cell.trial)" + if ($Run.head_sha -ne $ExpectedSha -or + $Run.head_branch -ne 'features/646383-sql-api-300-trial-comparison' -or + $Run.event -ne 'workflow_dispatch' -or $Run.run_attempt -ne 1 -or + $Run.path -ne '.github/workflows/CICD.yaml' -or + $Run.display_title -ne 'SQL API 300 comparison - originals' -or + $Job.run_id -ne $Run.id -or $Job.conclusion -ne 'failure' -or $Job.status -ne 'completed' -or + $Job.name -notmatch "(?:^| / )Trial $([regex]::Escape($identity))$") { + throw 'Replacement must identify a failed original cell from this exact comparison revision.' + } + $communication = @($Annotations | Where-Object { + $_.annotation_level -eq 'failure' -and + $_.message -match '(?is)self-hosted runner.*lost communication with (?:the )?server' + }) + if ($communication.Count -eq 0) { throw 'No authoritative runner-communication annotation; no replacement authorized.' } + [PSCustomObject]@{ + runId = [string]$Run.id; jobId = [string]$Job.id; trialIdentity = $identity + event = $Run.event; runAttempt = $Run.run_attempt; workflow = $Run.path + workflowSha = $Run.head_sha; annotation = @($communication.message) + reason = 'Confirmed GitHub runner communication failure; retained separately from original results' + } +} + +function Get-SqlComparisonArtifact { + @( + @{ country = 'W1'; kind = 'apps'; id = '11374170356'; digest = '683343673e6efeb7699734b250d2ee5004d2d83fe08e57f324c4c645c09612db' } + @{ country = 'W1'; kind = 'tests'; id = '11374145359'; digest = 'f75ce06920437328e5d79a5512eddcc2425624bb696c339c108478c83554171d' } + @{ country = 'DE'; kind = 'apps'; id = '11376415128'; digest = '409af00bb5e6922224626f94a4d4f5625f4a9bf13b9398e84cc101a41a313dd5' } + @{ country = 'DE'; kind = 'tests'; id = '11375174896'; digest = '4e1edde77089e0b586679b168f11123a8181a99c63c3a82b42323503340a5d3b' } + ) +} + +function Assert-SqlComparisonSnapshot { + param([string]$Directory, [ValidateSet('W1', 'DE')][string]$Country) + $proof = @(Get-Content (Join-Path $Directory 'artifact-proof.json') -Raw -ErrorAction Stop | ConvertFrom-Json) + if ($proof.Count -ne 2) { throw 'Snapshot must contain the two original country artifacts.' } + foreach ($pin in @(Get-SqlComparisonArtifact | Where-Object country -eq $Country)) { + $entry = @($proof | Where-Object { $_.pin.id -eq $pin.id }) + if ($entry.Count -ne 1 -or $entry[0].metadata.id -ne $pin.id -or + $entry[0].metadata.expired -or $entry[0].metadata.workflow_run.id -ne 37372848860 -or + $entry[0].metadata.workflow_run.head_sha -ne 'c4953dceffe02a017adad34973e1955017bf5d20' -or + $entry[0].metadata.digest -ne "sha256:$($pin.digest)" -or + (Get-FileHash (Join-Path $Directory "$($pin.id).zip") -Algorithm SHA256 -ErrorAction Stop).Hash -ne $pin.digest) { + throw 'Immutable snapshot archive or original provenance differs from the pinned source.' + } + } + return $proof +} + +function Save-SqlComparisonSnapshot { + param([string]$ArtifactId, [string]$RunId, [string]$HeadSha, + [ValidateSet('W1', 'DE')][string]$Country, [string]$Directory) + if ($ArtifactId -notmatch '^\d{1,20}$' -or $RunId -notmatch '^\d{1,20}$' -or -not $env:GH_TOKEN) { + throw 'Explicit snapshot identity and Actions credential are required.' + } + $headers = @{ Authorization = "Bearer $env:GH_TOKEN"; Accept = 'application/vnd.github+json' } + $uri = "https://api.github.com/repos/microsoft/BCApps/actions/artifacts/$ArtifactId" + $metadata = Invoke-RestMethod $uri -Headers $headers + if ($metadata.expired -or $metadata.workflow_run.id -ne $RunId -or + $metadata.workflow_run.head_sha -ne $HeadSha -or + $metadata.name -ne "sql-api-comparison-packages-$Country-$RunId" -or + $metadata.digest -notmatch '^sha256:([0-9a-f]{64})$') { + throw 'Snapshot must be the immutable country package archive from this comparison revision.' + } + $digest = $Matches[1] + $null = New-Item -ItemType Directory -Path $Directory -Force + $zip = "$Directory.zip" + Invoke-WebRequest "$uri/zip" -Headers $headers -OutFile $zip -UseBasicParsing + if ((Get-FileHash $zip -Algorithm SHA256).Hash -ne $digest) { throw 'Snapshot transport hash mismatch.' } + Expand-Archive $zip $Directory -Force + Assert-SqlComparisonSnapshot -Directory $Directory -Country $Country +} + +Export-ModuleMember -Function Get-SqlComparisonCell, Assert-SqlComparisonReplacement, Get-SqlComparisonArtifact, + Assert-SqlComparisonSnapshot, Save-SqlComparisonSnapshot diff --git a/build/scripts/SqlResetPilot/TwoArm.Tests.ps1 b/build/scripts/SqlResetPilot/ComparisonRunner.Tests.ps1 similarity index 79% rename from build/scripts/SqlResetPilot/TwoArm.Tests.ps1 rename to build/scripts/SqlResetPilot/ComparisonRunner.Tests.ps1 index 4e2a19610a1..882883aff99 100644 --- a/build/scripts/SqlResetPilot/TwoArm.Tests.ps1 +++ b/build/scripts/SqlResetPilot/ComparisonRunner.Tests.ps1 @@ -5,7 +5,7 @@ try { } finally { $env:BC_SQL_PILOT_ARM = $discoveryArm } -Describe 'Exact manual two-arm experiment isolation' { +Describe 'Exact manual three-arm comparison isolation' { BeforeEach { $script:saved = @{} foreach ($key in @('GITHUB_REPOSITORY','GITHUB_REF','GITHUB_EVENT_NAME','GITHUB_RUN_ATTEMPT', @@ -13,11 +13,11 @@ Describe 'Exact manual two-arm experiment isolation' { $script:saved[$key] = [Environment]::GetEnvironmentVariable($key) } $env:GITHUB_REPOSITORY = 'microsoft/BCApps' - $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-two-arm-experiment' + $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-300-trial-comparison' $env:GITHUB_EVENT_NAME = 'workflow_dispatch' $env:GITHUB_RUN_ATTEMPT = '1' $env:BC_SQL_PILOT_ARM = 'control' - $env:BC_SQL_API_EXPERIMENT = 'B' + $env:BC_SQL_API_EXPERIMENT = 'retry' $env:BC_SQL_PILOT_OUTPUT = $TestDrive } AfterEach { @@ -42,14 +42,19 @@ Describe 'Exact manual two-arm experiment isolation' { credential = [PSCredential]::Empty; companyName = 'My Company' } } - It 'adds exactly the one named method only in A and does not mutate baseline config' { - $env:BC_SQL_API_EXPERIMENT = 'A' - $disabled = @(Get-DisabledTestsForApp 'Test App') - $disabled.Count | Should -Be 1 - $disabled[0].codeunitId | Should -Be 148318 - $disabled[0].method | Should -BeExactly 'CapabilitiesProjectsEnabledViaAPI' - $env:BC_SQL_API_EXPERIMENT = 'B' - @(Get-DisabledTestsForApp 'Test App').Count | Should -Be 0 + It 'never adds an exclusion in any arm' { + foreach ($arm in @('control', 'warmup', 'retry')) { + $env:BC_SQL_API_EXPERIMENT = $arm + @(Get-DisabledTestsForApp 'Test App').Count | Should -Be 0 + } + } + It 'enables app and companies warmup only in the two warmup arms' { + $env:BC_SQL_API_EXPERIMENT = 'control' + Test-SqlApiWarmupEnabled | Should -BeFalse + foreach ($arm in @('warmup', 'retry')) { + $env:BC_SQL_API_EXPERIMENT = $arm + Test-SqlApiWarmupEnabled | Should -BeTrue + } } It 'cannot activate on ' -ForEach @( @{ Kind = 'PR'; Key = 'GITHUB_EVENT_NAME'; Value = 'pull_request' } @@ -61,7 +66,7 @@ Describe 'Exact manual two-arm experiment isolation' { [Environment]::SetEnvironmentVariable($Key, $Value) Test-SqlApiExperiment | Should -BeFalse } - It 'queues only a confirmed B failure and never a generic transient' { + It 'queues only a confirmed retry-arm failure and never a generic transient' { $result = [PSCustomObject]@{ Outcome = 'SqlPoolRetry'; AppName = $script:item.Key; Tenant = 'tenant2'; SqlRetryEvidence = @{ TestCases = @('one') } } Register-TestJobOutcome $result $script:state $script:state.transient.Count | Should -Be 1 @@ -71,11 +76,25 @@ Describe 'Exact manual two-arm experiment isolation' { $script:state.transient.Count | Should -Be 1 $script:state.hasFailures | Should -BeTrue } - It 'never queues SQL retry in A' { - $env:BC_SQL_API_EXPERIMENT = 'A' - Register-TestJobOutcome ([PSCustomObject]@{ Outcome = 'SqlPoolRetry' }) $script:state - $script:state.hasFailures | Should -BeTrue - $script:state.transient.Count | Should -Be 0 + It 'never queues SQL retry in control or warmup' { + foreach ($arm in @('control', 'warmup')) { + $env:BC_SQL_API_EXPERIMENT = $arm + Register-TestJobOutcome ([PSCustomObject]@{ Outcome = 'SqlPoolRetry' }) $script:state + $script:state.hasFailures | Should -BeTrue + $script:state.transient.Count | Should -Be 0 + } + } + It 'restores control workers but performs no companies probe' { + $env:BC_SQL_API_EXPERIMENT = 'control' + Mock Reset-BcTestTenant {} + Mock Invoke-SqlPilotCompaniesProbe { throw 'Control must never probe.' } + Mock Start-RequiredDisabledDispatch {} + Mock Wait-ForAllTestJobs {} + Invoke-RequiredDisabledTestExecution $script:parameters @($script:item) @($script:tenant) ` + 'default-test-template' 'runner.ps1' 'IntegrationTest' | Should -BeTrue + Should -Invoke Reset-BcTestTenant -Times 1 -Exactly + Should -Invoke Invoke-SqlPilotCompaniesProbe -Times 0 + Should -Invoke Start-RequiredDisabledDispatch -Times 1 -Exactly } It 'retries the full CU without BCH ReRun and writes to a separate result file' { $script:state.sqlRetryEvidence[$script:item.Key] = @{ TestCases = @(@{ Name = 'one'; Skipped = $false }) } @@ -149,15 +168,17 @@ Describe 'Exact manual two-arm experiment isolation' { } Should -Invoke Get-SqlApiTestRetryEvidence -Times 0 } - It 'rejects silently reenabled A and inherited B exclusions in actual JUnit' { + It 'rejects inherited exclusions in actual JUnit in every arm' { $path = Join-Path $TestDrive 'selection.xml' $context = [PSCustomObject]@{ CodeunitId = 148318; ResultFiles = @{ JUnitResultFileName = $path } } '' | Set-Content $path - Test-SqlApiExperimentSelection $context 'A' | Should -BeTrue - Test-SqlApiExperimentSelection $context 'B' | Should -BeFalse + foreach ($arm in @('control', 'warmup', 'retry')) { + Test-SqlApiExperimentSelection $context $arm | Should -BeFalse + } '' | Set-Content $path - Test-SqlApiExperimentSelection $context 'A' | Should -BeFalse - Test-SqlApiExperimentSelection $context 'B' | Should -BeTrue + foreach ($arm in @('control', 'warmup', 'retry')) { + Test-SqlApiExperimentSelection $context $arm | Should -BeTrue + } } } } diff --git a/build/scripts/SqlResetPilot/ContainerPreflight.ps1 b/build/scripts/SqlResetPilot/ContainerPreflight.ps1 index 4ab3eebc661..927cf6fc551 100644 --- a/build/scripts/SqlResetPilot/ContainerPreflight.ps1 +++ b/build/scripts/SqlResetPilot/ContainerPreflight.ps1 @@ -1,10 +1,11 @@ param([hashtable]$Parameters) $ErrorActionPreference = 'Stop' -if ($env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-two-arm-experiment' -or - $env:BC_SQL_API_EXPERIMENT -notin @('A', 'B') -or +if ($env:GITHUB_REPOSITORY -ne 'microsoft/BCApps' -or + $env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-300-trial-comparison' -or + $env:BC_SQL_API_EXPERIMENT -notin @('control', 'warmup', 'retry') -or $env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or $env:BC_SQL_PILOT_ARM -ne 'control' -or -not $env:BC_SQL_PILOT_OUTPUT -or - $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^[1-5]$' -or + $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^(?:[1-9]|[1-4][0-9]|50)$' -or $env:GITHUB_RUN_ID -notmatch '^\d{1,20}$') { throw 'Refusing SQL pilot outside its disposable manually dispatched CI job.' } diff --git a/build/scripts/SqlResetPilot/Finalize.Tests.ps1 b/build/scripts/SqlResetPilot/Finalize.Tests.ps1 index 412ac9ac889..74a83ef09e1 100644 --- a/build/scripts/SqlResetPilot/Finalize.Tests.ps1 +++ b/build/scripts/SqlResetPilot/Finalize.Tests.ps1 @@ -16,11 +16,11 @@ Describe 'SQL pilot final cleanup ownership' { $env:BC_SQL_PILOT_ARM = 'control' $env:BC_SQL_PILOT_COUNTRY = 'DE' $env:BC_SQL_PILOT_TRIAL = '2' - $env:BC_SQL_API_EXPERIMENT = 'B' + $env:BC_SQL_API_EXPERIMENT = 'retry' $env:BcContainerHelperPath = Join-Path $PSScriptRoot 'BcContainerHelper.ps1' Mock Import-Module {} Mock Test-Path { $true } - Mock Get-Content { '{"runId":"123","arm":"control","country":"DE","trial":"2","experiment":"B","container":"bcbuildprojectsTestAppsDETrial2B123"}' } + Mock Get-Content { '{"runId":"123","arm":"control","country":"DE","trial":"2","experiment":"retry","container":"bcbuildprojectsTestAppsDETrial2retry123"}' } Mock Get-ChildItem {} Mock New-Item {} Mock Copy-Item {} @@ -43,7 +43,7 @@ Describe 'SQL pilot final cleanup ownership' { It 'exports before disposing only the registered container' { & (Join-Path $PSScriptRoot 'Finalize.ps1') Should -Invoke Get-BcContainerEventLog -Times 1 -Exactly -Scope It - Should -Invoke Remove-BcContainer -Times 1 -Exactly -Scope It -ParameterFilter { $containerName -eq 'bcbuildprojectsTestAppsDETrial2B123' } + Should -Invoke Remove-BcContainer -Times 1 -Exactly -Scope It -ParameterFilter { $containerName -eq 'bcbuildprojectsTestAppsDETrial2retry123' } } It 'still tears down if exporting fails and keeps that failure visible' { Mock Get-BcContainerEventLog { throw 'event export failed' } @@ -54,7 +54,7 @@ Describe 'SQL pilot final cleanup ownership' { Mock Get-ChildItem { [PSCustomObject]@{ FullName = 'TestResults.xml' } } & (Join-Path $PSScriptRoot 'Finalize.ps1') Should -Invoke Get-ChildItem -Times 1 -Exactly -Scope It -ParameterFilter { - $Path -like '*Test Apps DE Trial2B\.buildartifacts' -and $Filter -eq 'TestResults*.xml' -and $File + $Path -like '*Test Apps DE Trial2retry\.buildartifacts' -and $Filter -eq 'TestResults*.xml' -and $File } Should -Invoke Copy-Item -Times 1 -Exactly -Scope It -ParameterFilter { $Destination -like '*clean-results\buildartifacts' diff --git a/build/scripts/SqlResetPilot/Finalize.ps1 b/build/scripts/SqlResetPilot/Finalize.ps1 index ec5e15bf5d0..b9a3c03c616 100644 --- a/build/scripts/SqlResetPilot/Finalize.ps1 +++ b/build/scripts/SqlResetPilot/Finalize.ps1 @@ -2,13 +2,13 @@ $ErrorActionPreference = 'Stop' $output = Join-Path $env:GITHUB_WORKSPACE 'sql-reset-pilot-output' $ownershipPath = Join-Path $output 'container-ownership.json' if (-not (Test-Path $ownershipPath)) { - Write-Host 'No owned container was registered; no container deletion is authorized.' + Write-Output 'No owned container was registered; no container deletion is authorized.' return } $ownership = Get-Content $ownershipPath -Raw | ConvertFrom-Json if ($ownership.runId -ne $env:GITHUB_RUN_ID -or $ownership.arm -ne $env:BC_SQL_PILOT_ARM -or - $env:BC_SQL_API_EXPERIMENT -notin @('A', 'B') -or $ownership.experiment -ne $env:BC_SQL_API_EXPERIMENT -or - $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^[1-5]$' -or + $env:BC_SQL_API_EXPERIMENT -notin @('control', 'warmup', 'retry') -or $ownership.experiment -ne $env:BC_SQL_API_EXPERIMENT -or + $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^(?:[1-9]|[1-4][0-9]|50)$' -or $ownership.country -ne $env:BC_SQL_PILOT_COUNTRY -or $ownership.trial -ne $env:BC_SQL_PILOT_TRIAL -or $ownership.container -ne "bcbuildprojectsTestApps$($env:BC_SQL_PILOT_COUNTRY)Trial$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)$($env:GITHUB_RUN_ID)") { throw 'Cleanup ownership mismatch.' diff --git a/build/scripts/SqlResetPilot/Lifecycle.psm1 b/build/scripts/SqlResetPilot/Lifecycle.psm1 index db975202184..0acbc839510 100644 --- a/build/scripts/SqlResetPilot/Lifecycle.psm1 +++ b/build/scripts/SqlResetPilot/Lifecycle.psm1 @@ -123,18 +123,17 @@ function Select-SqlPilotPrefix { $ids = @(139700,139702,139703,139706,139725,139726,139732,139739,139742,139745,139802,139803,139806,139826,139832,139854,139972,139780,148315,148318,148343) if ($WorkItems.Count -lt $ids.Count) { throw 'Discovery returned an incomplete pilot prefix.' } for ($i = 0; $i -lt $ids.Count; $i++) { - if ([int]$WorkItems[$i].CodeunitId -ne $ids[$i]) { throw "Pinned 21-codeunit discovery order differs at position $i; do not reorder or reduce the cohort." } + if ([int]$WorkItems[$i].CodeunitId -ne $ids[$i]) { throw "Original W1 discovery order differs at position $i." } } @($WorkItems | Select-Object -First $ids.Count) } <# .SYNOPSIS - Probes companies from the host after an owned worker restore, with bounded readiness retries. + Makes one authenticated companies request from the host after an owned worker restore. .DESCRIPTION Uses the disposable container's existing password credential and private API endpoint. - Only transient HTTP and connection/timeout failures may retry, up to three attempts. - No redirects, response bodies, passwords or authorization headers are logged. + No retries, redirects, response bodies, passwords or authorization headers are logged. #> function Invoke-SqlPilotCompaniesProbe { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingAllowUnencryptedAuthentication', '', @@ -150,14 +149,12 @@ function Invoke-SqlPilotCompaniesProbe { $script:pilot.Workers[$Tenant].Generation -lt 1) { throw 'Companies probe requires an owned, freshly restored worker.' } - $maximumAttempts = if ($env:BC_SQL_API_EXPERIMENT -in @('A', 'B')) { 1 } else { 3 } $trace = Join-Path $script:pilot.OutputDirectory 'companies-probes.jsonl' $observation = @{ tenant = $Tenant; generation = $script:pilot.Workers[$Tenant].Generation startedUtc = [DateTime]::UtcNow.ToString('o'); status = $null; passed = $false clientRequestId = [guid]::NewGuid().ToString(); serverRequestId = $null - uri = $null; retries = 0; attempts = 0; recordType = 'outcome' - outcome = 'failed'; recovered = $false; maximumAttempts = $maximumAttempts + uri = $null; retries = 0 } $clock = [Diagnostics.Stopwatch]::StartNew() try { @@ -174,84 +171,26 @@ function Invoke-SqlPilotCompaniesProbe { $uri = [UriBuilder]::new($scheme, $ip, [int]$config.ODataServicesPort, "$($config.ServerInstance)/api/v2.0/companies", "?tenant=$Tenant").Uri.AbsoluteUri $observation.uri = $uri - for ($attemptNumber = 1; $attemptNumber -le $maximumAttempts; $attemptNumber++) { - $attempt = @{ - recordType = 'attempt'; probeId = $observation.clientRequestId - tenant = $Tenant; generation = $observation.generation; uri = $uri - attempt = $attemptNumber; startedUtc = [DateTime]::UtcNow.ToString('o') - status = $null; passed = $false; retryable = $false - clientRequestId = [guid]::NewGuid().ToString(); serverRequestId = $null - } - $attemptClock = [Diagnostics.Stopwatch]::StartNew() - try { - $response = Invoke-WebRequest -Uri $uri -Method Get -Authentication Basic -Credential $Credential ` - -AllowUnencryptedAuthentication -SkipCertificateCheck -SkipHttpErrorCheck -MaximumRedirection 0 ` - -MaximumRetryCount 0 -TimeoutSec 20 -OperationTimeoutSeconds 20 -ErrorAction Stop ` - -Headers @{ Accept = 'application/json'; 'client-request-id' = $attempt.clientRequestId } - $attempt.status = [int]$response.StatusCode - foreach ($key in @('request-id', 'x-ms-request-id')) { - $value = [string]($response.Headers[$key] | Select-Object -First 1) - if ($value -match '^[a-zA-Z0-9._:-]{1,128}$') { $attempt.serverRequestId = $value; break } - } - if ($attempt.status -ne 200) { - $attempt.errorCategory = 'HttpStatus' - $attempt.retryable = $attempt.status -in @(500, 502, 503, 504) - } else { - $body = $response.Content | ConvertFrom-Json -ErrorAction Stop - if (-not $body.PSObject.Properties['value'] -or - @($body.value | Where-Object { $_.name -eq $CompanyName -and $_.id }).Count -ne 1) { - throw 'Companies probe did not return exactly one expected company.' - } - $attempt.passed = $true - } - } catch { - $attempt.errorType = $_.Exception.GetType().FullName - $attempt.errorCategory = 'PermanentFailure' - # Inspect types, never exception messages (which may contain authentication material). - if ($null -eq $attempt.status) { - $exception = $_.Exception - while ($null -ne $exception) { - if ($exception -is [TimeoutException] -or - $exception -is [OperationCanceledException] -or - ($exception -is [Net.Http.HttpRequestException] -and - $exception.PSObject.Properties['HttpRequestError'] -and - $exception.HttpRequestError -in @('ConnectionError', 'NameResolutionError', 'ResponseEnded')) -or - ($exception -is [Net.Sockets.SocketException] -and - $exception.SocketErrorCode -in @('ConnectionRefused', 'ConnectionReset', 'ConnectionAborted', - 'HostNotFound', 'HostUnreachable', 'NetworkDown', 'NetworkReset', 'NetworkUnreachable', 'TimedOut', 'TryAgain')) -or - ($exception -is [Net.WebException] -and - $exception.Status -in @('Timeout', 'ConnectFailure', 'ConnectionClosed', 'KeepAliveFailure', - 'ReceiveFailure', 'SendFailure', 'NameResolutionFailure', 'ProxyNameResolutionFailure'))) { - $attempt.retryable = $true - $attempt.errorCategory = 'ConnectionOrTimeout' - break - } - $exception = $exception.InnerException - } - } - } finally { - $attempt.completedUtc = [DateTime]::UtcNow.ToString('o') - $attempt.elapsedMilliseconds = $attemptClock.ElapsedMilliseconds - $attempt | ConvertTo-Json -Compress | Add-Content $trace - $observation.attempts = $attemptNumber - $observation.retries = $attemptNumber - 1 - $observation.status = $attempt.status - $observation.serverRequestId = $attempt.serverRequestId - } - if ($attempt.passed) { - $observation.passed = $true - $observation.recovered = $attemptNumber -gt 1 - $observation.outcome = if ($observation.recovered) { 'recovered' } else { 'first-try-success' } - break - } - if (-not $attempt.retryable -or $attemptNumber -eq $maximumAttempts) { break } - Start-Sleep -Seconds 2 + $response = Invoke-WebRequest -Uri $uri -Method Get -Authentication Basic -Credential $Credential ` + -AllowUnencryptedAuthentication -SkipCertificateCheck -SkipHttpErrorCheck -MaximumRedirection 0 ` + -MaximumRetryCount 0 -TimeoutSec 60 -ErrorAction Stop ` + -Headers @{ Accept = 'application/json'; 'client-request-id' = $observation.clientRequestId } + $observation.status = [int]$response.StatusCode + foreach ($key in @('request-id', 'x-ms-request-id')) { + $value = [string]($response.Headers[$key] | Select-Object -First 1) + if ($value -match '^[a-zA-Z0-9._:-]{1,128}$') { $observation.serverRequestId = $value; break } + } + if ($response.StatusCode -ne 200) { throw 'Companies probe returned non-200 status.' } + $body = $response.Content | ConvertFrom-Json -ErrorAction Stop + if (-not $body.PSObject.Properties['value'] -or + @($body.value | Where-Object { $_.name -eq $CompanyName -and $_.id }).Count -ne 1) { + throw 'Companies probe did not return exactly one expected company.' } - if (-not $observation.passed) { throw 'Companies probe exhausted eligible attempts or encountered a permanent failure.' } + $observation.passed = $true } catch { $observation.errorType = $_.Exception.GetType().FullName # HTTP exception details can contain authentication material; never emit the original error. - throw "Companies probe failed for $Tenant after $($observation.attempts) attempt(s); see sanitized companies-probes.jsonl." + throw "Companies probe failed for $Tenant; see sanitized companies-probes.jsonl. No retry was attempted." } finally { $observation.completedUtc = [DateTime]::UtcNow.ToString('o') $observation.elapsedMilliseconds = $clock.ElapsedMilliseconds diff --git a/build/scripts/SqlResetPilot/PlanComparison.ps1 b/build/scripts/SqlResetPilot/PlanComparison.ps1 new file mode 100644 index 00000000000..8ed8f941ee4 --- /dev/null +++ b/build/scripts/SqlResetPilot/PlanComparison.ps1 @@ -0,0 +1,88 @@ +$ErrorActionPreference = 'Stop' +if ($env:GITHUB_REPOSITORY -ne 'microsoft/BCApps' -or + $env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-300-trial-comparison' -or + $env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or + $env:GITHUB_RUN_ID -notmatch '^\d{1,20}$' -or $env:GITHUB_SHA -notmatch '^[0-9a-f]{40}$') { + throw 'Comparison planning is restricted to the exact manual diagnostic branch, attempt one.' +} +Import-Module (Join-Path $PSScriptRoot 'Comparison.psm1') -Force +function Read-GitHubJson([string]$Path) { + $text = & gh api $Path + if ($LASTEXITCODE -ne 0) { throw "GitHub metadata unavailable for $Path; refusing to schedule." } + $text | ConvertFrom-Json +} +$cells = @(Get-SqlComparisonCell -Mode $env:COMPARISON_MODE -Arm $env:REPLACEMENT_ARM ` + -Country $env:REPLACEMENT_COUNTRY -Trial $env:REPLACEMENT_TRIAL) +$replacement = $null +$directory = Join-Path $env:GITHUB_WORKSPACE '.sql-api-comparison-plan' +$null = New-Item -ItemType Directory -Path $directory -Force +if ($env:COMPARISON_MODE -eq 'replacement') { + if ($env:ORIGINAL_RUN -notmatch '^\d{1,20}$' -or $env:ORIGINAL_JOB -notmatch '^\d{1,20}$') { + throw 'Replacement requires explicit original run and job IDs.' + } + $run = Read-GitHubJson "repos/microsoft/BCApps/actions/runs/$env:ORIGINAL_RUN" + $job = Read-GitHubJson "repos/microsoft/BCApps/actions/jobs/$env:ORIGINAL_JOB" + if ($job.check_run_url -notmatch '^https://api\.github\.com/repos/microsoft/BCApps/check-runs/(\d+)$') { + throw 'Unexpected check-run URL.' + } + $annotations = @(Read-GitHubJson "repos/microsoft/BCApps/check-runs/$($Matches[1])/annotations?per_page=100") + $replacement = Assert-SqlComparisonReplacement -Run $run -Job $job -Annotations $annotations ` + -Cell $cells[0] -ExpectedSha $env:GITHUB_SHA +} elseif ($env:ORIGINAL_RUN -or $env:ORIGINAL_JOB) { + throw 'Original schedule cannot specify previous runs or jobs.' +} +$proof = @() +if ($replacement) { + $artifacts = Read-GitHubJson "repos/microsoft/BCApps/actions/runs/$env:ORIGINAL_RUN/artifacts?per_page=100&name=sql-api-comparison-packages-$($env:REPLACEMENT_COUNTRY)-$env:ORIGINAL_RUN" + $snapshot = @($artifacts.artifacts) + if ($snapshot.Count -ne 1) { throw 'Expected exactly one original comparison country snapshot.' } + $proof = @(Save-SqlComparisonSnapshot -ArtifactId $snapshot[0].id -RunId $env:ORIGINAL_RUN ` + -HeadSha $env:GITHUB_SHA -Country $env:REPLACEMENT_COUNTRY -Directory (Join-Path $directory $env:REPLACEMENT_COUNTRY)) + "w1Package=$(if ($env:REPLACEMENT_COUNTRY -eq 'W1') { $snapshot[0].id } else { '0' })" | Add-Content $env:GITHUB_OUTPUT + "dePackage=$(if ($env:REPLACEMENT_COUNTRY -eq 'DE') { $snapshot[0].id } else { '0' })" | Add-Content $env:GITHUB_OUTPUT + "packageRun=$env:ORIGINAL_RUN" | Add-Content $env:GITHUB_OUTPUT +} else { + if (-not $env:GH_TOKEN) { throw 'Actions credential is required to preserve immutable source archives.' } + $headers = @{ Authorization = "Bearer $env:GH_TOKEN"; Accept = 'application/vnd.github+json' } + foreach ($pin in Get-SqlComparisonArtifact) { + $metadata = Read-GitHubJson "repos/microsoft/BCApps/actions/artifacts/$($pin.id)" + if ($metadata.expired -or $metadata.workflow_run.id -ne 37372848860 -or + $metadata.workflow_run.head_sha -ne 'c4953dceffe02a017adad34973e1955017bf5d20' -or + $metadata.digest -ne "sha256:$($pin.digest)") { + throw "Pinned artifact $($pin.id) is expired or differs; no fallback permitted." + } + $destination = Join-Path $directory $pin.country + $null = New-Item -ItemType Directory -Path $destination -Force + $zip = Join-Path $destination "$($pin.id).zip" + Invoke-WebRequest "https://api.github.com/repos/microsoft/BCApps/actions/artifacts/$($pin.id)/zip" ` + -Headers $headers -OutFile $zip -UseBasicParsing + if ((Get-FileHash $zip -Algorithm SHA256).Hash -ne $pin.digest) { throw 'Source archive hash mismatch.' } + $proof += @{ pin = $pin; metadata = $metadata; verifiedUtc = [datetime]::UtcNow.ToString('o') } + } + foreach ($country in @('W1', 'DE')) { + $countryProof = @($proof | Where-Object { $_.pin.country -eq $country }) + ConvertTo-Json -InputObject $countryProof -Depth 12 | + Set-Content (Join-Path $directory "$country\artifact-proof.json") -Encoding utf8 + $null = Assert-SqlComparisonSnapshot -Directory (Join-Path $directory $country) -Country $country + } + "packageRun=$env:GITHUB_RUN_ID" | Add-Content $env:GITHUB_OUTPUT +} +$manifest = @{ + run = $env:GITHUB_RUN_ID; head = $env:GITHUB_SHA; mode = $env:COMPARISON_MODE + createdUtc = [datetime]::UtcNow.ToString('o'); originalCellCount = $(if ($replacement) { 0 } else { $cells.Count }) + replacementOf = $replacement; cells = $cells; globalContainerLimit = 6 + packagePolicy = 'Original ZIP bytes and source metadata preserved in immutable 14-day country snapshots; SHA256 verified on every download. No rebuild or package substitution.' + batchCount = $(if ($replacement) { 1 } else { 10 }); cellsPerOriginalBatch = 30 + expectedCompleteCohort = @{ codeunits = 21; cases = 253; skipped = 19; addedExclusions = 0 } + outcomePolicy = 'Original first-attempt failures, retries, final failures and communication-invalid jobs remain distinct; never count skipped/cancelled/incomplete jobs as passes.' +} +$manifest | ConvertTo-Json -Depth 12 | Set-Content (Join-Path $directory 'matrix-manifest.json') -Encoding utf8 +ConvertTo-Json -InputObject @($proof) -Depth 12 | Set-Content (Join-Path $directory 'artifact-proof.json') -Encoding utf8 +"mode=$env:COMPARISON_MODE" | Add-Content $env:GITHUB_OUTPUT +"replacement=$(ConvertTo-Json -InputObject $replacement -Depth 8 -Compress)" | Add-Content $env:GITHUB_OUTPUT +foreach ($batch in 1..10) { + $selected = @($cells | Select-Object -Skip (($batch - 1) * 30) -First 30) + $matrix = @{ include = $selected } | ConvertTo-Json -Depth 6 -Compress + "batch$($batch.ToString('00'))=$matrix" | Add-Content $env:GITHUB_OUTPUT +} +Write-Output "Validated $($cells.Count) distinct scheduled cells; global container limit six. No automatic communication replacements." diff --git a/build/scripts/SqlResetPilot/Preflight.Tests.ps1 b/build/scripts/SqlResetPilot/Preflight.Tests.ps1 index 0b8e806fce1..51cd9a618b7 100644 --- a/build/scripts/SqlResetPilot/Preflight.Tests.ps1 +++ b/build/scripts/SqlResetPilot/Preflight.Tests.ps1 @@ -5,12 +5,13 @@ BeforeAll { Describe 'Experiment country and trial container ownership' { BeforeEach { $script:saved = @{} - foreach ($name in @('GITHUB_REF','GITHUB_EVENT_NAME','GITHUB_RUN_ATTEMPT','GITHUB_RUN_ID', + foreach ($name in @('GITHUB_REPOSITORY','GITHUB_REF','GITHUB_EVENT_NAME','GITHUB_RUN_ATTEMPT','GITHUB_RUN_ID', 'BC_SQL_PILOT_ARM','BC_SQL_PILOT_COUNTRY','BC_SQL_PILOT_TRIAL','BC_SQL_PILOT_OUTPUT','BC_SQL_API_EXPERIMENT','Settings')) { $script:saved[$name] = [Environment]::GetEnvironmentVariable($name) } - $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-two-arm-experiment' - $env:BC_SQL_API_EXPERIMENT = 'B' + $env:GITHUB_REPOSITORY = 'microsoft/BCApps' + $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-300-trial-comparison' + $env:BC_SQL_API_EXPERIMENT = 'retry' $env:GITHUB_EVENT_NAME = 'workflow_dispatch' $env:GITHUB_RUN_ATTEMPT = '1' $env:GITHUB_RUN_ID = '123' @@ -23,7 +24,7 @@ Describe 'Experiment country and trial container ownership' { companyName = 'My Company'; enableCleanTestCodeunitExecution = $true; enableTaskScheduler = $false } | ConvertTo-Json $script:parameters = @{ - ContainerName = 'bcbuildprojectsTestAppsDETrial2B123' + ContainerName = 'bcbuildprojectsTestAppsDETrial2retry123' platformArtifactUrl = 'https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/platform/30.0.55665.0/platform' } Mock Get-Module { { '6.1.19-preview2811389' } } -ParameterFilter { $Name -eq 'BcContainerHelper' } @@ -37,8 +38,8 @@ Describe 'Experiment country and trial container ownership' { $record = Get-Content (Join-Path $TestDrive 'container-ownership.json') -Raw | ConvertFrom-Json $record.country | Should -Be 'DE' $record.trial | Should -Be '2' - $record.container | Should -Be 'bcbuildprojectsTestAppsDETrial2B123' - $record.experiment | Should -Be 'B' + $record.container | Should -Be 'bcbuildprojectsTestAppsDETrial2retry123' + $record.experiment | Should -Be 'retry' $script:parameters.useGenericImage | Should -Be 'mcr.microsoft.com/businesscentral@sha256:c899d12093ad7bbdbfd08ccc0e6294e0f98c682c7e35db4ecfbca345fb068492' } It 'refuses a preexisting container rather than replacing it' { @@ -69,4 +70,22 @@ Describe 'Experiment country and trial container ownership' { $env:Settings = $settings | ConvertTo-Json { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw '*settings differ*' } + It 'allows trial 50 but never trial 51 even with matching container names' { + $env:BC_SQL_PILOT_TRIAL = '50' + $script:parameters.ContainerName = 'bcbuildprojectsTestAppsDETrial50retry123' + & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters + $env:BC_SQL_PILOT_TRIAL = '51' + $script:parameters.ContainerName = 'bcbuildprojectsTestAppsDETrial51retry123' + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw + } + It 'rejects obsolete exclusion arms and other branches or events' { + $env:BC_SQL_API_EXPERIMENT = 'A' + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw + $env:BC_SQL_API_EXPERIMENT = 'retry' + $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-two-arm-experiment' + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw + $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-300-trial-comparison' + $env:GITHUB_EVENT_NAME = 'pull_request' + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw + } } diff --git a/build/scripts/SqlResetPilot/Prepare.ps1 b/build/scripts/SqlResetPilot/Prepare.ps1 index ccdeac21bca..e77ada3dda4 100644 --- a/build/scripts/SqlResetPilot/Prepare.ps1 +++ b/build/scripts/SqlResetPilot/Prepare.ps1 @@ -1,11 +1,11 @@ $ErrorActionPreference = 'Stop' $base = 'c4953dceffe02a017adad34973e1955017bf5d20' if ($env:GITHUB_REPOSITORY -ne 'microsoft/BCApps' -or - $env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-two-arm-experiment' -or - $env:BC_SQL_API_EXPERIMENT -notin @('A', 'B') -or + $env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-300-trial-comparison' -or + $env:BC_SQL_API_EXPERIMENT -notin @('control', 'warmup', 'retry') -or $env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or $env:BC_SQL_PILOT_ARM -ne 'control' -or $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or - $env:BC_SQL_PILOT_TRIAL -notmatch '^[1-5]$' -or $env:GITHUB_RUN_ID -notmatch '^\d{1,20}$') { + $env:BC_SQL_PILOT_TRIAL -notmatch '^(?:[1-9]|[1-4][0-9]|50)$' -or $env:GITHUB_RUN_ID -notmatch '^\d{1,20}$') { throw 'This diagnostic is restricted to its explicitly dispatched disposable CI branch, attempt one.' } git merge-base --is-ancestor $base HEAD @@ -25,8 +25,14 @@ New-Item -ItemType Directory $output -Force | Out-Null "BC_SQL_PILOT_OUTPUT=$output" | Add-Content $env:GITHUB_ENV @{ phase = 'package-preflight'; arm = $env:BC_SQL_PILOT_ARM; run = $env:GITHUB_RUN_ID; utc = [DateTime]::UtcNow.ToString('o') } | ConvertTo-Json | Set-Content (Join-Path $output 'start.json') -Encoding UTF8 -$headers = @{ Authorization = "Bearer $env:GH_TOKEN"; Accept = 'application/vnd.github+json' } -$api = 'https://api.github.com/repos/microsoft/BCApps' +Import-Module (Join-Path $PSScriptRoot 'Comparison.psm1') -Force +$replacement = $env:BC_SQL_COMPARISON_REPLACEMENT | ConvertFrom-Json +$expectedSnapshotRun = if ($replacement) { [string]$replacement.runId } else { $env:GITHUB_RUN_ID } +if ($env:BC_SQL_COMPARISON_PACKAGE_RUN -ne $expectedSnapshotRun) { throw 'Snapshot source run differs from original/replacement provenance.' } +$snapshotDirectory = Join-Path $env:GITHUB_WORKSPACE 'sql-api-package-snapshot' +$snapshotProof = @(Save-SqlComparisonSnapshot -ArtifactId $env:BC_SQL_COMPARISON_PACKAGE_ARTIFACT ` + -RunId $expectedSnapshotRun -HeadSha $env:GITHUB_SHA -Country $env:BC_SQL_PILOT_COUNTRY -Directory $snapshotDirectory) +$snapshotProof | ConvertTo-Json -Depth 12 | Set-Content (Join-Path $output 'original-artifact-proof.json') $artifacts = @( @{ Kind = 'apps'; Id = 11374170356; Digest = '683343673e6efeb7699734b250d2ee5004d2d83fe08e57f324c4c645c09612db' }, @{ Kind = 'tests'; Id = 11374145359; Digest = 'f75ce06920437328e5d79a5512eddcc2425624bb696c339c108478c83554171d' } @@ -39,15 +45,14 @@ if ($env:BC_SQL_PILOT_COUNTRY -eq 'DE') { } $manifest = @() foreach ($artifact in $artifacts) { - $metadata = Invoke-RestMethod "$api/actions/artifacts/$($artifact.Id)" -Headers $headers + $metadata = @($snapshotProof | Where-Object { $_.pin.id -eq [string]$artifact.Id })[0].metadata if ($metadata.expired -or $metadata.workflow_run.id -ne 37372848860 -or $metadata.workflow_run.head_sha -ne $base -or $metadata.digest -ne "sha256:$($artifact.Digest)") { throw 'Exact artifact provenance/digest check failed; no moving-build fallback is permitted.' } $destination = Join-Path $env:GITHUB_WORKSPACE "sql-reset-pilot-packages\$($artifact.Kind)" New-Item -ItemType Directory $destination -Force | Out-Null - $zip = "$destination.zip" - Invoke-WebRequest "$api/actions/artifacts/$($artifact.Id)/zip" -Headers $headers -OutFile $zip -UseBasicParsing + $zip = Join-Path $snapshotDirectory "$($artifact.Id).zip" if ((Get-FileHash $zip -Algorithm SHA256).Hash -ne $artifact.Digest) { throw 'Artifact archive hash mismatch.' } Expand-Archive $zip $destination $files = @(Get-ChildItem $destination -Filter '*.app' -Recurse -File | Sort-Object FullName) @@ -63,12 +68,17 @@ foreach ($artifact in $artifacts) { } @{ experimentHead = $env:GITHUB_SHA; run = $env:GITHUB_RUN_ID; arm = $env:BC_SQL_PILOT_ARM - experiment = $env:BC_SQL_API_EXPERIMENT; baseline = '8a7ebc7d665c0a812c2b3b4902c63df570959954' + experiment = $env:BC_SQL_API_EXPERIMENT; sharedBaseline = 'fbd7ec46c636ee5f9d940cb81e5abbc1df90f055' + probeAndWarmupBaseline = 'fcc1776c6b165199dd66e4227675b1cc31da7a8f' + trialIdentity = "$($env:BC_SQL_API_EXPERIMENT)/$($env:BC_SQL_PILOT_COUNTRY)/$($env:BC_SQL_PILOT_TRIAL)" + replacementOf = $replacement + packageSnapshot = @{ runId = $expectedSnapshotRun; artifactId = $env:BC_SQL_COMPARISON_PACKAGE_ARTIFACT } country = $env:BC_SQL_PILOT_COUNTRY; trial = $env:BC_SQL_PILOT_TRIAL; project = $trialProject - warmup = 'original-first-app-before-clean-lane'; companiesProbe = 'per-restored-worker-single-attempt' - companiesProbeMaximumAttempts = 1; companiesProbeTimeoutSeconds = 20; companiesProbeRetryDelaySeconds = 0 - additionalDisabledTest = $(if ($env:BC_SQL_API_EXPERIMENT -eq 'A') { '148318.CapabilitiesProjectsEnabledViaAPI' } else { $null }) - warmupRetries = 0; maximumEvidenceGatedRetriesPerCodeunit = $(if ($env:BC_SQL_API_EXPERIMENT -eq 'B') { 1 } else { 0 }) + warmup = $(if ($env:BC_SQL_API_EXPERIMENT -eq 'control') { 'none' } else { 'original-first-app-before-clean-lane' }) + companiesProbe = $(if ($env:BC_SQL_API_EXPERIMENT -eq 'control') { 'none' } else { 'per-restored-worker-single-attempt' }) + companiesProbeMaximumAttempts = 1; companiesProbeTimeoutSeconds = 60; companiesProbeRetryDelaySeconds = 0 + additionalDisabledTests = @() + warmupRetries = 0; maximumEvidenceGatedRetriesPerCodeunit = $(if ($env:BC_SQL_API_EXPERIMENT -eq 'retry') { 1 } else { 0 }) genericTestRetries = 0; schedulerRetries = 0; ciRetries = 0 budgetMinutes = 120; startedUtc = [DateTime]::UtcNow.ToString('o') artifacts = $manifest diff --git a/build/scripts/SqlResetPilot/README.md b/build/scripts/SqlResetPilot/README.md index 7a4a52293ec..d0cc7f55b80 100644 --- a/build/scripts/SqlResetPilot/README.md +++ b/build/scripts/SqlResetPilot/README.md @@ -1,99 +1,131 @@ -# SQL API two-arm diagnostic — AB#646383 — DO NOT MERGE - -One manual `CICD.yaml` dispatch on `features/646383-sql-api-two-arm-experiment` -runs **A/B × W1/DE × five trials = 20 independent disposable containers**. -Maximum parallelism is **five globally**, timeout 120 minutes per cell (2,400 -runner-minutes maximum). Exact branch and run-attempt-one guards reject reruns. -The exact experiment PR head is excluded from automatic PR Initialization; -there is no full-country pipeline, job retry, replacement run, or local NST work. - -## Shared immutable baseline - -Both arms start at warmup head `8a7ebc7d665c0a812c2b3b4902c63df570959954`. -AL/package source remains **c4953dceffe02a017adad34973e1955017bf5d20**. -`Prepare.ps1` verifies ancestry, no source diff, exact source run **37372848860**, -artifact metadata/digests, ZIP SHA256 and package hashes. Missing/expired -artifacts fail closed; there is no rebuild or moving-package fallback. +# SQL API 300-trial diagnostic — AB#646383 — DO NOT MERGE + +Explicit user authorization: 2026-10-07. One manual `CICD.yaml` dispatch on +`features/646383-sql-api-300-trial-comparison` with `mode=originals` schedules +**3 arms × W1/DE × 50 indices = 300 original disposable-container trials**. +The separate historical DE replacement `37630750803` is not part of these 300. +No exclusion arm, automatic job rerun, production fix, shared NST, or AL changes. + +## Arms + +| Identity | Pre-clean first-app warmup | One companies probe after each worker restore | Evidence-qualified whole-CU retry | +| --- | --- | --- | --- | +| control | No | No | No | +| warmup | Yes | Yes | No | +| retry | Yes | Yes | At most once per CU | + +All arms use the same discovery, pristine-template freeze, owned worker reset, +21-CU order, four tenants (default plus three workers), ordinary baseline skip +configuration, artifacts and diagnostic instrumentation. The common source is +`fbd7ec46c636ee5f9d940cb81e5abbc1df90f055`, with obsolete method exclusion removed. +`Lifecycle.psm1` and its probe tests are restored exactly from original warmup +`fcc1776c6b165199dd66e4227675b1cc31da7a8f`: **one request, TimeoutSec 60, +MaximumRetryCount 0**, no probe retry. Both warmup arms use that identical code. +The template is frozen before first-app warmup on default. Each three-worker +batch completes all restores (then all probes, when enabled) before test dispatch. +As in the original pilot, terminal failures drain the current batch and stop +later CUs; incomplete trials are never passes. This rule is identical across arms. + +## Scheduling and resource bound + +`PlanComparison.ps1` validates pins and writes the canonical manifest before +any containers start. Ten sequential reusable-workflow batches each contain +30 cells (five indices × two countries × three arms), below the 256 matrix limit. +Each batch has `fail-fast: false`, maximum parallelism **six**, and depends on +completion of the preceding entire batch. Failed trials do not skip subsequent +batches. Queue order rotates arms and countries across balanced six-cell index +blocks; runner placement/execution order is not guaranteed and remains a possible +confound to report. Workflow-level concurrency serializes this comparison and +its explicit replacements; it does not cancel unrelated work. + +Each cell has a 120-minute timeout. Worst-case allocation is 36,000 runner-minutes, +with approximately 100 hours of test execution at the six-container ceiling; +actual queues, setup and trial durations determine elapsed time. The requested +300 are not reduced to fit an EOD target. No guarantee of finishing by tomorrow. + +Exact repository, branch, manual event and `run_attempt == 1` gates protect +planning, execution and provisioning. Automatic PR Initialization excludes this +exact branch. No PR is necessary for dispatch; existing draft PRs remain untouched. + +## Immutable packages and platform + +AL/package source **c4953dceffe02a017adad34973e1955017bf5d20**, source run +**37372848860**. `Prepare.ps1` checks ancestry, no source diff, artifact metadata, +archive SHA256 and per-package hashes. Plan checks and downloads all four original +artifacts while unexpired, then uploads their **unchanged ZIP bytes** and original +metadata into immutable, 14-day W1/DE snapshot artifacts on this run. This avoids +the original artifacts' 2026-10-08 expiry cutting off later bounded batches. +Every cell validates its snapshot's exact run/revision/name, transport digest, +original artifact IDs/source/digests, inner ZIP hashes, and extracted package hashes. +There is no rebuild or package substitution. Explicit replacements reuse the +original comparison's country snapshot, not a newer build. Snapshot expiry/drift +fails closed. Package archive IDs remain those of the original source in provenance; +the separate snapshot artifact/run IDs are additionally recorded. | Country | App artifact | Test artifact | | --- | --- | --- | | W1 | 11374170356 | 11374145359 | | DE | 11376415128 | 11375174896 | -NST **30.0.55665.0**, application **30.0.55683.0**, BCH +Exact SHA256 pins are recorded in `Comparison.psm1`, `Prepare.ps1`, and uploaded +`artifact-proof.json`. NST **30.0.55665.0**, application **30.0.55683.0**, BCH **6.1.19-preview2811389**, AL-Go `91b96c2b294be6f823277dafe6f03350abfb9d23`, generic image `sha256:c899d12093ad7bbdbfd08ccc0e6294e0f98c682c7e35db4ecfbca345fb068492`, -PowerShell 7, four tenants, existing password-file authentication and `My Company` -are unchanged. No auth/provider changes or AL assertion changes. - -Both arms freeze the pristine template before ordinary first-app warmup, then -restore each clean batch under the original tenant database names, probe every -restored worker, and only then dispatch. Companies probes use **one attempt**, -20-second connect/read timeouts, no hidden HTTP retries or redirects. This -disables the optional three-attempt readiness behavior added after the original -warmup run `37608087700` at `fcc1776c6b165199dd66e4227675b1cc31da7a8f`. -The newer baseline's nested `.buildartifacts` collection is retained. - -## Treatments - -**A — exclude one method:** the existing `Get-DisabledTestsForApp` runner config -adds only `{codeunitId:148318, method:CapabilitiesProjectsEnabledViaAPI}`. -No source or package is edited; the whole Capabilities codeunit stays selected. -All existing baseline disabled tests remain unchanged. No test retries. - -**B — all baseline-enabled tests remain enabled:** no additional disabled entry. -At most **one whole-codeunit retry**, and only after every failed JUnit case has: - -* The observed GET HTTP500/null-reference response and a valid CorrelationId. -* An NST Application event 701 from `MicrosoftDynamicsNavServer$...`, obtained - from this container during this dispatch's UTC time window. -* Matching `ClientSessionId`, `RootException: NullReferenceException`, and frames - `NavSqlConnectionScope.AcquireSqlConnectionFromPool`, - `MetadataProvider.GetRelativeHelpUrl`, and `PageDataProvider.GetNavRecordDataAsync`. - -Generic HTTP500, generic NRE, real/mixed assertions, stale/unmatched events, -partial XML, stopped jobs, missing evidence or persistence errors never qualify. -No automatic polling/wait for evidence. This conservative additional observed -metadata-stack requirement can reject other SQL-pool shapes. - -The entire current batch drains before a retry. The retry uses the same worker -after another pristine-template restore and fresh single-attempt companies probe. -`ReRun` is explicitly removed: BCH must run **every method**, not just failures. -Retry files use a unique suffix. Recovery requires the exact original test names, -count and skip states with no failures/errors. Only verified recovery replaces -that CU in the final merged result; original and retry files remain separately -preserved. A failed/incomplete retry stays failed and cannot retry again. -Any unrelated terminal batch failure stops later batches (including queued retries). - -## Cohort and interpretation - -The exact ordered 21-CU prefix remains: +PowerShell 7, existing password-file authentication, and `My Company` are unchanged. + +## Retry evidence and accounting + +Every failed JUnit case must contain the observed GET 500/InternalServerError, +null-reference response, and CorrelationId. Each must match an NST Application +event 701 in that dispatch's UTC window, with the same ClientSessionId, +RootException NullReferenceException, and all three observed stack frames: +`NavSqlConnectionScope.AcquireSqlConnectionFromPool`, +`MetadataProvider.GetRelativeHelpUrl`, `PageDataProvider.GetNavRecordDataAsync`. +Generic HTTP500/NRE, mixed assertions, stale/unmatched events, stopped jobs, +missing/partial XML, or evidence persistence failure never authorize retry. + +All arms retain matching first-attempt evidence, but only `retry` schedules +one whole-CU replay on its freshly restored worker with another single probe. +BCH `ReRun` is removed so all methods run. Recovery requires the exact original +names, count and skip states with no failures/errors. Original and retry result +snapshots remain separate. Only validated recovery replaces that CU in final XML. +Failed retries remain failures and cannot retry again. + +Full completed coverage requires **21 suites, 253 cases, 19 baseline skips**; +the 234 non-skipped cases must pass for an unrecovered first-attempt pass. +`148318.CapabilitiesProjectsEnabledViaAPI` must appear exactly once and remain +enabled in every arm. Never count baseline skips, missing results, cancellations +or communication-invalid trials as passing. Report original SQL failures, +retry recoveries, final failures and infrastructure invalids separately. + +Ordered cohort: 139700,139702,139703,139706,139725,139726,139732,139739,139742,139745, 139802,139803,139806,139826,139832,139854,139972,139780,148315,148318,148343. -Order drift fails closed. This is not the full API suite. -Previous complete W1 coverage was 253 cases (234 passing, 19 skipped). -A should turn only the target method into an additional skip; B retains baseline -coverage. Actual counts and any missing/incomplete coverage must be reported per -trial; do not count recovered first failures as first-attempt passes. - -Containers derive from `Test Apps Trial` at unchanged -descriptor depth. Preflight refuses existing containers; cleanup verifies -country/trial/experiment/run/exact name before deleting only the owned container. - -Artifacts preserve provenance, pins, ordered worklist, SQL reset timeline, -single-attempt probe records, worker logs, EVTX, separately labeled warmup, -project-root and nested final XML, plus: -* `test-attempts/--1/`: original result snapshot and outcome. -* `test-attempts/--2/`: retry result snapshot and outcome. -* `SqlApiRetryEvidence/`: original CU XML and correlated NST events. - -Per-CU snapshots may include previously completed suites on that worker; count -only the outcome's CU when aggregating first attempts. Final merged XML contains -one result per CU, with recovery replacing rather than duplicating cases. -Runner loss/cancellation can prevent final export and is **inconclusive**. -Passes are non-reproductions, not proof of a production fix or SQL-name reuse safety. - -Local validation uses Pester 5+ and PSScriptAnalyzer, with all live container -operations mocked. Tests: `build\scripts\SqlResetPilot`, -`build\scripts\tests\SqlApiTestRetry.Test.ps1`, and -`build\scripts\tests\ParallelTestExecution.Test.ps1`. + +Artifacts are unique by arm/country/index/run and retained 14 days. Always-run +Finalize and upload preserve ownership/cleanup evidence, raw worker logs, EVTX, +nested `.buildartifacts` results, ordered prefix, reset timeline, probe records, +warmup outcomes, per-attempt XML/outcomes and correlated NST events. Per-CU +snapshots can contain earlier suites on that worker: aggregate only their named CU. +Runner loss may prevent final export/cleanup; parent monitoring must audit that, +not interpret absent evidence as success. + +## Explicit communication replacements — not automatically dispatched + +After summarizing original communication failures, invoke the same workflow with +`mode=replacement`, `replacementArm`, `replacementCountry`, `replacementTrial`, +`originalRun`, and `originalJob`. This schedules exactly one cell in Batch01; +Batch02–10 skip. The planner verifies the source run is this comparison's +**originals** run at the exact same SHA, the failed job matches the canonical +identity, and its authoritative failure annotation says the self-hosted runner +lost communication with the server. Genuine test/SQL failures cannot pass this gate. +`replacementOf` retains source run/job/event/attempt/workflow SHA and annotation. +The original stays invalid in original-300 accounting; replacements are separate. +Do not use GitHub's normal rerun button (attempt-two gates intentionally reject it). +Dispatch replacements one at a time: GitHub concurrency permits one pending run +and can supersede an older pending run if multiple are submitted. + +Local validation: focused Pester suites under `SqlResetPilot`, +`tests/SqlApiTestRetry.Test.ps1`, `tests/ParallelTestExecution.Test.ps1`; +PSScriptAnalyzer, parsed YAML dependency/matrix checks and `git diff --check`. +All local container operations are mocked. diff --git a/build/scripts/SqlResetPilot/Snapshot.Tests.ps1 b/build/scripts/SqlResetPilot/Snapshot.Tests.ps1 new file mode 100644 index 00000000000..d038b36d944 --- /dev/null +++ b/build/scripts/SqlResetPilot/Snapshot.Tests.ps1 @@ -0,0 +1,86 @@ +BeforeAll { + Import-Module (Join-Path $PSScriptRoot 'Comparison.psm1') -Force +} + +Describe 'Immutable country package snapshots' { + BeforeEach { + $script:proof = @( + Get-SqlComparisonArtifact | Where-Object country -eq DE | ForEach-Object { + [PSCustomObject]@{ + pin = $_ + metadata = [PSCustomObject]@{ + id = $_.id; expired = $false; digest = "sha256:$($_.digest)" + workflow_run = [PSCustomObject]@{ + id = 37372848860; head_sha = 'c4953dceffe02a017adad34973e1955017bf5d20' + } + } + } + } + ) + $script:directory = Join-Path $TestDrive 'snapshot' + $null = New-Item -ItemType Directory $script:directory -Force + $script:proof | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $script:directory 'artifact-proof.json') + Mock Get-FileHash -ModuleName Comparison { + $pin = Get-SqlComparisonArtifact | Where-Object { $Path -like "*$($_.id).zip" } + [PSCustomObject]@{ Hash = $pin.digest } + } + } + It 'requires both exact ZIP hashes and original unexpired source provenance' { + @(Assert-SqlComparisonSnapshot -Directory $script:directory -Country DE).Count | Should -Be 2 + Should -Invoke Get-FileHash -ModuleName Comparison -Times 2 -Exactly + } + It 'rejects changed ZIP bytes regardless of correct metadata' { + Mock Get-FileHash -ModuleName Comparison { [PSCustomObject]@{ Hash = 'wrong' } } + { Assert-SqlComparisonSnapshot -Directory $script:directory -Country DE } | Should -Throw + } + It 'rejects source drift and incomplete or wrong-country snapshots' { + { Assert-SqlComparisonSnapshot -Directory $script:directory -Country W1 } | Should -Throw + $script:proof[0].metadata.workflow_run.id = 1 + $script:proof | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $script:directory 'artifact-proof.json') + { Assert-SqlComparisonSnapshot -Directory $script:directory -Country DE } | Should -Throw + @($script:proof[1]) | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $script:directory 'artifact-proof.json') + { Assert-SqlComparisonSnapshot -Directory $script:directory -Country DE } | Should -Throw + } + It 'rejects snapshots captured from already-expired original artifacts' { + $script:proof[0].metadata.expired = $true + $script:proof | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $script:directory 'artifact-proof.json') + { Assert-SqlComparisonSnapshot -Directory $script:directory -Country DE } | Should -Throw + } +} + +Describe 'Snapshot download transport and ownership' { + BeforeEach { + $script:oldToken = $env:GH_TOKEN + $env:GH_TOKEN = 'synthetic-test-token' + $script:metadata = [PSCustomObject]@{ + expired = $false; workflow_run = [PSCustomObject]@{ id = 100; head_sha = 'revision' } + name = 'sql-api-comparison-packages-DE-100'; digest = ('sha256:' + ('a' * 64)) + } + Mock Invoke-RestMethod -ModuleName Comparison { $script:metadata } + Mock Invoke-WebRequest -ModuleName Comparison {} + Mock Get-FileHash -ModuleName Comparison { [PSCustomObject]@{ Hash = ('a' * 64) } } + Mock Expand-Archive -ModuleName Comparison {} + Mock Assert-SqlComparisonSnapshot -ModuleName Comparison { 'validated' } + } + AfterEach { $env:GH_TOKEN = $script:oldToken } + It 'validates owned immutable transport before extracting and checking source ZIPs' { + Save-SqlComparisonSnapshot -ArtifactId 200 -RunId 100 -HeadSha revision -Country DE ` + -Directory (Join-Path $TestDrive 'snapshot') | Should -Be 'validated' + Should -Invoke Invoke-WebRequest -ModuleName Comparison -Times 1 -Exactly + Should -Invoke Expand-Archive -ModuleName Comparison -Times 1 -Exactly + Should -Invoke Assert-SqlComparisonSnapshot -ModuleName Comparison -Times 1 -Exactly + } + It 'rejects wrong run, head, name or expired snapshot before download' { + { Save-SqlComparisonSnapshot 200 101 revision DE (Join-Path $TestDrive 'snapshot') } | Should -Throw + { Save-SqlComparisonSnapshot 200 100 different DE (Join-Path $TestDrive 'snapshot') } | Should -Throw + { Save-SqlComparisonSnapshot 200 100 revision W1 (Join-Path $TestDrive 'snapshot') } | Should -Throw + $script:metadata.expired = $true + { Save-SqlComparisonSnapshot 200 100 revision DE (Join-Path $TestDrive 'snapshot') } | Should -Throw + Should -Invoke Invoke-WebRequest -ModuleName Comparison -Times 0 + } + It 'never extracts a corrupt transport archive' { + Mock Get-FileHash -ModuleName Comparison { [PSCustomObject]@{ Hash = 'corrupted' } } + { Save-SqlComparisonSnapshot 200 100 revision DE (Join-Path $TestDrive 'snapshot') } | Should -Throw + Should -Invoke Expand-Archive -ModuleName Comparison -Times 0 + } +} From 06da09448ec36e214e785a90cfbe549f10c08de1 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Fri, 9 Oct 2026 13:18:37 +0200 Subject: [PATCH 22/22] Isolate ten original control trials on published SQL platform fix (AB#653457) Pin NST 30.0.56092.0 while preserving original PR2 compiled artifacts, control lifecycle and authentication. Restrict the authorized manual experiment to five sequential W1/DE pairs and two container slots. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91 --- .github/workflows/CICD.yaml | 173 ++---------- .github/workflows/PullRequestHandler.yaml | 2 +- .github/workflows/SqlApiComparisonBatch.yaml | 41 +-- build/Packages.json | 2 +- build/scripts/NewBcContainer.ps1 | 4 +- build/scripts/ParallelTestExecution.psm1 | 10 + .../SqlResetPilot/ContainerPreflight.ps1 | 18 +- .../scripts/SqlResetPilot/Finalize.Tests.ps1 | 13 +- build/scripts/SqlResetPilot/Finalize.ps1 | 8 +- .../SqlResetPilot/PlanPlatformUptake.ps1 | 30 ++ .../SqlResetPilot/PlatformUptake.Tests.ps1 | 260 ++++++++++++++++++ .../scripts/SqlResetPilot/PlatformUptake.json | 88 ++++++ .../scripts/SqlResetPilot/PlatformUptake.psm1 | 57 ++++ .../scripts/SqlResetPilot/Preflight.Tests.ps1 | 47 +++- build/scripts/SqlResetPilot/Prepare.ps1 | 35 ++- build/scripts/SqlResetPilot/README.md | 43 ++- 16 files changed, 596 insertions(+), 235 deletions(-) create mode 100644 build/scripts/SqlResetPilot/PlanPlatformUptake.ps1 create mode 100644 build/scripts/SqlResetPilot/PlatformUptake.Tests.ps1 create mode 100644 build/scripts/SqlResetPilot/PlatformUptake.json create mode 100644 build/scripts/SqlResetPilot/PlatformUptake.psm1 diff --git a/.github/workflows/CICD.yaml b/.github/workflows/CICD.yaml index c295adb0d6a..083434e58da 100644 --- a/.github/workflows/CICD.yaml +++ b/.github/workflows/CICD.yaml @@ -1,105 +1,51 @@ -name: 'SQL API 300 trial comparison - DO NOT MERGE' -run-name: SQL API 300 comparison - ${{ inputs.mode }} +name: 'SQL platform fix uptake - DO NOT MERGE' +run-name: SQL platform 30.0.56092.0 - ten original control cells on: workflow_dispatch: inputs: - mode: - description: originals schedules all 300; replacement schedules one proven communication-invalid cell - type: choice - options: [originals, replacement] - default: originals - required: true - replacementArm: - description: Replacement only - control, warmup, or retry - type: string - replacementCountry: - description: Replacement only - W1 or DE - type: string - replacementTrial: - description: Replacement only - original index 1 through 50 - type: string - originalRun: - description: Replacement only - this comparison's original run ID - type: string - originalJob: - description: Replacement only - communication-failed original trial job ID + authorization: + description: Exact authorization AB653457-fixed-platform-original-control-10 type: string + required: true permissions: actions: read - checks: read contents: read id-token: write packages: read -# Serialize this comparison and its explicit replacements, never unrelated workflows. concurrency: - group: sql-api-646383-300-trial-comparison + group: sql-platform-653457-fixed-uptake cancel-in-progress: false jobs: Plan: - if: github.repository == 'microsoft/BCApps' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/features/646383-sql-api-300-trial-comparison' && github.run_attempt == 1 + if: github.repository == 'microsoft/BCApps' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/features/653457-sql-platform-fix-uptake' && github.run_attempt == 1 && inputs.authorization == 'AB653457-fixed-platform-original-control-10' runs-on: windows-latest timeout-minutes: 10 outputs: - mode: ${{ steps.plan.outputs.mode }} - replacement: ${{ steps.plan.outputs.replacement }} - w1Package: ${{ steps.w1.outputs.artifact-id || steps.plan.outputs.w1Package }} - dePackage: ${{ steps.de.outputs.artifact-id || steps.plan.outputs.dePackage }} - packageRun: ${{ steps.plan.outputs.packageRun }} batch01: ${{ steps.plan.outputs.batch01 }} batch02: ${{ steps.plan.outputs.batch02 }} batch03: ${{ steps.plan.outputs.batch03 }} batch04: ${{ steps.plan.outputs.batch04 }} batch05: ${{ steps.plan.outputs.batch05 }} - batch06: ${{ steps.plan.outputs.batch06 }} - batch07: ${{ steps.plan.outputs.batch07 }} - batch08: ${{ steps.plan.outputs.batch08 }} - batch09: ${{ steps.plan.outputs.batch09 }} - batch10: ${{ steps.plan.outputs.batch10 }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - - name: Validate immutable artifacts and exact trial manifest + with: + fetch-depth: 0 + - name: Verify original package transport and canonical fixed-runtime plan id: plan shell: pwsh env: GH_TOKEN: ${{ github.token }} - COMPARISON_MODE: ${{ inputs.mode }} - REPLACEMENT_ARM: ${{ inputs.replacementArm }} - REPLACEMENT_COUNTRY: ${{ inputs.replacementCountry }} - REPLACEMENT_TRIAL: ${{ inputs.replacementTrial }} - ORIGINAL_RUN: ${{ inputs.originalRun }} - ORIGINAL_JOB: ${{ inputs.originalJob }} - run: .\build\scripts\SqlResetPilot\PlanComparison.ps1 - - name: Preserve exact W1 source ZIPs beyond original expiry - id: w1 - if: inputs.mode == 'originals' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: sql-api-comparison-packages-W1-${{ github.run_id }} - retention-days: 14 - compression-level: 0 - if-no-files-found: error - include-hidden-files: true - path: .sql-api-comparison-plan/W1/* - - name: Preserve exact DE source ZIPs beyond original expiry - id: de - if: inputs.mode == 'originals' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: sql-api-comparison-packages-DE-${{ github.run_id }} - retention-days: 14 - compression-level: 0 - if-no-files-found: error - include-hidden-files: true - path: .sql-api-comparison-plan/DE/* + BC_SQL_UPTAKE_AUTHORIZATION: ${{ inputs.authorization }} + run: .\build\scripts\SqlResetPilot\PlanPlatformUptake.ps1 - name: Preserve scheduling and package provenance if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: - name: sql-api-comparison-manifest-${{ github.run_id }} + name: sql-platform-uptake-manifest-${{ github.run_id }} retention-days: 14 if-no-files-found: error include-hidden-files: true @@ -111,115 +57,40 @@ jobs: secrets: inherit with: matrix: ${{ needs.Plan.outputs.batch01 }} - replacement: ${{ needs.Plan.outputs.replacement }} - w1Package: ${{ needs.Plan.outputs.w1Package }} - dePackage: ${{ needs.Plan.outputs.dePackage }} - packageRun: ${{ needs.Plan.outputs.packageRun }} + authorization: ${{ inputs.authorization }} Batch02: needs: [Plan, Batch01] - if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + if: always() && !cancelled() && needs.Plan.result == 'success' uses: ./.github/workflows/SqlApiComparisonBatch.yaml secrets: inherit with: matrix: ${{ needs.Plan.outputs.batch02 }} - replacement: ${{ needs.Plan.outputs.replacement }} - w1Package: ${{ needs.Plan.outputs.w1Package }} - dePackage: ${{ needs.Plan.outputs.dePackage }} - packageRun: ${{ needs.Plan.outputs.packageRun }} + authorization: ${{ inputs.authorization }} Batch03: needs: [Plan, Batch02] - if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + if: always() && !cancelled() && needs.Plan.result == 'success' uses: ./.github/workflows/SqlApiComparisonBatch.yaml secrets: inherit with: matrix: ${{ needs.Plan.outputs.batch03 }} - replacement: ${{ needs.Plan.outputs.replacement }} - w1Package: ${{ needs.Plan.outputs.w1Package }} - dePackage: ${{ needs.Plan.outputs.dePackage }} - packageRun: ${{ needs.Plan.outputs.packageRun }} + authorization: ${{ inputs.authorization }} Batch04: needs: [Plan, Batch03] - if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + if: always() && !cancelled() && needs.Plan.result == 'success' uses: ./.github/workflows/SqlApiComparisonBatch.yaml secrets: inherit with: matrix: ${{ needs.Plan.outputs.batch04 }} - replacement: ${{ needs.Plan.outputs.replacement }} - w1Package: ${{ needs.Plan.outputs.w1Package }} - dePackage: ${{ needs.Plan.outputs.dePackage }} - packageRun: ${{ needs.Plan.outputs.packageRun }} + authorization: ${{ inputs.authorization }} Batch05: needs: [Plan, Batch04] - if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' + if: always() && !cancelled() && needs.Plan.result == 'success' uses: ./.github/workflows/SqlApiComparisonBatch.yaml secrets: inherit with: matrix: ${{ needs.Plan.outputs.batch05 }} - replacement: ${{ needs.Plan.outputs.replacement }} - w1Package: ${{ needs.Plan.outputs.w1Package }} - dePackage: ${{ needs.Plan.outputs.dePackage }} - packageRun: ${{ needs.Plan.outputs.packageRun }} - - Batch06: - needs: [Plan, Batch05] - if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' - uses: ./.github/workflows/SqlApiComparisonBatch.yaml - secrets: inherit - with: - matrix: ${{ needs.Plan.outputs.batch06 }} - replacement: ${{ needs.Plan.outputs.replacement }} - w1Package: ${{ needs.Plan.outputs.w1Package }} - dePackage: ${{ needs.Plan.outputs.dePackage }} - packageRun: ${{ needs.Plan.outputs.packageRun }} - - Batch07: - needs: [Plan, Batch06] - if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' - uses: ./.github/workflows/SqlApiComparisonBatch.yaml - secrets: inherit - with: - matrix: ${{ needs.Plan.outputs.batch07 }} - replacement: ${{ needs.Plan.outputs.replacement }} - w1Package: ${{ needs.Plan.outputs.w1Package }} - dePackage: ${{ needs.Plan.outputs.dePackage }} - packageRun: ${{ needs.Plan.outputs.packageRun }} - - Batch08: - needs: [Plan, Batch07] - if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' - uses: ./.github/workflows/SqlApiComparisonBatch.yaml - secrets: inherit - with: - matrix: ${{ needs.Plan.outputs.batch08 }} - replacement: ${{ needs.Plan.outputs.replacement }} - w1Package: ${{ needs.Plan.outputs.w1Package }} - dePackage: ${{ needs.Plan.outputs.dePackage }} - packageRun: ${{ needs.Plan.outputs.packageRun }} - - Batch09: - needs: [Plan, Batch08] - if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' - uses: ./.github/workflows/SqlApiComparisonBatch.yaml - secrets: inherit - with: - matrix: ${{ needs.Plan.outputs.batch09 }} - replacement: ${{ needs.Plan.outputs.replacement }} - w1Package: ${{ needs.Plan.outputs.w1Package }} - dePackage: ${{ needs.Plan.outputs.dePackage }} - packageRun: ${{ needs.Plan.outputs.packageRun }} - - Batch10: - needs: [Plan, Batch09] - if: always() && !cancelled() && needs.Plan.result == 'success' && needs.Plan.outputs.mode == 'originals' - uses: ./.github/workflows/SqlApiComparisonBatch.yaml - secrets: inherit - with: - matrix: ${{ needs.Plan.outputs.batch10 }} - replacement: ${{ needs.Plan.outputs.replacement }} - w1Package: ${{ needs.Plan.outputs.w1Package }} - dePackage: ${{ needs.Plan.outputs.dePackage }} - packageRun: ${{ needs.Plan.outputs.packageRun }} + authorization: ${{ inputs.authorization }} diff --git a/.github/workflows/PullRequestHandler.yaml b/.github/workflows/PullRequestHandler.yaml index ddba89afff4..e7875411767 100644 --- a/.github/workflows/PullRequestHandler.yaml +++ b/.github/workflows/PullRequestHandler.yaml @@ -35,7 +35,7 @@ jobs: Initialization: needs: [ PregateCheck ] - if: (!failure() && !cancelled()) && github.head_ref != 'features/646383-sql-api-warmup-experiment' && github.head_ref != 'features/646383-sql-api-two-arm-experiment' && github.head_ref != 'features/646383-sql-api-300-trial-comparison' + if: (!failure() && !cancelled()) && github.head_ref != 'features/646383-sql-api-warmup-experiment' && github.head_ref != 'features/646383-sql-api-two-arm-experiment' && github.head_ref != 'features/646383-sql-api-300-trial-comparison' && github.head_ref != 'features/653457-sql-platform-fix-uptake' runs-on: [ windows-latest ] outputs: projects: ${{ steps.determineProjectsToBuild.outputs.ProjectsJson }} diff --git a/.github/workflows/SqlApiComparisonBatch.yaml b/.github/workflows/SqlApiComparisonBatch.yaml index dbd1ef17622..3b4930a2a8b 100644 --- a/.github/workflows/SqlApiComparisonBatch.yaml +++ b/.github/workflows/SqlApiComparisonBatch.yaml @@ -1,4 +1,4 @@ -name: 'SQL API comparison batch - DO NOT MERGE' +name: 'SQL platform uptake pair - DO NOT MERGE' on: workflow_call: @@ -6,16 +6,7 @@ on: matrix: required: true type: string - replacement: - required: true - type: string - w1Package: - required: true - type: string - dePackage: - required: true - type: string - packageRun: + authorization: required: true type: string @@ -28,10 +19,10 @@ permissions: jobs: Pilot: name: Trial ${{ matrix.experiment }}/${{ matrix.country }}/${{ matrix.trial }} - if: github.repository == 'microsoft/BCApps' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/features/646383-sql-api-300-trial-comparison' && github.run_attempt == 1 + if: github.repository == 'microsoft/BCApps' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/features/653457-sql-platform-fix-uptake' && github.run_attempt == 1 && inputs.authorization == 'AB653457-fixed-platform-original-control-10' strategy: fail-fast: false - max-parallel: 6 + max-parallel: 2 matrix: ${{ fromJSON(inputs.matrix) }} runs-on: GitHub-BCAppsPL timeout-minutes: 120 @@ -45,9 +36,7 @@ jobs: BC_SQL_API_EXPERIMENT: ${{ matrix.experiment }} BC_SQL_PILOT_COUNTRY: ${{ matrix.country }} BC_SQL_PILOT_TRIAL: ${{ matrix.trial }} - BC_SQL_COMPARISON_REPLACEMENT: ${{ inputs.replacement }} - BC_SQL_COMPARISON_PACKAGE_ARTIFACT: ${{ matrix.country == 'W1' && inputs.w1Package || inputs.dePackage }} - BC_SQL_COMPARISON_PACKAGE_RUN: ${{ inputs.packageRun }} + BC_SQL_UPTAKE_AUTHORIZATION: ${{ inputs.authorization }} BuildMode: IntegrationTests steps: - name: Checkout experiment @@ -81,7 +70,7 @@ jobs: uses: microsoft/AL-Go/Actions/ReadSettings@91b96c2b294be6f823277dafe6f03350abfb9d23 with: shell: pwsh - project: build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }} + project: build/projects/Test Apps ${{ matrix.country }} Uptake${{ matrix.trial }}${{ matrix.experiment }} buildMode: IntegrationTests - name: Read normal disposable CI license and package secrets @@ -98,7 +87,7 @@ jobs: Secrets: '${{ steps.secrets.outputs.Secrets }}' with: shell: pwsh - project: build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }} + project: build/projects/Test Apps ${{ matrix.country }} Uptake${{ matrix.trial }}${{ matrix.experiment }} buildMode: IntegrationTests artifact: ${{ steps.packages.outputs.artifact }} installAppsJson: ${{ steps.packages.outputs.apps }} @@ -113,16 +102,16 @@ jobs: if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: sql-api-comparison-${{ matrix.experiment }}-${{ matrix.country }}-${{ matrix.trial }}-${{ github.run_id }} + name: sql-platform-uptake-${{ matrix.experiment }}-${{ matrix.country }}-${{ matrix.trial }}-${{ github.run_id }} retention-days: 14 include-hidden-files: true if-no-files-found: error path: | sql-reset-pilot-output/** - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/TestResults*.xml - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/ContainerEventLog.evtx - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/BuildOutput.txt - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/.buildartifacts/**/*.xml - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/.buildartifacts/**/*.log - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/.buildartifacts/**/*.evtx - build/projects/Test Apps ${{ matrix.country }} Trial${{ matrix.trial }}${{ matrix.experiment }}/.buildartifacts/**/BuildOutput.txt + build/projects/Test Apps ${{ matrix.country }} Uptake${{ matrix.trial }}${{ matrix.experiment }}/TestResults*.xml + build/projects/Test Apps ${{ matrix.country }} Uptake${{ matrix.trial }}${{ matrix.experiment }}/ContainerEventLog.evtx + build/projects/Test Apps ${{ matrix.country }} Uptake${{ matrix.trial }}${{ matrix.experiment }}/BuildOutput.txt + build/projects/Test Apps ${{ matrix.country }} Uptake${{ matrix.trial }}${{ matrix.experiment }}/.buildartifacts/**/*.xml + build/projects/Test Apps ${{ matrix.country }} Uptake${{ matrix.trial }}${{ matrix.experiment }}/.buildartifacts/**/*.log + build/projects/Test Apps ${{ matrix.country }} Uptake${{ matrix.trial }}${{ matrix.experiment }}/.buildartifacts/**/*.evtx + build/projects/Test Apps ${{ matrix.country }} Uptake${{ matrix.trial }}${{ matrix.experiment }}/.buildartifacts/**/BuildOutput.txt diff --git a/build/Packages.json b/build/Packages.json index 3afa71226b9..2ec65533b0a 100644 --- a/build/Packages.json +++ b/build/Packages.json @@ -9,7 +9,7 @@ "_comment": "Used to fetch app baselines from BC artifacts" }, "BCPlatform": { - "Version": "30.0.55665.0", + "Version": "30.0.56092.0", "Source": "BCPlatform", "_comment": "The BC platform version, fetched from the platform CDN" } diff --git a/build/scripts/NewBcContainer.ps1 b/build/scripts/NewBcContainer.ps1 index 974459b5f5d..c5b82654c7c 100644 --- a/build/scripts/NewBcContainer.ps1 +++ b/build/scripts/NewBcContainer.ps1 @@ -40,8 +40,8 @@ if ($env:BC_SQL_PILOT_ARM) { } } $runtime | ConvertTo-Json -Depth 6 | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT 'runtime-preflight.json') - if (@($runtime.nst).Count -ne 1 -or $runtime.nst[0].version -notmatch '^30\.0\.55665\.0(?:\s|$)') { - throw 'Installed NST is not the predeclared 30.0.55665.0 runtime.' + if (@($runtime.nst).Count -ne 1 -or $runtime.nst[0].version -notmatch '^30\.0\.56092\.0(?:\s|$)') { + throw 'Installed NST is not the predeclared 30.0.56092.0 runtime.' } } diff --git a/build/scripts/ParallelTestExecution.psm1 b/build/scripts/ParallelTestExecution.psm1 index db246b6f5d7..f19ad9deb82 100644 --- a/build/scripts/ParallelTestExecution.psm1 +++ b/build/scripts/ParallelTestExecution.psm1 @@ -52,6 +52,12 @@ function Get-DisabledTestsForApp { } function Test-SqlApiExperiment { + if ($env:GITHUB_REF -eq 'refs/heads/features/653457-sql-platform-fix-uptake') { + return ($env:GITHUB_REPOSITORY -eq 'microsoft/BCApps' -and + $env:GITHUB_EVENT_NAME -eq 'workflow_dispatch' -and $env:GITHUB_RUN_ATTEMPT -eq '1' -and + $env:BC_SQL_UPTAKE_AUTHORIZATION -eq 'AB653457-fixed-platform-original-control-10' -and + $env:BC_SQL_PILOT_ARM -eq 'control' -and $env:BC_SQL_API_EXPERIMENT -eq 'control') + } return ($env:GITHUB_REPOSITORY -eq 'microsoft/BCApps' -and $env:GITHUB_REF -eq 'refs/heads/features/646383-sql-api-300-trial-comparison' -and $env:GITHUB_EVENT_NAME -eq 'workflow_dispatch' -and $env:GITHUB_RUN_ATTEMPT -eq '1' -and @@ -59,6 +65,10 @@ function Test-SqlApiExperiment { } function Test-SqlApiWarmupEnabled { + if ($env:GITHUB_REF -eq 'refs/heads/features/653457-sql-platform-fix-uptake') { + if (-not (Test-SqlApiExperiment)) { throw 'Invalid control-only uptake context; refusing warmup fallback.' } + return $false + } return ($env:BC_SQL_PILOT_ARM -and (-not (Test-SqlApiExperiment) -or $env:BC_SQL_API_EXPERIMENT -ne 'control')) } diff --git a/build/scripts/SqlResetPilot/ContainerPreflight.ps1 b/build/scripts/SqlResetPilot/ContainerPreflight.ps1 index 927cf6fc551..3a1aa2675a0 100644 --- a/build/scripts/SqlResetPilot/ContainerPreflight.ps1 +++ b/build/scripts/SqlResetPilot/ContainerPreflight.ps1 @@ -1,15 +1,9 @@ param([hashtable]$Parameters) $ErrorActionPreference = 'Stop' -if ($env:GITHUB_REPOSITORY -ne 'microsoft/BCApps' -or - $env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-300-trial-comparison' -or - $env:BC_SQL_API_EXPERIMENT -notin @('control', 'warmup', 'retry') -or - $env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or - $env:BC_SQL_PILOT_ARM -ne 'control' -or -not $env:BC_SQL_PILOT_OUTPUT -or - $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^(?:[1-9]|[1-4][0-9]|50)$' -or - $env:GITHUB_RUN_ID -notmatch '^\d{1,20}$') { - throw 'Refusing SQL pilot outside its disposable manually dispatched CI job.' -} -$expectedName = "bcbuildprojectsTestApps$($env:BC_SQL_PILOT_COUNTRY)Trial$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)$($env:GITHUB_RUN_ID)" +Import-Module (Join-Path $PSScriptRoot 'PlatformUptake.psm1') -Force +Assert-SqlPlatformUptakeContext -Cell +if (-not $env:BC_SQL_PILOT_OUTPUT) { throw 'Missing owned output directory.' } +$expectedName = "bcbuildprojectsTestApps$($env:BC_SQL_PILOT_COUNTRY)Uptake$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)$($env:GITHUB_RUN_ID)" if ($Parameters.ContainerName -ne $expectedName -or (Test-BcContainer -containerName $expectedName)) { throw 'Pilot container name is unexpected or already exists. No replacement is authorized.' } @@ -26,7 +20,7 @@ if (-not (Get-Command New-BcContainer).Parameters.ContainsKey('useGenericImage') throw 'Exact BCH does not expose the supported generic-image parameter.' } $Parameters.useGenericImage = 'mcr.microsoft.com/businesscentral@sha256:c899d12093ad7bbdbfd08ccc0e6294e0f98c682c7e35db4ecfbca345fb068492' -if ($Parameters.platformArtifactUrl -ne 'https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/platform/30.0.55665.0/platform') { +if ($Parameters.platformArtifactUrl -ne 'https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/platform/30.0.56092.0/platform') { throw 'Platform artifact is not the pinned source platform.' } @{ @@ -34,7 +28,7 @@ if ($Parameters.platformArtifactUrl -ne 'https://bcinsider-fvh2ekdjecfjd6gk.b02. country = $env:BC_SQL_PILOT_COUNTRY; trial = $env:BC_SQL_PILOT_TRIAL experiment = $env:BC_SQL_API_EXPERIMENT helperVersion = $helperVersion; image = $Parameters.useGenericImage - imageGenericTag = '1.0.2.128'; platform = '30.0.55665.0' + imageGenericTag = '1.0.2.128'; platform = '30.0.56092.0' hostOS = [Environment]::OSVersion.Version.ToString() registeredUtc = [DateTime]::UtcNow.ToString('o') } | ConvertTo-Json | Set-Content (Join-Path $env:BC_SQL_PILOT_OUTPUT 'container-ownership.json') -Encoding UTF8 diff --git a/build/scripts/SqlResetPilot/Finalize.Tests.ps1 b/build/scripts/SqlResetPilot/Finalize.Tests.ps1 index 74a83ef09e1..32c8ad29c12 100644 --- a/build/scripts/SqlResetPilot/Finalize.Tests.ps1 +++ b/build/scripts/SqlResetPilot/Finalize.Tests.ps1 @@ -16,11 +16,11 @@ Describe 'SQL pilot final cleanup ownership' { $env:BC_SQL_PILOT_ARM = 'control' $env:BC_SQL_PILOT_COUNTRY = 'DE' $env:BC_SQL_PILOT_TRIAL = '2' - $env:BC_SQL_API_EXPERIMENT = 'retry' + $env:BC_SQL_API_EXPERIMENT = 'control' $env:BcContainerHelperPath = Join-Path $PSScriptRoot 'BcContainerHelper.ps1' Mock Import-Module {} Mock Test-Path { $true } - Mock Get-Content { '{"runId":"123","arm":"control","country":"DE","trial":"2","experiment":"retry","container":"bcbuildprojectsTestAppsDETrial2retry123"}' } + Mock Get-Content { '{"runId":"123","arm":"control","country":"DE","trial":"2","experiment":"control","container":"bcbuildprojectsTestAppsDEUptake2control123"}' } Mock Get-ChildItem {} Mock New-Item {} Mock Copy-Item {} @@ -43,7 +43,7 @@ Describe 'SQL pilot final cleanup ownership' { It 'exports before disposing only the registered container' { & (Join-Path $PSScriptRoot 'Finalize.ps1') Should -Invoke Get-BcContainerEventLog -Times 1 -Exactly -Scope It - Should -Invoke Remove-BcContainer -Times 1 -Exactly -Scope It -ParameterFilter { $containerName -eq 'bcbuildprojectsTestAppsDETrial2retry123' } + Should -Invoke Remove-BcContainer -Times 1 -Exactly -Scope It -ParameterFilter { $containerName -eq 'bcbuildprojectsTestAppsDEUptake2control123' } } It 'still tears down if exporting fails and keeps that failure visible' { Mock Get-BcContainerEventLog { throw 'event export failed' } @@ -54,7 +54,7 @@ Describe 'SQL pilot final cleanup ownership' { Mock Get-ChildItem { [PSCustomObject]@{ FullName = 'TestResults.xml' } } & (Join-Path $PSScriptRoot 'Finalize.ps1') Should -Invoke Get-ChildItem -Times 1 -Exactly -Scope It -ParameterFilter { - $Path -like '*Test Apps DE Trial2retry\.buildartifacts' -and $Filter -eq 'TestResults*.xml' -and $File + $Path -like '*Test Apps DE Uptake2control\.buildartifacts' -and $Filter -eq 'TestResults*.xml' -and $File } Should -Invoke Copy-Item -Times 1 -Exactly -Scope It -ParameterFilter { $Destination -like '*clean-results\buildartifacts' @@ -89,4 +89,9 @@ Describe 'SQL pilot final cleanup ownership' { { & (Join-Path $PSScriptRoot 'Finalize.ps1') } | Should -Throw Should -Invoke Remove-BcContainer -Times 0 -Exactly -Scope It } + It 'never deletes an original comparison container' { + Mock Get-Content { '{"runId":"123","arm":"control","country":"DE","trial":"2","experiment":"control","container":"bcbuildprojectsTestAppsDETrial2control123"}' } + { & (Join-Path $PSScriptRoot 'Finalize.ps1') } | Should -Throw + Should -Invoke Remove-BcContainer -Times 0 -Exactly -Scope It + } } diff --git a/build/scripts/SqlResetPilot/Finalize.ps1 b/build/scripts/SqlResetPilot/Finalize.ps1 index b9a3c03c616..46d4eb13de6 100644 --- a/build/scripts/SqlResetPilot/Finalize.ps1 +++ b/build/scripts/SqlResetPilot/Finalize.ps1 @@ -7,10 +7,10 @@ if (-not (Test-Path $ownershipPath)) { } $ownership = Get-Content $ownershipPath -Raw | ConvertFrom-Json if ($ownership.runId -ne $env:GITHUB_RUN_ID -or $ownership.arm -ne $env:BC_SQL_PILOT_ARM -or - $env:BC_SQL_API_EXPERIMENT -notin @('control', 'warmup', 'retry') -or $ownership.experiment -ne $env:BC_SQL_API_EXPERIMENT -or - $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^(?:[1-9]|[1-4][0-9]|50)$' -or + $env:BC_SQL_API_EXPERIMENT -ne 'control' -or $ownership.experiment -ne $env:BC_SQL_API_EXPERIMENT -or + $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^[1-5]$' -or $ownership.country -ne $env:BC_SQL_PILOT_COUNTRY -or $ownership.trial -ne $env:BC_SQL_PILOT_TRIAL -or - $ownership.container -ne "bcbuildprojectsTestApps$($env:BC_SQL_PILOT_COUNTRY)Trial$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)$($env:GITHUB_RUN_ID)") { + $ownership.container -ne "bcbuildprojectsTestApps$($env:BC_SQL_PILOT_COUNTRY)Uptake$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)$($env:GITHUB_RUN_ID)") { throw 'Cleanup ownership mismatch.' } Import-Module (Join-Path (Split-Path $env:BcContainerHelperPath -Parent) 'BcContainerHelper.psm1') -ErrorAction Stop @@ -19,7 +19,7 @@ try { $events = Get-BcContainerEventLog -containerName $ownership.container -doNotOpen Copy-Item $events (Join-Path $output 'final-container.evtx') } - $project = Join-Path $env:GITHUB_WORKSPACE "build\projects\Test Apps $($env:BC_SQL_PILOT_COUNTRY) Trial$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)" + $project = Join-Path $env:GITHUB_WORKSPACE "build\projects\Test Apps $($env:BC_SQL_PILOT_COUNTRY) Uptake$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)" $resultLocations = @{ 'project-root' = $project 'buildartifacts' = Join-Path $project '.buildartifacts' diff --git a/build/scripts/SqlResetPilot/PlanPlatformUptake.ps1 b/build/scripts/SqlResetPilot/PlanPlatformUptake.ps1 new file mode 100644 index 00000000000..b25f6f377bd --- /dev/null +++ b/build/scripts/SqlResetPilot/PlanPlatformUptake.ps1 @@ -0,0 +1,30 @@ +$ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot 'PlatformUptake.psm1') -Force +Assert-SqlPlatformUptakeContext +$pin = Get-SqlPlatformUptake +git merge-base --is-ancestor $pin.comparisonBase HEAD +if ($LASTEXITCODE -ne 0) { throw 'Original comparison commit must remain an ancestor.' } +git diff --exit-code $pin.applicationSource HEAD -- src +if ($LASTEXITCODE -ne 0) { throw 'Application source differs from compiled PR2.' } +if ((Get-Content (Join-Path $PSScriptRoot '..\..\Packages.json') -Raw | ConvertFrom-Json).BCPlatform.Version -ne $pin.runtime) { + throw 'BCPlatform.Version differs from the published fixed runtime.' +} +$directory = Join-Path $env:GITHUB_WORKSPACE '.sql-api-comparison-plan' +$null = New-Item -ItemType Directory -Path $directory -Force +$proof = @() +foreach ($country in @('W1', 'DE')) { + $proof += @(Save-SqlPlatformUptakeSnapshot -Country $country -Directory (Join-Path $directory $country)) +} +$cells = @(Get-SqlPlatformUptakeCell) +@{ + provenance = $pin; run = $env:GITHUB_RUN_ID; head = $env:GITHUB_SHA + createdUtc = [datetime]::UtcNow.ToString('o'); cells = $cells + originalCellCount = 10; globalContainerLimit = 2; batchCount = 5; cellsPerOriginalBatch = 2 + outcomePolicy = 'Original first attempts only. Incomplete, skipped, cancelled and communication-invalid trials are never passes. No replacements or reruns.' +} | ConvertTo-Json -Depth 15 | Set-Content (Join-Path $directory 'matrix-manifest.json') -Encoding utf8 +ConvertTo-Json -InputObject $proof -Depth 12 | Set-Content (Join-Path $directory 'artifact-proof.json') -Encoding utf8 +foreach ($batch in 1..5) { + $matrix = @{ include = @($cells | Select-Object -Skip (($batch - 1) * 2) -First 2) } | ConvertTo-Json -Depth 6 -Compress + "batch$($batch.ToString('00'))=$matrix" | Add-Content $env:GITHUB_OUTPUT +} +Write-Output 'Validated ten original control cells in five sequential W1/DE pairs; maximum two containers.' diff --git a/build/scripts/SqlResetPilot/PlatformUptake.Tests.ps1 b/build/scripts/SqlResetPilot/PlatformUptake.Tests.ps1 new file mode 100644 index 00000000000..eaa65e386ef --- /dev/null +++ b/build/scripts/SqlResetPilot/PlatformUptake.Tests.ps1 @@ -0,0 +1,260 @@ +$discoveryArm = $env:BC_SQL_PILOT_ARM +try { + $env:BC_SQL_PILOT_ARM = 'control' + Import-Module (Join-Path $PSScriptRoot '..\ParallelTestExecution.psm1') -Force +} finally { $env:BC_SQL_PILOT_ARM = $discoveryArm } +BeforeAll { + Import-Module (Join-Path $PSScriptRoot 'PlatformUptake.psm1') -Force + Import-Module (Join-Path $PSScriptRoot 'Comparison.psm1') -Force + Import-Module powershell-yaml + $script:root = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path +} + +Describe 'Fixed-platform canonical manifest and immutable control protocol' { + It 'pins official build resource rather than publisher source' { + $pin = Get-SqlPlatformUptake + $pin.runtime | Should -Be '30.0.56092.0' + $pin.publication.fixCommit | Should -Be '7a4d33b8848c5795474eb6d517724e3ae5043baf' + $pin.publication.officialBuild | Should -Be 2860912 + $pin.publication.actualResourceBuild | Should -Be $pin.publication.officialBuild + $pin.publication.actualResourceVersion | Should -Be $pin.runtime + $pin.publication.publisherRun | Should -Be 2861912 + $pin.publication.publisherSelfSourceIsNotBinaryProvenance | Should -BeTrue + } + It 'preserves all four original application archive pins' { + $pin = Get-SqlPlatformUptake + $pin.applicationArtifacts.Count | Should -Be 4 + foreach ($original in Get-SqlComparisonArtifact) { + $artifact = @($pin.applicationArtifacts | Where-Object id -eq $original.id) + $artifact.Count | Should -Be 1 + $artifact[0].digest | Should -Be $original.digest + $artifact[0].country | Should -Be $original.country + $artifact[0].compiledPackageSetSha256 | Should -Match '^[0-9a-f]{64}$' + } + } + It 'has only ten distinct original control cells and the unchanged cohort' { + $cells = @(Get-SqlPlatformUptakeCell) + $cells.Count | Should -Be 10 + @($cells | Where-Object experiment -ne 'control').Count | Should -Be 0 + @($cells | ForEach-Object { "$($_.country)/$($_.trial)" } | Select-Object -Unique).Count | Should -Be 10 + foreach ($country in @('W1', 'DE')) { + (@($cells | Where-Object country -eq $country).trial -join ',') | Should -Be '1,2,3,4,5' + } + $plan = (Get-SqlPlatformUptake).trialPlan + $plan.codeunits.Count | Should -Be 21 + $plan.cases | Should -Be 253 + $plan.expectedPass | Should -Be 234 + $plan.existingSkip | Should -Be 19 + $plan.mountedTenants | Should -Be 4 + $plan.workers | Should -Be 3 + $plan.retries | Should -Be 0 + $plan.addedSkips | Should -Be 0 + $plan.warmup | Should -BeFalse + $plan.companiesProbe | Should -BeFalse + $plan.readinessWait | Should -BeFalse + } + It 'updates every executable runtime guard, not just Packages.json' { + (Get-Content (Join-Path $script:root 'build\Packages.json') -Raw | ConvertFrom-Json).BCPlatform.Version | Should -Be '30.0.56092.0' + $preflight = Get-Content (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Raw + $preflight | Should -Match ([regex]::Escape('/platform/30.0.56092.0/platform')) + $preflight | Should -Match "platform = '30.0.56092.0'" + $container = Get-Content (Join-Path $script:root 'build\scripts\NewBcContainer.ps1') -Raw + $container | Should -Match ([regex]::Escape("'^30\.0\.56092\.0(?:\s|$)'")) + $container | Should -Match 'runtime-preflight.json' + foreach ($source in @($preflight, $container)) { + $source | Should -Not -Match '55665' + } + } + It 'rejects the old installed NST file version, not just the artifact URL' { + $tokens = $null; $errors = $null + $ast = [Management.Automation.Language.Parser]::ParseFile( + (Join-Path $script:root 'build\scripts\NewBcContainer.ps1'), [ref]$tokens, [ref]$errors) + $guard = $ast.Find({ param($node) + $node -is [Management.Automation.Language.IfStatementAst] -and + $node.Clauses[0].Item1.Extent.Text -like '*runtime.nst*' + }, $true) + $guard | Should -Not -BeNullOrEmpty + $check = [scriptblock]::Create($guard.Extent.Text) + $runtime = @{ nst = @(@{ version = '30.0.56092.0' }) } + { & $check } | Should -Not -Throw + foreach ($version in @('30.0.55665.0', '28.0.56092.0', '30.0.56092.01', '')) { + $runtime = @{ nst = @(@{ version = $version }) } + { & $check } | Should -Throw '*Installed NST*' + } + $runtime = @{ nst = @() } + { & $check } | Should -Throw '*Installed NST*' + } + It 'preserves original source, fixtures, auth, settings and lifecycle byte-for-byte' { + $paths = @('src', 'build/projects', 'build/scripts/ApiTestCredential.psm1', + 'build/scripts/SqlResetPilot/Lifecycle.psm1', 'build/scripts/RunTests.ps1', + 'build/scripts/SqlApiTestRetry.psm1') + $diff = & git -C $script:root diff 0227094059f2f26f3fcb1b7f85a3285c17e78b8e -- @paths + $LASTEXITCODE | Should -Be 0 + $diff | Should -BeNullOrEmpty + } +} + +Describe 'Parsed workflow capacity and no automatic fanout' { + BeforeAll { + $script:workflow = Get-Content (Join-Path $script:root '.github\workflows\CICD.yaml') -Raw | ConvertFrom-Yaml + $script:batch = Get-Content (Join-Path $script:root '.github\workflows\SqlApiComparisonBatch.yaml') -Raw | ConvertFrom-Yaml + } + It 'permits manual authorization only and never ordinary CI' { + @($script:workflow.on.Keys).Count | Should -Be 1 + $script:workflow.on.Contains('workflow_dispatch') | Should -BeTrue + $script:workflow.jobs.Count | Should -Be 6 + foreach ($gate in @($script:workflow.jobs.Plan.if, $script:batch.jobs.Pilot.if)) { + $gate | Should -Match "github.repository == 'microsoft/BCApps'" + $gate | Should -Match "github.event_name == 'workflow_dispatch'" + $gate | Should -Match "github.ref == 'refs/heads/features/653457-sql-platform-fix-uptake'" + $gate | Should -Match 'github.run_attempt == 1' + $gate | Should -Match "inputs.authorization == 'AB653457-fixed-platform-original-control-10'" + } + $pr = Get-Content (Join-Path $script:root '.github\workflows\PullRequestHandler.yaml') -Raw | ConvertFrom-Yaml + $pr.jobs.Initialization.if | Should -Match "github.head_ref != 'features/653457-sql-platform-fix-uptake'" + } + It 'serializes all five pairs with an aggregate maximum of two cells' { + $script:batch.jobs.Pilot.strategy.'max-parallel' | Should -Be 2 + $script:batch.jobs.Pilot.strategy.'fail-fast' | Should -BeFalse + $script:workflow.concurrency.'cancel-in-progress' | Should -BeFalse + $script:workflow.jobs.Batch01.needs | Should -Be 'Plan' + foreach ($index in 2..5) { + $job = $script:workflow.jobs[('Batch{0:00}' -f $index)] + ($job.needs -join ',') | Should -Be ('Plan,Batch{0:00}' -f ($index - 1)) + $job.if | Should -Be "always() && !cancelled() && needs.Plan.result == 'success'" + } + foreach ($index in 1..5) { + $job = $script:workflow.jobs[('Batch{0:00}' -f $index)] + $job.with.matrix | Should -Be ('${{ needs.Plan.outputs.batch' + ('{0:00}' -f $index) + ' }}') + } + } + It 'retains always-run owned cleanup and raw event/result uploads' { + $steps = $script:batch.jobs.Pilot.steps + @($steps | Where-Object { $_.run -like '*Finalize.ps1*' })[0].if | Should -Be 'always()' + $upload = @($steps | Where-Object { $_.uses -like 'actions/upload-artifact@*' })[0] + $upload.if | Should -Be 'always()' + $upload.with.path | Should -Match '\.buildartifacts/\*\*/\*\.evtx' + $upload.with.path | Should -Match 'Uptake' + $script:batch.jobs.Pilot.env.BC_SQL_PILOT_ARM | Should -Be 'control' + } +} + +Describe 'Exact authorized cell context' { + BeforeEach { + $script:saved = @{} + $values = @{ + GITHUB_REPOSITORY = 'microsoft/BCApps'; GITHUB_REF = 'refs/heads/features/653457-sql-platform-fix-uptake' + GITHUB_EVENT_NAME = 'workflow_dispatch'; GITHUB_RUN_ATTEMPT = '1'; GITHUB_RUN_ID = '123' + GITHUB_SHA = ('a' * 40); BC_SQL_UPTAKE_AUTHORIZATION = 'AB653457-fixed-platform-original-control-10' + BC_SQL_PILOT_ARM = 'control'; BC_SQL_API_EXPERIMENT = 'control'; BC_SQL_PILOT_COUNTRY = 'W1'; BC_SQL_PILOT_TRIAL = '1' + } + foreach ($envName in $values.Keys) { + $script:saved[$envName] = [Environment]::GetEnvironmentVariable($envName) + [Environment]::SetEnvironmentVariable($envName, $values[$envName]) + } + } + AfterEach { + foreach ($envName in $script:saved.Keys) { [Environment]::SetEnvironmentVariable($envName, $script:saved[$envName]) } + } + It 'accepts exactly the ten selected cells' { + foreach ($cell in Get-SqlPlatformUptakeCell) { + $env:BC_SQL_PILOT_COUNTRY = $cell.country + $env:BC_SQL_PILOT_TRIAL = [string]$cell.trial + { Assert-SqlPlatformUptakeContext -Cell } | Should -Not -Throw + } + } + It 'runs the planner to emit only five sequential country-pair matrices' { + $oldWorkspace = $env:GITHUB_WORKSPACE; $oldOutput = $env:GITHUB_OUTPUT + try { + $env:GITHUB_WORKSPACE = $TestDrive + $env:GITHUB_OUTPUT = Join-Path $TestDrive 'uptake-planner-output.txt' + if (Test-Path $env:GITHUB_OUTPUT) { Remove-Item $env:GITHUB_OUTPUT } + Mock Import-Module {} + Mock Save-SqlPlatformUptakeSnapshot { @{ country = $Country; verified = $true } } + Mock git { $global:LASTEXITCODE = 0 } + & (Join-Path $PSScriptRoot 'PlanPlatformUptake.ps1') + $outputs = @(Get-Content $env:GITHUB_OUTPUT) + $outputs.Count | Should -Be 5 + foreach ($index in 1..5) { + $parts = $outputs[$index - 1] -split '=', 2 + $parts[0] | Should -Be ('batch{0:00}' -f $index) + $matrix = $parts[1] | ConvertFrom-Json + $matrix.include.Count | Should -Be 2 + ($matrix.include.country -join ',') | Should -Be 'W1,DE' + ($matrix.include.trial -join ',') | Should -Be "$index,$index" + ($matrix.include.experiment -join ',') | Should -Be 'control,control' + } + $manifest = Get-Content (Join-Path $TestDrive '.sql-api-comparison-plan\matrix-manifest.json') -Raw | ConvertFrom-Json + $manifest.originalCellCount | Should -Be 10 + $manifest.globalContainerLimit | Should -Be 2 + $manifest.provenance.publication.actualResourceBuild | Should -Be 2860912 + Should -Invoke Save-SqlPlatformUptakeSnapshot -Times 2 -Exactly + } finally { $env:GITHUB_WORKSPACE = $oldWorkspace; $env:GITHUB_OUTPUT = $oldOutput } + } + InModuleScope ParallelTestExecution { + It 'selects original control with no warmup, companies probe, or SQL retry' { + Test-SqlApiExperiment | Should -BeTrue + Test-SqlApiWarmupEnabled | Should -BeFalse + $state = [PSCustomObject]@{ + jobs = @(); hasFailures = $false; transient = @(); retried = @{}; retryTenant = @{}; sqlRetryEvidence = @{} + } + Register-TestJobOutcome ([PSCustomObject]@{ Outcome = 'SqlPoolRetry' }) $state + $state.hasFailures | Should -BeTrue + $state.transient.Count | Should -Be 0 + Mock Reset-BcTestTenant {} + Mock Invoke-SqlPilotCompaniesProbe { throw 'Probe forbidden.' } + Mock Start-RequiredDisabledDispatch {} + Mock Wait-ForAllTestJobs {} + $parameters = @{ containerName = 'owned'; companyName = 'My Company'; credential = [PSCredential]::Empty } + $item = [PSCustomObject]@{ Key = 'app::148318'; AppName = 'app'; CodeunitId = '148318'; TestCount = 3 } + Invoke-RequiredDisabledTestExecution $parameters @($item) @([PSCustomObject]@{ Id = 'tenant2'; DatabaseName = 'tenant2' }) ` + 'default-test-template' 'runner.ps1' 'IntegrationTest' | Should -BeTrue + Should -Invoke Reset-BcTestTenant -Times 1 -Exactly + Should -Invoke Invoke-SqlPilotCompaniesProbe -Times 0 + Should -Invoke Start-RequiredDisabledDispatch -Times 1 -Exactly + } + It 'fails closed instead of entering legacy warmup for invalid uptake authorization or arm' { + $env:BC_SQL_UPTAKE_AUTHORIZATION = '' + { Test-SqlApiWarmupEnabled } | Should -Throw '*refusing warmup fallback*' + $env:BC_SQL_UPTAKE_AUTHORIZATION = 'AB653457-fixed-platform-original-control-10' + foreach ($arm in @('warmup', 'retry')) { + $env:BC_SQL_API_EXPERIMENT = $arm + Test-SqlApiExperiment | Should -BeFalse + { Test-SqlApiWarmupEnabled } | Should -Throw '*refusing warmup fallback*' + } + } + } + It 'rejects =' -ForEach @( + @{ Key = 'GITHUB_REF'; Value = 'refs/heads/features/646383-sql-api-300-trial-comparison' } + @{ Key = 'GITHUB_EVENT_NAME'; Value = 'pull_request' } + @{ Key = 'GITHUB_RUN_ATTEMPT'; Value = '2' } + @{ Key = 'BC_SQL_UPTAKE_AUTHORIZATION'; Value = '' } + @{ Key = 'GITHUB_REPOSITORY'; Value = 'other/BCApps' } + @{ Key = 'BC_SQL_API_EXPERIMENT'; Value = 'warmup' } + @{ Key = 'BC_SQL_API_EXPERIMENT'; Value = 'retry' } + @{ Key = 'BC_SQL_PILOT_ARM'; Value = 'fresh' } + @{ Key = 'BC_SQL_PILOT_TRIAL'; Value = '6' } + @{ Key = 'BC_SQL_PILOT_TRIAL'; Value = '01' } + @{ Key = 'BC_SQL_PILOT_COUNTRY'; Value = 'US' } + ) { + [Environment]::SetEnvironmentVariable($Key, $Value) + { Assert-SqlPlatformUptakeContext -Cell } | Should -Throw + } +} + +Describe 'Compiled package and transport fail-closed checks' { + It 'rejects changed compiled package hashes or missing packages' { + { Assert-SqlPlatformUptakePackageSet -Country W1 -Kind apps -Files @(@{ name = 'modified.app'; sha256 = ('a' * 64) }) } | Should -Throw + { Assert-SqlPlatformUptakePackageSet -Country DE -Kind tests -Files @() } | Should -Throw + } + It 'rejects a snapshot whose immutable ID has the wrong transport digest before downloading' { + $old = $env:GH_TOKEN + try { + $env:GH_TOKEN = 'synthetic' + Mock Invoke-RestMethod -ModuleName PlatformUptake { @{ id = '11487493885'; digest = 'sha256:wrong' } } + Mock Invoke-WebRequest -ModuleName PlatformUptake { throw 'Download must not occur.' } + { Save-SqlPlatformUptakeSnapshot -Country W1 -Directory $TestDrive } | Should -Throw '*ID/digest differs*' + Should -Invoke Invoke-WebRequest -ModuleName PlatformUptake -Times 0 + } finally { $env:GH_TOKEN = $old } + } +} diff --git a/build/scripts/SqlResetPilot/PlatformUptake.json b/build/scripts/SqlResetPilot/PlatformUptake.json new file mode 100644 index 00000000000..9459164a837 --- /dev/null +++ b/build/scripts/SqlResetPilot/PlatformUptake.json @@ -0,0 +1,88 @@ +{ + "schemaVersion": 1, + "authorization": "AB653457-fixed-platform-original-control-10", + "branch": "features/653457-sql-platform-fix-uptake", + "comparisonBase": "0227094059f2f26f3fcb1b7f85a3285c17e78b8e", + "originalSnapshotRun": "37634358042", + "applicationSource": "c4953dceffe02a017adad34973e1955017bf5d20", + "applicationRun": "37372848860", + "applicationVersion": "30.0.55683.0", + "previousRuntime": "30.0.55665.0", + "runtime": "30.0.56092.0", + "publication": { + "observedUtc": "2026-10-09T11:00:34Z", + "fixPullRequest": "https://microsoft.ghe.com/bic/BC-Platform/pull/47167", + "fixCommit": "7a4d33b8848c5795474eb6d517724e3ae5043baf", + "officialDefinition": 698, + "officialBuild": 2860912, + "officialSource": "3ed6bbc6787b3f832c1ba7d2163a49bfec75f50f", + "commitsAheadOfFix": 1, + "officialResult": "Succeeded", + "publisherDefinition": 2336, + "publisherRun": 2861912, + "publisherResult": "Succeeded", + "actualResource": "_Platform-Core-OfficialBuild", + "actualResourceBuild": 2860912, + "actualResourceVersion": "30.0.56092.0", + "publisherSelfSourceIsNotBinaryProvenance": true, + "index": "https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/platform/indexes/platform.json", + "creationUtc": "2026-10-09T10:47:20Z" + }, + "snapshots": [ + { + "country": "W1", + "id": "11487493885", + "digest": "09be1c12a5fe014fd2c1e0a6ab075e809f5693a9294dc8aa949c53961d1b50bd" + }, + { + "country": "DE", + "id": "11486874993", + "digest": "786e121028e19304a94fcf86da7d40326701a5a19631adbcaf3302dac9abb5ac" + } + ], + "applicationArtifacts": [ + { + "country": "W1", "kind": "apps", "id": "11374170356", + "digest": "683343673e6efeb7699734b250d2ee5004d2d83fe08e57f324c4c645c09612db", + "compiledPackageCount": 117, + "compiledPackageSetSha256": "e54b977a72f2232cc293e1442740252c1e82f553c9c21da2fa86c9273b4d220b" + }, + { + "country": "W1", "kind": "tests", "id": "11374145359", + "digest": "f75ce06920437328e5d79a5512eddcc2425624bb696c339c108478c83554171d", + "compiledPackageCount": 131, + "compiledPackageSetSha256": "a556c75d78c7becac2050e8068931e279196cc382a7d86846044ac298182e339" + }, + { + "country": "DE", "kind": "apps", "id": "11376415128", + "digest": "409af00bb5e6922224626f94a4d4f5625f4a9bf13b9398e84cc101a41a313dd5", + "compiledPackageCount": 123, + "compiledPackageSetSha256": "0288721b1bfdecc31c13200ef40c3d5205e937b18478e0fc6768e43df82a65f3" + }, + { + "country": "DE", "kind": "tests", "id": "11375174896", + "digest": "4e1edde77089e0b586679b168f11123a8181a99c63c3a82b42323503340a5d3b", + "compiledPackageCount": 131, + "compiledPackageSetSha256": "8074c1c7e2251b573a66914f5e66e9f93a251abeddc747e63812d4c009bcf912" + } + ], + "trialPlan": { + "arm": "control", + "countries": ["W1", "DE"], + "trials": [1, 2, 3, 4, 5], + "cellCount": 10, + "firstPairBeforeRemainingPairs": true, + "globalContainerLimit": 2, + "mountedTenants": 4, + "workers": 3, + "warmup": false, + "companiesProbe": false, + "readinessWait": false, + "retries": 0, + "addedSkips": 0, + "codeunits": [139700,139702,139703,139706,139725,139726,139732,139739,139742,139745,139802,139803,139806,139826,139832,139854,139972,139780,148315,148318,148343], + "cases": 253, + "expectedPass": 234, + "existingSkip": 19 + } +} diff --git a/build/scripts/SqlResetPilot/PlatformUptake.psm1 b/build/scripts/SqlResetPilot/PlatformUptake.psm1 new file mode 100644 index 00000000000..e33fc26e991 --- /dev/null +++ b/build/scripts/SqlResetPilot/PlatformUptake.psm1 @@ -0,0 +1,57 @@ +function Get-SqlPlatformUptake { + Get-Content (Join-Path $PSScriptRoot 'PlatformUptake.json') -Raw | ConvertFrom-Json +} + +function Assert-SqlPlatformUptakeContext { + param([switch]$Cell) + if ($env:GITHUB_REPOSITORY -ne 'microsoft/BCApps' -or + $env:GITHUB_REF -ne 'refs/heads/features/653457-sql-platform-fix-uptake' -or + $env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or + $env:BC_SQL_UPTAKE_AUTHORIZATION -ne 'AB653457-fixed-platform-original-control-10' -or + $env:GITHUB_RUN_ID -notmatch '^\d{1,20}$' -or $env:GITHUB_SHA -notmatch '^[0-9a-f]{40}$') { + throw 'Fixed-platform uptake requires its exact authorized manual branch and attempt one.' + } + if ($Cell -and ($env:BC_SQL_PILOT_ARM -ne 'control' -or $env:BC_SQL_API_EXPERIMENT -ne 'control' -or + $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or $env:BC_SQL_PILOT_TRIAL -notmatch '^[1-5]$')) { + throw 'Fixed-platform uptake permits only the ten original control cells.' + } +} + +function Get-SqlPlatformUptakeCell { + foreach ($trial in 1..5) { + foreach ($country in @('W1', 'DE')) { + [PSCustomObject]@{ experiment = 'control'; country = $country; trial = $trial } + } + } +} + +function Save-SqlPlatformUptakeSnapshot { + param([ValidateSet('W1', 'DE')][string]$Country, [string]$Directory) + $pin = Get-SqlPlatformUptake + $snapshot = @($pin.snapshots | Where-Object country -eq $Country)[0] + if (-not $env:GH_TOKEN) { throw 'Actions credential is required for immutable package transport.' } + $metadata = Invoke-RestMethod "https://api.github.com/repos/microsoft/BCApps/actions/artifacts/$($snapshot.id)" ` + -Headers @{ Authorization = "Bearer $env:GH_TOKEN"; Accept = 'application/vnd.github+json' } + if ($metadata.id -ne $snapshot.id -or $metadata.digest -ne "sha256:$($snapshot.digest)") { + throw 'Original comparison snapshot ID/digest differs from the canonical uptake manifest.' + } + Import-Module (Join-Path $PSScriptRoot 'Comparison.psm1') -Force + Save-SqlComparisonSnapshot -ArtifactId $snapshot.id -RunId $pin.originalSnapshotRun ` + -HeadSha $pin.comparisonBase -Country $Country -Directory $Directory +} + +function Assert-SqlPlatformUptakePackageSet { + param([ValidateSet('W1', 'DE')][string]$Country, [string]$Kind, [array]$Files) + $pin = @((Get-SqlPlatformUptake).applicationArtifacts | Where-Object { $_.country -eq $Country -and $_.kind -eq $Kind }) + $canonical = @($Files | Sort-Object name | ForEach-Object { "$($_.name)=$($_.sha256.ToLowerInvariant())" }) -join "`n" + $sha = [Security.Cryptography.SHA256]::Create() + try { $digest = ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonical)))).Replace('-', '').ToLowerInvariant() } + finally { $sha.Dispose() } + if ($pin.Count -ne 1 -or $Files.Count -ne $pin[0].compiledPackageCount -or + $digest -ne $pin[0].compiledPackageSetSha256) { + throw 'Extracted compiled packages differ from the original control package set.' + } +} + +Export-ModuleMember -Function Get-SqlPlatformUptake, Assert-SqlPlatformUptakeContext, + Get-SqlPlatformUptakeCell, Save-SqlPlatformUptakeSnapshot, Assert-SqlPlatformUptakePackageSet diff --git a/build/scripts/SqlResetPilot/Preflight.Tests.ps1 b/build/scripts/SqlResetPilot/Preflight.Tests.ps1 index 51cd9a618b7..97061e8e196 100644 --- a/build/scripts/SqlResetPilot/Preflight.Tests.ps1 +++ b/build/scripts/SqlResetPilot/Preflight.Tests.ps1 @@ -6,12 +6,15 @@ Describe 'Experiment country and trial container ownership' { BeforeEach { $script:saved = @{} foreach ($name in @('GITHUB_REPOSITORY','GITHUB_REF','GITHUB_EVENT_NAME','GITHUB_RUN_ATTEMPT','GITHUB_RUN_ID', - 'BC_SQL_PILOT_ARM','BC_SQL_PILOT_COUNTRY','BC_SQL_PILOT_TRIAL','BC_SQL_PILOT_OUTPUT','BC_SQL_API_EXPERIMENT','Settings')) { + 'BC_SQL_PILOT_ARM','BC_SQL_PILOT_COUNTRY','BC_SQL_PILOT_TRIAL','BC_SQL_PILOT_OUTPUT','BC_SQL_API_EXPERIMENT','Settings', + 'GITHUB_SHA','BC_SQL_UPTAKE_AUTHORIZATION')) { $script:saved[$name] = [Environment]::GetEnvironmentVariable($name) } $env:GITHUB_REPOSITORY = 'microsoft/BCApps' - $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-300-trial-comparison' - $env:BC_SQL_API_EXPERIMENT = 'retry' + $env:GITHUB_REF = 'refs/heads/features/653457-sql-platform-fix-uptake' + $env:GITHUB_SHA = 'a' * 40 + $env:BC_SQL_UPTAKE_AUTHORIZATION = 'AB653457-fixed-platform-original-control-10' + $env:BC_SQL_API_EXPERIMENT = 'control' $env:GITHUB_EVENT_NAME = 'workflow_dispatch' $env:GITHUB_RUN_ATTEMPT = '1' $env:GITHUB_RUN_ID = '123' @@ -24,8 +27,8 @@ Describe 'Experiment country and trial container ownership' { companyName = 'My Company'; enableCleanTestCodeunitExecution = $true; enableTaskScheduler = $false } | ConvertTo-Json $script:parameters = @{ - ContainerName = 'bcbuildprojectsTestAppsDETrial2retry123' - platformArtifactUrl = 'https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/platform/30.0.55665.0/platform' + ContainerName = 'bcbuildprojectsTestAppsDEUptake2control123' + platformArtifactUrl = 'https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/platform/30.0.56092.0/platform' } Mock Get-Module { { '6.1.19-preview2811389' } } -ParameterFilter { $Name -eq 'BcContainerHelper' } Mock Test-BcContainer { $false } @@ -38,8 +41,9 @@ Describe 'Experiment country and trial container ownership' { $record = Get-Content (Join-Path $TestDrive 'container-ownership.json') -Raw | ConvertFrom-Json $record.country | Should -Be 'DE' $record.trial | Should -Be '2' - $record.container | Should -Be 'bcbuildprojectsTestAppsDETrial2retry123' - $record.experiment | Should -Be 'retry' + $record.container | Should -Be 'bcbuildprojectsTestAppsDEUptake2control123' + $record.experiment | Should -Be 'control' + $record.platform | Should -Be '30.0.56092.0' $script:parameters.useGenericImage | Should -Be 'mcr.microsoft.com/businesscentral@sha256:c899d12093ad7bbdbfd08ccc0e6294e0f98c682c7e35db4ecfbca345fb068492' } It 'refuses a preexisting container rather than replacing it' { @@ -70,22 +74,37 @@ Describe 'Experiment country and trial container ownership' { $env:Settings = $settings | ConvertTo-Json { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw '*settings differ*' } - It 'allows trial 50 but never trial 51 even with matching container names' { - $env:BC_SQL_PILOT_TRIAL = '50' - $script:parameters.ContainerName = 'bcbuildprojectsTestAppsDETrial50retry123' + It 'allows trial 5 but never trial 6 even with matching container names' { + $env:BC_SQL_PILOT_TRIAL = '5' + $script:parameters.ContainerName = 'bcbuildprojectsTestAppsDEUptake5control123' & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters - $env:BC_SQL_PILOT_TRIAL = '51' - $script:parameters.ContainerName = 'bcbuildprojectsTestAppsDETrial51retry123' + $env:BC_SQL_PILOT_TRIAL = '6' + $script:parameters.ContainerName = 'bcbuildprojectsTestAppsDEUptake6control123' { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw } It 'rejects obsolete exclusion arms and other branches or events' { $env:BC_SQL_API_EXPERIMENT = 'A' { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw - $env:BC_SQL_API_EXPERIMENT = 'retry' + $env:BC_SQL_API_EXPERIMENT = 'control' $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-two-arm-experiment' { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw - $env:GITHUB_REF = 'refs/heads/features/646383-sql-api-300-trial-comparison' + $env:GITHUB_REF = 'refs/heads/features/653457-sql-platform-fix-uptake' $env:GITHUB_EVENT_NAME = 'pull_request' { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw } + It 'rejects the old platform, floating platform and missing authorization' { + foreach ($version in @('30.0.55665.0', 'latest', '28.0.56092.0')) { + $script:parameters.platformArtifactUrl = "https://bcinsider-fvh2ekdjecfjd6gk.b02.azurefd.net/platform/$version/platform" + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw '*pinned source platform*' + } + $env:BC_SQL_UPTAKE_AUTHORIZATION = '' + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw '*authorized manual branch*' + } + It 'rejects warmup and retry before any container inspection' { + foreach ($arm in @('warmup', 'retry')) { + $env:BC_SQL_API_EXPERIMENT = $arm + { & (Join-Path $PSScriptRoot 'ContainerPreflight.ps1') -Parameters $script:parameters } | Should -Throw '*ten original control cells*' + } + Should -Invoke Test-BcContainer -Times 0 + } } diff --git a/build/scripts/SqlResetPilot/Prepare.ps1 b/build/scripts/SqlResetPilot/Prepare.ps1 index e77ada3dda4..69ce79c59f9 100644 --- a/build/scripts/SqlResetPilot/Prepare.ps1 +++ b/build/scripts/SqlResetPilot/Prepare.ps1 @@ -1,20 +1,15 @@ $ErrorActionPreference = 'Stop' $base = 'c4953dceffe02a017adad34973e1955017bf5d20' -if ($env:GITHUB_REPOSITORY -ne 'microsoft/BCApps' -or - $env:GITHUB_REF -ne 'refs/heads/features/646383-sql-api-300-trial-comparison' -or - $env:BC_SQL_API_EXPERIMENT -notin @('control', 'warmup', 'retry') -or - $env:GITHUB_EVENT_NAME -ne 'workflow_dispatch' -or $env:GITHUB_RUN_ATTEMPT -ne '1' -or - $env:BC_SQL_PILOT_ARM -ne 'control' -or $env:BC_SQL_PILOT_COUNTRY -notin @('W1', 'DE') -or - $env:BC_SQL_PILOT_TRIAL -notmatch '^(?:[1-9]|[1-4][0-9]|50)$' -or $env:GITHUB_RUN_ID -notmatch '^\d{1,20}$') { - throw 'This diagnostic is restricted to its explicitly dispatched disposable CI branch, attempt one.' -} +Import-Module (Join-Path $PSScriptRoot 'PlatformUptake.psm1') -Force +Assert-SqlPlatformUptakeContext -Cell +$uptake = Get-SqlPlatformUptake git merge-base --is-ancestor $base HEAD if ($LASTEXITCODE -ne 0) { throw 'PR2 base is not an ancestor.' } git diff --exit-code $base HEAD -- src if ($LASTEXITCODE -ne 0) { throw 'AL/package source differs from the pinned compiled PR2 source.' } $project = "build\projects\Test Apps $($env:BC_SQL_PILOT_COUNTRY)" -$trialProject = "$project Trial$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)" +$trialProject = "$project Uptake$($env:BC_SQL_PILOT_TRIAL)$($env:BC_SQL_API_EXPERIMENT)" if (Test-Path $trialProject) { throw 'Trial project must not already exist.' } # AL-Go derives its container name from the project path. Preserve wrapper depth and country settings. Copy-Item $project $trialProject -Recurse @@ -25,13 +20,12 @@ New-Item -ItemType Directory $output -Force | Out-Null "BC_SQL_PILOT_OUTPUT=$output" | Add-Content $env:GITHUB_ENV @{ phase = 'package-preflight'; arm = $env:BC_SQL_PILOT_ARM; run = $env:GITHUB_RUN_ID; utc = [DateTime]::UtcNow.ToString('o') } | ConvertTo-Json | Set-Content (Join-Path $output 'start.json') -Encoding UTF8 -Import-Module (Join-Path $PSScriptRoot 'Comparison.psm1') -Force -$replacement = $env:BC_SQL_COMPARISON_REPLACEMENT | ConvertFrom-Json -$expectedSnapshotRun = if ($replacement) { [string]$replacement.runId } else { $env:GITHUB_RUN_ID } -if ($env:BC_SQL_COMPARISON_PACKAGE_RUN -ne $expectedSnapshotRun) { throw 'Snapshot source run differs from original/replacement provenance.' } +$expectedSnapshotRun = $uptake.originalSnapshotRun +if ($env:BC_SQL_COMPARISON_REPLACEMENT -and $env:BC_SQL_COMPARISON_REPLACEMENT -ne 'null') { + throw 'No replacement trials are authorized.' +} $snapshotDirectory = Join-Path $env:GITHUB_WORKSPACE 'sql-api-package-snapshot' -$snapshotProof = @(Save-SqlComparisonSnapshot -ArtifactId $env:BC_SQL_COMPARISON_PACKAGE_ARTIFACT ` - -RunId $expectedSnapshotRun -HeadSha $env:GITHUB_SHA -Country $env:BC_SQL_PILOT_COUNTRY -Directory $snapshotDirectory) +$snapshotProof = @(Save-SqlPlatformUptakeSnapshot -Country $env:BC_SQL_PILOT_COUNTRY -Directory $snapshotDirectory) $snapshotProof | ConvertTo-Json -Depth 12 | Set-Content (Join-Path $output 'original-artifact-proof.json') $artifacts = @( @{ Kind = 'apps'; Id = 11374170356; Digest = '683343673e6efeb7699734b250d2ee5004d2d83fe08e57f324c4c645c09612db' }, @@ -60,10 +54,12 @@ foreach ($artifact in $artifacts) { $list = Join-Path $destination 'packages.json' ConvertTo-Json -InputObject @($files.FullName) | Set-Content $list -Encoding UTF8 "$($artifact.Kind)=$list" | Add-Content $env:GITHUB_OUTPUT + $packageFiles = @($files | ForEach-Object { @{ name = $_.Name; sha256 = (Get-FileHash $_.FullName).Hash } }) + Assert-SqlPlatformUptakePackageSet -Country $env:BC_SQL_PILOT_COUNTRY -Kind $artifact.Kind -Files $packageFiles $manifest += @{ artifactId = $artifact.Id; sourceRun = 37372848860; sourceHead = $base kind = $artifact.Kind; archiveSha256 = $artifact.Digest - files = @($files | ForEach-Object { @{ name = $_.Name; sha256 = (Get-FileHash $_.FullName).Hash } }) + files = $packageFiles } } @{ @@ -71,8 +67,9 @@ foreach ($artifact in $artifacts) { experiment = $env:BC_SQL_API_EXPERIMENT; sharedBaseline = 'fbd7ec46c636ee5f9d940cb81e5abbc1df90f055' probeAndWarmupBaseline = 'fcc1776c6b165199dd66e4227675b1cc31da7a8f' trialIdentity = "$($env:BC_SQL_API_EXPERIMENT)/$($env:BC_SQL_PILOT_COUNTRY)/$($env:BC_SQL_PILOT_TRIAL)" - replacementOf = $replacement - packageSnapshot = @{ runId = $expectedSnapshotRun; artifactId = $env:BC_SQL_COMPARISON_PACKAGE_ARTIFACT } + platformUptake = $uptake + replacementOf = $null + packageSnapshot = @{ runId = $expectedSnapshotRun; artifactId = @($uptake.snapshots | Where-Object country -eq $env:BC_SQL_PILOT_COUNTRY)[0].id } country = $env:BC_SQL_PILOT_COUNTRY; trial = $env:BC_SQL_PILOT_TRIAL; project = $trialProject warmup = $(if ($env:BC_SQL_API_EXPERIMENT -eq 'control') { 'none' } else { 'original-first-app-before-clean-lane' }) companiesProbe = $(if ($env:BC_SQL_API_EXPERIMENT -eq 'control') { 'none' } else { 'per-restored-worker-single-attempt' }) @@ -82,4 +79,4 @@ foreach ($artifact in $artifacts) { genericTestRetries = 0; schedulerRetries = 0; ciRetries = 0 budgetMinutes = 120; startedUtc = [DateTime]::UtcNow.ToString('o') artifacts = $manifest -} | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $output 'provenance.json') -Encoding UTF8 +} | ConvertTo-Json -Depth 15 | Set-Content (Join-Path $output 'provenance.json') -Encoding UTF8 diff --git a/build/scripts/SqlResetPilot/README.md b/build/scripts/SqlResetPilot/README.md index d0cc7f55b80..812907ad0f7 100644 --- a/build/scripts/SqlResetPilot/README.md +++ b/build/scripts/SqlResetPilot/README.md @@ -1,4 +1,45 @@ -# SQL API 300-trial diagnostic — AB#646383 — DO NOT MERGE +# Fixed-platform uptake — AB#653457 — DO NOT MERGE + +This isolated branch starts at original comparison commit +`0227094059f2f26f3fcb1b7f85a3285c17e78b8e`, not current main. Its only +runtime treatment is **30.0.55665.0 → 30.0.56092.0**. The platform fix +`7a4d33b8848c5795474eb6d517724e3ae5043baf` is an ancestor of official build +2860912 (definition 698). Publisher 2861912 (definition 2336) consumed that +exact `_Platform-Core-OfficialBuild` resource; its own source is not binary +provenance. CDN publication was verified 2026-10-09T11:00:34Z. + +The new manual `CICD.yaml` requires branch +`features/653457-sql-platform-fix-uptake`, attempt one, and authorization +`AB653457-fixed-platform-original-control-10`. It schedules **ten original +control cells**, W1/DE indices 1–5, in five sequential pairs, each with at +most two containers. Pair one completes before pairs two–five; failures +remain failures and do not suppress later original pairs. No replacements, +reruns, readiness waits, warmups, companies probes, added skips or compilation. +Automatic PR initialization excludes this branch. No ordinary full CI exists +in this branch's manual-only CICD workflow. + +`PlatformUptake.json` is the canonical fixed-build, application archive, +compiled-package-set, snapshot transport and selected-plan provenance. +`PlanPlatformUptake.ps1` validates the original comparison's still-live country +snapshots; it does **not** download expired source artifacts or substitute +newer packages. Every cell repeats the transport, source, ZIP and extracted +package hash checks. Package-set fingerprints are SHA256 of UTF-8, +LF-joined `name=lowercase-sha256` lines sorted by name, without trailing LF. +They were taken from original control W1/DE trial-one provenance. + +The exact 21 CUs/253 methods/234 expected passes/19 existing skips, four +mounted tenants/three workers, password-file authentication, helper, image, +application artifact, company, clean-template lifecycle and failure finalizers +remain the original control protocol below. Installed NST process file version +must match 30.0.56092.0; `runtime-preflight.json` is runtime evidence, not the +publication manifest. Owned names use `Uptakecontrol` and reject +preexisting containers; teardown cannot touch original comparison containers. + +Local unit/static checks are not installed-runtime or SQL-fix verification. +Parent review, capacity/duplicate-run checks and one authorized dispatch remain +necessary. Historical baseline, readiness and tenant20 campaigns are unchanged. + +## Historical original-300 protocol (reference only; not this branch's schedule) Explicit user authorization: 2026-10-07. One manual `CICD.yaml` dispatch on `features/646383-sql-api-300-trial-comparison` with `mode=originals` schedules