Factories
The (faker, overrides) factories of the training types, one per type, and the rules they follow across the monorepo.
Import from @workspace/training/factories. A factory builds a valid, random instance of a training type for tests, API mocks and dev scenarios. Every type has one factory, it lives in packages/training/src/factories/, one file per entity, and the barrel exports them all.
Convention
- Signature
(faker, overrides). TheFakerinstance is always a parameter, never the global one imported inside the factory: tests pass the sharedfaker, a scenario runner passes a seeded instance so a run replays identically. A factory that delegates to builders or other factories hands them the same instance.@faker-js/fakeris a type-only import in the factory files. - Defaults are random. A literal default is reserved for a field whose value is the type's neutral state (
isArchived: false,review: {}). A test pins throughoverridesonly the fields its assertions read. - The owning package owns the factory. Consumers import it, never redeclare it. DTO factories live in
@workspace/api-contract/factoriesand are built from these domain factories through the mappers. - Production code never imports
/factories. A Biome rule rejects@workspace/*/factoriesoutside tests,packages/api-mockand the app dev chunk./testingholds fakes and mocks only and does not re-export the factories, see Testing.
The repo-wide rules, the DTO factories of @workspace/api-contract, the @workspace/bridge/factories exemption and how to add a factory are in The /factories and /testing exports.
// A test pins only the fields its assertions read; everything else stays random.
export function archivedGroup(faker: Faker): Group {
return makeGroup(faker, { isArchived: true, name: "Monday beginners" });
}
export function userTheme(faker: Faker, ownerLabel: string): BoardTheme {
return makeBoardTheme(faker, {
type: "user_theme",
labels: { default: ownerLabel },
});
}
// Factories compose: a nested factory receives the same faker instance.
export function premiumMonthly(faker: Faker): PlanWithPrices {
return makePlanWithPrices(faker, { planKey: "premium", rank: 2 });
// one monthly price whose planId is the plan's id
}
export function premiumWithoutPrices(faker: Faker): PlanWithPrices {
return { ...makePlan(faker, { planKey: "premium" }), prices: [] };
}
export function editorRecord(faker: Faker, code: string) {
return makePersistableEditorBoardWrite(faker, { code });
}Catalogue
All factories take (faker: Faker, overrides: Partial<T> = {}) and return T, except where the table says otherwise.
Accounts
| Factory | Returns | Notable defaults |
|---|---|---|
makePerson | Person | type: "person", email derived from the random first and last name, displayName their join, country a country code, accountLanguage and appLanguage drawn from fr and en, auctionSystem from makeAuctionSystem, isActive: true, role: "user", avatarTrusted: false, handle: null, handleLocked: false, onboardedAt and createdAt in the past. |
makeIdentitySnapshot | CreatingIdentitySnapshot | A user_ identity id, lowercase email, random names, an avatar URL, a signup source from SIGNUP_SOURCES, updatedAt in the past; no role. |
makeOrganisation | Organisation | type: "organisation", a company name as displayName and organisationName, otherwise as makePerson. |
makePersonOwner | PersonOwner | Names taken from overrides when given, displayName their join or null, avatarUrl: null. |
makeOrganisationOwner | OrganisationOwner | name from overrides or a company name, displayName the same, avatarUrl: null. |
Boards and themes
| Factory | Returns | Notable defaults |
|---|---|---|
makeBoard(faker, options) | Board | options is Partial<Board> without game and metadata, plus hands (compact string, default two hands AKQ2.KQ3.A54.K32 / JT98.JT9.KQJ.JT9), auction, dealer and metadata: Partial<BoardMetadata>. The game comes from createBoardGame of @workspace/bridge/factories, mode show-contract, scoring unset, empty play. origin: "registered", partnership: "NS", ownership: { kind: "user" }, supportedLanguages: ["en"], metadata sharing: "private", level: 1. |
makeBoardDeal | BoardDeal | Random hands, auction S / 1S pass 2S AP, empty play, contract 2S by South, dealer South, IMP scoring, player: "S", review: {}. |
makeBoardSummary | BoardSummary | id: null, code from makeDealCode, name: "Test deal", random hands, auction and play null, scoring EXPLICIT_UNSET, vuln: "NONE", source: "app", fixed createdAt of 2024-01-01. |
makeBoardTheme | BoardTheme | Random type from BOARD_THEME_TYPE_VALUES, two-word fr and en labels, usageCount: 0. |
makeBookmark | Bookmark | Random userId and boardId, createdAt in the past. |
makeDealCode(faker) | string | A random deal code of DEAL_CODE_LENGTH characters from the production SHORT_CODE_ALPHABET. No overrides: pin it in the consuming factory's overrides when the test asserts on it. |
makeAuctionSystem | AuctionSystem | identifier drawn from natural, strong_club, precision, sayc; sortOrder 1 to 10. |
makePersistableBoardWrite | PersistableBoardWrite | code: "TEST1234", userId: "user-uuid-1", random cards with derived cardCounts, contract: "3NT" by South with result: "9", level: 3, gatedAt fixed to 2026-01-01, gatedVersion: 1. |
makePersistableEditorBoardWrite | PersistableEditorBoardWrite | code: "EDIT1234", random cards, name: "Editor Board", no auction, play or contract. |
makeDealView | AppDealView | userId: null, random cards, name: "Test deal", scoring UNSET, vuln: "NONE", declarer and dealer South, source: "app", review: [], supportedLanguages: []. |
makeEditorDealView | EditorDealView | As makeDealView with name: "Editor deal" and source: "editor". |
Groups, students, sessions
| Factory | Returns | Notable defaults |
|---|---|---|
makeGroup | Group | Two-word name, color from a fixed palette of eight, startDate in the past as YYYY-MM-DD, endDate: null, isWeekly: true, isArchived: false, defaultSessionTime: "14:00:00", owner: null, random studentCount and sessionCount, nextSession and nextExpectedDate null. |
makeSessionPreview | SessionPreview | name as Session n, future date, boardCount 0 to 12. |
makeStudent | Student | Integer id, random names and email, phone present with probability 0.4, isArchived: false. |
makeStudentWithGroupCount | StudentWithGroupCount | makeStudent plus groupCount 0 to 5. |
makeCreatedGroupSession | CreatedGroupSession | status: "created"; overrides exclude status and accept visibility: Partial<SessionVisibility>. Random code of 8 characters, occurrenceKey built from groupId and the date, isWeekly true with probability 0.7, empty boards and linkedCollections, visibility disabled. |
makeRawCreatedGroupSession | RawCreatedGroupSession | Same as above with boardIds instead of boards. |
makeRawLinkedCollection | RawLinkedCollection | boardCount falls back to boardIds.length, position: 0. |
makeGroupSessionSummary | GroupSessionSummary | Accepts visibility: Partial<SessionVisibility>; visibility disabled by default. |
makeGroupSessionPreview | GroupSessionPreview | Future date, random group name and color, boardCount 0 to 12. |
makePublicSessionData | PublicSessionData | Code of SESSION_CODE_LENGTH characters from SHORT_CODE_ALPHABET, random authorName, empty boards, content from makePublicContent. |
makeExpectedGroupSession | ExpectedGroupSession | status: "expected", occurrenceKey from groupId and date (both honoured from overrides). |
makePublicLink | PublicLink | Code of 8 characters, visibility disabled, content from makePublicContent. |
makePublicContent(faker, overrides?: PublicContent) | PublicContent | Returns overrides as is when given. Otherwise one greeting paragraph followed by the boardsList and downloadSection magic-var blocks. |
makeSessionVisibility(overrides?) | SessionVisibility | Takes no Faker: enabled: false, availableFrom and availableTo null. |
Collections and sharing
| Factory | Returns | Notable defaults |
|---|---|---|
makeDealCollectionSummary | DealCollectionSummary | Three-word name, isArchived: false, alphanumeric code of 8, boardCount 0 to 24, visibility disabled. |
makeSharingGroup | SharingGroup | Three-word name, description: null. |
makeSharingGroupMembership | SharingGroupMembership | Owner display name and email derived from the same random names, ownerAvatarUrl: null. |
Plans, subscriptions, entitlements
| Factory | Returns | Notable defaults |
|---|---|---|
makePlanGrid | PlanGrid | gridKey a slug with a random suffix, identical en and fr labels, a bpc_ portal configuration id. |
makePlan | Plan | planKey slugified from a product name, rank 1 to 9, preset diffusions_per_period: 350 and feature custom_branding, providerProductId: null, status: "active", purchasable: true. |
makeNewPlan | NewPlan | What the admin writes to create a tier; the key carries a random suffix so parallel tests never collide. Random diffusions_per_period, rank 1 to 90. |
makeNewPlanPrice | NewPlanPrice | Monthly, intervalCount: 1, priceCents 100 to 500 000, currency: "EUR", providerPriceId: null, trialDays: null. |
makePlanPrice | PlanPrice | Monthly, priceCents 100 to 10 000, a price_ provider id. |
makePlanWithPrices | PlanWithPrices | makePlan with one monthly price bound to the plan's id, unless overrides.prices is given. |
makeSubscription | Subscription | status: "active", cus_ and sub_ provider ids, billing window from three days ago to twenty-seven days ahead, cancelAtPeriodEnd: false. |
makeNewEntitlementGrant | NewEntitlementGrant | An admin grant raising groups_per_user_max to 21 to 500, empty features, open since recently and closing soon, grantedBy and note null. |
makeEntitlementGrant | EntitlementGrant | makeNewEntitlementGrant plus id, revokedAt: null, source: "admin", subscriptionId: null, quotaPeriodStart: null. |
Ads and announcements
| Factory | Returns | Notable defaults |
|---|---|---|
makeCreative | AdsCreative | Vertical 160 by 600 with a random image URL. |
makeCampaignVisual | NewCampaignVisual | A narrow rail visual without click destination nor alt text. |
makeSponsorship | AdsSponsorship | An open-ended campaign active since the past, matching every deal, without budget: precedence: "under_plan", priority: 0, servesBudget: null, validUntil: null, DEFAULT_ADS_SPONSORSHIP_RULES. |
makeCampaign | AdsCampaign | makeSponsorship plus delivered: 0 and one narrow-rail BrandAsset owned by the campaign, unless assets is given. |
makeBrandAsset | BrandAsset | An active account-owned asset whose dimensions are exactly those of its slot (rail-160 by default); storageKey from brandAssetStorageKey, sourceStorageKey, clickUrl and alt null. slot and owner are read from overrides before the rest. |
makeAnnouncement | Announcement | A recently published news item without action nor expiry, pinned: false, fr and en title and body. |
makeAnnouncementForAccount | AnnouncementForAccount | makeAnnouncement plus readAt: null. |
Emails
| Factory | Returns | Notable defaults |
|---|---|---|
makeTestEmailMessage | TestEmailMessage | A faker recipient, locale fr or en, environment local or prod. |
makeNewOutboundEmail | NewOutboundEmail | A test message, an idempotency key prefixed test:, no sender account. |
makeEmailSuppression | EmailSuppression | A lower-cased faker address, reason bounced or complained, an email id. |
makeOutboundEmail | OutboundEmail | The same plus an id, status: "queued", attempts: 0, externalId, lastError and sentAt null, updatedAt equal to createdAt. |
Events
| Factory | Returns | Notable defaults |
|---|---|---|
makeIngestedEvent | IngestedEvent | Port-shape event, what a verifier returns before persistence. Neutral provider: "test-provider" and eventType: "test.event" so a domain test never encodes a provider; externalId prefixed evt_, payload: { foo: "bar" }. |
makePersistedIngestedEvent | PersistedIngestedEvent | Entity-shape event after repo.create: same defaults plus id, status: "pending", attempts: 0, lastError and processedAt null, updatedAt equal to createdAt. |
Usage analytics
Serves, daily play rollups, the rollup boundary between rolled-up days and live telemetry, perceived difficulty, and the usage stats and analytics read by the teacher app.
Testing
The fakes and mocks of @workspace/training/testing, what each one implements and its knobs, and how the package's own tests are laid out.