Skip to main content

Testing Guide

Bilko —Drop Testing Guide

Status: Active but partially stale — implementation moved to Kotlin/Ktor API and current Playwright partitioning is in progress Version: 2.1 Last Updated:updated: 2026-05-2103-03 Author:Source: ALAI Documentation Team

Canonical testing policy: see TEST-STRATEGY.mdsrc/drop-app/vitest.config.ts, E2E-TEST-PLAN.mdplaywright.config.ts, andpackage.json, DEMO-TESTING-PLAN.md. Older sections in this guide may still mention Express/Prisma/API Vitest inventory and should not override the current Ktor + Playwright policy.tests/


Table of Contents

  1. Testing Philosophy
  2. Testing Pyramid
  3. Tech Stack
  4. Actual Test Files
  5. Running Tests
  6. Test Configuration
  7. Test Setup & Mocking
  8. CI Integration
  9. Writing New Tests
  10. Coverage Reporting
  11. Testing Best Practices
  12. Debugging Tests

1. Testing PhilosophyFrameworks

Financial software has a higher correctness bar than typical web apps. Bilko's testing strategy prioritizes:

  1. Financial Logic Accuracy — VAT calculations, double-entry bookkeeping, currency conversion
  2. Data Integrity — No lost transactions, no balance discrepancies
  3. Regression Prevention — Once fixed, bugs stay fixed
  4. Fast Feedback — Prefer fast unit tests and targeted integration/contract tests; use real PostgreSQL/Testcontainers where behavior depends on the database

2. Testing Pyramid

         /\
        /E2E\        ← 10% (Critical user flows only)
       /------\
      /  Integ \     ← 20% (API endpoints, mocked DB)
     /----------\
    /    Unit    \   ← 70% (Business logic, financial engine)
   /--------------\

Distribution:

  • 70% Unit Tests — Fast, isolated, test business logic in @bilko/core
  • 20% Integration Tests — Test API endpoints with mocked Prisma client
  • 10% E2E Tests — Full API integration test (single file in tests/e2e/)

Coverage Targets by Module

ModuleFramework UnitVersion IntegrationType ReasonConfig
@bilko/core accounting engineVitest 95%^4.0.18 N/AUnit, Integration, Performance, Regression Double-entry errors = financial lossvitest.config.ts
@bilko/core tax/VAT calculationsPlaywright 95%^1.58.2 N/AE2E (browser) Tax miscalculations = regulatory penalty
@bilko/coreplaywright.config.ts multi-currency90%N/AFX errors = revenue leakage
Auth API85%90%Security boundary
Invoice API80%90%Core revenue feature
Expense API80%85%Core cost tracking
Reports API75%80%Regulatory output
Banking API75%75%Complex matching logic
Multi-tenant isolationN/A100%GDPR + security critical

3. Tech StackPrerequisites

Docker

is required.runagainstarealPostgreSQL
TestTests Type Framework Purpose
UnitVitest@bilko/core business logic (financial engine)
IntegrationVitest + SupertestAPI endpoint testing with mocked Prisma
E2EVitest + SupertestFull API integration (tests/e2e/api.test.ts)

Why Vitest (not Jest)

  • Native ESM support, Vite-based — faster than Jest
  • Compatible with Turborepo workspace structure
  • Watch mode with HMR
  • Same API as Jest (easy migration)

Why Supertest (not Postman)

  • Programmatic API testing within Vitest
  • Works with Express app16 instance directly
  • (port
  • No5433). runningThere server required

4. Actual Test Files

apps/api/tests/ — Mock Suite (11 test files)

Tests with mocked Prisma — fast,is no databasein-memory required.SQLite fallback.

# 
One-time setup (first time fresh clone) make db-test-setup #
FileTestsWhator Itafter Covers
This: starts Docker, creates drop_test DB, enables pgcrypto, pushes Drizzle schema

The DATABASE_URL for tests is set automatically by tests/setup.ts

