From d331712fccf7585dc1e842a9bb12fa23d531dc33 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 21 Sep 2026 12:59:35 +0200 Subject: [PATCH 01/16] Repair and re-enable Expense Agent permission and posted-history tests Consolidate role assertion and real posted-history fixture repairs on main; re-enable fourteen Expense Agent tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../src/ExpensePermissionsTest.Codeunit.al | 39 ++++++--- .../test/src/SpendRequestTest.Codeunit.al | 21 ++--- .../Expense_Agent_Tests.DisabledTest.json | 81 ------------------- 3 files changed, 37 insertions(+), 104 deletions(-) diff --git a/src/Apps/W1/ExpenseAgent/test/src/ExpensePermissionsTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/ExpensePermissionsTest.Codeunit.al index db277c3b38d..0c68ac933ab 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/ExpensePermissionsTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/ExpensePermissionsTest.Codeunit.al @@ -508,6 +508,15 @@ codeunit 148338 "Expense Permissions Test" SpendRequestToGLLink: Record "Spend Request To G/L Link"; ExpenseUser: Record "Expense User"; ExpenseReportHeader: Record "Expense Report Header"; + SpendRequestCanRead: Boolean; + SpendRequestDetailCanRead: Boolean; + SpendRequestToGLLinkCanRead: Boolean; + SpendRequestCanWrite: Boolean; + SpendRequestDetailCanWrite: Boolean; + ExpenseUserCanRead: Boolean; + ExpenseReportHeaderCanRead: Boolean; + ExpenseUserCanWrite: Boolean; + ExpenseReportHeaderCanWrite: Boolean; begin Initialize(); @@ -516,18 +525,28 @@ codeunit 148338 "Expense Permissions Test" LibraryLowerPermissions.SetExactPermissionSet(PermissionSetId); // [WHEN] The effective table permissions are evaluated. - // [THEN] BaseApp rights are not added to these roles; app-owned rights follow the role level. - Assert.IsFalse(SpendRequest.ReadPermission(), 'The role must not grant direct BaseApp request access.'); - Assert.IsFalse(SpendRequestDetail.ReadPermission(), 'The role must not grant direct BaseApp detail access.'); - Assert.IsFalse(SpendRequestToGLLink.ReadPermission(), 'The role must not grant direct BaseApp ledger-link access.'); - Assert.IsFalse(SpendRequest.WritePermission(), 'The role must not grant direct BaseApp request writes.'); - Assert.IsFalse(SpendRequestDetail.WritePermission(), 'The role must not grant direct BaseApp detail writes.'); - Assert.IsTrue(ExpenseUser.ReadPermission(), 'The role must retain read access to app-owned expense users.'); - Assert.IsTrue(ExpenseReportHeader.ReadPermission(), 'The role must retain read access to app-owned reports.'); - Assert.AreEqual(CanEdit, ExpenseUser.WritePermission(), 'Expense user write access must follow the role level.'); - Assert.AreEqual(CanEdit, ExpenseReportHeader.WritePermission(), 'Expense report write access must follow the role level.'); + SpendRequestCanRead := SpendRequest.ReadPermission(); + SpendRequestDetailCanRead := SpendRequestDetail.ReadPermission(); + SpendRequestToGLLinkCanRead := SpendRequestToGLLink.ReadPermission(); + SpendRequestCanWrite := SpendRequest.WritePermission(); + SpendRequestDetailCanWrite := SpendRequestDetail.WritePermission(); + ExpenseUserCanRead := ExpenseUser.ReadPermission(); + ExpenseReportHeaderCanRead := ExpenseReportHeader.ReadPermission(); + ExpenseUserCanWrite := ExpenseUser.WritePermission(); + ExpenseReportHeaderCanWrite := ExpenseReportHeader.WritePermission(); RestoreFullPermissions(); LibraryLowerPermissions.StopLoggingNAVPermissions(); + + // [THEN] BaseApp rights are not added to these roles; app-owned rights follow the role level. + Assert.IsFalse(SpendRequestCanRead, 'The role must not grant direct BaseApp request access.'); + Assert.IsFalse(SpendRequestDetailCanRead, 'The role must not grant direct BaseApp detail access.'); + Assert.IsFalse(SpendRequestToGLLinkCanRead, 'The role must not grant direct BaseApp ledger-link access.'); + Assert.IsFalse(SpendRequestCanWrite, 'The role must not grant direct BaseApp request writes.'); + Assert.IsFalse(SpendRequestDetailCanWrite, 'The role must not grant direct BaseApp detail writes.'); + Assert.IsTrue(ExpenseUserCanRead, 'The role must retain read access to app-owned expense users.'); + Assert.IsTrue(ExpenseReportHeaderCanRead, 'The role must retain read access to app-owned reports.'); + Assert.AreEqual(CanEdit, ExpenseUserCanWrite, 'Expense user write access must follow the role level.'); + Assert.AreEqual(CanEdit, ExpenseReportHeaderCanWrite, 'Expense report write access must follow the role level.'); end; local procedure VerifyTravelRequestDetailUpdateIndirectly(PermissionSetId: Code[20]) diff --git a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al index 2f49d2404aa..70731ba985e 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al @@ -607,7 +607,7 @@ codeunit 148339 "Spend Request Test" // [SCENARIO] Posted report references prevent deletion even when the net spent amount is zero. Initialize(); - // [GIVEN] A normally posted report with offsetting amounts and a header-level request link. + // [GIVEN] A normally posted zero-amount report with a header-level request link. CreatePostedTravelRequestReport(SpendRequest, PostedExpenseReportHeader, true); PostedExpenseReportHeader.TestField("Spend Request No.", SpendRequest."No."); Commit(); @@ -2329,9 +2329,9 @@ codeunit 148339 "Spend Request Test" PostedExpenseReportLine.SetRange("Document No.", PostedExpenseReportHeader."No."); PostedExpenseReportLine.SetRange("Spend Request No.", SpendRequest."No."); PostedExpenseReportLine.SetRange("Expense User No.", SpendRequest."Requested For"); - Assert.RecordCount(PostedExpenseReportLine, 2); + Assert.RecordCount(PostedExpenseReportLine, 1); PostedExpenseReportLine.CalcSums("Amount (LCY)"); - Assert.AreEqual(0, PostedExpenseReportLine."Amount (LCY)", 'Both offsetting posted lines must remain linked to the same request and expense user.'); + Assert.AreEqual(0, PostedExpenseReportLine."Amount (LCY)", 'The posted zero-amount line must remain linked to the same request and expense user.'); end; local procedure VerifyReapprovedTravelRequest(SpendRequest: Record "Spend Request"; PostedExpenseReportHeader: Record "Posted Expense Report Header"; SubmitterExpenseUserNo: Code[20]; ApproverExpenseUserNo: Code[20]) @@ -2350,7 +2350,6 @@ codeunit 148339 "Spend Request Test" var ExpenseReportHeader: Record "Expense Report Header"; ExpenseReportLine: Record "Expense Report Line"; - BalancingExpenseReportLine: Record "Expense Report Line"; ExpenseReportPost: Codeunit "Expense Report-Post"; begin if AssignOnHeader then @@ -2360,21 +2359,17 @@ codeunit 148339 "Spend Request Test" ExpenseReportLine.SetRange("Document No.", ExpenseReportHeader."No."); ExpenseReportLine.FindFirst(); - LibraryExpense.CreateExpenseReportLine( - BalancingExpenseReportLine, ExpenseReportHeader, ExpenseReportHeader."Expense User No.", - ExpenseReportLine."Expense Category", ExpenseReportLine."Payment Method Code", true, - ExpenseReportLine."Expense Currency Code", -ExpenseReportLine.Amount); - if not AssignOnHeader then begin - BalancingExpenseReportLine.Validate("Spend Request No.", SpendRequest."No."); - BalancingExpenseReportLine.Modify(true); - end; + // Negative non-correction entries do not reverse recorded spend. + // Normal posting of a zero-amount line still creates genuine posted history. + ExpenseReportLine.Validate(Amount, 0); + ExpenseReportLine.Modify(true); ExpenseReportHeader.PerformManualRelease(); ExpenseReportPost.PostExpenseReport(ExpenseReportHeader); PostedExpenseReportHeader.Get(ExpenseReportHeader."Last Posting No."); SpendRequest.Get(SpendRequest."No."); SpendRequest.CalcFields("Total Spent Amount (LCY)"); - Assert.AreEqual(0, SpendRequest."Total Spent Amount (LCY)", 'Offsetting posted amounts must leave zero net spend.'); + Assert.AreEqual(0, SpendRequest."Total Spent Amount (LCY)", 'The posted zero-amount report must leave zero net spend.'); end; local procedure CreateAndPostExpenseReportWithSpendRequest(var ExpenseReportHeader: Record "Expense Report Header"; var SpendRequest: Record "Spend Request"; NumberOfLines: Integer) 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 b76af3e4384..75e9eceba14 100644 --- a/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json +++ b/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json @@ -293,86 +293,5 @@ "codeunitId": 148348, "codeunitName": "Expense VAT Spec. API Test", "method": "AgentVATSpecificationIsInsertedThroughAPI" - }, - { - "bugId": 650277, - "codeunitId": 148339, - "codeunitName": "Spend Request Test", - "method": "DeleteTravelRequestWithPostedReportIsBlocked" - }, - { - "bugId": 650277, - "codeunitId": 148339, - "codeunitName": "Spend Request Test", - "method": "DeleteTravelRequestWithPostedLineIsBlocked" - }, - { - "bugId": 650277, - "codeunitId": 148339, - "codeunitName": "Spend Request Test", - "method": "CreateExpenseReportRejectsPostedHeader" - }, - { - "bugId": 650277, - "codeunitId": 148339, - "codeunitName": "Spend Request Test", - "method": "CreateExpenseReportRejectsPostedLine" - }, - { - "bugId": 650277, - "codeunitId": 148339, - "codeunitName": "Spend Request Test", - "method": "CreateExpenseReportAllowsOtherPostedTraveler" - }, - { - "bugId": 650277, - "codeunitId": 148339, - "codeunitName": "Spend Request Test", - "method": "ReapproveTravelRequestWithPostedHeader" - }, - { - "bugId": 650277, - "codeunitId": 148339, - "codeunitName": "Spend Request Test", - "method": "ReapproveTravelRequestWithPostedLine" - }, - { - "bugId": 650277, - "codeunitId": 148339, - "codeunitName": "Spend Request Test", - "method": "AutoReapproveTravelRequestWithPostedHistory" - }, - { - "codeunitId": 148339, - "codeunitName": "Spend Request Test", - "method": "ApproveTravelRequestPageAction" - }, - { - "codeunitId": 148339, - "codeunitName": "Spend Request Test", - "method": "SubmitTravelRequestPageAction" - }, - { - "codeunitId": 148339, - "codeunitName": "Spend Request Test", - "method": "RejectTravelRequestPageAction" - }, - { - "bugId": 650245, - "codeunitId": 148338, - "codeunitName": "Expense Permissions Test", - "method": "ExpenseMgmtReadRetainsAppPermissions" - }, - { - "bugId": 650245, - "codeunitId": 148338, - "codeunitName": "Expense Permissions Test", - "method": "ExpenseMgmtEditRetainsAppPermissions" - }, - { - "bugId": 650245, - "codeunitId": 148338, - "codeunitName": "Expense Permissions Test", - "method": "ExpenseMgmtAdminRetainsAppPermissions" } ] From eb3bf74cb621db93236c65eb0e7b7cd0216c249f Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 21 Sep 2026 13:13:56 +0200 Subject: [PATCH 02/16] Repair API currency fixtures and run Spend Request coverage as unit tests Re-enable six Travel Requests API cases with fresh cross-session persistence checks. Keep HTTP integration coverage separate from in-process Spend Request tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../src/API/TravelRequestsAPITest.Codeunit.al | 80 +++++++++++++------ .../test/src/SpendRequestTest.Codeunit.al | 2 +- .../Expense_Agent_Tests.DisabledTest.json | 30 ------- 3 files changed, 58 insertions(+), 54 deletions(-) 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 3016033f0af..5c6f4b9844a 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,12 @@ using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.SpendRequest; using Microsoft.HumanResources.Employee; -// These HTTP tests are excluded in Expense_Agent_Tests.DisabledTest.json per the PR review. -// Re-enable them after BCApps CI provisions an authenticated OData endpoint and a dedicated -// test company with committed fixtures and disabled test isolation, then remove the exclusions. +// These HTTP tests require an authenticated OData endpoint and a dedicated test company +// with committed fixtures and disabled test isolation. Remaining exclusions are listed in +// Expense_Agent_Tests.DisabledTest.json. // 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. +// Other HTTP scenarios remain excluded pending endpoint validation. codeunit 148347 "Travel Requests API Test" { Subtype = Test; @@ -292,7 +292,8 @@ codeunit 148347 "Travel Requests API Test" Request: JsonObject; TargetURL: Text; begin - // [SCENARIO] The user-scoped header API maps LCY without bypassing currency validation. + // [FEATURE] [AI test 1.0] + // [SCENARIO 650247] The user-scoped header API maps LCY without bypassing currency validation. Initialize(); // [GIVEN] A linked expense user creating a travel request. @@ -315,7 +316,8 @@ codeunit 148347 "Travel Requests API Test" Request: JsonObject; TargetURL: Text; begin - // [SCENARIO] The user-scoped detail API accepts LCY ISO codes and retains foreign-currency rules. + // [FEATURE] [AI test 1.0] + // [SCENARIO 650247] The user-scoped detail API accepts LCY ISO codes and retains foreign-currency rules. Initialize(); // [GIVEN] An open travel request owned by a linked expense user. @@ -336,7 +338,6 @@ codeunit 148347 "Travel Requests API Test" procedure TravelRequestsAPIPreservesAndUpdatesDates() var ExpenseUser: Record "Expense User"; - TravelRequest: Record "Spend Request"; Request: JsonObject; Response: JsonObject; ErrorResponse: JsonToken; @@ -349,7 +350,8 @@ codeunit 148347 "Travel Requests API Test" RequestBody: Text; ResponseText: Text; begin - // [SCENARIO] User-scoped POST and PATCH preserve and validate the final date pair. + // [FEATURE] [AI test 1.0] + // [SCENARIO 650247] User-scoped POST and PATCH preserve and validate the final date pair. Initialize(); // [GIVEN] A linked user and a future pair, with id first and end before start in the payload. @@ -371,13 +373,10 @@ codeunit 148347 "Travel Requests API Test" LibraryGraphMgt.PostToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 201); // [THEN] POST, GET, and storage retain the supplied dates and identity. - AssertAPIDates(ResponseText, StartDate, EndDate); - TravelRequest.GetBySystemId(RequestSystemId); - Assert.AreEqual(StartDate, TravelRequest."Expected Start Date", 'The API start date must be persisted.'); - Assert.AreEqual(EndDate, TravelRequest."Expected End Date", 'The API end date must be persisted.'); + VerifyTravelRequestDates(ResponseText, RequestSystemId, StartDate, EndDate); RecordURL := AppendPathToAPIURL(TargetURL, '(' + LibraryGraphMgt.StripBrackets(Format(RequestSystemId)) + ')'); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, RecordURL, 200); - AssertAPIDates(ResponseText, StartDate, EndDate); + VerifyTravelRequestDates(ResponseText, RequestSystemId, StartDate, EndDate); // [WHEN] A start-first PATCH moves the range beyond the old end. StartDate += 30; @@ -389,7 +388,7 @@ codeunit 148347 "Travel Requests API Test" LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(RecordURL, RequestBody, ResponseText, 200); // [THEN] The complete later range is accepted. - AssertAPIDates(ResponseText, StartDate, EndDate); + VerifyTravelRequestDates(ResponseText, RequestSystemId, StartDate, EndDate); // [WHEN] An end-first PATCH moves the range before the old start. StartDate := WorkDate() - 33; @@ -401,7 +400,7 @@ codeunit 148347 "Travel Requests API Test" LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(RecordURL, RequestBody, ResponseText, 200); // [THEN] The complete earlier range is accepted. - AssertAPIDates(ResponseText, StartDate, EndDate); + VerifyTravelRequestDates(ResponseText, RequestSystemId, StartDate, EndDate); // [WHEN] Only the end date is changed. EndDate += 1; @@ -411,7 +410,7 @@ codeunit 148347 "Travel Requests API Test" LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(RecordURL, RequestBody, ResponseText, 200); // [THEN] The omitted start remains unchanged. - AssertAPIDates(ResponseText, StartDate, EndDate); + VerifyTravelRequestDates(ResponseText, RequestSystemId, StartDate, EndDate); // [WHEN] A start-only PATCH would exceed the stored end. Clear(Request); @@ -421,12 +420,13 @@ codeunit 148347 "Travel Requests API Test" // [THEN] The date-range error is returned and the previous valid pair remains stored. Assert.ExpectedError(BadRequestResponseErr); + Assert.ExpectedErrorCode('Dialog'); Response.ReadFrom(ResponseText); Response.Get('error', ErrorResponse); ErrorResponse.AsObject().Get('message', ErrorMessage); Assert.AreNotEqual(0, StrPos(ErrorMessage.AsValue().AsText(), InvalidTravelRequestDatesErr), 'The invalid date range must cause the rejection.'); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, RecordURL, 200); - AssertAPIDates(ResponseText, StartDate, EndDate); + VerifyTravelRequestDates(ResponseText, RequestSystemId, StartDate, EndDate); end; [Test] @@ -640,7 +640,8 @@ codeunit 148347 "Travel Requests API Test" ResponseText: Text; TargetURL: Text; begin - // [SCENARIO] The owner can be supplied on POST and resent unchanged on PATCH. + // [FEATURE] [AI test 1.0] + // [SCENARIO 650247] The owner can be supplied on POST and resent unchanged on PATCH. Initialize(); // [GIVEN] An expense user linked to an employee. @@ -659,6 +660,7 @@ codeunit 148347 "Travel Requests API Test" // [THEN] The owner mismatch is rejected before a request can be inserted. Assert.ExpectedError(BadRequestResponseErr); + Assert.ExpectedErrorCode('Dialog'); Response.ReadFrom(ResponseText); Response.Get('error', ErrorResponse); ErrorResponse.AsObject().Get('message', ErrorMessage); @@ -674,6 +676,7 @@ codeunit 148347 "Travel Requests API Test" Response.ReadFrom(ResponseText); Response.Get('id', RequestId); Evaluate(TravelRequestSystemId, RequestId.AsValue().AsText()); + SelectLatestVersion(); TravelRequest.GetBySystemId(TravelRequestSystemId); Assert.AreEqual(ExpenseUser."Employee No.", TravelRequest."Requested By", 'POST must accept the travel request owner.'); Assert.AreEqual(TravelRequest."Document Type"::"Travel Request", TravelRequest."Document Type", 'POST must create a travel request.'); @@ -682,6 +685,10 @@ codeunit 148347 "Travel Requests API Test" // [THEN] PATCH succeeds without changing the owner. TargetURL := AppendPathToAPIURL(TargetURL, '(' + LibraryGraphMgt.StripBrackets(Format(TravelRequest.SystemId)) + ')'); AssertOwnerPreservingPatch(TargetURL, TravelRequest); + SelectLatestVersion(); + TravelRequest.GetBySystemId(TravelRequestSystemId); + Assert.AreEqual(ExpenseUser."Employee No.", TravelRequest."Requested By", 'PATCH must preserve the stored owner.'); + Assert.AreEqual('Updated business trip', TravelRequest.Purpose, 'PATCH must persist the updated purpose.'); end; [Test] @@ -691,6 +698,7 @@ codeunit 148347 "Travel Requests API Test" OtherExpenseUser: Record "Expense User"; TravelRequest: Record "Spend Request"; OriginalRequestedBy: Code[20]; + TravelRequestSystemId: Guid; Response: JsonObject; ErrorResponse: JsonToken; ErrorCode: JsonToken; @@ -699,7 +707,8 @@ codeunit 148347 "Travel Requests API Test" ResponseText: Text; TargetURL: Text; begin - // [SCENARIO] PATCH cannot reassign a travel request or change its status. + // [FEATURE] [AI test 1.0] + // [SCENARIO 650247] PATCH cannot reassign a travel request or change its status. Initialize(); // [GIVEN] An open travel request and another expense user. @@ -707,6 +716,7 @@ codeunit 148347 "Travel Requests API Test" LibraryExpense.CreateExpenseUser(OtherExpenseUser); CreateTravelRequest(TravelRequest, ExpenseUser."Employee No."); OriginalRequestedBy := TravelRequest."Requested By"; + TravelRequestSystemId := TravelRequest.SystemId; Commit(); // [WHEN] PATCH attempts to change the owner. @@ -717,6 +727,7 @@ codeunit 148347 "Travel Requests API Test" // [THEN] The API identifies the immutable owner and the affected request. Assert.ExpectedError(BadRequestResponseErr); + Assert.ExpectedErrorCode('Dialog'); AssertOwnerChangeError(ResponseText, TravelRequest); // [WHEN] PATCH attempts to change the status. @@ -726,13 +737,15 @@ codeunit 148347 "Travel Requests API Test" // [THEN] The API rejects the read-only status, leaving ownership and status unchanged. Assert.ExpectedError(BadRequestResponseErr); + Assert.ExpectedErrorCode('Dialog'); Response.ReadFrom(ResponseText); Response.Get('error', ErrorResponse); ErrorResponse.AsObject().Get('code', ErrorCode); ErrorResponse.AsObject().Get('message', ErrorMessage); Assert.AreEqual('BadRequest', ErrorCode.AsValue().AsText(), 'The status update must be rejected by the OData read-only guard.'); Assert.AreNotEqual(0, StrPos(ErrorMessage.AsValue().AsText(), StatusReadOnlyErr), 'The API error must identify the read-only status control.'); - TravelRequest.Get(TravelRequest."No."); + SelectLatestVersion(); + TravelRequest.GetBySystemId(TravelRequestSystemId); Assert.AreEqual( OriginalRequestedBy, TravelRequest."Requested By", 'The Travel Requests API must not change the Travel Request owner.'); @@ -771,11 +784,13 @@ codeunit 148347 "Travel Requests API Test" OtherExpenseUser: Record "Expense User"; TravelRequest: Record "Spend Request"; OriginalRequestedBy: Code[20]; + TravelRequestSystemId: Guid; RequestBody: Text; ResponseText: Text; TargetURL: Text; begin - // [SCENARIO] The legacy endpoint cannot bypass travel request ownership protection. + // [FEATURE] [AI test 1.0] + // [SCENARIO 650247] The legacy endpoint cannot bypass travel request ownership protection. Initialize(); // [GIVEN] A travel request owned by one of two expense users. @@ -783,6 +798,7 @@ codeunit 148347 "Travel Requests API Test" LibraryExpense.CreateExpenseUser(OtherExpenseUser); CreateTravelRequest(TravelRequest, ExpenseUser."Employee No."); OriginalRequestedBy := TravelRequest."Requested By"; + TravelRequestSystemId := TravelRequest.SystemId; Commit(); // [WHEN] PATCH through the legacy endpoint attempts to change the owner. @@ -793,8 +809,10 @@ codeunit 148347 "Travel Requests API Test" // [THEN] The owner change is rejected and the original owner is preserved. Assert.ExpectedError(BadRequestResponseErr); + Assert.ExpectedErrorCode('Dialog'); AssertOwnerChangeError(ResponseText, TravelRequest); - TravelRequest.Get(TravelRequest."No."); + SelectLatestVersion(); + TravelRequest.GetBySystemId(TravelRequestSystemId); Assert.AreEqual( OriginalRequestedBy, TravelRequest."Requested By", 'The legacy Spend Requests API must not change the Travel Request owner.'); @@ -820,7 +838,8 @@ codeunit 148347 "Travel Requests API Test" // [GIVEN] Configured LCY and a foreign currency with a non-unit exchange rate. GeneralLedgerSetup.Get(); GeneralLedgerSetup.TestField("LCY Code"); - ForeignCurrencyCode := LibraryERM.CreateCurrencyWithExchangeRate(Today(), 1, 2); + // The third argument is the adjustment rate; the relational rate is always 1. + ForeignCurrencyCode := LibraryERM.CreateCurrencyWithExchangeRate(Today(), 2, 2); Currency.Get(ForeignCurrencyCode); ForeignExchangeRate := Currency.GetExchangeRate(Today()); Assert.AreNotEqual(GeneralLedgerSetup."LCY Code", ForeignCurrencyCode, 'The fixture must use a foreign currency.'); @@ -885,6 +904,7 @@ codeunit 148347 "Travel Requests API Test" AssertTravelRequestCurrency(ResponseText, SystemId, IsDetail, GeneralLedgerSetup."LCY Code", '', 1); // [GIVEN] The request is no longer Open. + SelectLatestVersion(); if IsDetail then begin TravelRequestDetail.GetBySystemId(SystemId); TravelRequest.Get(TravelRequestDetail."Spend Request No."); @@ -923,6 +943,7 @@ codeunit 148347 "Travel Requests API Test" Request.WriteTo(RequestBody); asserterror LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 400); Assert.ExpectedError(BadRequestResponseErr); + Assert.ExpectedErrorCode('Dialog'); Response.ReadFrom(ResponseText); Response.Get('error', ErrorResponse); ErrorResponse.AsObject().Get('message', ErrorMessage); @@ -939,6 +960,8 @@ codeunit 148347 "Travel Requests API Test" Response.ReadFrom(ResponseText); Response.Get('currencyCode', CurrencyCode); Assert.AreEqual(APICurrencyCode, CurrencyCode.AsValue().AsText(), 'The API must expose the canonical currency code.'); + // HTTP writes run in another session; do not verify a cached record image. + SelectLatestVersion(); if IsDetail then begin TravelRequestDetail.GetBySystemId(SystemId); Assert.AreEqual(StoredCurrencyCode, TravelRequestDetail."Currency Code", 'The detail must store the BC currency representation.'); @@ -995,6 +1018,17 @@ codeunit 148347 "Travel Requests API Test" Assert.AreEqual(EndDate, EndDateToken.AsValue().AsDate(), 'The API must return the effective end date.'); end; + local procedure VerifyTravelRequestDates(ResponseText: Text; RequestSystemId: Guid; StartDate: Date; EndDate: Date) + var + TravelRequest: Record "Spend Request"; + begin + AssertAPIDates(ResponseText, StartDate, EndDate); + SelectLatestVersion(); + TravelRequest.GetBySystemId(RequestSystemId); + Assert.AreEqual(StartDate, TravelRequest."Expected Start Date", 'The API start date must be persisted.'); + Assert.AreEqual(EndDate, TravelRequest."Expected End Date", 'The API end date must be persisted.'); + end; + local procedure AssertOwnerPreservingPatch(TargetURL: Text; TravelRequest: Record "Spend Request") var Request: JsonObject; diff --git a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al index 70731ba985e..df1637d0c11 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al @@ -13,7 +13,7 @@ using Microsoft.HumanResources.Employee; codeunit 148339 "Spend Request Test" { Subtype = Test; - TestType = IntegrationTest; + TestType = UnitTest; TestPermissions = Disabled; var 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 75e9eceba14..285951710c4 100644 --- a/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json +++ b/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json @@ -29,21 +29,6 @@ "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", @@ -64,26 +49,11 @@ "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": "LegacySpendRequestsAPIAllowsUnchangedOwner" }, - { - "codeunitId": 148347, - "codeunitName": "Travel Requests API Test", - "method": "LegacySpendRequestsAPIRejectsTravelRequestOwnerChange" - }, { "codeunitId": 148318, "codeunitName": "Expense Capabilities API Test", From ce3e12a5d6ef671f0bb2ea26ccb666a56b7597dc Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 21 Sep 2026 13:53:18 +0200 Subject: [PATCH 03/16] Keep BCApps HTTP API tests excluded pending authentication support Retain NAV API enablement separately and omit added feature tags as requested. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../src/API/TravelRequestsAPITest.Codeunit.al | 14 +++------ .../Expense_Agent_Tests.DisabledTest.json | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 10 deletions(-) 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 5c6f4b9844a..2e093c5cd5c 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,12 @@ using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.SpendRequest; using Microsoft.HumanResources.Employee; -// These HTTP tests require an authenticated OData endpoint and a dedicated test company -// with committed fixtures and disabled test isolation. Remaining exclusions are listed in -// Expense_Agent_Tests.DisabledTest.json. +// 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. -// Other HTTP scenarios remain excluded pending endpoint validation. +// Only the HTTP scenarios are excluded. codeunit 148347 "Travel Requests API Test" { Subtype = Test; @@ -292,7 +292,6 @@ codeunit 148347 "Travel Requests API Test" Request: JsonObject; TargetURL: Text; begin - // [FEATURE] [AI test 1.0] // [SCENARIO 650247] The user-scoped header API maps LCY without bypassing currency validation. Initialize(); @@ -316,7 +315,6 @@ codeunit 148347 "Travel Requests API Test" Request: JsonObject; TargetURL: Text; begin - // [FEATURE] [AI test 1.0] // [SCENARIO 650247] The user-scoped detail API accepts LCY ISO codes and retains foreign-currency rules. Initialize(); @@ -350,7 +348,6 @@ codeunit 148347 "Travel Requests API Test" RequestBody: Text; ResponseText: Text; begin - // [FEATURE] [AI test 1.0] // [SCENARIO 650247] User-scoped POST and PATCH preserve and validate the final date pair. Initialize(); @@ -640,7 +637,6 @@ codeunit 148347 "Travel Requests API Test" ResponseText: Text; TargetURL: Text; begin - // [FEATURE] [AI test 1.0] // [SCENARIO 650247] The owner can be supplied on POST and resent unchanged on PATCH. Initialize(); @@ -707,7 +703,6 @@ codeunit 148347 "Travel Requests API Test" ResponseText: Text; TargetURL: Text; begin - // [FEATURE] [AI test 1.0] // [SCENARIO 650247] PATCH cannot reassign a travel request or change its status. Initialize(); @@ -789,7 +784,6 @@ codeunit 148347 "Travel Requests API Test" ResponseText: Text; TargetURL: Text; begin - // [FEATURE] [AI test 1.0] // [SCENARIO 650247] The legacy endpoint cannot bypass travel request ownership protection. Initialize(); 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 285951710c4..75e9eceba14 100644 --- a/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json +++ b/src/DisabledTests/Expense_Agent_Tests/Expense_Agent_Tests.DisabledTest.json @@ -29,6 +29,21 @@ "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", @@ -49,11 +64,26 @@ "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": "LegacySpendRequestsAPIAllowsUnchangedOwner" }, + { + "codeunitId": 148347, + "codeunitName": "Travel Requests API Test", + "method": "LegacySpendRequestsAPIRejectsTravelRequestOwnerChange" + }, { "codeunitId": 148318, "codeunitName": "Expense Capabilities API Test", From 79ef58e886514a5a273692d79d05789baf5d3935 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 21 Sep 2026 13:57:18 +0200 Subject: [PATCH 04/16] Keep existing API test scenario tags unnumbered Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../test/src/API/TravelRequestsAPITest.Codeunit.al | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 2e093c5cd5c..228b0f90469 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al @@ -292,7 +292,7 @@ codeunit 148347 "Travel Requests API Test" Request: JsonObject; TargetURL: Text; begin - // [SCENARIO 650247] The user-scoped header API maps LCY without bypassing currency validation. + // [SCENARIO] The user-scoped header API maps LCY without bypassing currency validation. Initialize(); // [GIVEN] A linked expense user creating a travel request. @@ -315,7 +315,7 @@ codeunit 148347 "Travel Requests API Test" Request: JsonObject; TargetURL: Text; begin - // [SCENARIO 650247] The user-scoped detail API accepts LCY ISO codes and retains foreign-currency rules. + // [SCENARIO] The user-scoped detail API accepts LCY ISO codes and retains foreign-currency rules. Initialize(); // [GIVEN] An open travel request owned by a linked expense user. @@ -348,7 +348,7 @@ codeunit 148347 "Travel Requests API Test" RequestBody: Text; ResponseText: Text; begin - // [SCENARIO 650247] User-scoped POST and PATCH preserve and validate the final date pair. + // [SCENARIO] User-scoped POST and PATCH preserve and validate the final date pair. Initialize(); // [GIVEN] A linked user and a future pair, with id first and end before start in the payload. @@ -637,7 +637,7 @@ codeunit 148347 "Travel Requests API Test" ResponseText: Text; TargetURL: Text; begin - // [SCENARIO 650247] The owner can be supplied on POST and resent unchanged on PATCH. + // [SCENARIO] The owner can be supplied on POST and resent unchanged on PATCH. Initialize(); // [GIVEN] An expense user linked to an employee. @@ -703,7 +703,7 @@ codeunit 148347 "Travel Requests API Test" ResponseText: Text; TargetURL: Text; begin - // [SCENARIO 650247] PATCH cannot reassign a travel request or change its status. + // [SCENARIO] PATCH cannot reassign a travel request or change its status. Initialize(); // [GIVEN] An open travel request and another expense user. @@ -784,7 +784,7 @@ codeunit 148347 "Travel Requests API Test" ResponseText: Text; TargetURL: Text; begin - // [SCENARIO 650247] The legacy endpoint cannot bypass travel request ownership protection. + // [SCENARIO] The legacy endpoint cannot bypass travel request ownership protection. Initialize(); // [GIVEN] A travel request owned by one of two expense users. From 0ed0d44f92c387438e52c98faf058f0b27f315c8 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 21 Sep 2026 17:01:23 +0200 Subject: [PATCH 05/16] Create posting setup before country-specific unit test initialization RU UnitTest databases need a complete posting setup before Library - ERM Country Data updates its accounts. Reuse standard library creators without changing test coverage or production behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../test/src/SpendRequestTest.Codeunit.al | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al index df1637d0c11..a7abb16f630 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al @@ -19,6 +19,7 @@ codeunit 148339 "Spend Request Test" var Assert: Codeunit "Assert"; LibraryExpense: Codeunit "Library - Expense"; + LibraryERM: Codeunit "Library - ERM"; LibraryRandom: Codeunit "Library - Random"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; IsInitialized: Boolean; @@ -2169,6 +2170,7 @@ codeunit 148339 "Spend Request Test" exit; LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Spend Request Test"); + CreateGeneralPostingSetup(); LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateGeneralPostingSetup(); LibraryERMCountryData.CreateGeneralPostingSetupData(); @@ -2184,6 +2186,17 @@ codeunit 148339 "Spend Request Test" LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Spend Request Test"); end; + local procedure CreateGeneralPostingSetup() + var + GeneralPostingSetup: Record "General Posting Setup"; + begin + // Country setup helpers need a complete posting setup even without demo data. + LibraryERM.CreateGeneralPostingSetupInvt(GeneralPostingSetup); + LibraryERM.SetGeneralPostingSetupSalesAccounts(GeneralPostingSetup); + LibraryERM.SetGeneralPostingSetupPurchAccounts(GeneralPostingSetup); + GeneralPostingSetup.Modify(true); + end; + local procedure CreateExpenseReportWithRefundableLine(var ExpenseReportLine: Record "Expense Report Line"; var ExpenseUser: Record "Expense User"; Refundable: Boolean) var ExpenseCategory: Record "Expense Category"; From bd80ea3efd467b4a29e277aaccfb9f74e95c6f8c Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 21 Sep 2026 21:25:19 +0200 Subject: [PATCH 06/16] Ensure country VAT fixtures reference existing accounts before unit tests Repair missing account references on the Normal VAT template used by country initialization. Preserve valid accounts and all test assertions; avoid inheriting dangling references from prior fixtures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../test/src/SpendRequestTest.Codeunit.al | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al index a7abb16f630..779dc59f643 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al @@ -5,9 +5,11 @@ namespace Microsoft.Test.ExpenseAgent; using Microsoft.ExpenseAgent; +using Microsoft.Finance.GeneralLedger.Account; using Microsoft.Finance.GeneralLedger.Preview; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.SpendRequest; +using Microsoft.Finance.VAT.Setup; using Microsoft.HumanResources.Employee; codeunit 148339 "Spend Request Test" @@ -2171,6 +2173,7 @@ codeunit 148339 "Spend Request Test" LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Spend Request Test"); CreateGeneralPostingSetup(); + InitializeVATPostingSetup(); LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateGeneralPostingSetup(); LibraryERMCountryData.CreateGeneralPostingSetupData(); @@ -2197,6 +2200,20 @@ codeunit 148339 "Spend Request Test" GeneralPostingSetup.Modify(true); end; + local procedure InitializeVATPostingSetup() + var + GLAccount: Record "G/L Account"; + VATPostingSetup: Record "VAT Posting Setup"; + begin + // Country helpers reuse this VAT template, including account references left by other tests. + LibraryERM.FindVATPostingSetup(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT"); + if not GLAccount.Get(VATPostingSetup."Sales VAT Account") then + VATPostingSetup.Validate("Sales VAT Account", LibraryERM.CreateGLAccountNo()); + if not GLAccount.Get(VATPostingSetup."Purchase VAT Account") then + VATPostingSetup.Validate("Purchase VAT Account", LibraryERM.CreateGLAccountNo()); + VATPostingSetup.Modify(true); + end; + local procedure CreateExpenseReportWithRefundableLine(var ExpenseReportLine: Record "Expense Report Line"; var ExpenseUser: Record "Expense User"; Refundable: Boolean) var ExpenseCategory: Record "Expense Category"; From 2d45e6f4171f08a9ccb9a0e59747ede4df8d1388 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Tue, 22 Sep 2026 12:13:38 +0200 Subject: [PATCH 07/16] Make Spend Request unit fixtures independent of demo data Create fresh expense and employee posting groups and payment methods. Restore singleton setup between tests and remove broad country demo-data normalization without changing the 74 scenarios or assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../test/src/SpendRequestTest.Codeunit.al | 102 +++++++++--------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al index 779dc59f643..0d48550732b 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al @@ -5,12 +5,12 @@ namespace Microsoft.Test.ExpenseAgent; using Microsoft.ExpenseAgent; -using Microsoft.Finance.GeneralLedger.Account; using Microsoft.Finance.GeneralLedger.Preview; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.SpendRequest; -using Microsoft.Finance.VAT.Setup; +using Microsoft.Foundation.AuditCodes; using Microsoft.HumanResources.Employee; +using Microsoft.HumanResources.Setup; codeunit 148339 "Spend Request Test" { @@ -21,8 +21,10 @@ codeunit 148339 "Spend Request Test" var Assert: Codeunit "Assert"; LibraryExpense: Codeunit "Library - Expense"; - LibraryERM: Codeunit "Library - ERM"; + LibraryERMCountryData: Codeunit "Library - ERM Country Data"; + LibraryHumanResource: Codeunit "Library - Human Resource"; LibraryRandom: Codeunit "Library - Random"; + LibrarySetupStorage: Codeunit "Library - Setup Storage"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; IsInitialized: Boolean; CloseConfirmReply: Boolean; @@ -1952,11 +1954,11 @@ codeunit 148339 "Spend Request Test" // [GIVEN] An expense user whose posting group has an expense account set up. LibraryExpense.CreateExpenseUser(ExpenseUser); Employee.Get(ExpenseUser."Employee No."); - LibraryExpense.UpdateExpenseAccountInEmployeePostingGroup(Employee."Employee Posting Group"); + CreateEmployeePostingSetup(Employee); // [GIVEN] An expense category and a payment method. - LibraryExpense.CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); - LibraryExpense.FindExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); + CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); + LibraryExpense.CreateExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); // [GIVEN] Two approved spend requests (header and line) with the user as traveler on both. CreateSpendRequestWithTraveler(HeaderSpendRequest, ExpenseUser."No.", HeaderSpendRequest.Status::Approved); @@ -2049,7 +2051,7 @@ codeunit 148339 "Spend Request Test" // [GIVEN] An open travel request and an active expense category. LibraryExpense.CreateSpendRequest(SpendRequest); - LibraryExpense.CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); // [GIVEN] A line whose type is Category. LibraryExpense.CreateSpendRequestDetail(SpendRequestDetail, SpendRequest."No.", 0); @@ -2076,7 +2078,7 @@ codeunit 148339 "Spend Request Test" // [GIVEN] An open travel request, an expense category, and a Lump Sum line. LibraryExpense.CreateSpendRequest(SpendRequest); - LibraryExpense.CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); LibraryExpense.CreateSpendRequestDetail(SpendRequestDetail, SpendRequest."No.", 0); SpendRequestDetail.Validate(Type, SpendRequestDetail.Type::"Lump Sum"); @@ -2100,7 +2102,7 @@ codeunit 148339 "Spend Request Test" // [GIVEN] A Category line with an expense category assigned. LibraryExpense.CreateSpendRequest(SpendRequest); - LibraryExpense.CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); LibraryExpense.CreateSpendRequestDetail(SpendRequestDetail, SpendRequest."No.", 0); SpendRequestDetail.Validate(Type, SpendRequestDetail.Type::Category); SpendRequestDetail.Validate("Expense Category Code", ExpenseCategory.Code); @@ -2127,7 +2129,7 @@ codeunit 148339 "Spend Request Test" // [GIVEN] An open travel request and an expense category. LibraryExpense.CreateSpendRequest(SpendRequest); - LibraryExpense.CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); + CreateExpenseCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" "); // [GIVEN] A Category line with a category. LibraryExpense.CreateSpendRequestDetail(CategoryLine, SpendRequest."No.", 0); @@ -2151,9 +2153,9 @@ codeunit 148339 "Spend Request Test" var ExpenseApprovalSetup: Record "Expense Approval Setup"; GeneralLedgerSetup: Record "General Ledger Setup"; - LibraryERMCountryData: Codeunit "Library - ERM Country Data"; begin LibraryTestInitialize.OnTestInitialize(Codeunit::"Spend Request Test"); + LibrarySetupStorage.Restore(); // Clear approval mappings together with the expense users between tests. ExpenseApprovalSetup.DeleteAll(); LibraryExpense.CleanUpBeforeTesting(); @@ -2172,46 +2174,46 @@ codeunit 148339 "Spend Request Test" exit; LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Spend Request Test"); - CreateGeneralPostingSetup(); - InitializeVATPostingSetup(); - LibraryERMCountryData.CreateVATData(); - LibraryERMCountryData.UpdateGeneralPostingSetup(); - LibraryERMCountryData.CreateGeneralPostingSetupData(); - LibraryERMCountryData.UpdatePurchasesPayablesSetup(); - LibraryERMCountryData.UpdateVATPostingSetup(); LibraryERMCountryData.UpdateJournalTemplMandatory(false); LibraryExpense.SetupNumberSeriesInExpenseMgmt(); LibraryExpense.InitializeExpenseSourceCode(); LibraryExpense.UpdateEnableApprovalWorkflowInAgentSetup(false); LibraryExpense.UpdateUseRulesInAgentSetup(false); + LibrarySetupStorage.Save(Database::"General Ledger Setup"); + LibrarySetupStorage.Save(Database::"Human Resources Setup"); + LibrarySetupStorage.Save(Database::"Source Code Setup"); + LibrarySetupStorage.Save(Database::"Expense Agent Setup"); IsInitialized := true; LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Spend Request Test"); end; - local procedure CreateGeneralPostingSetup() + local procedure CreateEmployeePostingSetup(var Employee: Record Employee) var - GeneralPostingSetup: Record "General Posting Setup"; + EmployeePostingGroup: Record "Employee Posting Group"; begin - // Country setup helpers need a complete posting setup even without demo data. - LibraryERM.CreateGeneralPostingSetupInvt(GeneralPostingSetup); - LibraryERM.SetGeneralPostingSetupSalesAccounts(GeneralPostingSetup); - LibraryERM.SetGeneralPostingSetupPurchAccounts(GeneralPostingSetup); - GeneralPostingSetup.Modify(true); + LibraryHumanResource.CreateEmployeePostingGroup(EmployeePostingGroup); + LibraryExpense.UpdateExpenseAccountInEmployeePostingGroup(EmployeePostingGroup.Code); + Employee.Validate("Employee Posting Group", EmployeePostingGroup.Code); + Employee.Modify(true); end; - local procedure InitializeVATPostingSetup() + local procedure CreateExpenseCategory(var ExpenseCategory: Record "Expense Category"; ReimbursementType: Enum "Expense Reimbursement Type"; ExpenseDetailRequired: Enum "Expense Detail Needed") var - GLAccount: Record "G/L Account"; - VATPostingSetup: Record "VAT Posting Setup"; + ExpensePostingGroup: Record "Expense Posting Group"; begin - // Country helpers reuse this VAT template, including account references left by other tests. - LibraryERM.FindVATPostingSetup(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT"); - if not GLAccount.Get(VATPostingSetup."Sales VAT Account") then - VATPostingSetup.Validate("Sales VAT Account", LibraryERM.CreateGLAccountNo()); - if not GLAccount.Get(VATPostingSetup."Purchase VAT Account") then - VATPostingSetup.Validate("Purchase VAT Account", LibraryERM.CreateGLAccountNo()); - VATPostingSetup.Modify(true); + LibraryExpense.CreateExpensePostingGroup(ExpensePostingGroup); + LibraryExpense.CreateExpenseCategory(ExpenseCategory, ReimbursementType, ExpenseDetailRequired); + ExpenseCategory.Validate("Posting Group", ExpensePostingGroup.Code); + ExpenseCategory.Modify(true); + end; + + local procedure CreateExpenseCategoryWithSubCategory(var ExpenseCategory: Record "Expense Category"; ReimbursementType: Enum "Expense Reimbursement Type"; ExpenseDetailRequired: Enum "Expense Detail Needed"; Refundable: Boolean) + var + ExpenseSubcategory: Record "Expense Subcategory"; + begin + CreateExpenseCategory(ExpenseCategory, ReimbursementType, ExpenseDetailRequired); + LibraryExpense.CreateExpenseSubCategory(ExpenseSubcategory, ExpenseCategory.Code, Refundable); end; local procedure CreateExpenseReportWithRefundableLine(var ExpenseReportLine: Record "Expense Report Line"; var ExpenseUser: Record "Expense User"; Refundable: Boolean) @@ -2221,8 +2223,8 @@ codeunit 148339 "Spend Request Test" ExpenseReportHeader: Record "Expense Report Header"; begin LibraryExpense.CreateExpenseUser(ExpenseUser); - LibraryExpense.CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); - LibraryExpense.FindExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); + CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); + LibraryExpense.CreateExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", '', ''); LibraryExpense.CreateExpenseReportLine(ExpenseReportLine, ExpenseReportHeader, ExpenseUser."No.", ExpenseCategory.Code, ExpensePaymentMethod.Code, Refundable, '', LibraryRandom.RandIntInRange(100, 1000)); end; @@ -2413,10 +2415,10 @@ codeunit 148339 "Spend Request Test" begin CreateReleasableSpendRequest(SpendRequest, ExpenseUser); Employee.Get(ExpenseUser."Employee No."); - LibraryExpense.UpdateExpenseAccountInEmployeePostingGroup(Employee."Employee Posting Group"); + CreateEmployeePostingSetup(Employee); - LibraryExpense.CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); - LibraryExpense.FindExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); + CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); + LibraryExpense.CreateExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); LibraryExpense.CreateSpendRequestDetail(SpendRequest."No.", LibraryRandom.RandIntInRange(100000, 100000)); LibraryExpense.SetSpendRequestStatus(SpendRequest, SpendRequest.Status::Approved); @@ -2440,10 +2442,10 @@ codeunit 148339 "Spend Request Test" begin CreateReleasableSpendRequest(SpendRequest, ExpenseUser); Employee.Get(ExpenseUser."Employee No."); - LibraryExpense.UpdateExpenseAccountInEmployeePostingGroup(Employee."Employee Posting Group"); + CreateEmployeePostingSetup(Employee); - LibraryExpense.CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); - LibraryExpense.FindExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); + CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); + LibraryExpense.CreateExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); LibraryExpense.CreateSpendRequestDetail(SpendRequest."No.", LibraryRandom.RandIntInRange(100000, 100000)); LibraryExpense.SetSpendRequestStatus(SpendRequest, SpendRequest.Status::Approved); @@ -2468,11 +2470,11 @@ codeunit 148339 "Spend Request Test" // An expense user whose posting group has an expense account set up. LibraryExpense.CreateExpenseUser(ExpenseUser); Employee.Get(ExpenseUser."Employee No."); - LibraryExpense.UpdateExpenseAccountInEmployeePostingGroup(Employee."Employee Posting Group"); + CreateEmployeePostingSetup(Employee); // A refundable category and a payment method. - LibraryExpense.CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); - LibraryExpense.FindExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); + CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); + LibraryExpense.CreateExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); // An approved spend request with the user as a traveler. CreateSpendRequestWithTraveler(SpendRequest, ExpenseUser."No.", SpendRequest.Status::Approved); @@ -2493,13 +2495,9 @@ codeunit 148339 "Spend Request Test" NonRefundableCategory: Record "Expense Category"; ExpensePaymentMethod: Record "Expense Payment Method"; ExpenseReportLine: Record "Expense Report Line"; - ExpensePostingGroup: Record "Expense Posting Group"; begin - LibraryExpense.CreateExpensePostingGroup(ExpensePostingGroup); - LibraryExpense.CreateExpenseCategoryWithSubCategory(NonRefundableCategory, NonRefundableCategory."Reimbursement Type"::"Company Paid", NonRefundableCategory."Expense Detail Required"::" ", false); - NonRefundableCategory.Validate("Posting Group", ExpensePostingGroup.Code); - NonRefundableCategory.Modify(true); - LibraryExpense.FindExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Company Paid"); + CreateExpenseCategoryWithSubCategory(NonRefundableCategory, NonRefundableCategory."Reimbursement Type"::"Company Paid", NonRefundableCategory."Expense Detail Required"::" ", false); + LibraryExpense.CreateExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Company Paid"); LibraryExpense.CreateExpenseReportLine(ExpenseReportLine, ExpenseReportHeader, ExpenseUserNo, NonRefundableCategory.Code, ExpensePaymentMethod.Code, false, '', LibraryRandom.RandIntInRange(100, 1000)); end; From 67d0e3c8842cd3f466ef78f520202321f3fee2a8 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Tue, 22 Sep 2026 14:55:47 +0200 Subject: [PATCH 08/16] Remove shadowing Human Resource test library declarations Reuse the shared LibraryHumanResource instance in both employee mapping tests to eliminate the two new AA0198 warnings. Test bodies and assertions are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al index 0d48550732b..fa01308e327 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al @@ -89,7 +89,6 @@ codeunit 148339 "Spend Request Test" ExpenseUser: Record "Expense User"; Employee: Record Employee; UnlinkedEmployee: Record Employee; - LibraryHumanResource: Codeunit "Library - Human Resource"; begin // [SCENARIO] The API source field filters both linked and unlinked employees without HTTP. Initialize(); @@ -138,7 +137,6 @@ codeunit 148339 "Spend Request Test" SpendRequest: Record "Spend Request"; Employee: Record Employee; Traveler: Record Traveler; - LibraryHumanResource: Codeunit "Library - Human Resource"; begin // [SCENARIO] An employee without an Expense User cannot be added as a traveler. Initialize(); From 87357b7210d270166fd53004a44ffafcc2717fed Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Tue, 22 Sep 2026 18:03:39 +0200 Subject: [PATCH 09/16] Reset unique expense payment fixtures between Spend Request tests Clear Employee Paid and Company Paid payment methods after expense transaction cleanup, then use the library find-or-create helper within each test. This preserves reimbursement-type uniqueness without inheriting prior test data or weakening assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../test/src/SpendRequestTest.Codeunit.al | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al index fa01308e327..d3189a862a3 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/SpendRequestTest.Codeunit.al @@ -1956,7 +1956,7 @@ codeunit 148339 "Spend Request Test" // [GIVEN] An expense category and a payment method. CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); - LibraryExpense.CreateExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); + LibraryExpense.FindExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); // [GIVEN] Two approved spend requests (header and line) with the user as traveler on both. CreateSpendRequestWithTraveler(HeaderSpendRequest, ExpenseUser."No.", HeaderSpendRequest.Status::Approved); @@ -2150,6 +2150,7 @@ codeunit 148339 "Spend Request Test" local procedure Initialize() var ExpenseApprovalSetup: Record "Expense Approval Setup"; + ExpensePaymentMethod: Record "Expense Payment Method"; GeneralLedgerSetup: Record "General Ledger Setup"; begin LibraryTestInitialize.OnTestInitialize(Codeunit::"Spend Request Test"); @@ -2158,6 +2159,10 @@ codeunit 148339 "Spend Request Test" ExpenseApprovalSetup.DeleteAll(); LibraryExpense.CleanUpBeforeTesting(); LibraryExpense.CleanTransactionalData(); + // Reimbursement types are unique; reuse only methods created within the current test. + ExpensePaymentMethod.SetFilter( + "Reimbursement Type", '%1|%2', ExpensePaymentMethod."Reimbursement Type"::"Employee Paid", ExpensePaymentMethod."Reimbursement Type"::"Company Paid"); + ExpensePaymentMethod.DeleteAll(); CloseConfirmCount := 0; CloseConfirmReply := false; SpendReqPreviewShown := false; @@ -2222,7 +2227,7 @@ codeunit 148339 "Spend Request Test" begin LibraryExpense.CreateExpenseUser(ExpenseUser); CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); - LibraryExpense.CreateExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); + LibraryExpense.FindExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); LibraryExpense.CreateExpenseReport(ExpenseReportHeader, ExpenseUser."No.", '', ''); LibraryExpense.CreateExpenseReportLine(ExpenseReportLine, ExpenseReportHeader, ExpenseUser."No.", ExpenseCategory.Code, ExpensePaymentMethod.Code, Refundable, '', LibraryRandom.RandIntInRange(100, 1000)); end; @@ -2416,7 +2421,7 @@ codeunit 148339 "Spend Request Test" CreateEmployeePostingSetup(Employee); CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); - LibraryExpense.CreateExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); + LibraryExpense.FindExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); LibraryExpense.CreateSpendRequestDetail(SpendRequest."No.", LibraryRandom.RandIntInRange(100000, 100000)); LibraryExpense.SetSpendRequestStatus(SpendRequest, SpendRequest.Status::Approved); @@ -2443,7 +2448,7 @@ codeunit 148339 "Spend Request Test" CreateEmployeePostingSetup(Employee); CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); - LibraryExpense.CreateExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); + LibraryExpense.FindExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); LibraryExpense.CreateSpendRequestDetail(SpendRequest."No.", LibraryRandom.RandIntInRange(100000, 100000)); LibraryExpense.SetSpendRequestStatus(SpendRequest, SpendRequest.Status::Approved); @@ -2472,7 +2477,7 @@ codeunit 148339 "Spend Request Test" // A refundable category and a payment method. CreateExpenseCategoryWithSubCategory(ExpenseCategory, ExpenseCategory."Reimbursement Type"::"Employee Paid", ExpenseCategory."Expense Detail Required"::" ", true); - LibraryExpense.CreateExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); + LibraryExpense.FindExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Employee Paid"); // An approved spend request with the user as a traveler. CreateSpendRequestWithTraveler(SpendRequest, ExpenseUser."No.", SpendRequest.Status::Approved); @@ -2495,7 +2500,7 @@ codeunit 148339 "Spend Request Test" ExpenseReportLine: Record "Expense Report Line"; begin CreateExpenseCategoryWithSubCategory(NonRefundableCategory, NonRefundableCategory."Reimbursement Type"::"Company Paid", NonRefundableCategory."Expense Detail Required"::" ", false); - LibraryExpense.CreateExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Company Paid"); + LibraryExpense.FindExpensePaymentMethod(ExpensePaymentMethod, ExpensePaymentMethod."Reimbursement Type"::"Company Paid"); LibraryExpense.CreateExpenseReportLine(ExpenseReportLine, ExpenseReportHeader, ExpenseUserNo, NonRefundableCategory.Code, ExpensePaymentMethod.Code, false, '', LibraryRandom.RandIntInRange(100, 1000)); end; From af10e6231ed49fc2bc697805035a87b406581db3 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Wed, 23 Sep 2026 15:42:07 +0200 Subject: [PATCH 10/16] Respect Graph test helper response and error contracts Clear HTTP response buffers before calls and assert failed requests through the helper-raised error, retaining status, domain and persistence checks. Strengthen date POST identity diagnostics without weakening the missing-record assertion. API exclusions and production behavior remain unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../src/API/TravelRequestsAPITest.Codeunit.al | 104 ++++++++---------- 1 file changed, 48 insertions(+), 56 deletions(-) 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 228b0f90469..8ae36058125 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al @@ -102,6 +102,7 @@ codeunit 148347 "Travel Requests API Test" TargetURL := LibraryGraphMgt.CreateTargetURL( Format(TravelRequest.SystemId), Page::"Travel Requests API", TravelRequestsServiceNameTok); TargetURL := AppendPathToAPIURL(TargetURL, '/' + TravelersServiceNameTok); + Clear(ResponseText); LibraryGraphMgt.PostToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 201); // [THEN] The Traveler stores the corresponding Expense User number. @@ -134,6 +135,7 @@ codeunit 148347 "Travel Requests API Test" TargetURL += '&$expand=travelers,employees' else TargetURL += '?$expand=travelers,employees'; + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, TargetURL, 200); // [THEN] Each traveler itself returns the correct mapping, not just a nested employee entity. @@ -173,10 +175,7 @@ codeunit 148347 "Travel Requests API Test" Employee: Record Employee; ExpenseUser: Record "Expense User"; TravelRequest: Record "Spend Request"; - ErrorResponse: JsonToken; - ErrorMessage: JsonToken; Request: JsonObject; - Response: JsonObject; RequestBody: Text; ResponseText: Text; TargetURL: Text; @@ -196,16 +195,13 @@ codeunit 148347 "Travel Requests API Test" TargetURL := LibraryGraphMgt.CreateTargetURL( Format(TravelRequest.SystemId), Page::"Travel Requests API", TravelRequestsServiceNameTok); TargetURL := AppendPathToAPIURL(TargetURL, '/' + TravelersServiceNameTok); + Clear(ResponseText); asserterror LibraryGraphMgt.PostToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 400); // [THEN] The API identifies the employee without an Expense User. Assert.ExpectedError(BadRequestResponseErr); - Response.ReadFrom(ResponseText); - Response.Get('error', ErrorResponse); - ErrorResponse.AsObject().Get('message', ErrorMessage); - Assert.AreNotEqual( - 0, StrPos(ErrorMessage.AsValue().AsText(), StrSubstNo(ExpenseUserNotLinkedErr, Employee."No.")), - 'The response must identify the employee without an Expense User.'); + Assert.ExpectedErrorCode('Dialog'); + Assert.ExpectedError(StrSubstNo(ExpenseUserNotLinkedErr, Employee."No.")); end; [Test] @@ -238,6 +234,7 @@ codeunit 148347 "Travel Requests API Test" TargetURL := AppendPathToAPIURL( TargetURL, '/' + TravelRequestsServiceNameTok + '(' + LibraryGraphMgt.StripBrackets(Format(TravelRequest.SystemId)) + ')/' + CreateExpenseReportActionTok); + Clear(ResponseText); LibraryGraphMgt.PostToWebServiceAndCheckResponseCode(TargetURL, '{}', ResponseText, 201); // [THEN] A new report is linked to the request and its Expense User. @@ -275,6 +272,7 @@ codeunit 148347 "Travel Requests API Test" Format(TravelRequest.SystemId), Page::"Travel Requests API", TravelRequestsServiceNameTok, ApproveTravelRequestActionTok); RequestBody := StrSubstNo(ApproveTravelRequestBodyLbl, ApproverExpenseUser."No."); + Clear(ResponseText); LibraryGraphMgt.PostToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 200); // [THEN] The request is approved and a report is created for Requested For. @@ -337,9 +335,6 @@ codeunit 148347 "Travel Requests API Test" var ExpenseUser: Record "Expense User"; Request: JsonObject; - Response: JsonObject; - ErrorResponse: JsonToken; - ErrorMessage: JsonToken; RequestSystemId: Guid; StartDate: Date; EndDate: Date; @@ -367,11 +362,13 @@ codeunit 148347 "Travel Requests API Test" TargetURL := LibraryGraphMgt.CreateTargetURL( Format(ExpenseUser.SystemId), Page::"Expense Users API", ExpenseUsersServiceNameTok); TargetURL := AppendPathToAPIURL(TargetURL, '/' + TravelRequestsServiceNameTok); + Clear(ResponseText); LibraryGraphMgt.PostToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 201); // [THEN] POST, GET, and storage retain the supplied dates and identity. VerifyTravelRequestDates(ResponseText, RequestSystemId, StartDate, EndDate); RecordURL := AppendPathToAPIURL(TargetURL, '(' + LibraryGraphMgt.StripBrackets(Format(RequestSystemId)) + ')'); + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, RecordURL, 200); VerifyTravelRequestDates(ResponseText, RequestSystemId, StartDate, EndDate); @@ -382,6 +379,7 @@ codeunit 148347 "Travel Requests API Test" Request.Add('expectedStartDate', Format(StartDate, 0, 9)); Request.Add('expectedEndDate', Format(EndDate, 0, 9)); Request.WriteTo(RequestBody); + Clear(ResponseText); LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(RecordURL, RequestBody, ResponseText, 200); // [THEN] The complete later range is accepted. @@ -394,6 +392,7 @@ codeunit 148347 "Travel Requests API Test" Request.Add('expectedEndDate', Format(EndDate, 0, 9)); Request.Add('expectedStartDate', Format(StartDate, 0, 9)); Request.WriteTo(RequestBody); + Clear(ResponseText); LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(RecordURL, RequestBody, ResponseText, 200); // [THEN] The complete earlier range is accepted. @@ -404,6 +403,7 @@ codeunit 148347 "Travel Requests API Test" Clear(Request); Request.Add('expectedEndDate', Format(EndDate, 0, 9)); Request.WriteTo(RequestBody); + Clear(ResponseText); LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(RecordURL, RequestBody, ResponseText, 200); // [THEN] The omitted start remains unchanged. @@ -413,15 +413,14 @@ codeunit 148347 "Travel Requests API Test" Clear(Request); Request.Add('expectedStartDate', Format(EndDate + 1, 0, 9)); Request.WriteTo(RequestBody); + Clear(ResponseText); asserterror LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(RecordURL, RequestBody, ResponseText, 400); // [THEN] The date-range error is returned and the previous valid pair remains stored. Assert.ExpectedError(BadRequestResponseErr); Assert.ExpectedErrorCode('Dialog'); - Response.ReadFrom(ResponseText); - Response.Get('error', ErrorResponse); - ErrorResponse.AsObject().Get('message', ErrorMessage); - Assert.AreNotEqual(0, StrPos(ErrorMessage.AsValue().AsText(), InvalidTravelRequestDatesErr), 'The invalid date range must cause the rejection.'); + Assert.ExpectedError(InvalidTravelRequestDatesErr); + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, RecordURL, 200); VerifyTravelRequestDates(ResponseText, RequestSystemId, StartDate, EndDate); end; @@ -458,6 +457,7 @@ codeunit 148347 "Travel Requests API Test" TargetURL += '&$expand=travelRequests' else TargetURL += '?$expand=travelRequests'; + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, TargetURL, 200); ResponseText := LowerCase(ResponseText); TravelRequestIdTxt := LowerCase(LibraryGraphMgt.StripBrackets(Format(TravelRequest.SystemId))); @@ -474,6 +474,7 @@ codeunit 148347 "Travel Requests API Test" // [WHEN] The owner is renamed and the same GUID-based URL is requested. ExpenseUser.Rename(CopyStr(Format(CreateGuid()), 1, MaxStrLen(ExpenseUser."No."))); Commit(); + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, TargetURL, 200); ResponseText := LowerCase(ResponseText); @@ -517,6 +518,7 @@ codeunit 148347 "Travel Requests API Test" TargetURL += '&$expand=travelRequest' else TargetURL += '?$expand=travelRequest'; + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, TargetURL, 200); // [THEN] The report's GUID and expanded request identify the originating travel request. @@ -559,6 +561,7 @@ codeunit 148347 "Travel Requests API Test" // [WHEN] The detail is retrieved through the API. TargetURL := LibraryGraphMgt.CreateTargetURL( Format(TravelRequestDetail.SystemId), Page::"Travel Request Details API", TravelRequestDetailsServiceNameTok); + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, TargetURL, 200); // [THEN] The payload includes the stored type and expense category. @@ -608,6 +611,7 @@ codeunit 148347 "Travel Requests API Test" TargetURL += '&$expand=travelRequests' else TargetURL += '?$expand=travelRequests'; + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, TargetURL, 200); ResponseText := LowerCase(ResponseText); AssignedTravelRequestIdTxt := LowerCase(LibraryGraphMgt.StripBrackets(Format(AssignedTravelRequest.SystemId))); @@ -630,8 +634,6 @@ codeunit 148347 "Travel Requests API Test" TravelRequest: Record "Spend Request"; Response: JsonObject; RequestId: JsonToken; - ErrorResponse: JsonToken; - ErrorMessage: JsonToken; TravelRequestSystemId: Guid; RequestBody: Text; ResponseText: Text; @@ -652,20 +654,17 @@ codeunit 148347 "Travel Requests API Test" // [WHEN] POST attempts to assign another employee under this user's GUID. RequestBody := StrSubstNo(RequestedByRequestBodyLbl, OtherExpenseUser."Employee No."); + Clear(ResponseText); asserterror LibraryGraphMgt.PostToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 400); // [THEN] The owner mismatch is rejected before a request can be inserted. Assert.ExpectedError(BadRequestResponseErr); Assert.ExpectedErrorCode('Dialog'); - Response.ReadFrom(ResponseText); - Response.Get('error', ErrorResponse); - ErrorResponse.AsObject().Get('message', ErrorMessage); - Assert.AreNotEqual( - 0, StrPos(ErrorMessage.AsValue().AsText(), TravelRequest.FieldCaption("Requested By")), - 'The rejection must identify the owner mismatch.'); + Assert.ExpectedError(TravelRequest.FieldCaption("Requested By")); // [WHEN] POST supplies the employee matching this user's GUID. RequestBody := StrSubstNo(RequestedByRequestBodyLbl, ExpenseUser."Employee No."); + Clear(ResponseText); LibraryGraphMgt.PostToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 201); // [THEN] The new travel request stores the supplied owner. @@ -695,10 +694,6 @@ codeunit 148347 "Travel Requests API Test" TravelRequest: Record "Spend Request"; OriginalRequestedBy: Code[20]; TravelRequestSystemId: Guid; - Response: JsonObject; - ErrorResponse: JsonToken; - ErrorCode: JsonToken; - ErrorMessage: JsonToken; RequestBody: Text; ResponseText: Text; TargetURL: Text; @@ -718,12 +713,13 @@ codeunit 148347 "Travel Requests API Test" TargetURL := LibraryGraphMgt.CreateTargetURL( Format(TravelRequest.SystemId), Page::"Travel Requests API", TravelRequestsServiceNameTok); RequestBody := StrSubstNo(RequestedByRequestBodyLbl, OtherExpenseUser."Employee No."); + Clear(ResponseText); asserterror LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 400); // [THEN] The API identifies the immutable owner and the affected request. Assert.ExpectedError(BadRequestResponseErr); Assert.ExpectedErrorCode('Dialog'); - AssertOwnerChangeError(ResponseText, TravelRequest); + VerifyOwnerChangeError(TravelRequest); // [WHEN] PATCH attempts to change the status. Clear(ResponseText); @@ -733,12 +729,7 @@ codeunit 148347 "Travel Requests API Test" // [THEN] The API rejects the read-only status, leaving ownership and status unchanged. Assert.ExpectedError(BadRequestResponseErr); Assert.ExpectedErrorCode('Dialog'); - Response.ReadFrom(ResponseText); - Response.Get('error', ErrorResponse); - ErrorResponse.AsObject().Get('code', ErrorCode); - ErrorResponse.AsObject().Get('message', ErrorMessage); - Assert.AreEqual('BadRequest', ErrorCode.AsValue().AsText(), 'The status update must be rejected by the OData read-only guard.'); - Assert.AreNotEqual(0, StrPos(ErrorMessage.AsValue().AsText(), StatusReadOnlyErr), 'The API error must identify the read-only status control.'); + Assert.ExpectedError('Error code: BadRequest. Error message: ' + StatusReadOnlyErr); SelectLatestVersion(); TravelRequest.GetBySystemId(TravelRequestSystemId); Assert.AreEqual( @@ -799,12 +790,13 @@ codeunit 148347 "Travel Requests API Test" TargetURL := LibraryGraphMgt.CreateTargetURL( Format(TravelRequest.SystemId), Page::"Spend Requests API", SpendRequestsServiceNameTok); RequestBody := StrSubstNo(RequestedByRequestBodyLbl, OtherExpenseUser."Employee No."); + Clear(ResponseText); asserterror LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 400); // [THEN] The owner change is rejected and the original owner is preserved. Assert.ExpectedError(BadRequestResponseErr); Assert.ExpectedErrorCode('Dialog'); - AssertOwnerChangeError(ResponseText, TravelRequest); + VerifyOwnerChangeError(TravelRequest); SelectLatestVersion(); TravelRequest.GetBySystemId(TravelRequestSystemId); Assert.AreEqual( @@ -853,6 +845,7 @@ codeunit 148347 "Travel Requests API Test" Commit(); // [WHEN] POST explicitly supplies the LCY ISO code. + Clear(ResponseText); LibraryGraphMgt.PostToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 201); // [THEN] It is stored as blank and returned as the configured LCY code. @@ -862,6 +855,7 @@ codeunit 148347 "Travel Requests API Test" SelectedCurrencyURL := RecordURL + '?$select=currencyCode' else SelectedCurrencyURL := RecordURL + '&$select=currencyCode'; + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, SelectedCurrencyURL, 200); AssertTravelRequestCurrency(ResponseText, SystemId, IsDetail, GeneralLedgerSetup."LCY Code", '', 1); @@ -875,6 +869,7 @@ codeunit 148347 "Travel Requests API Test" Clear(Request); Request.Add(AmountField, 200); Request.WriteTo(RequestBody); + Clear(ResponseText); LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(RecordURL, RequestBody, ResponseText, 200); // [THEN] Neither the foreign currency nor its exchange rate is reset to LCY. @@ -888,12 +883,14 @@ codeunit 148347 "Travel Requests API Test" // [THEN] Both explicit LCY and blank inputs return the canonical ISO code. AssertTravelRequestCurrency(ResponseText, SystemId, IsDetail, GeneralLedgerSetup."LCY Code", '', 1); + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, SelectedCurrencyURL, 200); AssertTravelRequestCurrency(ResponseText, SystemId, IsDetail, GeneralLedgerSetup."LCY Code", '', 1); // [WHEN] PATCH supplies an unknown non-LCY code. // [THEN] Currency validation rejects it rather than treating it as local currency. AssertCurrencyPatchError(RecordURL, InvalidCurrencyCode, InvalidCurrencyCode); + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, RecordURL, 200); AssertTravelRequestCurrency(ResponseText, SystemId, IsDetail, GeneralLedgerSetup."LCY Code", '', 1); @@ -910,6 +907,7 @@ codeunit 148347 "Travel Requests API Test" // [WHEN] PATCH attempts a currency change. // [THEN] The existing Open-status validation rejects it without changing currency. AssertCurrencyPatchError(RecordURL, ForeignCurrencyCode, StatusNotOpenErr); + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, RecordURL, 200); AssertTravelRequestCurrency(ResponseText, SystemId, IsDetail, GeneralLedgerSetup."LCY Code", '', 1); end; @@ -921,27 +919,23 @@ codeunit 148347 "Travel Requests API Test" begin Request.Add('currencyCode', CurrencyCode); Request.WriteTo(RequestBody); + Clear(ResponseText); LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 200); end; local procedure AssertCurrencyPatchError(TargetURL: Text; CurrencyCode: Code[10]; ExpectedError: Text) var Request: JsonObject; - Response: JsonObject; - ErrorResponse: JsonToken; - ErrorMessage: JsonToken; RequestBody: Text; ResponseText: Text; begin Request.Add('currencyCode', CurrencyCode); Request.WriteTo(RequestBody); + Clear(ResponseText); asserterror LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 400); Assert.ExpectedError(BadRequestResponseErr); Assert.ExpectedErrorCode('Dialog'); - Response.ReadFrom(ResponseText); - Response.Get('error', ErrorResponse); - ErrorResponse.AsObject().Get('message', ErrorMessage); - Assert.AreNotEqual(0, StrPos(ErrorMessage.AsValue().AsText(), ExpectedError), 'The expected table validation must cause the rejection.'); + Assert.ExpectedError(ExpectedError); end; local procedure AssertTravelRequestCurrency(ResponseText: Text; SystemId: Guid; IsDetail: Boolean; APICurrencyCode: Code[10]; StoredCurrencyCode: Code[10]; ExchangeRate: Decimal) @@ -985,6 +979,7 @@ codeunit 148347 "Travel Requests API Test" ResponseText: Text; begin TargetURL := LibraryGraphMgt.CreateTargetURL(Format(TravelerSystemId), Page::"Travelers API", TravelersServiceNameTok); + Clear(ResponseText); LibraryGraphMgt.GetFromWebServiceAndCheckResponseCode(ResponseText, TargetURL, 200); Response.ReadFrom(ResponseText); AssertTravelerEmployeeNumber(Response, ExpectedEmployeeNo); @@ -1015,8 +1010,13 @@ codeunit 148347 "Travel Requests API Test" local procedure VerifyTravelRequestDates(ResponseText: Text; RequestSystemId: Guid; StartDate: Date; EndDate: Date) var TravelRequest: Record "Spend Request"; + Response: JsonObject; + ResponseId: JsonToken; begin AssertAPIDates(ResponseText, StartDate, EndDate); + Response.ReadFrom(ResponseText); + Assert.IsTrue(Response.Get('id', ResponseId), 'The travel request response must contain its persisted identity.'); + Assert.AreEqual(RequestSystemId, ResponseId.AsValue().AsGuid(), 'The API must preserve the supplied travel request identity.'); SelectLatestVersion(); TravelRequest.GetBySystemId(RequestSystemId); Assert.AreEqual(StartDate, TravelRequest."Expected Start Date", 'The API start date must be persisted.'); @@ -1037,6 +1037,7 @@ codeunit 148347 "Travel Requests API Test" Request.Add('requestedBy', TravelRequest."Requested By"); Request.Add('purpose', ExpectedPurpose); Request.WriteTo(RequestBody); + Clear(ResponseText); LibraryGraphMgt.PatchToWebServiceAndCheckResponseCode(TargetURL, RequestBody, ResponseText, 200); Response.ReadFrom(ResponseText); @@ -1046,20 +1047,11 @@ codeunit 148347 "Travel Requests API Test" Assert.AreEqual(ExpectedPurpose, Purpose.AsValue().AsText(), 'PATCH must update the purpose when the owner is unchanged.'); end; - local procedure AssertOwnerChangeError(ResponseText: Text; TravelRequest: Record "Spend Request") - var - Response: JsonObject; - ErrorResponse: JsonToken; - ErrorMessage: JsonToken; - MessageText: Text; + local procedure VerifyOwnerChangeError(TravelRequest: Record "Spend Request") begin - Response.ReadFrom(ResponseText); - Response.Get('error', ErrorResponse); - ErrorResponse.AsObject().Get('message', ErrorMessage); - MessageText := ErrorMessage.AsValue().AsText(); - Assert.AreNotEqual(0, StrPos(MessageText, RequestedByCannotBeChangedErr), 'The API must reject the owner change.'); - Assert.AreNotEqual(0, StrPos(MessageText, TravelRequest.FieldCaption("Requested By")), 'The error must identify Requested By.'); - Assert.AreNotEqual(0, StrPos(MessageText, TravelRequest."No."), 'The error must identify the travel request.'); + Assert.ExpectedError(RequestedByCannotBeChangedErr); + Assert.ExpectedError(TravelRequest.FieldCaption("Requested By")); + Assert.ExpectedError(TravelRequest."No."); end; local procedure CreateTravelRequest(var TravelRequest: Record "Spend Request"; EmployeeNo: Code[20]) From ff267e34a20a15c3f2f6701473e2c2a83609aa4b Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Wed, 23 Sep 2026 15:44:54 +0200 Subject: [PATCH 11/16] Use supported GUID conversion for API identity diagnostics Match existing response ID parsing through Evaluate and AsText rather than an unavailable JsonValue conversion method. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../test/src/API/TravelRequestsAPITest.Codeunit.al | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 8ae36058125..6f2e9a9ab6d 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/TravelRequestsAPITest.Codeunit.al @@ -1012,11 +1012,13 @@ codeunit 148347 "Travel Requests API Test" TravelRequest: Record "Spend Request"; Response: JsonObject; ResponseId: JsonToken; + ResponseSystemId: Guid; begin AssertAPIDates(ResponseText, StartDate, EndDate); Response.ReadFrom(ResponseText); Assert.IsTrue(Response.Get('id', ResponseId), 'The travel request response must contain its persisted identity.'); - Assert.AreEqual(RequestSystemId, ResponseId.AsValue().AsGuid(), 'The API must preserve the supplied travel request identity.'); + Evaluate(ResponseSystemId, ResponseId.AsValue().AsText()); + Assert.AreEqual(RequestSystemId, ResponseSystemId, 'The API must preserve the supplied travel request identity.'); SelectLatestVersion(); TravelRequest.GetBySystemId(RequestSystemId); Assert.AreEqual(StartDate, TravelRequest."Expected Start Date", 'The API start date must be persisted.'); From 29d7bb5d1f7b2b3b8733d565ca6b0b21de80b5ab Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Thu, 24 Sep 2026 13:48:49 +0200 Subject: [PATCH 12/16] Persist owner and date-only Travel Request API posts Keep supplied date values on the source record while deferring paired validation. Default the route owner at insertion only when omitted, so owner-only POSTs produce a real record change while explicit owner validation remains intact. Validated all 15 HTTP API tests on tenant2-1 without changing their payloads or persistence assertions, plus 20 permission and 74 Spend Request unit tests and direct owner-scope edge checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../app/src/APIs/TravelRequestsAPI.Page.al | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Apps/W1/ExpenseAgent/app/src/APIs/TravelRequestsAPI.Page.al b/src/Apps/W1/ExpenseAgent/app/src/APIs/TravelRequestsAPI.Page.al index 09524c86fb8..51aeaf7021e 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/APIs/TravelRequestsAPI.Page.al +++ b/src/Apps/W1/ExpenseAgent/app/src/APIs/TravelRequestsAPI.Page.al @@ -48,6 +48,11 @@ page 7134 "Travel Requests API" { Caption = 'Requested By'; ToolTip = 'Specifies the employee who created the request. This value can be set only when creating the request.'; + + trigger OnValidate() + begin + RequestedByProvided := true; + end; } field(status; Rec.Status) { @@ -98,6 +103,8 @@ page 7134 "Travel Requests API" trigger OnValidate() begin + // Mark the record changed while deferring validation until the complete date pair is available. + Rec."Expected Start Date" := ExpectedStartDate; ExpectedStartDateProvided := true; end; } @@ -108,6 +115,7 @@ page 7134 "Travel Requests API" trigger OnValidate() begin + Rec."Expected End Date" := ExpectedEndDate; ExpectedEndDateProvided := true; end; } @@ -240,12 +248,10 @@ page 7134 "Travel Requests API" end; trigger OnNewRecord(BelowxRec: Boolean) - var - OwnerEmployeeNo: Code[20]; begin - OwnerEmployeeNo := ProcessOwnerFilter(); - if OwnerEmployeeNo <> '' then - Rec."Requested By" := OwnerEmployeeNo; + ProcessOwnerFilter(); + Clear(Rec."Requested By"); + RequestedByProvided := false; Clear(CurrencyCodeDisplay); Clear(ExpectedStartDate); Clear(ExpectedEndDate); @@ -340,6 +346,9 @@ page 7134 "Travel Requests API" trigger OnInsertRecord(BelowxRec: Boolean): Boolean begin Rec."Document Type" := Rec."Document Type"::"Travel Request"; + // Default the owner only at insertion so an owner-only POST remains a record change. + if not RequestedByProvided then + Rec."Requested By" := ProcessOwnerFilter(); Rec.TestField("Requested By"); CheckOwnerScope(); Rec.SetExpectedDatesForAPIInsert(ExpectedStartDate, ExpectedEndDate, ExpectedStartDateProvided, ExpectedEndDateProvided); @@ -426,6 +435,7 @@ page 7134 "Travel Requests API" ExpectedEndDate: Date; ExpectedStartDateProvided: Boolean; ExpectedEndDateProvided: Boolean; + RequestedByProvided: Boolean; StatusCannotBeChangedErr: Label 'can be changed only by submitting, approving, or rejecting the travel request'; RequestedByCannotBeChangedErr: Label 'cannot be changed'; TravelRequestMustBeApprovedErr: Label 'Travel request %1 must be approved before an expense report can be created.', Comment = '%1 = Travel Request No.'; From 40a64b5ad3bc00f37d55cdd8890e31db99d04153 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Fri, 25 Sep 2026 15:51:32 +0200 Subject: [PATCH 13/16] Correct Dataverse URL and JWT expiry test fixtures Use allowed Dataverse hosts for positive URL-normalization cases and explicitly exercise SaaS validation. Build JWT expiry fixtures with the canonical Unix Timestamp helper and a one-hour margin, preserving expiry and transport-security assertions. Both test apps compile locally. The target CDS case passes; full local validation is limited by unrelated setup failures and server-side MockTest alias resolution. Buddy execution is required. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../CDSConnectionWizardTests.Codeunit.al | 19 +++++++++------- src/Layers/W1/Tests/Misc/UTREST.Codeunit.al | 22 ++++++------------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/Layers/W1/Tests/CRM integration/CDSConnectionWizardTests.Codeunit.al b/src/Layers/W1/Tests/CRM integration/CDSConnectionWizardTests.Codeunit.al index 9fb0acd08b9..7c47cefaf86 100644 --- a/src/Layers/W1/Tests/CRM integration/CDSConnectionWizardTests.Codeunit.al +++ b/src/Layers/W1/Tests/CRM integration/CDSConnectionWizardTests.Codeunit.al @@ -10,6 +10,7 @@ codeunit 139194 "CDS Connection Wizard Tests" var Assert: Codeunit Assert; + LibraryEnvironment: Codeunit "Environment Info Test Library"; LibraryVariableStorage: Codeunit "Library - Variable Storage"; CryptographyManagement: Codeunit "Cryptography Management"; NoEnvironmentSelectedErr: Label 'To sign in the administrator user you must specify an environment.'; @@ -181,6 +182,7 @@ codeunit 139194 "CDS Connection Wizard Tests" // [FEATURE] [UT] // [SCENARIO] CDS Connection URL should comply with rules Initialize(); + LibraryEnvironment.SetTestabilitySoftwareAsAService(true); // [GIVEN] Empty CDS Connection URL Address := ''; @@ -202,19 +204,20 @@ codeunit 139194 "CDS Connection Wizard Tests" // [THEN] Error message that security connection (https) is required Assert.ExpectedError(MustUseHttpsErr); - // [GIVEN] CDS Connection URL = 'http://test.com:555/myOrg' - Address := 'https://test2.com:555/myOrg'; + // [GIVEN] A valid HTTPS Dataverse URL with an explicit port and organization path + Address := 'https://test2.dynamics.com:555/myOrg'; // [WHEN] The URL is checked CDSIntegrationImpl.CheckModifyConnectionURL(Address); - // [THEN] Error message that security connection (https) is required - Assert.AreEqual('https://test2.com:555/myOrg', Address, WrongConnectionStringErr); + // [THEN] The explicit port and organization path are preserved + Assert.AreEqual('https://test2.dynamics.com:555/myOrg', Address, WrongConnectionStringErr); - // [GIVEN] CDS Connection URL = 'http://test.com:555/myOrg' - Address := 'https://test3.com/myOrg'; + // [GIVEN] A valid HTTPS Dataverse URL using the default port + Address := 'https://test3.dynamics.com/myOrg'; // [WHEN] The URL is checked CDSIntegrationImpl.CheckModifyConnectionURL(Address); - // [THEN] Error message that security connection (https) is required - Assert.AreEqual('https://test3.com', Address, WrongConnectionStringErr); + // [THEN] The organization path is removed + Assert.AreEqual('https://test3.dynamics.com', Address, WrongConnectionStringErr); + LibraryEnvironment.SetTestabilitySoftwareAsAService(IsSaaS); end; [Test] diff --git a/src/Layers/W1/Tests/Misc/UTREST.Codeunit.al b/src/Layers/W1/Tests/Misc/UTREST.Codeunit.al index 22e6415164c..9ac101c132a 100644 --- a/src/Layers/W1/Tests/Misc/UTREST.Codeunit.al +++ b/src/Layers/W1/Tests/Misc/UTREST.Codeunit.al @@ -10,6 +10,7 @@ codeunit 139148 "UT REST" var Assert: Codeunit Assert; + UnixTimestamp: Codeunit System.DateTime."Unix Timestamp"; NoContentErr: Label 'The stream is empty.'; UnknownImageTypeErr: Label 'Unknown image type.'; XmlDocLoadErr: Label 'A call to System.Xml.XmlDocument.Load failed'; @@ -30,8 +31,8 @@ codeunit 139148 "UT REST" begin // [SCENARIO 227335] SOAPWebServiceRequestMgt.HasJWTExpired function returns TRUE in case of expired token - // [GIVEN] Mock token with exprired date less than current - AccessToken := MockTokenWithExpDate(CurrentDateTime - 1000); + // [GIVEN] Mock token that expired one hour ago + AccessToken := MockTokenWithExpDate(CurrentDateTime - 3600000); // [WHEN] Function SOAPWebServiceRequestMgt.HasJWTExpired is being run // [THEN] It returns TRUE @@ -47,11 +48,11 @@ codeunit 139148 "UT REST" begin // [SCENARIO 227335] SOAPWebServiceRequestMgt.HasJWTExpired function returns FALSE in case of not expired token - // [GIVEN] Mock token with exprired date less than current - AccessToken := MockTokenWithExpDate(CurrentDateTime + 1000); + // [GIVEN] Mock token that expires one hour from now + AccessToken := MockTokenWithExpDate(CurrentDateTime + 3600000); // [WHEN] Function SOAPWebServiceRequestMgt.HasJWTExpired is being run - // [THEN] It returns TRUE + // [THEN] It returns FALSE Assert.IsFalse(SOAPWebServiceRequestMgt.HasJWTExpired(AccessToken), InvalidValueErr); end; @@ -425,16 +426,8 @@ codeunit 139148 "UT REST" end; local procedure GetUnixTime(DateTimeValue: DateTime): Decimal - var - TypeHelper: Codeunit "Type Helper"; - TimeZoneOffset: Duration; begin - if not TypeHelper.GetUserTimezoneOffset(TimeZoneOffset) then - TimeZoneOffset := 0; - exit( - Round( - (DateTimeValue - CreateDateTime(DMY2Date(1, 1, 1970), 0T) - TimeZoneOffset) / 1000, - 1)); + exit(UnixTimestamp.CreateTimestampSeconds(DateTimeValue)); end; local procedure VerifyJsonText(JsonText: Text; ExpectedJsonText: Text) @@ -455,4 +448,3 @@ codeunit 139148 "UT REST" Assert.AreEqual(ExpectedXMLText, XMLText, InvalidValueErr); end; } - From c52631a9bf320bd76683331166501d7f605ade47 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 28 Sep 2026 10:46:28 +0200 Subject: [PATCH 14/16] Use valid Dataverse URLs in assisted setup test fixtures Replace the TEST placeholder in the two positive assisted-setup entry scenarios with an allowed HTTPS Dataverse address. Preserve both existing assertions, handlers and production URL validation. Compilation and publishing passed locally; runtime verification remains for NAV because local shared initialization rejects disabling encryption. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../CDSConnectionSetupTest.Codeunit.al | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Layers/W1/Tests/CRM integration/CDSConnectionSetupTest.Codeunit.al b/src/Layers/W1/Tests/CRM integration/CDSConnectionSetupTest.Codeunit.al index 399b8eecf5b..955e18ef828 100644 --- a/src/Layers/W1/Tests/CRM integration/CDSConnectionSetupTest.Codeunit.al +++ b/src/Layers/W1/Tests/CRM integration/CDSConnectionSetupTest.Codeunit.al @@ -152,13 +152,13 @@ codeunit 139196 "CDS Connection Setup Test" CDSConnectionSetup.DeleteAll(); // [GIVEN] CDS Connection Setup page is opened CDSConnectionSetupPage.OpenEdit(); - // [GIVEN] Server Address is "TEST" - CDSConnectionSetupPage."Server Address".SetValue('TEST'); + // [GIVEN] Server Address is a valid Dataverse URL + CDSConnectionSetupPage."Server Address".SetValue('https://test.dynamics.com'); // [WHEN] Assisted Setup is invoked CDSConnectionSetupPage."Assisted Setup".Invoke(); - // [THEN] CDS Connection Setup wizard is opened and Server Address = "TEST" + // [THEN] CDS Connection Setup wizard is opened with the same Server Address // Wizard page is opened in AssistedSetupModalHandler Assert.ExpectedMessage(CDSConnectionSetupPage."Server Address".Value(), LibraryVariableStorage.DequeueText()); end; @@ -176,13 +176,13 @@ codeunit 139196 "CDS Connection Setup Test" InitializeSetup(false); // [GIVEN] CDS Connection Setup page is opened CDSConnectionSetupPage.OpenEdit(); - // [GIVEN] Server Address is "TEST" - CDSConnectionSetupPage."Server Address".SetValue('TEST'); + // [GIVEN] Server Address is a valid Dataverse URL + CDSConnectionSetupPage."Server Address".SetValue('https://test.dynamics.com'); // [WHEN] Assisted Setup is invoked CDSConnectionSetupPage."Assisted Setup".Invoke(); - // [THEN] CDS Connection Setup wizard is opened and Server Address = "TEST" + // [THEN] CDS Connection Setup wizard is opened with the same Server Address // Wizard page is opened in AssistedSetupModalHandler Assert.ExpectedMessage(CDSConnectionSetupPage."Server Address".Value(), LibraryVariableStorage.DequeueText()); end; From 5de4cb76e597180a8566fb58cb284ae5d05568f8 Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Mon, 28 Sep 2026 11:32:15 +0200 Subject: [PATCH 15/16] Keep BCApps repair scope limited to Expense Agent Restore the unrelated CRM setup, CRM wizard and UT REST test files exactly to their pre-repair contents, as requested. Keep Expense Agent and Spend Request fixes unchanged; unrelated failures are handled by method-scoped NAV exclusions instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../CDSConnectionSetupTest.Codeunit.al | 12 +++++----- .../CDSConnectionWizardTests.Codeunit.al | 19 +++++++--------- src/Layers/W1/Tests/Misc/UTREST.Codeunit.al | 22 +++++++++++++------ 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/Layers/W1/Tests/CRM integration/CDSConnectionSetupTest.Codeunit.al b/src/Layers/W1/Tests/CRM integration/CDSConnectionSetupTest.Codeunit.al index 955e18ef828..399b8eecf5b 100644 --- a/src/Layers/W1/Tests/CRM integration/CDSConnectionSetupTest.Codeunit.al +++ b/src/Layers/W1/Tests/CRM integration/CDSConnectionSetupTest.Codeunit.al @@ -152,13 +152,13 @@ codeunit 139196 "CDS Connection Setup Test" CDSConnectionSetup.DeleteAll(); // [GIVEN] CDS Connection Setup page is opened CDSConnectionSetupPage.OpenEdit(); - // [GIVEN] Server Address is a valid Dataverse URL - CDSConnectionSetupPage."Server Address".SetValue('https://test.dynamics.com'); + // [GIVEN] Server Address is "TEST" + CDSConnectionSetupPage."Server Address".SetValue('TEST'); // [WHEN] Assisted Setup is invoked CDSConnectionSetupPage."Assisted Setup".Invoke(); - // [THEN] CDS Connection Setup wizard is opened with the same Server Address + // [THEN] CDS Connection Setup wizard is opened and Server Address = "TEST" // Wizard page is opened in AssistedSetupModalHandler Assert.ExpectedMessage(CDSConnectionSetupPage."Server Address".Value(), LibraryVariableStorage.DequeueText()); end; @@ -176,13 +176,13 @@ codeunit 139196 "CDS Connection Setup Test" InitializeSetup(false); // [GIVEN] CDS Connection Setup page is opened CDSConnectionSetupPage.OpenEdit(); - // [GIVEN] Server Address is a valid Dataverse URL - CDSConnectionSetupPage."Server Address".SetValue('https://test.dynamics.com'); + // [GIVEN] Server Address is "TEST" + CDSConnectionSetupPage."Server Address".SetValue('TEST'); // [WHEN] Assisted Setup is invoked CDSConnectionSetupPage."Assisted Setup".Invoke(); - // [THEN] CDS Connection Setup wizard is opened with the same Server Address + // [THEN] CDS Connection Setup wizard is opened and Server Address = "TEST" // Wizard page is opened in AssistedSetupModalHandler Assert.ExpectedMessage(CDSConnectionSetupPage."Server Address".Value(), LibraryVariableStorage.DequeueText()); end; diff --git a/src/Layers/W1/Tests/CRM integration/CDSConnectionWizardTests.Codeunit.al b/src/Layers/W1/Tests/CRM integration/CDSConnectionWizardTests.Codeunit.al index 7c47cefaf86..9fb0acd08b9 100644 --- a/src/Layers/W1/Tests/CRM integration/CDSConnectionWizardTests.Codeunit.al +++ b/src/Layers/W1/Tests/CRM integration/CDSConnectionWizardTests.Codeunit.al @@ -10,7 +10,6 @@ codeunit 139194 "CDS Connection Wizard Tests" var Assert: Codeunit Assert; - LibraryEnvironment: Codeunit "Environment Info Test Library"; LibraryVariableStorage: Codeunit "Library - Variable Storage"; CryptographyManagement: Codeunit "Cryptography Management"; NoEnvironmentSelectedErr: Label 'To sign in the administrator user you must specify an environment.'; @@ -182,7 +181,6 @@ codeunit 139194 "CDS Connection Wizard Tests" // [FEATURE] [UT] // [SCENARIO] CDS Connection URL should comply with rules Initialize(); - LibraryEnvironment.SetTestabilitySoftwareAsAService(true); // [GIVEN] Empty CDS Connection URL Address := ''; @@ -204,20 +202,19 @@ codeunit 139194 "CDS Connection Wizard Tests" // [THEN] Error message that security connection (https) is required Assert.ExpectedError(MustUseHttpsErr); - // [GIVEN] A valid HTTPS Dataverse URL with an explicit port and organization path - Address := 'https://test2.dynamics.com:555/myOrg'; + // [GIVEN] CDS Connection URL = 'http://test.com:555/myOrg' + Address := 'https://test2.com:555/myOrg'; // [WHEN] The URL is checked CDSIntegrationImpl.CheckModifyConnectionURL(Address); - // [THEN] The explicit port and organization path are preserved - Assert.AreEqual('https://test2.dynamics.com:555/myOrg', Address, WrongConnectionStringErr); + // [THEN] Error message that security connection (https) is required + Assert.AreEqual('https://test2.com:555/myOrg', Address, WrongConnectionStringErr); - // [GIVEN] A valid HTTPS Dataverse URL using the default port - Address := 'https://test3.dynamics.com/myOrg'; + // [GIVEN] CDS Connection URL = 'http://test.com:555/myOrg' + Address := 'https://test3.com/myOrg'; // [WHEN] The URL is checked CDSIntegrationImpl.CheckModifyConnectionURL(Address); - // [THEN] The organization path is removed - Assert.AreEqual('https://test3.dynamics.com', Address, WrongConnectionStringErr); - LibraryEnvironment.SetTestabilitySoftwareAsAService(IsSaaS); + // [THEN] Error message that security connection (https) is required + Assert.AreEqual('https://test3.com', Address, WrongConnectionStringErr); end; [Test] diff --git a/src/Layers/W1/Tests/Misc/UTREST.Codeunit.al b/src/Layers/W1/Tests/Misc/UTREST.Codeunit.al index 9ac101c132a..22e6415164c 100644 --- a/src/Layers/W1/Tests/Misc/UTREST.Codeunit.al +++ b/src/Layers/W1/Tests/Misc/UTREST.Codeunit.al @@ -10,7 +10,6 @@ codeunit 139148 "UT REST" var Assert: Codeunit Assert; - UnixTimestamp: Codeunit System.DateTime."Unix Timestamp"; NoContentErr: Label 'The stream is empty.'; UnknownImageTypeErr: Label 'Unknown image type.'; XmlDocLoadErr: Label 'A call to System.Xml.XmlDocument.Load failed'; @@ -31,8 +30,8 @@ codeunit 139148 "UT REST" begin // [SCENARIO 227335] SOAPWebServiceRequestMgt.HasJWTExpired function returns TRUE in case of expired token - // [GIVEN] Mock token that expired one hour ago - AccessToken := MockTokenWithExpDate(CurrentDateTime - 3600000); + // [GIVEN] Mock token with exprired date less than current + AccessToken := MockTokenWithExpDate(CurrentDateTime - 1000); // [WHEN] Function SOAPWebServiceRequestMgt.HasJWTExpired is being run // [THEN] It returns TRUE @@ -48,11 +47,11 @@ codeunit 139148 "UT REST" begin // [SCENARIO 227335] SOAPWebServiceRequestMgt.HasJWTExpired function returns FALSE in case of not expired token - // [GIVEN] Mock token that expires one hour from now - AccessToken := MockTokenWithExpDate(CurrentDateTime + 3600000); + // [GIVEN] Mock token with exprired date less than current + AccessToken := MockTokenWithExpDate(CurrentDateTime + 1000); // [WHEN] Function SOAPWebServiceRequestMgt.HasJWTExpired is being run - // [THEN] It returns FALSE + // [THEN] It returns TRUE Assert.IsFalse(SOAPWebServiceRequestMgt.HasJWTExpired(AccessToken), InvalidValueErr); end; @@ -426,8 +425,16 @@ codeunit 139148 "UT REST" end; local procedure GetUnixTime(DateTimeValue: DateTime): Decimal + var + TypeHelper: Codeunit "Type Helper"; + TimeZoneOffset: Duration; begin - exit(UnixTimestamp.CreateTimestampSeconds(DateTimeValue)); + if not TypeHelper.GetUserTimezoneOffset(TimeZoneOffset) then + TimeZoneOffset := 0; + exit( + Round( + (DateTimeValue - CreateDateTime(DMY2Date(1, 1, 1970), 0T) - TimeZoneOffset) / 1000, + 1)); end; local procedure VerifyJsonText(JsonText: Text; ExpectedJsonText: Text) @@ -448,3 +455,4 @@ codeunit 139148 "UT REST" Assert.AreEqual(ExpectedXMLText, XMLText, InvalidValueErr); end; } + From 2380c0bf2ed305059d6876ec04d4fafd890bcfcb Mon Sep 17 00:00:00 2001 From: Prangshuman Das Date: Wed, 30 Sep 2026 22:42:00 +0200 Subject: [PATCH 16/16] Give activity log E2E approver unlimited approval Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 988fb581-008c-4e0d-99c1-285cd45d49e7 --- .../test/src/API/ExpenseActivityLogAPITest.Codeunit.al | 1 + 1 file changed, 1 insertion(+) 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 07c3351e5b9..c25201f0a2d 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseActivityLogAPITest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/API/ExpenseActivityLogAPITest.Codeunit.al @@ -446,6 +446,7 @@ codeunit 148343 "Expense Activity Log API Test" CreateTestExpenseUser(ApproverExpenseUser); ApproverExpenseUser."Can Approve" := true; + ApproverExpenseUser.Validate("Unlimited Approval", true); ApproverExpenseUser."User Id For Approvals" := CopyStr('APPROVER-' + RunToken, 1, MaxStrLen(ApproverExpenseUser."User Id For Approvals")); ApproverExpenseUser.Modify();