navidocs/intelligence/session-4/testing-strategy.md
Claude 765f9b7be3
Session 4 complete: Implementation planning with 10 Haiku agents
Session 4 (Implementation Planning) has completed comprehensive 4-week sprint planning:

Deliverables:
- Week 1-4 detailed schedules (162 total hours)
- 24 API endpoints (OpenAPI 3.0 specification)
- 5 database migrations (100% rollback coverage)
- Testing strategy (70% unit, 50% integration, 10 E2E flows)
- 28 Gherkin acceptance criteria scenarios
- Dependency graph with critical path analysis
- Zero-downtime deployment runbook

Agents: S4-H01 through S4-H10 (all complete)
Token Cost: $2.66 (82% under $15 budget)
Efficiency: 82% Haiku delegation
Status: Ready for Week 1 implementation kickoff
2025-11-13 01:57:59 +00:00

66 KiB

NaviDocs Testing Strategy - Session 4

Testing Strategy Designer: S4-H06 Target Coverage: 70% unit tests, 50% integration tests, 10 E2E critical flows Status: COMPREHENSIVE TESTING FRAMEWORK


Executive Summary

This document outlines a complete testing strategy for NaviDocs features across all layers:

  • Backend: Express.js API services, database operations, background jobs
  • Frontend: Vue 3 components, state management (Pinia), routing
  • Integration: API endpoints, database transactions, webhook delivery
  • E2E: Critical user workflows and cross-system interactions

Tools Selected:

  • Unit Tests: Mocha + Chai (backend), Vitest (frontend)
  • Integration Tests: Supertest + SQLite in-memory database
  • E2E Tests: Playwright (browser automation)
  • CI/CD: GitHub Actions with automated test reporting

Part 1: Unit Testing Strategy

1.1 Unit Test Scope & Coverage Targets

Target Coverage: 70% (focused on high-value code paths)

In Scope:

  • Service layer logic (warranty calculations, validation, business rules)
  • Utility functions (date helpers, formatters, validators)
  • Middleware (authentication, authorization, error handling)
  • Data transformers (request/response serialization)
  • Event bus logic

Out of Scope (for unit testing):

  • Database queries (covered by integration tests)
  • External API calls (mocked in unit tests)
  • UI component rendering (covered by component tests)

1.2 Testing Tools & Setup

Dependencies to Add (Server)

npm install --save-dev \
  mocha \
  chai \
  chai-as-promised \
  sinon \
  nyc \
  @types/mocha \
  @types/node

New npm Scripts

Add to /home/user/navidocs/server/package.json:

{
  "scripts": {
    "test": "mocha",
    "test:watch": "mocha --watch --watch-extensions js",
    "test:coverage": "nyc mocha",
    "test:unit": "mocha test/unit/**/*.test.js",
    "test:unit:coverage": "nyc mocha test/unit/**/*.test.js"
  }
}

Mocha Configuration

Create /home/user/navidocs/server/.mocharc.json:

{
  "require": ["test/setup.js"],
  "spec": "test/**/*.test.js",
  "timeout": 5000,
  "slow": 1000,
  "reporter": "spec",
  "exit": true,
  "recursive": true
}

NYC (Coverage Reporter) Configuration

Create /home/user/navidocs/server/.nycrc.json:

{
  "reporter": ["text", "html", "lcov"],
  "report-dir": "./coverage",
  "exclude": [
    "node_modules/**",
    "test/**",
    "coverage/**",
    "**/*.test.js",
    "**/db/init.js"
  ],
  "all": true,
  "lines": 70,
  "functions": 70,
  "branches": 65,
  "statements": 70
}

1.3 Test File Organization

Directory Structure:

server/
├── test/
│   ├── setup.js                          # Global test configuration
│   ├── unit/
│   │   ├── services/
│   │   │   ├── warranty.service.test.js
│   │   │   ├── event-bus.service.test.js
│   │   │   ├── webhook.service.test.js
│   │   │   ├── notification.service.test.js
│   │   │   └── sale-workflow.service.test.js
│   │   ├── middleware/
│   │   │   ├── auth.middleware.test.js
│   │   │   └── error.middleware.test.js
│   │   ├── utils/
│   │   │   ├── date-helpers.test.js
│   │   │   ├── validators.test.js
│   │   │   └── formatters.test.js
│   │   └── workers/
│   │       └── warranty-expiration.worker.test.js
│   ├── integration/
│   │   ├── routes/
│   │   │   ├── warranty.routes.test.js
│   │   │   ├── sales.routes.test.js
│   │   │   ├── integrations.routes.test.js
│   │   │   └── auth.routes.test.js
│   │   ├── database/
│   │   │   ├── warranty-queries.test.js
│   │   │   └── migrations.test.js
│   │   └── jobs/
│   │       └── warranty-expiration-job.test.js
│   └── fixtures/
│       ├── test-data.js
│       ├── mocks.js
│       └── seeds.js
├── coverage/                             # Generated coverage reports
└── ...services, routes, etc...

1.4 Test Setup & Utilities

Global Test Setup (test/setup.js)

import { expect } from 'chai';
import sinon from 'sinon';

// Global test utilities
global.expect = expect;
global.sinon = sinon;

// Setup test environment
process.env.NODE_ENV = 'test';
process.env.JWT_SECRET = 'test-secret-key';
process.env.DATABASE_PATH = ':memory:';

// Clean up after each test
afterEach(() => {
  sinon.restore();
});

Test Fixtures (test/fixtures/test-data.js)

// Mock data for consistent testing
export const mockUser = {
  id: 'user-123',
  email: 'test@example.com',
  name: 'Test User',
  password_hash: 'hashed_password',
  created_at: Date.now(),
  updated_at: Date.now()
};

export const mockBoat = {
  id: 'boat-123',
  organization_id: 'org-123',
  user_id: 'user-123',
  entity_type: 'boat',
  name: 'Azimut 55S',
  make: 'Azimut',
  model: '55S',
  year: 2020,
  hull_id: 'AZI1234567890123',
  vessel_type: 'powerboat',
  length_feet: 55,
  created_at: Date.now(),
  updated_at: Date.now()
};

export const mockWarranty = {
  id: 'warranty-123',
  boat_id: 'boat-123',
  item_name: 'Engine',
  provider: 'Caterpillar',
  purchase_date: '2023-01-15',
  warranty_period_months: 24,
  expiration_date: '2025-01-15',
  coverage_amount: 50000,
  status: 'active',
  created_at: Date.now(),
  updated_at: Date.now()
};

export const mockWebhook = {
  id: 'webhook-123',
  organization_id: 'org-123',
  url: 'https://example.com/webhook',
  topics: JSON.stringify(['WARRANTY_EXPIRING', 'DOCUMENT_UPLOADED']),
  secret: 'webhook-secret-key',
  status: 'active',
  created_at: Date.now()
};

Stubs & Mocks (test/fixtures/mocks.js)

import sinon from 'sinon';

export function createAuthStub() {
  return {
    authenticateToken: sinon.stub().callsFake((req, res, next) => {
      req.user = { id: 'user-123', org_id: 'org-123' };
      next();
    }),
    requireRole: sinon.stub().returns((req, res, next) => next())
  };
}

export function createDatabaseStub() {
  return {
    prepare: sinon.stub(),
    exec: sinon.stub(),
    transaction: sinon.stub().callsFake((fn) => fn())
  };
}

export function createMailerStub() {
  return {
    sendEmail: sinon.stub().resolves({ messageId: 'msg-123' }),
    sendSMS: sinon.stub().resolves({ sid: 'sms-123' })
  };
}

1.5 Unit Test Examples

Example 1: Warranty Service Tests

File: /home/user/navidocs/server/test/unit/services/warranty.service.test.js

import { expect } from 'chai';
import sinon from 'sinon';
import warrantyService from '../../../services/warranty.service.js';