Shared test setup: Prisma mock, JWT helpers, test data factories
auth.test.ts11Register, login, refresh token, logout, GET /me — auth flows with mocked DB
invoices.test.ts11List, create, get, update, status change (send/pay), delete invoice endpoints
expenses.test.ts9List, create, get, update, approve/reject, delete expense endpoints
contacts.test.ts9List, create, get, update, delete contact endpoints
accounts.test.ts4List, get, create, delete chart-of-accounts endpoints
banking.test.ts10Bank account management, transaction import, reconciliation endpoints
reports.test.ts9P&L, balance sheet, VAT report, trial balance endpoints
transactions.test.ts9Transaction ledger list, filter, and detail endpoints
country.test.ts27Country plugin integration — tax rates for RS/BA/HR, invoice number formats
chatbot.test.ts9Chatbot message, history, clear history — rate limit (429) test
invoice-gl-reversal.test.ts6Invoice cancellation GL reversal — double-entry stays balanced
new-endpoints.test.ts~10Receipt, VAT export PDF/XML, dashboard, security audit log, data export

Total: ~120+ mock suite test cases:

apps/api/tests/unit/postgresql://drop:dev_only_not_a_secret@localhost:5433/drop_test
 — Unit Suite (4 test files)

Service-layer tests. Mocked Prisma. No HTTP layer.

FileTestsWhat It Covers
invoice-service-calculations.test.ts~15InvoiceService.createInvoice() arithmetic — line totals, VAT, FX
two-factor.test.ts8TwoFactorService — enable, verify, disable TOTP with backup codes
sef-submission.test.ts~10SefClient HTTP calls, InvoiceService.submitToSef() fire-and-forget
vat-calculation.test.ts~20Pure VAT functions from country-rs, country-ba, country-hr packages

apps/api/tests/e2e/ — E2E Suite (2 test files)

End-to-end workflow tests. Mocked services, no real DB required.

FileTestsWhat It Covers
api.test.tsFull Express stack integration test (live server, no mocks)
billing-flow.e2e.test.ts~8Full billing workflow: contact → invoice → send → pay → P&L → credit note

apps/api/tests/integration/ — Real DB Suite (5 test files)

Requires docker-compose.test.yml PostgreSQL. Run with npm run test:integration.

FileTestsWhat It Covers
auth.integration.test.ts4Registration + login + refresh against real DB
invoice.integration.test.ts5Full invoice CRUD lifecycle against real DB
credit-note-gl.integration.test.ts3Credit note GL entries balance in real DB
report.integration.test.ts3Reports with real seeded transactions
tenant-isolation.integration.test.ts5Cross-tenant data isolation (org A cannot read org B's data)

Grand Total: ~390 tests across 27 test files

packages/core/tests/ — Unit Tests (5 test files)

FileTestsWhat It Covers
accounting.test.ts20validateDoubleEntry, createJournalEntry, calculateTrialBalance — double-entry engine
chart-of-accounts.test.ts32Chart of accounts operations: account creation, hierarchy, account types
invoicing.test.ts22generateInvoiceNumber, calculateInvoiceTotals, validateLineItem
multi-currency.test.ts24Currency conversion, exchange rate locking, precision handling
tax.test.ts23VAT calculations for RS (20%), BA (17%), HR (25%), mixed rates, edge cases

Total: 121 unit test cases

Grand Total: ~220 tests across 14 test files


5. Running Tests

RunUnit All+ Integration Tests (Turborepo)Vitest)

# FromRecommended: projectvia rootMakefile (handles runsDocker all+ testsDATABASE_URL inautomatically)
all packages
npm runmake test

# Or with turbo directly
npx turboDirect run test(Docker 
must

Runalready APIbe Mockup) Suite

cd apps/apisrc/drop-app && DATABASE_URL=postgresql://drop:dev_only_not_a_secret@localhost:5433/drop_test npx vitest run

# Watch mode (development)
cd src/drop-app && DATABASE_URL=postgresql://drop:dev_only_not_a_secret@localhost:5433/drop_test npx vitest

# SpecificRun specific test file
npx vitest run tests/auth.test.ts
npx vitest run tests/chatbot.test.ts

# Specific test by name pattern
npx vitest run --reporter=verbose -t "POST /api/v1/auth/login"

Run API Unit Suite

cd apps/apisrc/drop-app npx&& vitest run tests/unit/

