---
name: catalyst-backend-testing
description: >
  Starting-point conventions for writing tests in the Aurora Catalyst backend. Covers unit tests for
  handlers (write + read archetypes), the catalyst-specific decorator-fires-in-TestingModule gotcha
  (`@ApplySchema` / `@Format` / `@Sanitize` / `@EmitEvent` wrap `descriptor.value` and execute on
  every `handler.main()` call including inside `TestingModule`), dual-context guard tests that fake
  both REST and GraphQL `ExecutionContext`, e2e tests with `supertest` against REST and GraphQL, and
  the `dPermissions.all` seed pattern required for any `@Auth`-protected endpoint test. Trigger:
  writing any `.spec.ts` or `.e2e-spec.ts` in `backend/`; setting up `TestingModule`; faking
  `HandlerMeta` with `timezone` + `repositoryOptions.auditing`; mocking `EventEmitter2` because the
  handler uses `@EmitEvent`; composing a supertest request against a protected endpoint;
  understanding why `expect(result.password).toBeDefined()` fails on a read handler. Fires even when
  the user says only "write a test for this handler". Does NOT apply to testing upstream
  `@aurorajs.dev/core-back` decorators themselves (upstream concern) or to testing
  `catalyst-cli`-generated scaffolds (generator concern). At the time of authoring, catalyst has
  ZERO production tests for its modules — this skill establishes the first conventions via
  synthesized templates anchored on real handler shapes.
license: MIT
metadata:
  author: aurora
  version: '1.0'
---

## ⚠ Starting Point — Read This First

**Catalyst has no production test code for its modules today.** An inventory at authoring time
(2026-04-23) found exactly two `.spec.ts` files in the backend, and both are NestJS CLI starters not
specific to catalyst:

- `backend/src/app.controller.spec.ts` — asserts `"Hello World!"`.
- `backend/test/app.e2e-spec.ts` — same starter shape.

The CLI has a `--tests` flag on `catalyst load back module`, but the flag is a silent no-op today:
`generateTestingFiles()` in `aurora-catalyst-cli/src/generator/handlers/back.handler.ts` checks for
a templates directory that does not exist and returns without emitting anything.

**This skill therefore establishes — not documents — catalyst's first testing conventions.** Every
code example below is **synthesized** against real handler shapes (`IamCreateTagHandler`,
`IamFindTagByIdHandler`, the same anchors `catalyst-handler-composer` uses). Every synthesized
snippet opens with `// SYNTHESIZED — reconcile when the first real test lands` to remind the reader
that it is a starting shape, not a canonical pattern extracted from production code. When real tests
land, this skill's examples should be reconciled against them.

The single catalyst-specific insight that justifies shipping this skill now — before real tests
exist — is the **decorator-fires-in-TestingModule gotcha** documented below. Nothing else in the
ecosystem teaches it.

## Step 0 — Testing Conventions Gate

Unlike the CLI-first gate in `catalyst-handler-composer` / `catalyst-field-schema` or the
reuse-first gate in `catalyst-nestjs-primitives`, this gate is an **establishing** gate: testing
conventions for catalyst are being set here, not described from prior practice. Most tasks today
fall in branch 3 or 4.

```
About to write a test?
  │
  ├── Does a similar `*.spec.ts` already exist on disk?  ───→ COPY its shape
  │   (Today: only AppController + app.e2e-spec starters
  │    exist. This branch activates once real tests land.)
  │
  ├── Does the CLI `--tests` flag scaffold one?          ───→ Use the scaffold
  │   (Today: the flag is a no-op. This branch activates
  │    when test templates land in the CLI.)
  │
  ├── Is this a pattern covered by this skill's         ───→ Follow the template
  │   templates below?                                       (write handler / read handler /
  │                                                           guard / e2e / seed pattern)
  │
  └── None of the above?                                 ───→ STOP and coordinate.
       (You're establishing a new convention.              You are in "establishing a
        Reach out before committing.)                        new convention" territory.
```

## Decorators Fire in TestingModule (Catalyst-specific)

This is the single most important section of this skill. It is the insight that makes the skill
worth shipping before real tests exist.

`@ApplySchema`, `@Format`, `@Sanitize`, and `@EmitEvent` from `@aurorajs.dev/core-back` are
`MethodDecorator`s that wrap `descriptor.value`. Verified in
`@aurorajs.dev/core-back/dist/modules/type-handlers/decorators/apply-schema.decorator.js`:

```typescript
function ApplySchema(schemaOrOptions) {
  const { schema, payloadIndex } = Array.isArray(schemaOrOptions)
    ? { schema: schemaOrOptions, payloadIndex: 0 }
    : { schema: schemaOrOptions.schema, payloadIndex: schemaOrOptions.payloadIndex ?? 0 };
  return applyDecorators(Format(schema), Sanitize({ schema, payloadIndex }));
}
```

`applyDecorators` composes `@Format` + `@Sanitize`. Both wrap the method descriptor — they run on
**every** `handler.main(...)` invocation, including inside `TestingModule`. This has three concrete
consequences for tests:

1. **Sanitize runs on the payload first.** If the entity has a `type: 'password'` field, the
   `PasswordTypeHandler.sanitize` bcrypt-hashes the plaintext before `main()` begins. A test that
   does `handler.main({ password: 'hunter2' }, meta)` and asserts the service mock received
   `{ password: 'hunter2' }` will **fail** — the service receives the bcrypt hash.

2. **`@EmitEvent` requires `EventEmitter2` in providers.** The decorator reads `this.eventEmitter`
   and calls `emit(...)` after `main()` resolves. If `TestingModule` does not provide an
   `EventEmitter2` (even a mock one), the emit either throws or silently leaks to a real event bus
   depending on the environment.

3. **Format masks return values.** `@Format(FieldSchema)` runs on the handler's return value and
   strips fields the schema marks as masked (password returns `undefined`, auditing columns may be
   normalized). A test asserting `expect(result.password).toBeDefined()` on a read-handler return
   will **fail** — the format decorator already masked it.

Every template below accounts for these behaviors. If you stray from the templates, re-read this
section before writing the assertions.

## Unit Test — Write Handler (anchored on `IamCreateTagHandler`)

```typescript
// SYNTHESIZED — no real test exists for IamCreateTagHandler today.
// Reconcile when the first real handler test lands in catalyst.
import { NotFoundException } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Test } from '@nestjs/testing';

import { IamCreateTagHandler, IamCreateTagService, IamFindTagByIdService } from '@app/iam/tag';

describe('IamCreateTagHandler', () => {
  let handler: IamCreateTagHandler;
  const createService = { main: jest.fn() };
  const findByIdService = { main: jest.fn() };
  const eventEmitter = { emit: jest.fn() }; // MANDATORY — @EmitEvent reads this.eventEmitter

  beforeEach(async () => {
    jest.clearAllMocks();
    const moduleRef = await Test.createTestingModule({
      providers: [
        IamCreateTagHandler,
        { provide: EventEmitter2, useValue: eventEmitter },
        { provide: IamCreateTagService, useValue: createService },
        { provide: IamFindTagByIdService, useValue: findByIdService },
      ],
    }).compile();

    handler = moduleRef.get(IamCreateTagHandler);
  });

  it('persists, emits iam.tag.created, and returns the hydrated entity', async () => {
    const payload = { id: 'uuid-1', name: 'feature' };
    const handlerMeta = {
      timezone: 'UTC',
      repositoryOptions: { auditing: null },
    };
    const hydrated = {
      id: 'uuid-1',
      name: 'feature',
      createdAt: '2026-04-23T00:00:00.000Z',
      updatedAt: null,
      deletedAt: null,
    };
    findByIdService.main.mockResolvedValue(hydrated);

    const result = await handler.main(payload, handlerMeta);

    expect(createService.main).toHaveBeenCalledWith(payload, handlerMeta);
    expect(findByIdService.main).toHaveBeenCalledWith('uuid-1', {}, handlerMeta);
    expect(eventEmitter.emit).toHaveBeenCalledWith('iam.tag.created', expect.anything());
    expect(result).toEqual(hydrated);
  });
});
```

**Note — password caveat.** `IamTag` has no `type: 'password'` field, so the assertion
`expect(createService.main).toHaveBeenCalledWith(payload, handlerMeta)` works as written. If you
port this template to a handler whose `FieldSchema` declares `type: 'password'` (for example an
`IamCreateUserHandler`), `@Sanitize` runs `bcrypt.hashSync` on the payload before the service call.
The service mock will receive a hash, not the plaintext — adjust the assertion to one of:

```typescript
// Option A — assert the shape + hash presence, not exact value:
expect(createService.main).toHaveBeenCalledWith(
  expect.objectContaining({ id: payload.id, password: expect.any(String) }),
  handlerMeta,
);

// Option B — stub bcrypt.hashSync to return a deterministic value:
jest.spyOn(bcrypt, 'hashSync').mockReturnValue('$2b$10$fixed-hash');
```