describe('WarrantyService', () => {
  describe('calculateExpirationDate', () => {
    it('should add warranty period to purchase date', () => {
      const purchaseDate = '2023-01-15';
      const warrantyMonths = 24;
      const result = warrantyService.calculateExpirationDate(purchaseDate, warrantyMonths);
      expect(result).to.equal('2025-01-15');
    });

    it('should handle leap year correctly', () => {
      const purchaseDate = '2023-12-15';
      const warrantyMonths = 1;
      const result = warrantyService.calculateExpirationDate(purchaseDate, warrantyMonths);
      expect(result).to.equal('2024-01-15');
    });

    it('should throw error for invalid date format', () => {
      expect(() => {
        warrantyService.calculateExpirationDate('invalid-date', 12);
      }).to.throw('Invalid date format');
    });

    it('should throw error for negative warranty period', () => {
      expect(() => {
        warrantyService.calculateExpirationDate('2023-01-15', -12);
      }).to.throw('Warranty period must be positive');
    });
  });

  describe('validateWarranty', () => {
    it('should validate warranty with all required fields', () => {
      const warranty = {
        boat_id: 'boat-123',
        item_name: 'Engine',
        purchase_date: '2023-01-15',
        warranty_period_months: 24,
        provider: 'Caterpillar'
      };

      const result = warrantyService.validateWarranty(warranty);
      expect(result.valid).to.be.true;
      expect(result.errors).to.be.empty;
    });

    it('should reject warranty with missing boat_id', () => {
      const warranty = {
        item_name: 'Engine',
        purchase_date: '2023-01-15',
        warranty_period_months: 24
      };

      const result = warrantyService.validateWarranty(warranty);
      expect(result.valid).to.be.false;
      expect(result.errors).to.include('boat_id is required');
    });

    it('should reject warranty with future purchase date', () => {
      const futureDate = new Date();
      futureDate.setDate(futureDate.getDate() + 1);
      const warranty = {
        boat_id: 'boat-123',
        item_name: 'Engine',
        purchase_date: futureDate.toISOString().split('T')[0],
        warranty_period_months: 24
      };

      const result = warrantyService.validateWarranty(warranty);
      expect(result.valid).to.be.false;
      expect(result.errors).to.include('purchase_date cannot be in future');
    });

    it('should validate warranty period constraints', () => {
      const warranty = {
        boat_id: 'boat-123',
        item_name: 'Engine',
        purchase_date: '2023-01-15',
        warranty_period_months: 999, // Unrealistic period
        provider: 'Caterpillar'
      };

      const result = warrantyService.validateWarranty(warranty);
      expect(result.valid).to.be.false;
      expect(result.errors).to.include('warranty_period_months exceeds maximum');
    });
  });

  describe('getExpiringWarranties', () => {
    let dbStub;

    beforeEach(() => {
      dbStub = sinon.stub(warrantyService.db, 'prepare');
    });

    afterEach(() => {
      dbStub.restore();
    });

    it('should return warranties expiring within specified days', () => {
      const expiringWarranties = [
        {
          id: 'warranty-123',
          item_name: 'Engine',
          expiration_date: '2025-12-10',
          days_until_expiration: 27
        },
        {
          id: 'warranty-124',
          item_name: 'Generator',
          expiration_date: '2025-11-20',
          days_until_expiration: 7
        }
      ];

      dbStub.returns({
        all: sinon.stub().returns(expiringWarranties)
      });

      const result = warrantyService.getExpiringWarranties(30);
      expect(result).to.have.lengthOf(2);
      expect(result[0].item_name).to.equal('Engine');
    });

    it('should filter by specific boat_id when provided', () => {
      dbStub.returns({
        all: sinon.stub().returns([{ id: 'warranty-123' }])
      });

      warrantyService.getExpiringWarranties(30, 'boat-123');
      expect(dbStub.getCall(0).args[0]).to.include('boat_id = ?');
    });
  });
});

Example 2: Authentication Middleware Tests

File: /home/user/navidocs/server/test/unit/middleware/auth.middleware.test.js

import { expect } from 'chai';
import sinon from 'sinon';
import { authenticateToken, requireRole } from '../../../middleware/auth.js';
import jwt from 'jsonwebtoken';

describe('Authentication Middleware', () => {
  describe('authenticateToken', () => {
    it('should attach user to request for valid token', () => {
      const req = {
        headers: {
          authorization: `Bearer ${jwt.sign({ id: 'user-123', org_id: 'org-123' }, 'test-secret')}`
        }
      };
      const res = {};
      const next = sinon.spy();

      authenticateToken(req, res, next);

      expect(req.user).to.exist;
      expect(req.user.id).to.equal('user-123');
      expect(next.calledOnce).to.be.true;
    });

    it('should return 401 for missing authorization header', () => {
      const req = { headers: {} };
      const res = {
        status: sinon.stub().returnsThis(),
        json: sinon.stub()
      };
      const next = sinon.spy();

      authenticateToken(req, res, next);

      expect(res.status.calledWith(401)).to.be.true;
      expect(next.called).to.be.false;
    });

    it('should return 401 for invalid token', () => {
      const req = {
        headers: { authorization: 'Bearer invalid-token' }
      };
      const res = {
        status: sinon.stub().returnsThis(),
        json: sinon.stub()
      };
      const next = sinon.spy();

      authenticateToken(req, res, next);

      expect(res.status.calledWith(401)).to.be.true;
    });

    it('should handle expired tokens', () => {
      const expiredToken = jwt.sign({ id: 'user-123' }, 'test-secret', {
        expiresIn: '-1h'
      });

      const req = {
        headers: { authorization: `Bearer ${expiredToken}` }
      };
      const res = {
        status: sinon.stub().returnsThis(),
        json: sinon.stub()
      };
      const next = sinon.spy();

      authenticateToken(req, res, next);

      expect(res.status.calledWith(401)).to.be.true;
    });
  });

  describe('requireRole', () => {
    it('should allow request for user with required role', () => {
      const req = {
        user: { id: 'user-123', role: 'admin' }
      };
      const res = {};
      const next = sinon.spy();

      const middleware = requireRole('admin');
      middleware(req, res, next);

      expect(next.calledOnce).to.be.true;
    });

    it('should deny request for user without required role', () => {
      const req = {
        user: { id: 'user-123', role: 'member' }
      };
      const res = {
        status: sinon.stub().returnsThis(),
        json: sinon.stub()
      };
      const next = sinon.spy();

      const middleware = requireRole('admin');
      middleware(req, res, next);

      expect(res.status.calledWith(403)).to.be.true;
      expect(next.called).to.be.false;
    });
  });
});

Example 3: Date Utility Tests

File: /home/user/navidocs/server/test/unit/utils/date-helpers.test.js

import { expect } from 'chai';
import {
  addMonths,
  daysUntil,
  formatDate,
  parseDate,
  isExpired
} from '../../../utils/date-helpers.js';

describe('Date Helpers', () => {
  describe('addMonths', () => {
    it('should add months to a date', () => {
      const result = addMonths('2023-01-15', 24);
      expect(result).to.equal('2025-01-15');
    });

    it('should handle month overflow', () => {
      const result = addMonths('2023-11-15', 3);
      expect(result).to.equal('2024-02-15');
    });

    it('should handle leap year dates', () => {
      const result = addMonths('2024-01-31', 1);
      expect(result).to.equal('2024-02-29'); // 2024 is leap year
    });
  });

  describe('daysUntil', () => {
    it('should calculate days until future date', () => {
      const futureDate = new Date();
      futureDate.setDate(futureDate.getDate() + 10);
      const result = daysUntil(futureDate.toISOString().split('T')[0]);
      expect(result).to.be.closeTo(10, 1); // Allow 1 day variance
    });

    it('should return negative for past date', () => {
      const pastDate = new Date();
      pastDate.setDate(pastDate.getDate() - 5);
      const result = daysUntil(pastDate.toISOString().split('T')[0]);
      expect(result).to.be.lessThan(0);
    });

    it('should return 0 for today', () => {
      const today = new Date().toISOString().split('T')[0];
      const result = daysUntil(today);
      expect(result).to.equal(0);
    });
  });

  describe('isExpired', () => {
    it('should return true for past expiration date', () => {
      const pastDate = new Date();
      pastDate.setDate(pastDate.getDate() - 1);
      const result = isExpired(pastDate.toISOString().split('T')[0]);
      expect(result).to.be.true;
    });

    it('should return false for future expiration date', () => {
      const futureDate = new Date();
      futureDate.setDate(futureDate.getDate() + 1);
      const result = isExpired(futureDate.toISOString().split('T')[0]);
      expect(result).to.be.false;
    });

    it('should return true for today (expired on or before)', () => {
      const today = new Date().toISOString().split('T')[0];
      const result = isExpired(today);
      expect(result).to.be.true;
    });
  });
});

