KwadMarket Docs
Remediation Plan

Testing

Backend test harness — implementation spec for plan phase 2

Feeds plan phase 2. Phase 0's uuid-crash fix has landed (crypto.randomUUID(), dep dropped); the Vitest migration below is still the durable fix (Jest itself stays a liability for other ESM-only deps). The existing test/app.e2e-spec.ts stub doesn't compile — replace it with the infra below.

1. Harness decisions

  • Runner: migrate apps/back from Jest to Vitest (apps/scraper already uses it; one mock API across the repo). Mechanical: vitest.config.ts mirroring the scraper's, jest.fnvi.fn. Do this before writing the new tests, not after.
  • E2E database: real Postgres via testcontainers (pnpm --filter back add -D @testcontainers/postgresql supertest). No mocked Prisma in e2e — the authorization bugs this suite must catch live in queries.
  • Unit tests: mock PrismaService (possible once Data access §1 lands) with plain vi.fn() objects, or prefer pure functions that need no mocks (Backend quality §6 extractions).

2. E2E infrastructure

apps/back/test/setup-e2e.ts
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import { execSync } from 'node:child_process';

let container: StartedPostgreSqlContainer;

export async function startTestDb(): Promise<string> {
  container = await new PostgreSqlContainer('postgres:16').start();
  const url = container.getConnectionUri();
  execSync('pnpm --filter @marketplace/database exec prisma migrate deploy', {
    env: { ...process.env, DATABASE_URL: url },
  });
  return url;
}
export async function stopTestDb() { await container?.stop(); }
apps/back/test/app.factory.ts — boots the real AppModule against the test DB
import { Test } from '@nestjs/testing';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from '../src/app.module.js';

export async function createTestApp() {
  const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile();
  const app = moduleRef.createNestApplication();
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }));
  app.setGlobalPrefix('api');
  await app.init();
  return app;
}
apps/back/test/auth.helper.ts
import request from 'supertest';
export async function registerAndLogin(app: INestApplication, email: string) {
  const res = await request(app.getHttpServer())
    .post('/api/auth/register')
    .send({ name: 'Test', email, password: 'password123' });
  return { token: res.body.accessToken as string, user: res.body.user };
}
export const auth = (token: string) => ({ Authorization: `Bearer ${token}` });

Redis / Meilisearch

For the e2e scope below, neither is exercised — provide fake providers for SearchService and the BullMQ queue in the testing module override (.overrideProvider(...)) so AppModule boots without them, OR run them as additional testcontainers later. Start with overrides (simpler).

3. The first test files (in order — these lock in the security fixes)

test/deals-authorization.e2e-spec.ts — THE regression suite for the IDOR fixes
describe('Deals authorization', () => {
  it('owner can update own draft', ...)                          // 200
  it('other user cannot PATCH someone else’s deal', ...)         // expect 403
  it('other user cannot publish someone else’s deal', ...)       // expect 403
  it('other user cannot add/delete images on someone else’s deal', ...) // 403
  it('admin can update any deal', ...)                           // 200
  it('publish only allowed from DRAFT or DECLINED', ...)         // 400 from PUBLISHED
  it('unauthenticated PATCH is rejected', ...)                   // 401
})
test/discussions-authorization.e2e-spec.ts
it('participant can read discussion', ...)               // 200
it('non-participant gets 403 on GET /discussions/:id', ...)
it('non-participant gets 403 on GET /messages/:discussionId', ...)
it('seller cannot start a discussion on own deal', ...)  // 400
test/auth.e2e-spec.ts
it('register → login → /auth/me round-trip', ...)
it('login with wrong password → 401 with generic message', ...)
it('register with existing email → 400', ...)
it('throttler: 6th login attempt within a minute → 429', ...)
it('/auth/me response contains no password field', ...)  // locks in Security §2
it('password reset token is single-use and expires', ...)
it('reset request for unknown email still returns success', ...) // anti-enumeration
test/deals-public.e2e-spec.ts — contract guard for the frontend
it('GET /deals returns { data, meta } with clamped limit (limit=10000 → 50 items max)', ...)
it('GET /deals/:id for missing id → 404', ...)

4. Unit tests for the extracted pure logic

After Backend quality §6:

  • products.query-builder.spec.tsbuildProductWhere covers: name search, category vs categoryIds, specIds AND-semantics, price range, status/includeDrafts combinations.
  • import-parser.spec.tsparseImportPayload: valid payload, missing fields, malformed JSON → typed errors.
  • pagination.spec.tspageArgs clamps limit to 50, floors page to 1; buildMeta math.
  • deals.mapper.spec.tsmapDeal with full relations and with nulls (no brand, no price).

5. Conventions

  • E2E files: apps/back/test/*.e2e-spec.ts; unit specs colocated *.spec.ts next to the source (matches the existing scraper specs).
  • Each e2e file creates its own users with unique emails; DB is shared per run — tests must not assume an empty DB, only their own fixtures (or use beforeEach transactions if flakiness appears).
  • No snapshot tests on API responses (churn); assert specific fields.
  • CI: turbo run test picks both apps up once apps/back has a working test script (CI already runs turbo run test).

On this page