Skip to main content

Testing

Moonlight uses Jest for both unit and E2E testing.

Test Database

A separate PostgreSQL instance runs on port 9433 for tests. It uses the same Flyway migrations as the main database.

# Start test database and Pub/Sub/storage dependencies
docker-compose up moon_test_db flyway-test pubsub minio minio-init

Running Tests

Unit Tests

cd moonlight-server
npm run test # Run once
npm run test:watch # Watch mode
npm run test:cov # With coverage

E2E Tests

cd moonlight-server
npm run test:e2e

E2E tests require infrastructure to be running (test database, Pub/Sub emulator, MinIO).

createTestApp() sets PUBSUB_EMULATOR_HOST (default localhost:8085) and uses project moonlight-test. Bootstrap automatically creates the required topics and subscriptions in the emulator on every app.init(). If the Pub/Sub emulator is not running, E2E suites will fail to start because messaging providers cannot connect to it.

The messaging.e2e-spec.ts suite covers:

  • IdempotencyService exactly-once semantics and TTL cleanup
  • Round-trip publish/subscribe through the emulator using PubSubPublisher

E2E Test Structure

moonlight-server/test/
├── helpers/
│ ├── test-app.ts # Test application setup
│ └── test-data.ts # Factories and cleanup
├── entry-lifecycle.e2e-spec.ts
├── document-workflow.e2e-spec.ts
├── void-refund-reimbursement.e2e-spec.ts
└── accounting.e2e-spec.ts

Test Suites

Entry Lifecycle

  • Create entries (INCOME/EXPENSE)
  • List and filter entries
  • Update entry status
  • Verify invalid status transitions are rejected
  • Verify audit trail
  • Check completeness tracking

Document Workflow

  • Upload BASIS and PAYMENT_PROOF documents
  • Accountant requests documents
  • Fulfill document requests
  • Approve/reject documents
  • Verify completeness after approval

Void, Refund & Reimbursement

  • Void an entry
  • Create full and partial refunds
  • Verify original entry is voided only after a full refund
  • Verify partial refunds do not void the original entry
  • Reject refunds larger than the original amount
  • Create employee reimbursements
  • Reject reimbursements larger than the original expense amount
  • Track related entries
  • Verify audit events

Accounting Connections

  • Create/list/update accounting connections
  • Deactivate connections
  • Sync status tracking
  • Webhook handling

CI Expectations

The backend pull request pipeline is expected to run:

  • npm run lint
  • npm run test
  • npm run test:e2e
  • npm run build

Test Helpers

createTestApp()

Creates a full NestJS application configured for the test database.

cleanDatabase(ds)

Truncates all tables for a clean test state.

seedBaseData(ds)

Creates minimal required data: workspace, project, parties, entry source.

createTestUser(ds, jwtService, options?)

Creates a user with owner permissions and returns a JWT token.

createTestEntry(ds, overrides?)

Creates an entry with optional field overrides.

Writing New Tests

import { TestContext, createTestApp, closeTestApp } from './helpers/test-app';
import { cleanDatabase, seedBaseData, createTestUser } from './helpers/test-data';

describe('My Feature (e2e)', () => {
let ctx: TestContext;
let userToken: string;

beforeAll(async () => {
ctx = await createTestApp();
await cleanDatabase(ctx.dataSource);
await seedBaseData(ctx.dataSource);
const user = await createTestUser(ctx.dataSource, ctx.jwtService);
userToken = user.token;
});

afterAll(async () => {
await cleanDatabase(ctx.dataSource);
await closeTestApp(ctx);
});

it('should do something', async () => {
const res = await request(ctx.app.getHttpServer())
.get('/my-endpoint')
.set('Authorization', `Bearer ${userToken}`)
.expect(200);

expect(res.body).toMatchObject({ ... });
});
});