Part 2: Integration Testing Strategy

2.1 Integration Test Scope

Target Coverage: 50% (API endpoints, database operations, background jobs)

In Scope:

  • API endpoint request/response validation
  • Database CRUD operations with migrations
  • Background job execution and event publishing
  • Webhook delivery with retries
  • Authentication and authorization flows
  • Multi-step workflows (sale initiation → package generation → transfer)

Out of Scope (for integration tests):

  • External service calls (mocked)
  • Email/SMS delivery (mocked)
  • File storage (in-memory temporary files)

2.2 Integration Testing Setup

Dependencies to Add (Server)

npm install --save-dev \
  supertest \
  sqlite3 \
  @types/supertest

Integration Test Configuration

Create /home/user/navidocs/server/test/integration/setup.js:

import Database from 'better-sqlite3';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

let db;

/**
 * Create in-memory database for testing
 */
export function setupTestDatabase() {
  // Use in-memory SQLite for fast tests
  db = new Database(':memory:', { verbose: console.log });

  // Load schema
  const schemaPath = join(__dirname, '../../db/schema.sql');
  const schema = readFileSync(schemaPath, 'utf-8');
  db.exec(schema);

  return db;
}

/**
 * Tear down database after test
 */
export function teardownTestDatabase() {
  if (db) {
    db.close();
  }
}

/**
 * Reset database state between tests
 */
export function resetDatabase() {
  const tables = db.prepare(`
    SELECT name FROM sqlite_master
    WHERE type='table' AND name NOT LIKE 'sqlite_%'
  `).all();

  db.exec('PRAGMA foreign_keys = OFF');
  tables.forEach(table => {
    db.exec(`DELETE FROM ${table.name}`);
  });
  db.exec('PRAGMA foreign_keys = ON');
}

export { db };

2.3 Integration Test Examples

Example 1: Warranty API Endpoint Tests

File: /home/user/navidocs/server/test/integration/routes/warranty.routes.test.js

import request from 'supertest';
import { expect } from 'chai';
import app from '../../../index.js';
import { setupTestDatabase, resetDatabase, teardownTestDatabase, db } from '../setup.js';
import { mockUser, mockBoat, mockWarranty } from '../../fixtures/test-data.js';
import jwt from 'jsonwebtoken';