# Specific service test
npx vitest run tests/unit/two-factor.test.ts
npx vitest run tests/unit/vat-calculation.test.ts

Run API E2E Suite

cd apps/api
npx vitest run tests/e2e/

# Full billing flow
npx vitest run tests/e2e/billing-flow.e2e.test.ts

Run Real DB Integration Suite

Requires Docker. Uses docker-compose.test.yml (port 5433).

# Start test database
docker-compose -f docker-compose.test.yml up -d

# Run integration tests
cd apps/api
npm run test:integration

# Or directly:DATABASE_URL=postgresql://drop:dev_only_not_a_secret@localhost:5433/drop_test npx vitest run tests/integration/

# Stop test database
docker-compose -f docker-compose.test.yml down

Run Core Unit Tests

cd packages/core
npx vitest run

# Watch mode
npx vitest

# Specific file
npx vitest run tests/tax.api-endpoints.test.ts

# WithRun with coverage
cd src/drop-app && DATABASE_URL=postgresql://drop:dev_only_not_a_secret@localhost:5433/drop_test npx vitest run --coverage

Run in Verbose Mode

npx vitest run --reporter=verbose

Test Without Building

Tests resolve workspace packages from TypeScript sourceConfiguration (not dist/) via vitest.config.ts aliases. No build step required before running tests.):


    6.
  • Environment: node
  • Test Configuration
  • pattern:

    apps/api-express/vitest.config.ts

    import { defineConfig } from 'vitest/config'
    import path from 'path'
    
    export default defineConfig({
      resolve: {
        alias: {
          // Resolves workspace packages from source — no dist/ build needed
          '@bilko/core': path.resolve(__dirname, '../../packages/core/src/index.ts'),
          '@bilko/domain-rs': path.resolve(__dirname, '../../packages/domain-rs/src/index.ts'),
          '@bilko/domain-ba': path.resolve(__dirname, '../../packages/domain-ba/src/index.ts'),
          '@bilko/domain-hr': path.resolve(__dirname, '../../packages/domain-hr/src/index.ts'),
          '@bilko/database': path.resolve(__dirname, '../../packages/database/src/index.ts'),
        },
      },
      test: {
        globals: false,
        environment: 'node',
        setupFiles: [], // Setup is imported per test file via tests/setup.ts
        include: ['tests/**/*.test.ts']ts
    
  • Setup file: tests/setup.ts (sets NODE_ENV=test, exclude:sets ['node_modules',DATABASE_URL)
  • 'dist'],
  • Path },alias: })@ -> ./src
  • packages/core/vitest.config.ts
  • fileParallelism:
    import { defineConfig } from 'vitest/config'
    
    export default defineConfig({
      test: {
        globals: true,
        root: '.',
        include: ['tests/**/*.test.ts'],
      },
    })
    

    Key Configuration Differences

    runPostgreSQLraceconditions:

  • Serial
  • 1toavoidrate
    Settingapps/api-expresspackages/core
    globalsfalseimportstests explicit truesequentially to describe/it/expectavoid global
    setupFiles None

    E2E Tests (per-filePlaywright)

    import
    # ofRun all E2E tests
    npx playwright test
    
    # Run specific project
    npx playwright test --project=user-flows
    npx playwright test --project=full-flows
    npx playwright test --project=input-chaos
    
    # Run with UI mode
    npx playwright test --ui
    
    # View HTML report
    npx playwright show-report
    

    Configuration (./setupplaywright.config.ts:3-38)

    None
    aliasesWorkspace package path aliasesNot needed
    environmentnodeexecution (explicit) Defaultworker) node
    limit conflicts
  • Auto-starts dev server (npm run dev) if not running
  • HTML reporter
  • Trace on first retry (CI mode: 2 retries)

7. Test Setup & MockingArchitecture

Setup

File: apps/api/tests/setup.ts

Sets NODE_ENV=test and DATABASE_URL=postgresql://drop:dev_only_not_a_secret@localhost:5433/drop_test for all Vitest tests.

Database Strategy (PostgreSQL — ADR-014)

