Skip to main content

Testing Guide

DropBilko — Testing Guide

Status: Active | Tests implemented across apps/api/ and packages/core/ Version: 1.0 Last updated:Updated: 2026-02-1325 Source:Author: src/drop-app/vitest.config.ts,ALAI playwright.config.ts,Documentation package.json, tests/Team


Table of Contents

  1. Testing Philosophy
  2. Testing Pyramid
  3. Tech Stack
  4. Actual Test FrameworksFiles
  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 Philosophy

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 — Tests run without a real database (mocked Prisma)

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

FrameworkModule VersionUnit TypeIntegration ConfigReason
Vitest@bilko/core accounting engine ^4.0.1895% Unit, Integration, Performance, RegressionN/A vitest.config.tsDouble-entry errors = financial loss
Playwright@bilko/core tax/VAT calculations ^1.58.295% E2E (browser)N/A Tax miscalculations = regulatory penalty
playwright.config.ts@bilko/core 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 Stack

Test TypeFrameworkPurpose
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 app instance directly
  • No running server required

4. Actual Test Files

apps/api/tests/ — API Integration Tests (9 test files + 1 e2e)

FileTestsWhat It Covers
setup.tsShared 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
e2e/api.test.tsFull end-to-end API test (no mocks)

Total: ~99 integration/E2E test cases

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

UnitRun + IntegrationAll Tests (Vitest)Turborepo)

# SingleFrom project root — runs all tests in all packages
npm run (CItest

mode)# npmOr with turbo directly
npx turbo run test

Run API Integration Tests

cd apps/api
npx vitest run

# Watch mode
(development)npx npm run test:watchvitest

# Run specificSpecific test file
npx vitest run tests/unit/auth.test.ts

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

Run withCore Unit Tests

cd packages/core
npx vitest run

# Watch mode
npx vitest

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

# With coverage
npx vitest run --coverage

Run in Verbose Mode

npx vitest run --reporter=verbose

Test Without Building

ConfigurationTests resolve workspace packages from TypeScript source (not dist/) via vitest.config.ts:4-14ts): aliases. No build step required before running tests.


  • Environment:

    6. node

  • Test pattern:Configuration

    apps/api/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/country-rs': path.resolve(__dirname, '../../packages/country-rs/src/index.ts'),
          '@bilko/country-ba': path.resolve(__dirname, '../../packages/country-ba/src/index.ts'),
          '@bilko/country-hr': path.resolve(__dirname, '../../packages/country-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'],
        exclude: ['node_modules', 'dist'],
      },
    })
    

    packages/core/vitest.config.ts

    import { defineConfig } from 'vitest/config'
    
    export default defineConfig({
      test: {
        globals: true,
        root: '.',
        include: ['tests/**/*.test.ts'],
      },
    })
    

    Key Configuration Differences

    Settingapps/apipackages/core
    globalsfalse — imports explicittrue — describe/it/expect global
    setupFilesNone (per-file import of ./setup)None
    aliasesWorkspace package path aliasesNot needed
    environmentnode (explicit)Default node

    7. Test Setup & Mocking

    apps/api/tests/setup.ts

    The API integration tests use a mocked Prisma client — tests run without a real PostgreSQL database. The setup file:

    1. Sets required environment variables (JWT secrets, rate limit config)
    2. Mocks the entire src/lib/prisma module with vi.mock()
    3. SetupProvides file:test data factories and JWT token generators
    tests/setup.ts// Import setup (setsmust be first import in test file)
    import {
      createTestUser,
      generateTestAccessToken,
      generateTestRefreshToken,
      TEST_USER_EMAIL,
      TEST_USER_ID,
      TEST_ORG_ID,
    } from './setup'
    

    Mock Prisma Pattern

    NODE_ENV=// In test file — reference the mocked prisma
    import { prisma } from '../src/lib/prisma'
    const 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()
    })
    

    Auth Token Generation

    // Generate a valid JWT for test requests
    const authToken = generateTestAccessToken()
  • Path// alias:Generate token with specific role const adminToken = generateTestAccessToken({ role: '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', () => {
      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 ./srcgithub/workflows/ci.yml

.

E2ECI TestsPipeline (Playwright)4 parallel jobs)

# Run.github/workflows/ci.yml
allon:
  E2Epush:
    testsbranches: ["*"]
  pull_request:
    branches: [main, staging]

jobs:
  lint:          # npx playwrightturbo run lint
  type-check:    # npx turbo run type-check (after prisma generate)
  test:          # npx turbo run test (unit + integration)
  build:         # Run specific project npx playwrightturbo 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 --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-reportschema=packages/database/prisma/schema.prisma