describe('Warranty API Endpoints', () => {
  let validToken;
  let userId;
  let boatId;

  before(() => {
    setupTestDatabase();
  });

  beforeEach(() => {
    resetDatabase();

    // Create test user
    userId = mockUser.id;
    db.prepare(`
      INSERT INTO users (id, email, name, password_hash, created_at, updated_at)
      VALUES (?, ?, ?, ?, ?, ?)
    `).run(userId, mockUser.email, mockUser.name, mockUser.password_hash, Date.now(), Date.now());

    // Create test organization
    db.prepare(`
      INSERT INTO organizations (id, name, type, created_at, updated_at)
      VALUES (?, ?, ?, ?, ?)
    `).run('org-123', 'Test Org', 'personal', Date.now(), Date.now());

    // Create user-org membership
    db.prepare(`
      INSERT INTO user_organizations (user_id, organization_id, role, joined_at)
      VALUES (?, ?, ?, ?)
    `).run(userId, 'org-123', 'admin', Date.now());

    // Create test boat
    boatId = mockBoat.id;
    db.prepare(`
      INSERT INTO entities (id, organization_id, user_id, entity_type, name, make, model, year,
                           hull_id, vessel_type, length_feet, created_at, updated_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(boatId, 'org-123', userId, mockBoat.entity_type, mockBoat.name, mockBoat.make,
           mockBoat.model, mockBoat.year, mockBoat.hull_id, mockBoat.vessel_type,
           mockBoat.length_feet, Date.now(), Date.now());

    // Generate test JWT
    validToken = jwt.sign(
      { id: userId, org_id: 'org-123' },
      process.env.JWT_SECRET || 'test-secret'
    );
  });

  after(() => {
    teardownTestDatabase();
  });

  describe('POST /api/warranties', () => {
    it('should create warranty with valid data', async () => {
      const response = await request(app)
        .post('/api/warranties')
        .set('Authorization', `Bearer ${validToken}`)
        .send({
          boat_id: boatId,
          item_name: 'Engine',
          provider: 'Caterpillar',
          purchase_date: '2023-01-15',
          warranty_period_months: 24,
          coverage_amount: 50000
        });

      expect(response.status).to.equal(201);
      expect(response.body).to.have.property('id');
      expect(response.body.item_name).to.equal('Engine');
      expect(response.body.expiration_date).to.equal('2025-01-15');
      expect(response.body.status).to.equal('active');
    });

    it('should return 400 for missing required fields', async () => {
      const response = await request(app)
        .post('/api/warranties')
        .set('Authorization', `Bearer ${validToken}`)
        .send({
          item_name: 'Engine',
          // Missing boat_id, purchase_date, warranty_period_months
        });

      expect(response.status).to.equal(400);
      expect(response.body.errors).to.be.an('array');
      expect(response.body.errors[0]).to.include('boat_id');
    });

    it('should return 401 for unauthorized request', async () => {
      const response = await request(app)
        .post('/api/warranties')
        .send({
          boat_id: boatId,
          item_name: 'Engine',
          purchase_date: '2023-01-15',
          warranty_period_months: 24
        });

      expect(response.status).to.equal(401);
    });

    it('should enforce tenant isolation', async () => {
      // Create warranty as user1
      const warranty = db.prepare(`
        INSERT INTO warranty_tracking (
          id, boat_id, item_name, provider, purchase_date, warranty_period_months,
          expiration_date, coverage_amount, status, created_at, updated_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(
        'warranty-other-org',
        'boat-other-org',
        'Engine',
        'Caterpillar',
        '2023-01-15',
        24,
        '2025-01-15',
        50000,
        'active',
        Date.now(),
        Date.now()
      );

      // Try to access with different user's token
      const otherUserToken = jwt.sign(
        { id: 'other-user', org_id: 'other-org' },
        process.env.JWT_SECRET || 'test-secret'
      );

      const response = await request(app)
        .get(`/api/warranties/warranty-other-org`)
        .set('Authorization', `Bearer ${otherUserToken}`);

      expect(response.status).to.equal(403);
    });
  });

  describe('GET /api/warranties/:id', () => {
    beforeEach(() => {
      db.prepare(`
        INSERT INTO warranty_tracking (
          id, boat_id, item_name, provider, purchase_date, warranty_period_months,
          expiration_date, coverage_amount, status, created_at, updated_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(
        mockWarranty.id,
        boatId,
        mockWarranty.item_name,
        mockWarranty.provider,
        mockWarranty.purchase_date,
        mockWarranty.warranty_period_months,
        mockWarranty.expiration_date,
        mockWarranty.coverage_amount,
        mockWarranty.status,
        Date.now(),
        Date.now()
      );
    });

    it('should retrieve warranty by id', async () => {
      const response = await request(app)
        .get(`/api/warranties/${mockWarranty.id}`)
        .set('Authorization', `Bearer ${validToken}`);

      expect(response.status).to.equal(200);
      expect(response.body.id).to.equal(mockWarranty.id);
      expect(response.body.item_name).to.equal('Engine');
    });

    it('should return 404 for non-existent warranty', async () => {
      const response = await request(app)
        .get('/api/warranties/non-existent-id')
        .set('Authorization', `Bearer ${validToken}`);

      expect(response.status).to.equal(404);
    });
  });

  describe('GET /api/warranties/expiring', () => {
    beforeEach(() => {
      const tomorrow = new Date();
      tomorrow.setDate(tomorrow.getDate() + 1);
      const tomorrowStr = tomorrow.toISOString().split('T')[0];

      // Create warranty expiring tomorrow (within 30 days)
      db.prepare(`
        INSERT INTO warranty_tracking (
          id, boat_id, item_name, provider, purchase_date, warranty_period_months,
          expiration_date, coverage_amount, status, created_at, updated_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(
        'warranty-soon',
        boatId,
        'Engine',
        'Caterpillar',
        '2023-01-15',
        24,
        tomorrowStr,
        50000,
        'active',
        Date.now(),
        Date.now()
      );

      // Create warranty expiring in 60 days (outside 30-day window)
      const in60Days = new Date();
      in60Days.setDate(in60Days.getDate() + 60);
      const in60DaysStr = in60Days.toISOString().split('T')[0];

      db.prepare(`
        INSERT INTO warranty_tracking (
          id, boat_id, item_name, provider, purchase_date, warranty_period_months,
          expiration_date, coverage_amount, status, created_at, updated_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(
        'warranty-later',
        boatId,
        'Generator',
        'Honda',
        '2023-01-15',
        24,
        in60DaysStr,
        30000,
        'active',
        Date.now(),
        Date.now()
      );
    });

    it('should return warranties expiring within specified days', async () => {
      const response = await request(app)
        .get('/api/warranties/expiring?days=30')
        .set('Authorization', `Bearer ${validToken}`);

      expect(response.status).to.equal(200);
      expect(response.body).to.be.an('array');
      expect(response.body).to.have.lengthOf(1);
      expect(response.body[0].item_name).to.equal('Engine');
    });

    it('should support multiple day thresholds', async () => {
      const response90 = await request(app)
        .get('/api/warranties/expiring?days=90')
        .set('Authorization', `Bearer ${validToken}`);

      expect(response90.status).to.equal(200);
      expect(response90.body).to.have.lengthOf(2); // Both warranties within 90 days
    });

    it('should filter by boat_id when provided', async () => {
      // Create another boat
      const otherBoatId = 'boat-other';
      db.prepare(`
        INSERT INTO entities (id, organization_id, user_id, entity_type, name, created_at, updated_at)
        VALUES (?, ?, ?, ?, ?, ?, ?)
      `).run(otherBoatId, 'org-123', userId, 'boat', 'Other Boat', Date.now(), Date.now());

      const response = await request(app)
        .get(`/api/warranties/expiring?days=30&boat_id=${boatId}`)
        .set('Authorization', `Bearer ${validToken}`);

      expect(response.status).to.equal(200);
      expect(response.body).to.have.lengthOf(1);
    });
  });

  describe('PUT /api/warranties/:id', () => {
    beforeEach(() => {
      db.prepare(`
        INSERT INTO warranty_tracking (
          id, boat_id, item_name, provider, purchase_date, warranty_period_months,
          expiration_date, coverage_amount, status, created_at, updated_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(
        mockWarranty.id,
        boatId,
        mockWarranty.item_name,
        mockWarranty.provider,
        mockWarranty.purchase_date,
        mockWarranty.warranty_period_months,
        mockWarranty.expiration_date,
        mockWarranty.coverage_amount,
        mockWarranty.status,
        Date.now(),
        Date.now()
      );
    });

    it('should update warranty status', async () => {
      const response = await request(app)
        .put(`/api/warranties/${mockWarranty.id}`)
        .set('Authorization', `Bearer ${validToken}`)
        .send({ status: 'claimed' });

      expect(response.status).to.equal(200);
      expect(response.body.status).to.equal('claimed');
    });

    it('should update coverage amount', async () => {
      const response = await request(app)
        .put(`/api/warranties/${mockWarranty.id}`)
        .set('Authorization', `Bearer ${validToken}`)
        .send({ coverage_amount: 60000 });

      expect(response.status).to.equal(200);
      expect(response.body.coverage_amount).to.equal(60000);
    });
  });

  describe('DELETE /api/warranties/:id', () => {
    beforeEach(() => {
      db.prepare(`
        INSERT INTO warranty_tracking (
          id, boat_id, item_name, provider, purchase_date, warranty_period_months,
          expiration_date, coverage_amount, status, created_at, updated_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(
        mockWarranty.id,
        boatId,
        mockWarranty.item_name,
        mockWarranty.provider,
        mockWarranty.purchase_date,
        mockWarranty.warranty_period_months,
        mockWarranty.expiration_date,
        mockWarranty.coverage_amount,
        mockWarranty.status,
        Date.now(),
        Date.now()
      );
    });

    it('should soft delete warranty', async () => {
      const response = await request(app)
        .delete(`/api/warranties/${mockWarranty.id}`)
        .set('Authorization', `Bearer ${validToken}`);

      expect(response.status).to.equal(204);

      // Verify soft delete (status changed to deleted)
      const warranty = db.prepare('SELECT status FROM warranty_tracking WHERE id = ?')
        .get(mockWarranty.id);
      expect(warranty.status).to.equal('deleted');
    });
  });
});

Example 2: Background Job Integration Tests

File: /home/user/navidocs/server/test/integration/jobs/warranty-expiration-job.test.js

import { expect } from 'chai';
import sinon from 'sinon';
import { setupTestDatabase, resetDatabase, teardownTestDatabase, db } from '../setup.js';
import warrantyExpirationWorker from '../../../workers/warranty-expiration.worker.js';
import eventBus from '../../../services/event-bus.service.js';

describe('Warranty Expiration Background Job', () => {
  before(() => {
    setupTestDatabase();
  });

  beforeEach(() => {
    resetDatabase();
  });

  after(() => {
    teardownTestDatabase();
  });

  it('should publish WARRANTY_EXPIRING event for warranties expiring in 30 days', async () => {
    const publishSpy = sinon.spy(eventBus, 'publish');

    // Create warranty expiring in 30 days
    const in30Days = new Date();
    in30Days.setDate(in30Days.getDate() + 30);
    const in30DaysStr = in30Days.toISOString().split('T')[0];

    db.prepare(`
      INSERT INTO warranty_tracking (
        id, boat_id, item_name, provider, purchase_date, warranty_period_months,
        expiration_date, coverage_amount, status, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      'warranty-123',
      'boat-123',
      'Engine',
      'Caterpillar',
      '2023-01-15',
      24,
      in30DaysStr,
      50000,
      'active',
      Date.now(),
      Date.now()
    );

    await warrantyExpirationWorker.process();

    expect(publishSpy.calledWith('WARRANTY_EXPIRING')).to.be.true;
    expect(publishSpy.getCall(0).args[1]).to.have.property('warranty_id', 'warranty-123');

    publishSpy.restore();
  });

  it('should not duplicate notifications for already notified warranties', async () => {
    const in30Days = new Date();
    in30Days.setDate(in30Days.getDate() + 30);
    const in30DaysStr = in30Days.toISOString().split('T')[0];

    // Create warranty with notification already sent
    db.prepare(`
      INSERT INTO warranty_tracking (
        id, boat_id, item_name, provider, purchase_date, warranty_period_months,
        expiration_date, coverage_amount, status, created_at, updated_at,
        notification_sent_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      'warranty-123',
      'boat-123',
      'Engine',
      'Caterpillar',
      '2023-01-15',
      24,
      in30DaysStr,
      50000,
      'active',
      Date.now(),
      Date.now(),
      Date.now() // Already notified
    );

    const publishSpy = sinon.spy(eventBus, 'publish');
    await warrantyExpirationWorker.process();

    expect(publishSpy.called).to.be.false;
    publishSpy.restore();
  });

  it('should handle multiple notification thresholds (90/30/14 days)', async () => {
    const publishSpy = sinon.spy(eventBus, 'publish');

    // Create warranties at different expiration points
    const in14Days = new Date();
    in14Days.setDate(in14Days.getDate() + 14);

    const in30Days = new Date();
    in30Days.setDate(in30Days.getDate() + 30);

    const in90Days = new Date();
    in90Days.setDate(in90Days.getDate() + 90);

    db.prepare(`
      INSERT INTO warranty_tracking (
        id, boat_id, item_name, provider, purchase_date, warranty_period_months,
        expiration_date, coverage_amount, status, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run('warranty-14', 'boat-123', 'Engine', 'Caterpillar', '2023-01-15', 24,
           in14Days.toISOString().split('T')[0], 50000, 'active', Date.now(), Date.now());

    db.prepare(`
      INSERT INTO warranty_tracking (
        id, boat_id, item_name, provider, purchase_date, warranty_period_months,
        expiration_date, coverage_amount, status, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run('warranty-30', 'boat-123', 'Generator', 'Honda', '2023-01-15', 24,
           in30Days.toISOString().split('T')[0], 30000, 'active', Date.now(), Date.now());

    db.prepare(`
      INSERT INTO warranty_tracking (
        id, boat_id, item_name, provider, purchase_date, warranty_period_months,
        expiration_date, coverage_amount, status, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run('warranty-90', 'boat-123', 'Batteries', 'Victron', '2023-01-15', 24,
           in90Days.toISOString().split('T')[0], 20000, 'active', Date.now(), Date.now());

    await warrantyExpirationWorker.process();

    expect(publishSpy.callCount).to.equal(3);

    publishSpy.restore();
  });
});

Part 3: End-to-End Testing Strategy

3.1 E2E Test Scope

Target: 10 critical user flows covering core features

Critical Flows to Test:

  1. User Registration → Create Boat → Upload Warranty Document
  2. Warranty Expiration Alert → Generate Claim Package → Download
  3. Sale Workflow → Generate As-Built Package → Transfer to Buyer
  4. Home Assistant Integration → Register Webhook → Verify Event Delivery
  5. User Authentication → Login → Access Protected Resources
  6. Create Multiple Warranties → Filter by Status → Update Warranty
  7. Organization Setup → Invite Team Member → Verify Permissions
  8. Offline Mode → Navigate Critical Documents → Sync When Online
  9. Search Documents → Filter Results → View Details
  10. Notification Settings → Configure Alerts → Receive Notification

3.2 E2E Testing Setup

Dependencies & Configuration

Playwright is already installed. Create /home/user/navidocs/client/playwright.config.js:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,

  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results/results.json' }],
    ['junit', { outputFile: 'test-results/results.xml' }]
  ],

  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:5173',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure'
  },

  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: !process.env.CI,
    timeout: 120 * 1000
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] }
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] }
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] }
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] }
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] }
    }
  ]
});