## Unit Test — Read Handler (anchored on `IamFindTagByIdHandler`)

```typescript
// SYNTHESIZED — no real test exists for IamFindTagByIdHandler today.
// Reconcile when the first real read-handler test lands.
import { NotFoundException } from '@nestjs/common';
import { Test } from '@nestjs/testing';

import { IamFindTagByIdHandler, IamFindTagByIdService } from '@app/iam/tag';

describe('IamFindTagByIdHandler', () => {
  let handler: IamFindTagByIdHandler;
  const findByIdService = { main: jest.fn() };

  beforeEach(async () => {
    jest.clearAllMocks();
    const moduleRef = await Test.createTestingModule({
      providers: [
        IamFindTagByIdHandler,
        { provide: IamFindTagByIdService, useValue: findByIdService },
      ],
    }).compile();

    handler = moduleRef.get(IamFindTagByIdHandler);
  });

  it('returns the tag when found', async () => {
    const tag = { id: 'uuid-1', name: 'feature' };
    findByIdService.main.mockResolvedValue(tag);

    const result = await handler.main('uuid-1', undefined, undefined);

    expect(result).toMatchObject({ id: 'uuid-1', name: 'feature' });
  });

  it('throws NotFoundException when the tag is missing', async () => {
    findByIdService.main.mockResolvedValue(null);

    await expect(handler.main('missing', undefined, undefined)).rejects.toThrow(NotFoundException);
  });
});
```

**Note — format caveat.** If the read handler's `FieldSchema` masks a field (e.g., a User entity's
`password`), the returned entity has `password === undefined`. A test asserting
`expect(result.password).toBeDefined()` would fail. Assert only on fields the schema exposes.

## Unit Test — Guard (dual-context)

Catalyst guards override `getRequest(context)` via `Api.getRequestFromExecutionContext(context)` so
they work on both REST (HTTP) and GraphQL contexts. See `catalyst-nestjs-primitives` for the full
pattern. Tests must fake both context types to exercise the override.

```typescript
// SYNTHESIZED — pattern anchored on AuthorizationPermissionsGuard.
// Reconcile when the first real guard test lands.
import { ExecutionContext } from '@nestjs/common';

const fakeHttpContext = {
  getType: () => 'http',
  switchToHttp: () => ({
    getRequest: () => ({
      user: { dPermissions: { all: ['iam.tag.create'] } },
    }),
  }),
  getClass: () => ({ name: 'IamCreateTagResolver' }),
} as unknown as ExecutionContext;

// For the GraphQL branch, replace getType with () => 'graphql'
// and stub GqlExecutionContext.create(context).getContext().req similarly.
```

`Api.getRequestFromExecutionContext()` inspects `context.getType()`; your fake must return `'http'`
or `'graphql'` to drive the branch you intend to test. A thorough guard test exercises both branches
to confirm the override works for both API styles.

## E2E Tests — supertest (REST + GraphQL)

Both snippets assume an already-bootstrapped `INestApplication` set up in `beforeAll`. The
authenticated account MUST hold the permission the endpoint's `@Auth(...)` decorator requires; see
the seeds section below for how to prepare it.

**REST variant:**

```typescript
// SYNTHESIZED — pattern for a REST endpoint protected by @Auth('iam.tag.create').
import * as request from 'supertest';

it('POST /iam/tag — creates a tag when the account has iam.tag.create', async () => {
  await request(app.getHttpServer())
    .post('/iam/tag')
    .set('Authorization', `Bearer ${authenticatedToken}`)
    .send({ id: 'uuid-1', name: 'feature' })
    .expect(201);
});
```

**GraphQL variant:**

```typescript
// SYNTHESIZED — pattern for the same operation exposed via GraphQL mutation.
it('POST /graphql — mutates iamCreateTag when the account has iam.tag.create', async () => {
  await request(app.getHttpServer())
    .post('/graphql')
    .set('Authorization', `Bearer ${authenticatedToken}`)
    .send({
      query: `mutation ($payload: IamCreateTagInput!) {
        iamCreateTag(payload: $payload) { id name }
      }`,
      variables: { payload: { id: 'uuid-1', name: 'feature' } },
    })
    .expect(200)
    .expect((res) => expect(res.body.data.iamCreateTag.id).toBe('uuid-1'));
});
```

