Fix API test authentication in BCApps - #10085
Conversation
5dee938 to
d459cb4
Compare
There was a problem hiding this comment.
Pull request overview
This PR centralizes Basic authentication injection for Library - Graph Mgt-based API tests so they can run in NavUserPassword (UserPassword) containers, and then re-enables several previously disabled API/E2E test suites. It replaces an app-specific Expense Agent auth helper with a shared TestLibraries subscriber and adds a container-side “credential bridge” file so the test runner can obtain the password without requiring Azure Key Vault.
Changes:
- Added
Library - Graph Auth Mgt.as an event subscriber to inject Basic auth for non-Windows test users, sourcing the password from a container file or (fallback) Azure Key Vault. - Removed the Expense Agent test-only auth helper + manual subscription binding; tests now rely on the shared subscriber.
- Re-enabled multiple API/E2E test suites by removing entries from various
*.DisabledTest.jsonfiles (and deleting the APIV1/APIV2 exclusion lists), and updated container provisioning to create the API-test password bridge file.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/Layers/W1/Tests/TestLibraries/LibraryGraphAuthMgt.Codeunit.al | New shared subscriber that injects Basic auth into Library - Graph Mgt requests for NavUserPassword scenarios. |
| build/scripts/NewBcContainer.ps1 | Writes the container password bridge file and sets ACLs so server-side AL can read it. |
| src/Apps/W1/ExpenseAgent/test/src/Helper/ExpenseAPITestAuthHelper.Codeunit.al | Removes app-specific auth injection helper in favor of shared TestLibraries implementation. |
| src/Apps/W1/ExpenseAgent/test/src/API/ExpenseUsersAPITest.Codeunit.al | Drops manual subscription binding to the removed helper. |
| src/Apps/W1/ExpenseAgent/test/src/API/ExpenseProjectsAPITest.Codeunit.al | Drops manual subscription binding to the removed helper. |
| src/Apps/W1/ExpenseAgent/test/src/API/ExpensePerDiemLocationsTest.Codeunit.al | Drops manual subscription binding to the removed helper. |
| src/Apps/W1/ExpenseAgent/test/src/API/ExpenseCapabilitiesAPITest.Codeunit.al | Drops manual subscription binding to the removed helper. |
| src/DisabledTests/Tests-Integration/Tests-Integration.DisabledTest.json | Re-enables specific integration API tests by removing disable entries. |
| src/DisabledTests/Tests-Graph/Tests-Graph.DisabledTest.json | Re-enables specific Graph E2E tests by removing disable entries. |
| src/DisabledTests/Sustainability_Tests/Sustainability_Tests.DisabledTest.json | Re-enables Sustainability API tests by removing disable entries. |
| src/DisabledTests/Quality_Management-Tests/Quality_Management-Tests.DisabledTest.json | Re-enables Quality Management API tests by removing disable entry. |
| src/DisabledTests/IRS_Forms_Tests/IRS_Forms_Tests.DisabledTest.json | Re-enables IRS 1099 API test by removing disable entry. |
| src/DisabledTests/E-Document_Core_Tests/E-Document Core Tests.DisabledTest.json | Re-enables E-Document API tests by removing disable entry. |
| src/DisabledTests/_Exclude_APIV2__Tests/_Exclude_APIV2__Tests.DisabledTest.json | Deletes the APIV2 exclusion list (re-enables APIV2 suite). |
| src/DisabledTests/_Exclude_APIV1__Tests/_Exclude_APIV1__Tests.DisabledTest.json | Deletes the APIV1 exclusion list (re-enables APIV1 suite). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Agentic PR Review - Round 1Recommendation: AcceptWhat this PR doesThis PR adds an explicit authentication bridge for API tests that use Library - Graph Mgt. in BCApps UserPassword containers. The subscriber is manual, each affected API test codeunit opts in, Windows-authenticated NAV gates keep their existing behavior, and local NAV UserPassword runs can use the existing Key Vault secret. The change addresses the gate difference directly. It does not change application API behavior, and it avoids making partner test code automatically depend on the BCApps credential bridge. SuggestionsNone. Risk assessment and necessityRisk: The change touches shared test infrastructure and re-enables many existing suites, so CI isolation and concurrency failures may still need separate gate work. The authentication subscriber itself is manually scoped and internal. Necessity: The change is required because BCApps runs these tests with UserPassword while NAV's normal uptake gates use Windows authentication. Without the bridge, the API suites fail with 401 responses and remain disabled.
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91
| if not User.Get(UserSecurityId()) then | ||
| Error(CurrentUserNotFoundErr); | ||
|
|
||
| if ContainerPasswordFileExists() then begin |
There was a problem hiding this comment.
GetAuthenticationPassword (new codeunit 131022 'Library - Graph Auth Mgt.') now hard-errors whenever the on-prem container password file is absent and the current user has no Windows Security ID: it falls through to TryGetNavEnlistmentPassword() and raises KeyVaultPasswordReadErr if the 'NavServerUserPassword' Key Vault secret cannot be retrieved. The deleted predecessor (Expense API Test Auth Helper) explicitly gated this whole path behind EnvironmentInfo.IsSaaSInfrastructure(), skipping Basic-Auth injection entirely on SaaS. That environment guard was dropped in the consolidation, and EnsureAuthenticationAvailable()/AddAuthentication() are now invoked unconditionally from trigger OnRun (or InitializeWebRequestWithURL) in ~146 API/E2E test codeunits across APIV1, APIV2, ExpenseAgent, Sustainability, Quality Management and IRS1099. In any environment without the container password file, without Windows-integrated auth, and without access to the NavServerUserPassword Key Vault secret (e.g. a SaaS test tenant, or a local/dev sandbox), these tests will now hard-fail during setup instead of silently skipping auth injection as before. Reinstate an explicit unsupported-environment guard (e.g. skip when running on SaaS, mirroring the removed ShouldInjectBasicAuth check) or make the Key Vault lookup best-effort when neither a container password file nor Windows auth is present.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
|
The file name src/Apps/W1/APIV2/test/src/APIV2itemLedgEntriesE2E.Codeunit.al does not mirror the declared object name 'APIV2 - Item Ledg. Entries E2E' in PascalCase. Rename it to APIV2ItemLedgEntriesE2E.Codeunit.al so file listings and code-search tools keep a stable file-system-to-object mapping. Knowledge: Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4 |
|
The file name src/Layers/W1/Tests/Integration/ApiRoutesE2E.Codeunit.al does not mirror the declared object name 'API Routes E2E' in PascalCase. Rename it to APIRoutesE2E.Codeunit.al so file listings and code-search tools keep a stable file-system-to-object mapping. Knowledge: Line mapping was unavailable, so this was posted as an issue comment. 👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4 |
| CODEUNIT.Run(CODEUNIT::"Correct Posted Sales Invoice", SalesInvoiceHeader); | ||
| end; | ||
|
|
||
| local procedure EnsureReasonCode() |
There was a problem hiding this comment.
The new EnsureReasonCode() helper hand-rolls a 'Reason Code' record via Init/Insert with a hard-coded primary key ('API-TEST') instead of using the existing LibraryERM fixture API. This makes these API tests depend on an ad-hoc, non-maintained record shape rather than a maintained test-library fixture, and duplicates the same pattern across four codeunits: src/Apps/W1/APIV1/test/src/SalesInvoicesE2E.Codeunit.al, src/Apps/W1/APIV2/test/src/APIV2SalesInvoicesE2E.Codeunit.al, src/Apps/W1/APIV1/test/src/APIV1SalesCreditMemosE2E.Codeunit.al, and src/Apps/W1/APIV2/test/src/APIV2SalesCreditMemosE2E.Codeunit.al.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| PurchaseLineGLAccount, PurchaseHeader, PurchaseLineGLAccount.Type::"G/L Account", GLAccount."No.", 1); | ||
| end; | ||
|
|
||
| procedure EnsureVATPostingSetupExists(VATBusPostingGroup: Code[20]; VATProdPostingGroup: Code[20]) |
There was a problem hiding this comment.
EnsureVATPostingSetupExists now creates 'VAT Posting Setup' records directly via Init/Insert instead of the existing LibraryERM.CreateVATPostingSetup helper. This bypasses the maintained test-library setup for a prerequisite record and can leave API test fixtures with a VAT posting setup shape that drifts from what the application actually requires as posting logic evolves.
Suggested fix (apply manually — could not be anchored as a one-click suggestion):
procedure EnsureVATPostingSetupExists(VATBusPostingGroup: Code[20]; VATProdPostingGroup: Code[20])
var
VATPostingSetup: Record "VAT Posting Setup";
LibraryERM: Codeunit "Library - ERM";
begin
if (VATBusPostingGroup = '') and (VATProdPostingGroup = '') then
exit;
if VATPostingSetup.Get(VATBusPostingGroup, VATProdPostingGroup) then
exit;
LibraryERM.CreateVATPostingSetup(VATPostingSetup, VATBusPostingGroup, VATProdPostingGroup);
end;Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| procedure InitializeWebRequestWithURL(var HttpWebRequestMgt: Codeunit "Http Web Request Mgt."; TargetURL: Text) | ||
| begin | ||
| HttpWebRequestMgt.Initialize(TargetURL); | ||
| LibraryGraphAuthMgt.AddAuthentication(HttpWebRequestMgt); |
There was a problem hiding this comment.
InitializeWebRequestWithURL now unconditionally injects Basic authentication (via LibraryGraphAuthMgt.AddAuthentication) for essentially every Graph/API E2E test request, and the helper both sets HttpWebRequest.Credentials and manually adds an explicit 'Authorization: Basic ' header. In this repo's container/dev test setup, BC endpoints are commonly exposed over plain http://, so these credentials (including the raw password embedded in the manually-built header) can be transmitted without TLS. Guard the Basic-auth injection so it only fires for HTTPS endpoints, or ensure the container/test endpoints this helper targets are TLS-only.
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
AI Review — Round 1Scope: 163 changed files (153 What this PR doesCentralizes API-test HTTP authentication into a new Overall this is a well-executed, mechanically consistent piece of test-infra work with strong new test coverage for the build-side changes. No correctness issues found in the core auth logic itself (secret handling is properly Findings🔴 S1 — Undisclosed production behavior change bundled into a test-only PR ( - ReportUsage := "Report Selection Usage"::"P.Return";
+ ReportUsage := "Report Selection Usage"::"P.Ret.Shpt.";This is a real bug fix — 🟠 S2 — Mock Key Vault provider is fully reinitialized on every single HTTP request 🟡 S3 — Regression-test precision loosened, likely as a rebase workaround 🟡 S4 — Confusing near-duplicate variable naming ( 🟡 S5 — Container-side password file is never cleaned up CI status (informational, not this PR's fault)Every "Build Apps *" check is currently failing on RecommendationAccept with Suggestions. The core auth-centralization work is sound, consistent, and well-tested; please address S1 (disclose/split/test the return-shipment report-usage fix) before merging, and consider S2–S5 as follow-ups.
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3952f078-a881-4da8-ad96-13b727e48a91
| local procedure CreateCancelledSalesInvoice(var SalesInvoiceHeader: Record "Sales Invoice Header") | ||
| begin | ||
| CreatePostedSalesInvoice(SalesInvoiceHeader); | ||
| EnsureReasonCode(); |
There was a problem hiding this comment.
LibrarySales.SetDefaultCancelReasonCodeForSalesAndReceivablesSetup() is a localized hook that writes setup data in layers such as AU, so this code can open a write transaction immediately before CODEUNIT.Run(...). The runtime rejects Codeunit.Run inside an open write transaction, so this helper needs a Commit() before the run or a read-only caller.
Suggested fix (apply manually — could not be anchored as a one-click suggestion):
EnsureReasonCode();
LibrarySales.SetDefaultCancelReasonCodeForSalesAndReceivablesSetup();
Commit();
CODEUNIT.Run(CODEUNIT::"Correct Posted Sales Invoice", SalesInvoiceHeader);Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| local procedure CreateCancelledSalesInvoice(var SalesInvoiceHeader: Record "Sales Invoice Header") | ||
| begin | ||
| CreatePostedSalesInvoice(SalesInvoiceHeader); | ||
| EnsureReasonCode(); |
There was a problem hiding this comment.
LibrarySales.SetDefaultCancelReasonCodeForSalesAndReceivablesSetup() is a localized hook that writes setup data in layers such as AU, so this code can open a write transaction immediately before CODEUNIT.Run(...). The runtime rejects Codeunit.Run inside an open write transaction, so this helper needs a Commit() before the run or a read-only caller.
Suggested fix (apply manually — could not be anchored as a one-click suggestion):
EnsureReasonCode();
LibrarySales.SetDefaultCancelReasonCodeForSalesAndReceivablesSetup();
Commit();
CODEUNIT.Run(CODEUNIT::"Correct Posted Sales Invoice", SalesInvoiceHeader);Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| procedure InitializeWebRequestWithURL(var HttpWebRequestMgt: Codeunit "Http Web Request Mgt."; TargetURL: Text) | ||
| begin | ||
| HttpWebRequestMgt.Initialize(TargetURL); | ||
| LibraryGraphAuthMgt.AddAuthentication(HttpWebRequestMgt); |
There was a problem hiding this comment.
LibraryGraphMgt.InitializeWebRequestWithURL is public and accepts a caller-supplied URL, but it now unconditionally invokes the internal auth helper to fetch the current user's password and attach Basic authentication. Access = Internal on the helper is not an authorization boundary, so any dependent extension that can call this public test-library surface can reuse it to send those credentials to an arbitrary endpoint.
Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| var | ||
| ReasonCode: Record "Reason Code"; | ||
| begin | ||
| if not ReasonCode.IsEmpty() then |
There was a problem hiding this comment.
This test helper hand-rolls a "Reason Code" fixture with Init/assignment/Insert even though the test library already provides LibraryERM.CreateReasonCode. Bypassing the library skips its Validate/Insert(true) path and hard-codes a record shape that can drift from the platform setup the correction flow expects, making the test depend on invalid or incomplete fixture data.
Suggested fix (apply manually — could not be anchored as a one-click suggestion):
if not ReasonCode.IsEmpty() then
exit;
LibraryERM.CreateReasonCode(ReasonCode);Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| var | ||
| ReasonCode: Record "Reason Code"; | ||
| begin | ||
| if not ReasonCode.IsEmpty() then |
There was a problem hiding this comment.
This test helper hand-rolls a "Reason Code" fixture with Init/assignment/Insert even though the test library already provides LibraryERM.CreateReasonCode. Bypassing the library skips its Validate/Insert(true) path and hard-codes a record shape that can drift from the platform setup the correction flow expects, making the test depend on invalid or incomplete fixture data.
Suggested fix (apply manually — could not be anchored as a one-click suggestion):
if not ReasonCode.IsEmpty() then
exit;
LibraryERM.CreateReasonCode(ReasonCode);Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| var | ||
| ReasonCode: Record "Reason Code"; | ||
| begin | ||
| if not ReasonCode.IsEmpty() then |
There was a problem hiding this comment.
This test helper hand-rolls a "Reason Code" fixture with Init/assignment/Insert even though the test library already provides LibraryERM.CreateReasonCode. Bypassing the library skips its Validate/Insert(true) path and hard-codes a record shape that can drift from the platform setup the correction flow expects, making the test depend on invalid or incomplete fixture data.
Suggested fix (apply manually — could not be anchored as a one-click suggestion):
if not ReasonCode.IsEmpty() then
exit;
LibraryERM.CreateReasonCode(ReasonCode);Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| var | ||
| ReasonCode: Record "Reason Code"; | ||
| begin | ||
| if not ReasonCode.IsEmpty() then |
There was a problem hiding this comment.
This test helper hand-rolls a "Reason Code" fixture with Init/assignment/Insert even though the test library already provides LibraryERM.CreateReasonCode. Bypassing the library skips its Validate/Insert(true) path and hard-codes a record shape that can drift from the platform setup the correction flow expects, making the test depend on invalid or incomplete fixture data.
Suggested fix (apply manually — could not be anchored as a one-click suggestion):
if not ReasonCode.IsEmpty() then
exit;
LibraryERM.CreateReasonCode(ReasonCode);Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
| var | ||
| VATPostingSetup: Record "VAT Posting Setup"; | ||
| begin | ||
| if (VATBusPostingGroup = '') and (VATProdPostingGroup = '') then |
There was a problem hiding this comment.
This test library now hand-rolls a "VAT Posting Setup" fixture with Init/field assignment/Insert instead of using LibraryERM.CreateVATPostingSetup. That bypasses the library's validated setup path and can leave future tests creating partially initialized posting data, especially when table validation or localization-specific setup changes.
Suggested fix (apply manually — could not be anchored as a one-click suggestion):
if (VATBusPostingGroup = '') and (VATProdPostingGroup = '') then
exit;
if VATPostingSetup.Get(VATBusPostingGroup, VATProdPostingGroup) then
exit;
LibraryERM.CreateVATPostingSetup(VATPostingSetup, VATBusPostingGroup, VATProdPostingGroup);Knowledge:
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.35.4
Summary
Library - Graph MgtAPI tests in UserPassword containersTesting
AB#646383