Add scripts to /home/user/navidocs/client/package.json:

{
  "scripts": {
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "test:e2e:headed": "playwright test --headed",
    "test:e2e:chrome": "playwright test --project=chromium"
  }
}

3.3 E2E Test Examples

Example 1: Warranty Expiration Alert Flow

File: /home/user/navidocs/client/tests/e2e/warranty-alerts.spec.js

import { test, expect } from '@playwright/test';

test.describe('Warranty Expiration Alert Flow', () => {
  test.beforeEach(async ({ page }) => {
    // Login before each test
    await page.goto('/');
    await page.fill('[data-testid="email-input"]', 'demo@example.com');
    await page.fill('[data-testid="password-input"]', 'demo-password');
    await page.click('[data-testid="login-button"]');
    await page.waitForURL('/dashboard');
  });

  test('should display warranty expiration alert badge', async ({ page }) => {
    // Navigate to boat with expiring warranty
    await page.click('text=Azimut 55S');
    await page.waitForURL('/boats/boat-123');

    // Verify alert badge appears
    const alertBadge = page.locator('[data-testid="warranty-alert-badge"]');
    await expect(alertBadge).toBeVisible();
    await expect(alertBadge).toContainText('Expires in 28 days');
  });

  test('should navigate to warranty details from alert', async ({ page }) => {
    await page.click('text=Azimut 55S');
    await page.click('[data-testid="warranty-alert-badge"]');
    await page.waitForURL('/boats/boat-123/warranties**');

    // Verify warranty table is visible
    const warrantyTable = page.locator('[data-testid="warranty-table"]');
    await expect(warrantyTable).toBeVisible();
  });

  test('should generate claim package for expiring warranty', async ({ page }) => {
    // Navigate to boat with expiring warranty
    await page.click('text=Azimut 55S');
    await page.click('[data-testid="warranty-alert-badge"]');

    // Find expiring warranty in table
    const warrantyRow = page.locator('tr').filter({ hasText: 'Engine' });
    await warrantyRow.locator('[data-testid="claim-package-button"]').click();

    // Verify claim package modal appears
    const modal = page.locator('[data-testid="claim-package-modal"]');
    await expect(modal).toBeVisible();

    // Verify claim package contents
    await expect(modal.locator('text=Warranty Document')).toBeVisible();
    await expect(modal.locator('text=Purchase Invoice')).toBeVisible();
    await expect(modal.locator('text=Claim Form')).toBeVisible();

    // Download claim package
    const downloadPromise = page.waitForEvent('download');
    await modal.locator('[data-testid="download-button"]').click();
    const download = await downloadPromise;

    // Verify download
    expect(download.suggestedFilename()).toMatch(/claim-package.*\.zip/);
  });

  test('should update warranty status to claimed', async ({ page }) => {
    await page.click('text=Azimut 55S');
    await page.click('[data-testid="warranty-alert-badge"]');

    // Find warranty and mark as claimed
    const warrantyRow = page.locator('tr').filter({ hasText: 'Engine' });
    await warrantyRow.locator('[data-testid="status-dropdown"]').click();
    await page.click('text=Claimed');

    // Verify status updated
    await expect(warrantyRow.locator('[data-testid="status-badge"]')).toContainText('Claimed');

    // Verify alert badge disappears
    const alertBadge = page.locator('[data-testid="warranty-alert-badge"]');
    await expect(alertBadge).not.toBeVisible();
  });

  test('should handle warranty past expiration date', async ({ page }) => {
    // Create warranty that already expired
    await page.goto('/boats/boat-123/warranties');
    await page.click('[data-testid="add-warranty-button"]');

    const pastDate = new Date();
    pastDate.setMonth(pastDate.getMonth() - 3);
    const formattedDate = pastDate.toISOString().split('T')[0];

    await page.fill('[data-testid="purchase-date-input"]', '2023-01-15');
    await page.fill('[data-testid="warranty-months-input"]', '12'); // Expired now
    await page.click('[data-testid="save-warranty-button"]');

    // Verify expired badge appears
    const expiredBadge = page.locator('[data-testid="expired-badge"]');
    await expect(expiredBadge).toBeVisible();
  });
});

Example 2: Sale Workflow E2E Test

File: /home/user/navidocs/client/tests/e2e/sale-workflow.spec.js

import { test, expect } from '@playwright/test';

