fix(test): persist generated API key + anchor rights selector + idempotent createUser

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=<id>` 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=<id>&) 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) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 14:30:36 +02:00
parent 2154bf319e
commit 18c5d0ebda

View File

@@ -68,6 +68,11 @@ async function createUser(
const { page, dolibarrAddress, imgFolderPath } = globalCtx; const { page, dolibarrAddress, imgFolderPath } = globalCtx;
await login.doAdminLogin(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`); await page.goto(`${dolibarrAddress}/user/card.php?action=create&mode=create`);
// Fill text inputs (login, lastname, password) without submitting yet. // Fill text inputs (login, lastname, password) without submitting yet.
@@ -128,6 +133,30 @@ async function readUserIdFromPage(page: Page): Promise<number | undefined> {
return undefined; 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<number | undefined> {
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=<userId>. Grant a set of rights to a user on /user/perms.php?id=<userId>.
@@ -150,10 +179,11 @@ async function assignRights(
// which would otherwise stale the locators. // which would otherwise stale the locators.
await page.goto(`${dolibarrAddress}/user/perms.php?id=${userId}`); await page.goto(`${dolibarrAddress}/user/perms.php?id=${userId}`);
// GUESS: the grant control is an <a> linking to action=addrights&rights=<id>. // The grant control is an <a> linking to action=addrights&rights=<id>. The
// We match on the href substring to avoid depending on row layout / icons. // trailing "&" anchors the id so rights=12 does NOT substring-match rights=121
// / rights=1232 etc. (Dolibarr hrefs are ...&rights=<id>&confirm=yes...).
const addLink = page.locator( const addLink = page.locator(
`a[href*="action=addrights"][href*="rights=${rightId}"]`, `a[href*="action=addrights"][href*="rights=${rightId}&"]`,
); );
if (await addLink.count() === 0) { if (await addLink.count() === 0) {
@@ -245,6 +275,18 @@ async function generateApiKey(
`id=${userId}.`, `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; return key;
} }