Both variants rely on the same authenticated account + seeded permissions. The only real difference
is the request shape.

## Seeds as Testing Utilities

The ONLY concrete testing utility catalyst has today are the seed files:

- `backend/src/@app/iam/iam.seed.ts` — exports `boundedContexts[]`, `permissions[]`, `roles[]`.
- `backend/src/@app/library/library.seed.ts` — same pattern for the library bounded context.
- `backend/src/@app/o-auth/o-auth.seed.ts` — same pattern for o-auth.

For any e2e test of an `@Auth('<permission>')`-protected endpoint, the authenticated account MUST
hold that permission in its `dPermissions.all`. The `AuthorizationPermissionsGuard` reads
`user.dPermissions.all` and compares via `Arr.intersects` — no permission match means the guard
throws `UnauthorizedException` with `statusCode: 403`. See `catalyst-nestjs-primitives` for the
convention.

**Synthesized e2e setup snippet:**

```typescript
// SYNTHESIZED — pattern for seeding permissions + an authenticated account.
import { permissions as iamPermissions } from '@app/iam/iam.seed';

beforeAll(async () => {
  // Load permissions from the seed into the test database.
  await permissionRepository.bulkInsert(iamPermissions);

  // Create an account whose dPermissions.all includes the target permission(s).
  const account = await accountRepository.create({
    id: 'test-account-uuid',
    username: 'tester',
    dPermissions: { all: ['iam.tag.create', 'iam.tag.get'] },
    // … other required fields per IamAccountFieldSchema
  });

  // Issue a JWT for that account and store it for requests.
  authenticatedToken = jwtService.sign({ sub: account.id });
});
```

The `dPermissions.all` array is the critical field. Hardcoding a subset in the test fixture drifts
from the real seed as permissions evolve — prefer importing the seed's `permissions` array and
pulling the ids you need, rather than hand-writing strings.

## Anti-patterns

1. **`handler.execute(...)`.** → Catalyst handlers have `main()`, not `execute()`. Aurora-old CQRS
   `ICommandHandler.execute(command)` is obsolete in catalyst. Call
   `handler.main(payload, handlerMeta)` directly.

2. **Testing `@ApplySchema` / `@Format` / `@Sanitize` / `@EmitEvent` decorator internals.** → Those
   live in `@aurorajs.dev/core-back` and are maintained upstream. Your catalyst test consumes their
   behavior; it does not test them. If you think you need to test the decorator itself, the bug
   belongs upstream.

3. **Writing a handler test without `EventEmitter2` in `providers`.** → Every write handler with
   `@EmitEvent` fails silently or errors because `this.eventEmitter.emit(...)` has no target. Always
   add `{ provide: EventEmitter2, useValue: { emit: jest.fn() } }` to the providers list.

4. **Asserting against plaintext passwords after `@ApplySchema`.** → `@Sanitize` bcrypt- hashed the
   payload before `main()` ran. Either stub `bcrypt.hashSync` or assert on the hashed form (use
   `expect.any(String)` or a deterministic stub value).

5. **Asserting `result.password` on a read handler.** → `@Format` returned `undefined` for fields
   the schema masks. Assert only on fields the schema exposes; use `catalyst-field-schema`'s field
   catalog to know what's masked.

6. **Hardcoding `dPermissions.all` in test fixtures instead of using the seeds.** → The real seed
   evolves; hardcoded fixtures drift. Import the seed's `permissions` array and compute
   `dPermissions.all` from it, so adding a new permission to `iam.seed.ts` automatically updates
   every test that reads it.

## Cross-references

- **`catalyst-handler-composer`** — handler shape under test. The templates here anchor on
  `IamCreateTagHandler` and `IamFindTagByIdHandler`, the same examples composer uses. When writing a
  test for a different handler archetype, read composer's archetype reference first.
- **`catalyst-field-schema`** — what the decorators do. Understanding why a password is hashed or
  why a field is masked on return requires the decorator + type-handler catalog from field-schema's
  reference.
- **`catalyst-nestjs-primitives`** — the dual-context pattern. Testing a catalyst guard means faking
  an `ExecutionContext` for both REST and GraphQL; the primitive itself is documented in
  nestjs-primitives, and this skill's guard section points back to it.
- **`catalyst-project-structure`** — where test files live on disk (co-located `.spec.ts` next to
  the handler they test, or under `test/` for e2e). The structure skill is the source of truth; this
  skill assumes it.