test.describe('Yacht Sale Workflow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
    await page.fill('[data-testid="email-input"]', 'seller@example.com');
    await page.fill('[data-testid="password-input"]', 'password');
    await page.click('[data-testid="login-button"]');
    await page.waitForURL('/dashboard');
  });

  test('should initiate sale and generate as-built package', async ({ page }) => {
    // Navigate to boat
    await page.click('text=Azimut 55S');
    await page.waitForURL('/boats/boat-123');

    // Click "List for Sale" button
    await page.click('[data-testid="list-for-sale-button"]');
    await page.waitForURL('/boats/boat-123/sale');

    // Fill sale details
    await page.fill('[data-testid="buyer-email-input"]', 'buyer@example.com');
    await page.fill('[data-testid="transfer-date-input"]', '2025-12-20');

    // Verify all documents are included
    const documentList = page.locator('[data-testid="document-list"]');
    await expect(documentList).toBeVisible();

    const documents = await documentList.locator('li').count();
    expect(documents).toBeGreaterThan(0);

    // Generate package
    await page.click('[data-testid="generate-package-button"]');
    await page.waitForSelector('[data-testid="package-generated-message"]');

    // Verify success message
    await expect(page.locator('[data-testid="package-generated-message"]')).toContainText(
      'Package generated successfully'
    );
  });

  test('should transfer documents to buyer', async ({ page }) => {
    // Setup: Create sale with generated package
    // (Assuming package is already generated from previous test)

    await page.click('text=Azimut 55S');
    await page.click('[data-testid="sale-status-badge"]');
    await page.waitForURL('/boats/boat-123/sale');

    // Click "Transfer to Buyer" button
    await page.click('[data-testid="transfer-button"]');

    // Verify confirmation dialog
    const dialog = page.locator('[data-testid="transfer-confirmation-dialog"]');
    await expect(dialog).toBeVisible();
    await expect(dialog).toContainText('buyer@example.com');

    // Confirm transfer
    await dialog.locator('[data-testid="confirm-button"]').click();

    // Verify transfer completed
    await expect(page.locator('[data-testid="transfer-success-message"]')).toBeVisible();
    await expect(page.locator('[data-testid="sale-status-badge"]')).toContainText('Transferred');
  });

  test('should send buyer notification email with download link', async ({ page, context }) => {
    // Intercept email notification request
    let emailSent = false;
    let downloadLink = null;

    page.on('request', (request) => {
      if (request.url().includes('/api/send-email')) {
        emailSent = true;
        downloadLink = request.postDataJSON().downloadLink;
      }
    });

    // Navigate to sale transfer
    await page.click('text=Azimut 55S');
    await page.click('[data-testid="sale-status-badge"]');
    await page.click('[data-testid="transfer-button"]');
    await page.locator('[data-testid="transfer-confirmation-dialog"] [data-testid="confirm-button"]').click();

    // Verify email was sent
    await page.waitForTimeout(1000);
    expect(emailSent).toBe(true);
    expect(downloadLink).toBeTruthy();

    // Verify download link works (in different context/incognito)
    const buyerPage = await context.newPage();
    const response = await buyerPage.goto(downloadLink);
    expect(response?.status()).toBe(200);

    // Verify ZIP file is downloadable
    const contentType = response?.headers()['content-type'];
    expect(contentType).toContain('application/zip');
  });

  test('should organize documents in package folders', async ({ page }) => {
    await page.click('text=Azimut 55S');
    await page.click('[data-testid="sale-status-badge"]');
    await page.click('[data-testid="package-contents-button"]');

    // Verify folder structure in preview
    const folderStructure = page.locator('[data-testid="folder-tree"]');
    await expect(folderStructure).toContainText('Registration');
    await expect(folderStructure).toContainText('Surveys');
    await expect(folderStructure).toContainText('Warranties');
    await expect(folderStructure).toContainText('Engine Manuals');
  });
});

Example 3: Home Assistant Integration E2E Test

File: /home/user/navidocs/client/tests/e2e/home-assistant-integration.spec.js

import { test, expect } from '@playwright/test';

test.describe('Home Assistant Integration', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
    await page.fill('[data-testid="email-input"]', 'admin@example.com');
    await page.fill('[data-testid="password-input"]', 'password');
    await page.click('[data-testid="login-button"]');
    await page.waitForURL('/dashboard');
  });

  test('should register Home Assistant webhook', async ({ page }) => {
    // Navigate to integrations
    await page.click('[data-testid="settings-button"]');
    await page.click('[data-testid="integrations-tab"]');

    // Click "Add Integration"
    await page.click('[data-testid="add-integration-button"]');
    await page.click('text=Home Assistant');

    // Fill webhook details
    await page.fill(
      '[data-testid="webhook-url-input"]',
      'https://ha.example.com/api/webhook/navidocs-12345'
    );

    // Select topics to subscribe
    await page.click('[data-testid="topic-WARRANTY_EXPIRING"]');
    await page.click('[data-testid="topic-DOCUMENT_UPLOADED"]');

    // Submit form
    await page.click('[data-testid="register-webhook-button"]');

    // Verify success
    await expect(page.locator('[data-testid="webhook-registered-message"]')).toBeVisible();
  });

  test('should validate webhook URL reachability', async ({ page }) => {
    await page.click('[data-testid="settings-button"]');
    await page.click('[data-testid="integrations-tab"]');
    await page.click('[data-testid="add-integration-button"]');
    await page.click('text=Home Assistant');

    // Enter unreachable URL
    await page.fill(
      '[data-testid="webhook-url-input"]',
      'https://unreachable-host-12345.example.com/webhook'
    );

    // Try to register
    await page.click('[data-testid="register-webhook-button"]');

    // Verify error message
    await expect(page.locator('[data-testid="webhook-error-message"]')).toContainText(
      'Unable to reach webhook URL'
    );
  });

  test('should display registered integrations', async ({ page }) => {
    // Assume webhook is already registered
    await page.click('[data-testid="settings-button"]');
    await page.click('[data-testid="integrations-tab"]');

    // Verify registered webhook appears in list
    const integrationCard = page.locator('[data-testid="integration-card-home-assistant"]');
    await expect(integrationCard).toBeVisible();
    await expect(integrationCard).toContainText('Home Assistant');
    await expect(integrationCard).toContainText('https://ha.example.com');
  });

  test('should deactivate/delete webhook integration', async ({ page }) => {
    await page.click('[data-testid="settings-button"]');
    await page.click('[data-testid="integrations-tab"]');

    // Find and click delete on integration
    const integrationCard = page.locator('[data-testid="integration-card-home-assistant"]');
    await integrationCard.locator('[data-testid="delete-button"]').click();

    // Confirm deletion
    const confirmDialog = page.locator('[data-testid="delete-confirmation-dialog"]');
    await expect(confirmDialog).toBeVisible();
    await confirmDialog.locator('[data-testid="confirm-button"]').click();

    // Verify integration removed
    await expect(integrationCard).not.toBeVisible();
  });
});

Part 4: Test Data Generation Strategy

4.1 Test Data Approach

Principle: Generate realistic, consistent, and reproducible test data

4.2 Database Seeding

Create /home/user/navidocs/server/test/fixtures/seeds.js:

import Database from 'better-sqlite3';
import { v4 as uuid } from 'uuid';
import bcrypt from 'bcrypt';

export class TestDataSeeder {
  constructor(db) {
    this.db = db;
  }

  /**
   * Create test user
   */
  async createUser(overrides = {}) {
    const user = {
      id: uuid(),
      email: overrides.email || `user-${Date.now()}@example.com`,
      name: overrides.name || 'Test User',
      password_hash: await bcrypt.hash('password123', 10),
      created_at: Date.now(),
      updated_at: Date.now(),
      ...overrides
    };

    this.db.prepare(`
      INSERT INTO users (id, email, name, password_hash, created_at, updated_at)
      VALUES (?, ?, ?, ?, ?, ?)
    `).run(user.id, user.email, user.name, user.password_hash, user.created_at, user.updated_at);

    return user;
  }

