From baa93fcfc3ce0e0be53f137f2dc9260a5e6fcfc4 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 12 Oct 2026 11:53:11 +0200 Subject: [PATCH] Re-enable Expense Agent API tests Remove the Expense API Test Auth Helper (no Key Vault secret, no subscriber) and apply the Expense app and test fixes. Keep only Expense disablements that NAV master also has. Enable clean test codeunit execution; APIV1/APIV2 tests that this surfaces are temporarily excluded (entries from #12325) and re-enabled by the following layers. AB#646383 AB#653393 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 | 18 +- .../ExpenseCapabilitiesAPITest.Codeunit.al | 4 - .../ExpensePerDiemLocationsTest.Codeunit.al | 3 +- .../API/ExpenseProjectsAPITest.Codeunit.al | 2 - .../src/API/ExpenseUsersAPITest.Codeunit.al | 2 - .../src/API/ExpenseVATSpecAPITest.Codeunit.al | 3 +- .../API/PoliciesToEvaluateAPITest.Codeunit.al | 2 - .../API/PolicyEvaluationsAPITest.Codeunit.al | 2 - .../src/API/TravelRequestsAPITest.Codeunit.al | 8 - .../src/ExpensePermissionsTest.Codeunit.al | 141 +++- .../ExpenseAPITestAuthHelper.Codeunit.al | 66 -- .../ExpDetailTest.PermissionSet.al | 20 + .../test/src/SpendRequestTest.Codeunit.al | 180 ++++- .../Expense_Agent_Tests.DisabledTest.json | 240 ------- .../_Exclude_APIV1__Tests.DisabledTest.json | 675 ++++++++++++++++++ .../_Exclude_APIV2__Tests.DisabledTest.json | 290 ++++++++ .../Tests/ERM/SpendRequestTests.Codeunit.al | 48 ++ 19 files changed, 1369 insertions(+), 365 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 5b6b1b2721c..e805b45f586 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 86303ef065c..ec9b195a627 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseActivityLogAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseActivityLogAPITest.Codeunit.al @@ -23,7 +23,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; @@ -477,11 +476,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); @@ -489,7 +491,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.'); @@ -503,7 +506,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.'); @@ -512,7 +516,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.'); @@ -804,7 +809,6 @@ codeunit 148343 "Expense Activity Log API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); 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/ExpenseCapabilitiesAPITest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseCapabilitiesAPITest.Codeunit.al index 5d7ceb83f2d..822bf798e1f 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseCapabilitiesAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseCapabilitiesAPITest.Codeunit.al @@ -19,7 +19,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; @@ -105,8 +104,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(); @@ -249,7 +246,6 @@ codeunit 148318 "Expense Capabilities API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); 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 bea8a98efa6..ac41f8d1005 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpensePerDiemLocationsTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpensePerDiemLocationsTest.Codeunit.al @@ -18,7 +18,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] @@ -83,11 +82,11 @@ 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); 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 632c3d77459..93d563eb35d 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseProjectsAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseProjectsAPITest.Codeunit.al @@ -25,7 +25,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; @@ -724,7 +723,6 @@ codeunit 148331 "Expense Projects API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); 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 00fafa3e091..7ed4334beac 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseUsersAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseUsersAPITest.Codeunit.al @@ -22,7 +22,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; @@ -126,7 +125,6 @@ codeunit 148315 "Expense Users API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); 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 8a57c7495e5..d036cc53c87 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseVATSpecAPITest.Codeunit.al @@ -12,6 +12,7 @@ codeunit 148348 "Expense VAT Spec. API Test" Subtype = Test; TestHttpRequestPolicy = AllowAllOutboundRequests; TestType = IntegrationTest; + RequiredTestIsolation = Disabled; TestPermissions = Disabled; var @@ -20,7 +21,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; @@ -90,7 +90,6 @@ codeunit 148348 "Expense VAT Spec. API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); 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 2824ca06a1a..36726ffbccd 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/PoliciesToEvaluateAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/PoliciesToEvaluateAPITest.Codeunit.al @@ -19,7 +19,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; @@ -118,7 +117,6 @@ codeunit 148345 "Policies To Evaluate API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); 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 bdc039bde58..f2f7e47f2b7 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/PolicyEvaluationsAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/PolicyEvaluationsAPITest.Codeunit.al @@ -19,7 +19,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; @@ -122,7 +121,6 @@ codeunit 148344 "Policy Evaluations API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); 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 7b802b24cb5..5d845c9a9e3 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; @@ -32,7 +26,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 @@ -1292,7 +1285,6 @@ codeunit 148347 "Travel Requests API Test" if IsInitialized then exit; - BindSubscription(APITestAuthHelper); 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 16d7e7ba931..8184e201491 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 @@ -190,20 +278,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."); @@ -614,8 +711,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); @@ -634,10 +730,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 22caab107b3..00000000000 --- a/src/Apps/W1/ExpenseAgent/test/src/Helper/ExpenseAPITestAuthHelper.Codeunit.al +++ /dev/null @@ -1,66 +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.Security.AccessControl; -using System.Text; - -/// -/// 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 HttpRequestMessage: HttpRequestMessage) - var - Base64Convert: Codeunit "Base64 Convert"; - RequestHeaders: HttpHeaders; - Password: SecretText; - begin - if not ShouldInjectBasicAuth() then - exit; - if not TryGetPassword(Password) then - exit; - HttpRequestMessage.GetHeaders(RequestHeaders); - if RequestHeaders.Contains('Authorization') or RequestHeaders.ContainsSecret('Authorization') then - RequestHeaders.Remove('Authorization'); - RequestHeaders.Add('Authorization', SecretStrSubstNo('Basic %1', Base64Convert.ToBase64(SecretStrSubstNo('%1:%2', 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..27a0e065b84 100644 --- a/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json +++ b/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json @@ -1,254 +1,14 @@ [ - { - "codeunitId": 148315, - "codeunitName": "Expense Users API Test", - "method": "EmployeesAPICanFilterExpenseUsers" - }, - { - "codeunitId": 148315, - "codeunitName": "Expense Users API Test", - "method": "UnlinkedExpenseUserIsHiddenFromAPI" - }, - { - "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": "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/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_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/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