The APIAll integration tests use a mockedshared PrismaPostgreSQL clienttest helper (tests/helpers/pg-test-db.ts):

  • Connects to the real drop_test database on port 5433
  • Runs Drizzle schema push once before each test suite
  • Truncates all tables between tests for isolation
  • No in-memory SQLitetestsfull runtest/prod without a real PostgreSQL database. The setup file:

    1. Sets required environment variables (JWT secrets, rate limit config)parity
    2. Mocks the entire src/lib/prisma module with vi.mock()
    3. Provides test data factories and JWT token generators
// Import setup (must be first import inIntegration test file)pattern (post-migration)
import { createTestUser,getTestDb, generateTestAccessToken,
  generateTestRefreshToken,
  TEST_USER_EMAIL,
  TEST_USER_ID,
  TEST_ORG_ID,cleanupTestDb } from '"../setup'

Mock Prisma Pattern

// In test file — reference the mocked prismahelpers/pg-test-db";
import { prismadb } from '../src/"@/lib/prisma'db";

constbeforeAll(async mockPrisma = prisma as any

// Configure mock for a specific test
mockPrisma.user.findUnique.mockResolvedValue(testUser)
mockPrisma.user.findUnique.mockResolvedValue(null) // simulate not found

// Clear mocks between tests
beforeEach(() => { vi.clearAllMocks(await getTestDb(); });
afterEach(async () => { await cleanupTestDb(); });

Unit tests that test pure logic (validation, auth utilities) mock @/lib/db directly without a real DB connection.

Note: better-sqlite3 has been removed. There is no new Database(":memory:") pattern anymore.

AuthE2E TokenTest GenerationHelpers

File: tests/e2e/input-chaos.spec.ts:21-42

loginAsDemo(page) -- Mocks the /api/auth/me endpoint to bypass rate limiting in chaos tests. Returns a pre-authenticated user object.

CHAOS_STRINGS -- Dictionary of malicious/edge-case inputs used across E2E tests:

  • Empty, spaces, XSS, SQL injection, HTML injection
  • Unicode, RTL override, null bytes
  • Very long strings (10K chars)
  • Norwegian (aeoa), Bosnian (sdccz), Japanese characters
  • Zalgo text, special characters

Writing Tests

Integration Test Pattern (PostgreSQL)

import { describe, it, expect, beforeAll, afterEach } from "vitest";
import { getTestDb, cleanupTestDb } from "..// Generate a valid JWT for test requests
const authToken = generateTestAccessToken()helpers/pg-test-db";

// GenerateDATABASE_URL tokenis withset specificby roletests/setup.ts const adminTokenno =manual generateTestAccessToken({config role:needed

'admin' })
const viewerToken = generateTestAccessToken({ role: 'viewer' })

// Generate refresh token (used in cookies)
const refreshToken = generateTestRefreshToken(TEST_USER_ID)

Service Mocking Pattern (for route tests)