  /**
   * Create test organization
   */
  createOrganization(overrides = {}) {
    const org = {
      id: uuid(),
      name: overrides.name || `Test Org ${Date.now()}`,
      type: overrides.type || 'personal',
      created_at: Date.now(),
      updated_at: Date.now(),
      ...overrides
    };

    this.db.prepare(`
      INSERT INTO organizations (id, name, type, created_at, updated_at)
      VALUES (?, ?, ?, ?, ?)
    `).run(org.id, org.name, org.type, org.created_at, org.updated_at);

    return org;
  }

  /**
   * Create test boat entity
   */
  createBoat(userId, orgId, overrides = {}) {
    const boat = {
      id: uuid(),
      organization_id: orgId,
      user_id: userId,
      entity_type: 'boat',
      name: overrides.name || `Test Boat ${Date.now()}`,
      make: overrides.make || 'Azimut',
      model: overrides.model || '55S',
      year: overrides.year || 2020,
      hull_id: overrides.hull_id || uuid().substring(0, 16),
      vessel_type: overrides.vessel_type || 'powerboat',
      length_feet: overrides.length_feet || 55,
      created_at: Date.now(),
      updated_at: Date.now(),
      ...overrides
    };

    this.db.prepare(`
      INSERT INTO entities (
        id, organization_id, user_id, entity_type, name, make, model, year,
        hull_id, vessel_type, length_feet, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      boat.id, boat.organization_id, boat.user_id, boat.entity_type, boat.name,
      boat.make, boat.model, boat.year, boat.hull_id, boat.vessel_type,
      boat.length_feet, boat.created_at, boat.updated_at
    );

    return boat;
  }

  /**
   * Create test warranty
   */
  createWarranty(boatId, overrides = {}) {
    const purchaseDate = overrides.purchase_date || '2023-01-15';
    const warrantyMonths = overrides.warranty_period_months || 24;

    // Calculate expiration date
    const purchaseDateTime = new Date(purchaseDate);
    purchaseDateTime.setMonth(purchaseDateTime.getMonth() + warrantyMonths);
    const expirationDate = purchaseDateTime.toISOString().split('T')[0];

    const warranty = {
      id: uuid(),
      boat_id: boatId,
      item_name: overrides.item_name || 'Engine',
      provider: overrides.provider || 'Caterpillar',
      purchase_date: purchaseDate,
      warranty_period_months: warrantyMonths,
      expiration_date: expirationDate,
      coverage_amount: overrides.coverage_amount || 50000,
      claim_instructions: overrides.claim_instructions || 'Contact provider',
      status: overrides.status || 'active',
      created_at: Date.now(),
      updated_at: Date.now(),
      ...overrides
    };

    this.db.prepare(`
      INSERT INTO warranty_tracking (
        id, boat_id, item_name, provider, purchase_date, warranty_period_months,
        expiration_date, coverage_amount, claim_instructions, status, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      warranty.id, warranty.boat_id, warranty.item_name, warranty.provider,
      warranty.purchase_date, warranty.warranty_period_months, warranty.expiration_date,
      warranty.coverage_amount, warranty.claim_instructions, warranty.status,
      warranty.created_at, warranty.updated_at
    );

    return warranty;
  }

  /**
   * Create multiple warranties for a boat (bulk seeding)
   */
  createWarranties(boatId, count = 3) {
    const warranties = [];
    const items = ['Engine', 'Generator', 'Batteries', 'Air Conditioning', 'Navigation System'];

    for (let i = 0; i < count; i++) {
      const warranty = this.createWarranty(boatId, {
        item_name: items[i % items.length],
        purchase_date: `${2023 + Math.floor(i / 2)}-01-15`,
        warranty_period_months: 12 + (i * 6)
      });
      warranties.push(warranty);
    }

    return warranties;
  }

  /**
   * Create test webhook
   */
  createWebhook(orgId, overrides = {}) {
    const webhook = {
      id: uuid(),
      organization_id: orgId,
      url: overrides.url || `https://example.com/webhook/${uuid()}`,
      topics: overrides.topics || JSON.stringify(['WARRANTY_EXPIRING', 'DOCUMENT_UPLOADED']),
      secret: overrides.secret || uuid(),
      status: overrides.status || 'active',
      created_at: Date.now(),
      ...overrides
    };

    this.db.prepare(`
      INSERT INTO webhooks (
        id, organization_id, url, topics, secret, status, created_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?)
    `).run(
      webhook.id, webhook.organization_id, webhook.url, webhook.topics,
      webhook.secret, webhook.status, webhook.created_at
    );

    return webhook;
  }
}

/**
 * Helper function to create complete test scenario
 */
export async function setupTestScenario(db, overrides = {}) {
  const seeder = new TestDataSeeder(db);

  // Create user
  const user = await seeder.createUser(overrides.user || {});

  // Create organization
  const org = seeder.createOrganization(overrides.org || {});

  // Link user to organization
  db.prepare(`
    INSERT INTO user_organizations (user_id, organization_id, role, joined_at)
    VALUES (?, ?, ?, ?)
  `).run(user.id, org.id, 'admin', Date.now());

  // Create boat
  const boat = seeder.createBoat(user.id, org.id, overrides.boat || {});

  // Create warranties
  const warrantyCount = overrides.warrantyCount || 3;
  const warranties = seeder.createWarranties(boat.id, warrantyCount);

  // Create webhook if specified
  let webhook = null;
  if (overrides.webhook !== false) {
    webhook = seeder.createWebhook(org.id, overrides.webhook || {});
  }

  return {
    user,
    org,
    boat,
    warranties,
    webhook,
    seeder
  };
}

4.3 Faker-Based Generation for Complex Data

Add faker library:

npm install --save-dev @faker-js/faker

Create /home/user/navidocs/server/test/fixtures/fake-data-generator.js:

import { faker } from '@faker-js/faker';
import { v4 as uuid } from 'uuid';

export class FakeDataGenerator {
  /**
   * Generate realistic user data
   */
  static generateUser() {
    return {
      id: uuid(),
      email: faker.internet.email(),
      name: faker.person.fullName(),
      password_hash: 'hashed_' + faker.string.uuid(),
      created_at: faker.date.past().getTime(),
      updated_at: Date.now()
    };
  }

  /**
   * Generate realistic boat data
   */
  static generateBoat(userId, orgId) {
    const makes = ['Azimut', 'Sunseeker', 'Ferretti', 'Benetti', 'Lürssen', 'Heesen'];
    const types = ['powerboat', 'sailboat', 'catamaran', 'trawler'];
    const make = faker.helpers.arrayElement(makes);

    return {
      id: uuid(),
      organization_id: orgId,
      user_id: userId,
      entity_type: 'boat',
      name: `${make} ${faker.string.numeric(4)}`,
      make,
      model: faker.string.numeric(2) + 'S',
      year: faker.datatype.number({ min: 2010, max: 2024 }),
      hull_id: faker.string.alphanumeric(17).toUpperCase(),
      vessel_type: faker.helpers.arrayElement(types),
      length_feet: faker.datatype.number({ min: 35, max: 200 }),
      created_at: faker.date.past().getTime(),
      updated_at: Date.now()
    };
  }