ConfigurationThis (is required before playwright.config.ts:3-38type-check): and test because the API source references @prisma/client types.

    Concurrency

  • Serial
    concurrency:
      executiongroup: (1ci-${{ worker)github.ref to}}
      avoidcancel-in-progress: ratetrue
    limit
    conflicts
  • Cancels

  • Auto-startsin-flight devCI server (npm run dev) if not running
  • HTML reporter
  • Traceruns on firstthe retrysame (branch when new commits are pushed.

    CI mode:Gate 2Rules

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

    Test9. Architecture

    Writing

    Setup

    File: tests/setup.ts:1

    Sets NODE_ENV=test for all Vitest tests.

    Mocking Strategy

    All unit/integration tests mock the following Next.js modules:

    • next/server -- NextResponse.json() returns plain objects
    • next/headers -- cookies() returns mock getters/setters
    • @/lib/db -- Uses in-memory SQLite (better-sqlite3 with :memory:) for isolation

    Each test file creates and destroys its own database in beforeEach/afterEach.

    E2E Test Helpers

    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

    WritingNew Tests

    UnitAPI Integration Test — Pattern

    // apps/api/tests/my-feature.test.ts
    import { describe, it, expect, vi, beforeEach, afterEachbeforeEach } from "vitest";'vitest'
    import Databaserequest from "better-sqlite3";'supertest'
    letimport testDb:{ Database.Database;generateTestAccessToken, TEST_ORG_ID, TEST_USER_ID } from './setup'
    import app from '../src/app'
    
    // Mock dependenciesthe before importservice
    vi.mock("@/lib/db"'../src/services/my-feature.service', () => ({
      getDb:return {
        myFeatureService: {
          listItems: vi.fn(() => testDb),
          //createItem: ...vi.fn(),
        other}
      db functions}
    })
    
    import { myFeatureService } from '../src/services/my-feature.service'
    const mockService = myFeatureService as any
    const authToken = generateTestAccessToken();
    
    describe("Feature"'GET /api/v1/my-feature', () => {
      beforeEach(() => {
        testDb = new Database(":memory:"vi.clearAllMocks();
        // Create schema...
      });
    
      afterEach(it('returns 200 with paginated list', async () => {
        testDb.close();mockService.listItems.mockResolvedValue({ data: [], total: 0, page: 1, pageSize: 20 });
    
        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("does'returns something"401 without auth', async () => {
        const res = await request(app).get('/api/v1/my-feature')
        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', () => { ... })
    
    // It block: be specific about scenario and expected outcome
    it('returns 201 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)>80%Integration-level coverage
    Services>80%Business logic layer

    11. Testing Best Practices

    1. Arrange-Act-Assert (AAA)

    it('creates invoice with correct totals', async () => {
      // Arrange,ARRANGE
      Act,const AssertnewInvoice = mockInvoice({ status: 'draft' });
      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')
    })
    

    E2E2. Test PatternBehavior, Not Implementation

    import// {BAD test, expecttests }internal frommock "@playwright/test";call test.describe.configure({order
    mode: "serial" }expect(mockService.createInvoice.mock.calls[0][0]);.toBe(orgId)
    
    // AvoidGOOD rate limitstests test.describe("Feature",observable API behavior
    expect(res.status).toBe(201)
    expect(res.body).toHaveProperty('invoiceNumber')
    

    3. Always Clear Mocks

    beforeEach(() => {
      test("user flow", async ({ pagevi.clearAllMocks()
    })
    =>
    {

    4. awaitTest page.goto("http: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 Tests

    //localhost:3000/login"); awaitBAD
    page.locator(const orgId = 'input[type="email"]').fill("[email protected]");123e4567-e89b-12d3-a456-426614174000'
    
    await// page.locator('input[type="password"]').fill("demo1234");GOOD
    awaitconst page.locator('button[type="submit"]').click();orgId await= page.waitForURL("**TEST_ORG_ID  /dashboard",/ {from timeout:setup.ts, 15000generated });
      });
    });per-run
    

    Test12. ResultsDebugging Tests

    Current

    Run statusin (2026-02-13):

    Verbose

    VitestMode

    Testnpx Filesvitest 6run passed (6)
    Tests       40 passed (40)
    Duration    1.40s--reporter=verbose
    

    PlaywrightRun Single Test by Name

    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"
    }
    


    Test

    Last Updated: 2026-02-25 Status: Active — tests implemented Coverage Areas

    Target: >95%for@bilko/corefinanciallogic,>80%forAPIroutes

    Area Unit Integration E2E Performance
    Password hashing (bcrypt)auth.test.tsapi-routes.test.ts--api-benchmarks.test.ts
    JWT sign/verifyauth.test.ts------
    Rate limitingmiddleware.test.tsapi-routes.test.tsuser-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----