From 18c5d0ebdaf59c1f0608f7178d289af7ebfb0eee Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Mon, 29 Jun 2026 14:30:36 +0200 Subject: [PATCH] fix(test): persist generated API key + anchor rights selector + idempotent createUser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First real run against the sandbox revealed three issues in userSetup.ts: 1. generateApiKey generated the key client-side and read it into the file but never submitted the edit form, so Dolibarr never persisted api_key (DB stayed NULL → the key could not authenticate). Now it clicks Save after generating. 2. assignRights matched `rights=` as an href substring, so a short id like 12 (facture creer) also matched rights=121 / rights=1232 and .first() clicked the wrong link — facture creer was never granted. Anchored with a trailing "&" (rights=&) for an exact match. 3. createUser was not idempotent: a re-run hit the existing login and failed to parse a new id. Added findUserId (look up by login via the user list) and return the existing id instead of creating a duplicate. Verified the symptoms live: ai_agent_sandbox (rowid 4) had api_key NULL and was missing only facture/creer among the 11 intended rights. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/scripts/admin/userSetup.ts | 48 ++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/test/scripts/admin/userSetup.ts b/test/scripts/admin/userSetup.ts index 58604d7..9d31103 100644 --- a/test/scripts/admin/userSetup.ts +++ b/test/scripts/admin/userSetup.ts @@ -68,6 +68,11 @@ async function createUser( const { page, dolibarrAddress, imgFolderPath } = globalCtx; await login.doAdminLogin(globalCtx); + // Idempotent: if the login already exists (e.g. a re-run), return its id rather + // than submitting a duplicate create (which Dolibarr rejects). + const existingId = await findUserId(globalCtx, userLogin); + if (existingId !== undefined) return existingId; + await page.goto(`${dolibarrAddress}/user/card.php?action=create&mode=create`); // Fill text inputs (login, lastname, password) without submitting yet. @@ -128,6 +133,30 @@ async function readUserIdFromPage(page: Page): Promise { return undefined; } +/* + Find an existing user's numeric id by login via the user list, so createUser is + idempotent across re-runs. Matches the table row containing the login, then reads + the id from that row's user-card link. +*/ +async function findUserId( + globalCtx: UserCtx, + userLogin: string, +): Promise { + const { page, dolibarrAddress } = globalCtx; + await page.goto( + `${dolibarrAddress}/user/list.php?search_login=${encodeURIComponent(userLogin)}`, + ); + await page.waitForLoadState("domcontentloaded"); + const cardLink = page + .locator("tr", { hasText: userLogin }) + .locator('a[href*="/user/card.php?id="]') + .first(); + if (await cardLink.count() === 0) return undefined; + const href = (await cardLink.getAttribute("href")) ?? ""; + const m = href.match(/[?&]id=(\d+)/); + return m ? Number(m[1]) : undefined; +} + /* Grant a set of rights to a user on /user/perms.php?id=. @@ -150,10 +179,11 @@ async function assignRights( // which would otherwise stale the locators. await page.goto(`${dolibarrAddress}/user/perms.php?id=${userId}`); - // GUESS: the grant control is an linking to action=addrights&rights=. - // We match on the href substring to avoid depending on row layout / icons. + // The grant control is an linking to action=addrights&rights=. The + // trailing "&" anchors the id so rights=12 does NOT substring-match rights=121 + // / rights=1232 etc. (Dolibarr hrefs are ...&rights=&confirm=yes...). const addLink = page.locator( - `a[href*="action=addrights"][href*="rights=${rightId}"]`, + `a[href*="action=addrights"][href*="rights=${rightId}&"]`, ); if (await addLink.count() === 0) { @@ -245,6 +275,18 @@ async function generateApiKey( `id=${userId}.`, ); } + + // Persist: the generate control only fills the field client-side. Submit the + // edit form so Dolibarr saves api_key to the DB — otherwise it stays NULL and + // the generated key never authenticates. + const saveBtn = page.locator('input[name="save"], button[name="save"]'); + if (await saveBtn.count() > 0) { + await saveBtn.first().click(); + } else { + await page.click(':nth-match(input[type="submit"], 1)'); + } + await page.waitForLoadState("domcontentloaded"); + return key; }