// Mock entire service module
vi.mock('../src/services/invoice.service'describe("Feature", () => {
  const mockService = {
    listInvoices: vi.fn(),
    createInvoice: vi.fn(),
    // ...
  }
  return { invoiceService: mockService }
})

import { invoiceService } from '../src/services/invoice.service'
const mockInvoiceService = invoiceService as any

// Configure per test
mockInvoiceService.createInvoice.mockResolvedValue(newInvoice)

// Verify calls
expect(mockInvoiceService.createInvoice).toHaveBeenCalledWith(
  TEST_ORG_ID,
  TEST_USER_ID,
  expect.any(Object),
)

8. CI Integration

Tests run via GitHub Actions on every push and pull request. See .github/workflows/ci.yml.

CI Pipeline (4 parallel jobs)

# .github/workflows/ci.yml
on:
  push:
    branches: ['*']
  pull_request:
    branches: [main, staging]

jobs:
  lint: # npx turbo run lint
  type-check: # npx turbo run type-check (after prisma generate)
  test: # npx turbo run test (unit + integration)
  build: # npx turbo run build (needs lint, type-check, test)

Job Details

JobCommandNeeds Prisma GenerateBlocks
lintnpx turbo run lintNobuild
type-checknpx turbo run type-checkYesbuild
testnpx turbo run testYesbuild
buildnpx turbo run buildYesDeploy

Prisma Client Generation (required in CI)

- name: Generate Prisma Client
  run: npx prisma generate --schema=packages/database/prisma/schema.prisma

This is required before type-check and test because the API source references @prisma/client types.

Concurrency

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

Cancels in-flight CI runs on the same branch when new commits are pushed.

CI Gate Rules

GateConditionBlocks
LintAny lint errorPR merge + build
Type checkAny TypeScript errorPR merge + build
TestsAny test failurePR merge + build
BuildLint/type-check/test all passDeploy

9. Writing New Tests

API Integration Test — Pattern

// apps/api/tests/my-feature.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import request from 'supertest'
import { generateTestAccessToken, TEST_ORG_ID, TEST_USER_ID } from './setup'
import app from '../src/app'

// Mock the service
vi.mock('../src/services/my-feature.service', () => {
  return {
    myFeatureService: {
      listItems: vi.fn(),
      createItem: vi.fn(),
    },
  }
})

import { myFeatureService } from '../src/services/my-feature.service'
const mockService = myFeatureService as any
const authToken = generateTestAccessToken()

describe('GET /api/v1/my-feature', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('returns 200 with paginated list', beforeAll(async () => {
    mockService.listItems.mockResolvedValue({await data:getTestDb(); [],// total:Ensure 0,schema page:is 1,pushed pageSize:to 20drop_test
  });

  const res = await request(app)
      .get('/api/v1/my-feature')
      .set('Authorization', `Bearer ${authToken}`)

    expect(res.status).toBe(200)
    expect(res.body).toHaveProperty('data')
  })

  it('returns 401 without auth', afterEach(async () => {
    const res = await request(app).get('/api/v1/my-feature'cleanupTestDb()
    expect(res.status).toBe(401)
  })
})

Core Unit Test — Pattern

// packages/core/tests/my-calculation.test.ts
import { describe, it, expect } from 'vitest'
import Decimal from 'decimal.js'
import { myCalculation } from '../src/my-module/index'

describe('myCalculation', () => {
  it('returns correct result for standard input', () => {
    const result = myCalculation('100', '20')
    expect(result.eq(new Decimal('20'))).toBe(true)
  })

  it('handles zero input', () => {
    const result = myCalculation('0', '20')
    expect(result.eq(new Decimal('0'))).toBe(true)
  })

  it('throws for negative amounts', () => {
    expect(() => myCalculation('-100', '20')).toThrow()
  })
})

Naming Conventions

// Describe block: HTTP method + route OR function name
describe('POST /api/v1/invoices', () => { ... })
describe('calculateInvoiceTotals', () => { ... }); // ItTruncate block:all betables specificbetween abouttests
  scenario and expected outcome});

  it('returns"does 201something with created invoice when data is valid', ...)
it('returns 400 when customerId is missing', ...)
it('calculates Serbian VAT at 20% on 100 RSD as 20 RSD', ...)
it('throws for negative amounts', ...)

10. Coverage Reporting

Generate Coverage (core unit tests)

cd packages/core
npx vitest run --coverage

Coverage Output Format

File                     | % Stmts | % Branch | % Funcs | % Lines
-------------------------|---------|----------|---------|--------
All files                |   XX.X  |   XX.X   |   XX.X  |  XX.X
 accounting/index.ts     |   95.0  |   90.0   |  100.0  |  95.0
 invoicing/index.ts      |   88.2  |   80.5   |   85.0  |  88.2
 tax/index.ts            |   92.0  |   88.0   |  100.0  |  92.0

Coverage Targets

CategoryTargetRationale
Financial logic (@bilko/core)>95%Critical for correctness
API routes (apps/api-express)>80%Integration-level coverage
Services>80%Business logic layer

11. Testing Best Practices

1. Arrange-Act-Assert (AAA)

it('creates invoice with correct totals'DB", async () => {
    // ARRANGEArrange, constAct, newInvoiceAssert =against mockInvoice({real status: 'draft'PostgreSQL
  })
  mockInvoiceService.createInvoice.mockResolvedValue(newInvoice)

  // ACT
  const res = await request(app)
    .post('/api/v1/invoices')
    .set('Authorization', `Bearer ${authToken}`)
    .send({ customerId: CUSTOMER_UUID, items: [...];
})

  // ASSERT
  expect(res.status).toBe(201)
  expect(res.body.invoiceNumber).toBe('INV-2026-001')
});

