-
Notifications
You must be signed in to change notification settings - Fork 943
Web 1014 playwright test data factories client group user and reverse order cleanup guard #3686
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
devvaansh
wants to merge
1
commit into
openMF:dev
Choose a base branch
from
devvaansh:WEB-1014-playwright-test-data-factories-client-group-user-and-reverse-order-cleanup-guard
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| /** | ||
| * Copyright since 2026 Mifos Initiative | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| */ | ||
|
|
||
| /** | ||
| * Shared resolver helpers for the test-data factories. | ||
| * | ||
| * The only piece of cross-factory state is "what office should we | ||
| * attach this entity to?" — every factory in this PR defaults to the | ||
| * first office returned by Fineract, and dedupes that lookup through | ||
| * the {@link ApiSetupManager} so a test that creates a client, a | ||
| * group, and a user only pays one `/api/v1/offices` round-trip. | ||
| * | ||
| * Portability note: this module imports only from the in-tree | ||
| * `ApiSetupManager`. The React port can copy it verbatim. | ||
| */ | ||
|
|
||
| import type { ApiSetupManager } from '../utils/api-setup-manager'; | ||
|
|
||
| /** | ||
| * Stable cache key for "the first office id". Exported so unit specs | ||
| * can assert deduplication without re-implementing the convention. | ||
| */ | ||
| export const FIRST_OFFICE_CACHE_KEY = 'office:first'; | ||
|
|
||
| /** | ||
| * Resolve the first office id, sharing the result across every | ||
| * factory invocation in the current process. The Fineract demo data | ||
| * ships exactly one office (the Head Office), so this is effectively | ||
| * a one-shot lookup; the `dedupe` wrapper still matters because | ||
| * parallel factory calls within the same test would otherwise fire | ||
| * the request twice. | ||
| */ | ||
| export async function resolveDefaultOfficeId(setup: ApiSetupManager): Promise<number> { | ||
| return setup.dedupe(FIRST_OFFICE_CACHE_KEY, () => setup.api.getFirstOfficeId()); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| /** | ||
| * Copyright since 2026 Mifos Initiative | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| */ | ||
|
|
||
| import { test, expect } from '../fixtures/test-fixtures'; | ||
| import { createTestClient, DEFAULT_TEST_CLIENT_LASTNAME } from './client.factory'; | ||
| import { E2E_NAME_PATTERN } from '../utils/naming'; | ||
|
|
||
| // Live-backend specs — run under the `integration` Playwright project | ||
| // (testMatch: /playwright\/factories\/.*\.spec\.ts/ in | ||
| // playwright.config.ts). No browser, no auth-setup dependency; the | ||
| // tests issue HTTP directly against the Fineract endpoint configured | ||
| // via the existing `fineractApi` fixture. | ||
| test.use({ storageState: { cookies: [], origins: [] } }); | ||
|
|
||
| test.describe('createTestClient() against live Fineract', () => { | ||
| test('creates a pending client matching the TestClient shape', async ({ apiSetup, cleanupGuard }) => { | ||
| const client = await createTestClient(apiSetup, cleanupGuard); | ||
|
|
||
| expect(typeof client.resourceId).toBe('number'); | ||
| expect(client.resourceId).toBeGreaterThan(0); | ||
| expect(client.officeId).toBeGreaterThan(0); | ||
| // Default display name is `<generatedFirstname> <lastname>` — | ||
| // assert both halves so a regression that drops the suffix is caught. | ||
| expect(client.displayName.startsWith('E2E_client_S')).toBe(true); | ||
| expect(client.displayName.endsWith(` ${DEFAULT_TEST_CLIENT_LASTNAME}`)).toBe(true); | ||
| const firstname = client.displayName.split(' ')[0]; | ||
| expect(firstname).toMatch(E2E_NAME_PATTERN); | ||
|
|
||
| // Round-trip the create through the GET endpoint to confirm | ||
| // Fineract really persisted what the projection claims. | ||
| const fetched = await apiSetup.api.getClient(client.resourceId); | ||
| expect(fetched.id).toBe(client.resourceId); | ||
| expect(fetched.displayName).toBe(client.displayName); | ||
| expect(fetched.officeId).toBe(client.officeId); | ||
| expect(fetched.active).toBe(false); | ||
| expect(fetched.status?.value).toBe('Pending'); | ||
| }); | ||
|
|
||
| test('honours firstname / lastname / submittedOnDate overrides', async ({ apiSetup, cleanupGuard }) => { | ||
| const client = await createTestClient(apiSetup, cleanupGuard, { | ||
| firstname: 'OverrideF', | ||
| lastname: 'OverrideL', | ||
| submittedOnDate: '15 March 2024' | ||
| }); | ||
| expect(client.displayName).toBe('OverrideF OverrideL'); | ||
| const fetched = await apiSetup.api.getClient(client.resourceId); | ||
| expect(fetched.timeline?.submittedOnDate).toEqual([ | ||
| 2024, | ||
| 3, | ||
| 15 | ||
| ]); | ||
| }); | ||
|
|
||
| test('queues a working deleter on the cleanup-guard', async ({ apiSetup, cleanupGuard }) => { | ||
| const client = await createTestClient(apiSetup, cleanupGuard); | ||
| expect(cleanupGuard.size()).toBe(1); | ||
|
|
||
| const summary = await cleanupGuard.flush(); | ||
| expect(summary.ok).toBe(1); | ||
| expect(summary.failed).toEqual([]); | ||
|
|
||
| // Confirm Fineract really hard-deleted the row by asserting the | ||
| // subsequent GET 404s. We catch via try/catch instead of | ||
| // `expect(...).rejects.toThrow()` because `getClient` throws a | ||
| // plain `Error` with the 404 status embedded in the message. | ||
| await expect(apiSetup.api.getClient(client.resourceId)).rejects.toThrow(/404|not found/i); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| /** | ||
| * Copyright since 2026 Mifos Initiative | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| */ | ||
|
|
||
| /** | ||
| * Factory for a freshly-created Fineract client owned by the current | ||
| * Playwright test. | ||
| * | ||
| * Design goals (per GSoC 2026 proposal WA-2.9): | ||
| * - Default to a *pending* (`active: false`) client. Fineract only | ||
| * hard-deletes clients in pending state with no attached accounts, | ||
| * so the {@link CleanupGuard} teardown registered here MUST be | ||
| * able to succeed. Tests that need an active client can pass | ||
| * `{ active: true, activationDate }` via `overrides` and accept | ||
| * that their cleanup will fail loudly (the guard reports it but | ||
| * does not throw). | ||
| * - Build a unique, shard-tagged name via {@link generateE2EName} | ||
| * so cleanup-grep tooling can identify orphaned rows. | ||
| * - Dedupe the office lookup through {@link ApiSetupManager} so a | ||
| * test that creates three resources only pays one | ||
| * `/api/v1/offices` round-trip. | ||
| * - Register the deleter immediately after a successful POST and | ||
| * never before — a half-completed create must not leave a stale | ||
| * id queued for teardown. | ||
| * | ||
| * Portability note: this module imports only from the in-tree | ||
| * Playwright infrastructure (no Angular, no React, no Material). The | ||
| * React port can adopt it verbatim once its Playwright suite needs | ||
| * test-data factories. | ||
| */ | ||
|
|
||
| import type { ApiSetupManager } from '../utils/api-setup-manager'; | ||
| import type { CleanupGuard } from '../utils/cleanup-guard'; | ||
| import { generateE2EName } from '../utils/naming'; | ||
| import type { TestClient } from '../types/test-data.types'; | ||
| import { resolveDefaultOfficeId } from './_shared'; | ||
|
|
||
| /** Default lastname applied to every pending test client. */ | ||
| export const DEFAULT_TEST_CLIENT_LASTNAME = 'E2E'; | ||
|
|
||
| /** Default submitted-on date applied to every pending test client. */ | ||
| export const DEFAULT_TEST_CLIENT_SUBMITTED_ON_DATE = '01 January 2024'; | ||
|
|
||
| /** Fineract `legalFormId` for an individual person. */ | ||
| const LEGAL_FORM_PERSON = 1; | ||
|
|
||
| /** Date format expected by the create-client endpoint. */ | ||
| const DEFAULT_DATE_FORMAT = 'dd MMMM yyyy'; | ||
|
|
||
| /** Locale expected by the create-client endpoint. */ | ||
| const DEFAULT_LOCALE = 'en'; | ||
|
|
||
| /** Caller-supplied tweaks to the default pending-client payload. */ | ||
| export interface CreateTestClientOverrides { | ||
| /** Override the auto-generated firstname. */ | ||
| firstname?: string; | ||
| /** Override the default lastname (`'E2E'`). */ | ||
| lastname?: string; | ||
| /** Override the default office id (first office returned by Fineract). */ | ||
| officeId?: number; | ||
| /** Override the default submitted-on date. */ | ||
| submittedOnDate?: string; | ||
| /** | ||
| * Extra payload fields merged AFTER the defaults — use to flip | ||
| * `active: true`, set an activation date, attach to a group, etc. | ||
| * Caller owns the cleanup-fail risk for non-deletable shapes. | ||
| */ | ||
| extra?: Record<string, unknown>; | ||
| } | ||
|
|
||
| /** | ||
| * Create a pending client owned by the current test and queue its | ||
| * deletion on the supplied {@link CleanupGuard}. | ||
| * | ||
| * @param setup The per-test {@link ApiSetupManager}. Carries the | ||
| * authenticated `FineractApiClient` and shares | ||
| * deduped setup calls across factories. | ||
| * @param guard The per-test {@link CleanupGuard}. The returned | ||
| * client's deleter is pushed onto this stack before | ||
| * this function returns. | ||
| * @param overrides See {@link CreateTestClientOverrides}. | ||
| * @returns A {@link TestClient} projection built from the create | ||
| * response and the input — no follow-up GET is issued, so | ||
| * callers needing post-creation state (timeline, status | ||
| * transitions) should call `setup.api.getClient(id)` | ||
| * themselves. | ||
| */ | ||
| export async function createTestClient( | ||
| setup: ApiSetupManager, | ||
| guard: CleanupGuard, | ||
| overrides: CreateTestClientOverrides = {} | ||
| ): Promise<TestClient> { | ||
| const officeId = overrides.officeId ?? (await resolveDefaultOfficeId(setup)); | ||
| const firstname = overrides.firstname ?? generateE2EName('client'); | ||
| const lastname = overrides.lastname ?? DEFAULT_TEST_CLIENT_LASTNAME; | ||
| const submittedOnDate = overrides.submittedOnDate ?? DEFAULT_TEST_CLIENT_SUBMITTED_ON_DATE; | ||
|
|
||
| const payload: Record<string, unknown> = { | ||
| officeId, | ||
| firstname, | ||
| lastname, | ||
| legalFormId: LEGAL_FORM_PERSON, | ||
| active: false, | ||
| submittedOnDate, | ||
| dateFormat: DEFAULT_DATE_FORMAT, | ||
| locale: DEFAULT_LOCALE, | ||
| ...overrides.extra | ||
| }; | ||
|
|
||
| const response = await setup.api.createClient(payload); | ||
| // Fineract returns both `clientId` and `resourceId` on create — they | ||
| // are always equal but `resourceId` is the documented envelope field. | ||
| const resourceId: number = response.resourceId ?? response.clientId; | ||
| if (typeof resourceId !== 'number') { | ||
| throw new Error( | ||
| `createTestClient: Fineract create-client response missing numeric resourceId/clientId, got ${JSON.stringify( | ||
| response | ||
| )}` | ||
| ); | ||
| } | ||
|
|
||
| // Register the deleter BEFORE returning so a caller that forgets to | ||
| // await our result still gets cleanup on test exit. | ||
| guard.register(`client:${resourceId}`, async () => { | ||
| await setup.api.deleteClient(resourceId); | ||
| }); | ||
|
|
||
| return { | ||
| resourceId, | ||
| displayName: `${firstname} ${lastname}`, | ||
| officeId | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| /** | ||
| * Copyright since 2026 Mifos Initiative | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| */ | ||
|
|
||
| import { test, expect } from '../fixtures/test-fixtures'; | ||
| import { createTestGroup } from './group.factory'; | ||
| import { E2E_NAME_PATTERN } from '../utils/naming'; | ||
|
|
||
| // Live-backend specs — run under the `integration` Playwright project | ||
| // (testMatch: /playwright\/factories\/.*\.spec\.ts/ in | ||
| // playwright.config.ts). No browser, no auth-setup dependency. | ||
| test.use({ storageState: { cookies: [], origins: [] } }); | ||
|
|
||
| test.describe('createTestGroup() against live Fineract', () => { | ||
| test('creates a pending group matching the TestGroup shape', async ({ apiSetup, cleanupGuard }) => { | ||
| const group = await createTestGroup(apiSetup, cleanupGuard); | ||
|
|
||
| expect(typeof group.resourceId).toBe('number'); | ||
| expect(group.resourceId).toBeGreaterThan(0); | ||
| expect(group.officeId).toBeGreaterThan(0); | ||
| // displayName is mirrored from `name` for groups — assert the | ||
| // E2E pattern on it directly. | ||
| expect(group.displayName).toMatch(E2E_NAME_PATTERN); | ||
| expect(group.displayName.startsWith('E2E_group_S')).toBe(true); | ||
|
|
||
| // Round-trip the create through the GET endpoint. | ||
| const fetched = await apiSetup.api.getGroup(group.resourceId); | ||
| expect(fetched.id).toBe(group.resourceId); | ||
| expect(fetched.name).toBe(group.displayName); | ||
| expect(fetched.officeId).toBe(group.officeId); | ||
| expect(fetched.active).toBe(false); | ||
| expect(fetched.status?.value).toBe('Pending'); | ||
| }); | ||
|
|
||
| test('honours name / submittedOnDate overrides', async ({ apiSetup, cleanupGuard }) => { | ||
| // Use a timestamp suffix so the override is still unique enough | ||
| // not to clash with a previous test run. | ||
| const overrideName = `OverrideGroup_${Date.now()}`; | ||
| const group = await createTestGroup(apiSetup, cleanupGuard, { | ||
| name: overrideName, | ||
| submittedOnDate: '15 March 2024' | ||
| }); | ||
| expect(group.displayName).toBe(overrideName); | ||
| const fetched = await apiSetup.api.getGroup(group.resourceId); | ||
| expect(fetched.timeline?.submittedOnDate).toEqual([ | ||
| 2024, | ||
| 3, | ||
| 15 | ||
| ]); | ||
| }); | ||
|
|
||
| test('queues a working deleter on the cleanup-guard', async ({ apiSetup, cleanupGuard }) => { | ||
| const group = await createTestGroup(apiSetup, cleanupGuard); | ||
| expect(cleanupGuard.size()).toBe(1); | ||
|
|
||
| const summary = await cleanupGuard.flush(); | ||
| expect(summary.ok).toBe(1); | ||
| expect(summary.failed).toEqual([]); | ||
|
|
||
| await expect(apiSetup.api.getGroup(group.resourceId)).rejects.toThrow(/404|not exist/i); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Override name can collide across parallel workers.
OverrideGroup_${Date.now()}relies solely on a millisecond timestamp, with no random component. Since group names are unique within an office in Fineract (seegroup.factory.tsLines 20-22), two workers executing this test in the same millisecond would collide and the secondcreateGroupwould fail, producing flaky runs. Add a random suffix to make the override deterministically unique.🔧 Proposed fix
As per path instructions: tests should have "minimal brittle timing dependencies".
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Path instructions