  /**
   * Generate realistic warranty data
   */
  static generateWarranty(boatId) {
    const purchaseDate = faker.date.past({ years: 3 });
    const warrantyMonths = faker.datatype.number({ min: 12, max: 60 });
    const expirationDate = new Date(purchaseDate);
    expirationDate.setMonth(expirationDate.getMonth() + warrantyMonths);

    const items = ['Engine', 'Generator', 'Batteries', 'Air Conditioning',
                   'Navigation System', 'Water Maker', 'Autopilot', 'Refrigeration'];
    const providers = ['Caterpillar', 'Honda', 'Yachtcare', 'Marine Tech', 'Naval Academy'];

    return {
      id: uuid(),
      boat_id: boatId,
      item_name: faker.helpers.arrayElement(items),
      provider: faker.helpers.arrayElement(providers),
      purchase_date: purchaseDate.toISOString().split('T')[0],
      warranty_period_months: warrantyMonths,
      expiration_date: expirationDate.toISOString().split('T')[0],
      coverage_amount: faker.datatype.number({ min: 10000, max: 100000 }),
      claim_instructions: faker.lorem.sentences(2),
      status: faker.helpers.arrayElement(['active', 'expired', 'claimed']),
      created_at: faker.date.past().getTime(),
      updated_at: Date.now()
    };
  }
}

Part 5: Test Execution & CI/CD Integration

5.1 GitHub Actions Workflow

Create /home/user/navidocs/.github/workflows/test.yml:

name: Comprehensive Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  unit-tests:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x, 20.x]

    steps:
      - uses: actions/checkout@v3

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install server dependencies
        run: cd server && npm ci

      - name: Run unit tests
        run: cd server && npm run test:unit

      - name: Generate coverage report
        run: cd server && npm run test:unit:coverage

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./server/coverage/lcov.info
          flags: unit-tests
          name: codecov-unit

  integration-tests:
    runs-on: ubuntu-latest

    services:
      redis:
        image: redis:7
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v3

      - name: Use Node.js 20.x
        uses: actions/setup-node@v3
        with:
          node-version: 20.x
          cache: 'npm'

      - name: Install server dependencies
        run: cd server && npm ci

      - name: Run integration tests
        run: cd server && npm run test:integration
        env:
          REDIS_URL: redis://localhost:6379
          DATABASE_PATH: :memory:

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: integration-test-results
          path: server/test-results/

  e2e-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Use Node.js 20.x
        uses: actions/setup-node@v3
        with:
          node-version: 20.x
          cache: 'npm'

      - name: Install client dependencies
        run: cd client && npm ci

      - name: Install Playwright browsers
        run: cd client && npx playwright install --with-deps

      - name: Start backend server
        run: |
          cd server && npm ci
          npm start &
          sleep 5

      - name: Run E2E tests
        run: cd client && npm run test:e2e

      - name: Upload Playwright report
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          path: client/playwright-report/
          retention-days: 30

  coverage:
    runs-on: ubuntu-latest
    needs: [unit-tests, integration-tests]

    steps:
      - uses: actions/checkout@v3

      - name: Download coverage artifacts
        uses: actions/download-artifact@v3
        with:
          name: unit-coverage

      - name: Check coverage thresholds
        run: |
          # Parse coverage report and check thresholds
          # This is a simplified example - you may use nyc or another tool
          echo "Coverage check passed"

  test-report:
    runs-on: ubuntu-latest
    if: always()
    needs: [unit-tests, integration-tests, e2e-tests]

    steps:
      - name: Generate test report
        run: |
          echo "Test Report Summary"
          echo "==================="
          echo "Unit Tests: ${{ needs.unit-tests.result }}"
          echo "Integration Tests: ${{ needs.integration-tests.result }}"
          echo "E2E Tests: ${{ needs.e2e-tests.result }}"

5.2 Package.json Scripts for Local Testing

Update /home/user/navidocs/server/package.json:

{
  "scripts": {
    "test": "mocha",
    "test:watch": "mocha --watch --watch-extensions js",
    "test:unit": "mocha test/unit/**/*.test.js",
    "test:unit:watch": "mocha --watch test/unit/**/*.test.js",
    "test:unit:coverage": "nyc mocha test/unit/**/*.test.js",
    "test:integration": "mocha test/integration/**/*.test.js",
    "test:integration:watch": "mocha --watch test/integration/**/*.test.js",
    "test:all": "npm run test:unit && npm run test:integration",
    "test:all:coverage": "nyc mocha test/**/*.test.js",
    "test:coverage:report": "nyc report --reporter=html && open coverage/index.html"
  }
}

Update /home/user/navidocs/client/package.json:

{
  "scripts": {
    "test": "playwright test",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "test:e2e:debug": "playwright test --debug",
    "test:e2e:headed": "playwright test --headed",
    "test:e2e:chrome": "playwright test --project=chromium",
    "test:e2e:report": "playwright show-report"
  }
}

Part 6: Testing Best Practices & Coverage Targets

6.1 Coverage Targets by Layer

Layer Tool Target Coverage Focus Areas
Unit Tests Mocha + Chai 70% Services, utilities, middleware, validators
Integration Tests Supertest 50% API endpoints, database ops, job execution
E2E Tests Playwright 10 critical flows Full user workflows, cross-system interactions

6.2 Test Naming Conventions

// ✅ Good: Descriptive, specific, matches Given/When/Then format
describe('WarrantyService.calculateExpirationDate', () => {
  it('should add warranty period months to purchase date correctly', () => {});
  it('should handle leap year month boundary correctly', () => {});
});

// ❌ Bad: Vague, unclear intent
describe('Warranty tests', () => {
  it('works', () => {});
  it('test expiration', () => {});
});

6.3 Test Isolation & Cleanup

describe('API Endpoints', () => {
  beforeEach(() => {
    // Setup fresh test state
    setupTestDatabase();
    seedTestData();
  });

  afterEach(() => {
    // Cleanup
    resetDatabase();
    sinon.restore(); // Clear stubs
  });

  it('should do something', async () => {
    // Test code
  });
});

6.4 Mocking Strategy

// ✅ Good: Mock external dependencies, test internal logic
const mailerStub = sinon.stub(emailService, 'send').resolves({ messageId: 'msg-123' });

// ❌ Bad: Mocking too much, defeats the purpose of integration tests
const dbStub = sinon.stub(db, 'prepare'); // Don't mock DB in integration tests

6.5 Assertion Best Practices

// ✅ Good: Specific assertions with meaningful messages
expect(response.status).to.equal(201);
expect(response.body).to.have.property('id');
expect(response.body.expiration_date).to.equal('2025-01-15');

// ❌ Bad: Vague assertions
expect(response).to.be.ok;
expect(result).to.equal(true);

Part 7: Running Tests & Monitoring

7.1 Local Test Execution

# Unit tests only
npm run test:unit

# Unit tests with coverage
npm run test:unit:coverage

# Watch mode for TDD
npm run test:unit:watch

# All tests
npm run test:all

# Generate HTML coverage report
npm run test:coverage:report

# E2E tests
npm run test:e2e

# E2E tests in UI mode
npm run test:e2e:ui

# E2E with headless browser
npm run test:e2e:headed

7.2 Coverage Report Interpretation

Coverage metrics:

  • Statements: Individual executable statements
  • Branches: Conditional paths (if/else)
  • Functions: Callable functions
  • Lines: Physical lines of code

Target minimums:

  • Statements: 70%
  • Branches: 65%
  • Functions: 70%
  • Lines: 70%

7.3 Continuous Integration Checks

The GitHub Actions workflow will:

  1. Run unit tests on every PR
  2. Generate coverage reports
  3. Fail PR if coverage drops below thresholds
  4. Run integration tests with in-memory database
  5. Execute E2E tests against test environment
  6. Generate consolidated test report

Summary

This testing strategy provides:

Comprehensive coverage across all layers (unit, integration, E2E) Realistic test data generation with Faker and seeding utilities Clear organization with dedicated test directories Automation via GitHub Actions for continuous testing Best practices for assertion, mocking, and test isolation Documentation and examples for each testing layer Performance monitoring with coverage reports

Next Steps:

  1. Install testing dependencies (Mocha, Chai, Supertest, Playwright)
  2. Create test directory structure
  3. Implement test setup and fixtures
  4. Write unit tests for service layer (target: 70% coverage)
  5. Write integration tests for APIs (target: 50% coverage)
  6. Create E2E test suite (target: 10 critical flows)
  7. Configure CI/CD pipeline
  8. Monitor coverage trends over time

IF.bus Protocol - Status Update:

  • S4-H06 testing strategy complete
  • Ready to transmit to S4-H10 for deployment validation