2.Unit Test Behavior,Pattern Not(pure Implementationlogic, no DB)

//import BAD{ describe, testsit, internalexpect, mockvi call} orderfrom expect(mockService.createInvoice.mock.calls[0][0]).toBe(orgId)"vitest";

// GOODMock @/lib/db entirely for pure unit tests
observablevi.mock("@/lib/db", API behavior
expect(res.status).toBe(201)
expect(res.body).toHaveProperty('invoiceNumber'() 
=>

3.({ Alwaysdb: Clear{ Mocks

select:
beforeEach(vi.fn(), insert: vi.fn() },
}));

describe("Feature", () => {
  vi.clearAllMocks(it("validates input", () => {
    // Arrange, Act, Assert — no DB needed
  });
});

4.E2E Test Edge Cases for Financial Logic

Always test in core unit tests:

  • Zero amounts (calculateInvoiceTotals([]))
  • Empty input arrays
  • Negative values (should throw)
  • Decimal precision (quantity: '3', unitPrice: '33.33')
  • Large amounts (overflow protection)

5. Never Hard-Code UUIDs in TestsPattern

//import BAD{ consttest, orgIdexpect =} '123e4567-e89b-12d3-a456-426614174000'from "@playwright/test";

test.describe.configure({ mode: "serial" }); // GOODAvoid constrate orgIdlimits

test.describe("Feature", () => TEST_ORG_ID{
  test("user flow", async ({ page }) => {
    await page.goto("http://localhost:3000/login");
    fromawait setup.ts,page.locator('input[type="email"]').fill("[email protected]");
    generatedawait per-runpage.locator('input[type="password"]').fill("demo1234");
    await page.locator('button[type="submit"]').click();
    await page.waitForURL("**/dashboard", { timeout: 15000 });
  });
});

12.Test Debugging TestsResults

Current status (2026-03-03): Migrated to PostgreSQL. All tests run against drop_test on port 5433.

Run in Verbose ModeVitest

npx# vitestRun: runmake --reporter=verbosetest
# All test files use PostgreSQL — no SQLite, no better-sqlite3

Run Single Test by NamePlaywright

npx vitest run -t "returns 201 with created invoice"

Debug Mode (Node Inspector)

npx vitest --inspect-brk
# Then attach VS Code debugger or open chrome://inspect

Print Mock Call History

// In a test, add temporarily:
console.log(mockService.createInvoice.mock.calls)

VS Code Launch Configuration

{
  "type": "node",
  "request": "launch",
  "name": "Debug Vitest",
  "runtimeExecutable": "npx",
  "runtimeArgs": ["vitest", "--inspect-brk", "--no-coverage"],
  "console": "integratedTerminal",
  "cwd": "${workspaceFolder}/apps/api-express"
}


Last

Test Updated:Coverage 2026-03-02Areas

Status:Active~390testsacross27files +unit+E2E+real-DBsuites)Target:>95%for@bilko/corefinanciallogic,>80%APIroutes

Area Unit Integration E2E Performance
Password hashing (mockbcrypt) auth.test.ts api-routes.test.ts -- api-benchmarks.test.ts
JWT Coveragesign/verify auth.test.ts -- -- --
Rate forlimiting middleware.test.ts api-routes.test.ts user-flows.spec.tsapi-benchmarks.test.ts
Input validationvalidation.test.ts--input-chaos.spec.ts--
Database schemadb.test.tsapi-endpoints.test.ts----
Feature flagsfeature-flags.test.ts------
Utility functionsutils.test.ts------
Registration flow--api-endpoints.test.tsuser-flows.spec.ts--
Login flow--api-endpoints.test.tsuser-flows.spec.ts--
Remittance--api-endpoints.test.tsfull-flows.spec.ts--
QR Payment--api-endpoints.test.tsfull-flows.spec.ts--
XSS/SQLi preventionvalidation.test.ts--input-chaos.spec.ts--
Session management--api-routes.test.tsfull-flows.spec.ts--
Known bug regressions--known-bugs.test.ts----