const { spawn } = require('child_process'); const port = 3999; const baseUrl = `http://127.0.0.1:${port}`; const child = spawn(process.execPath, ['server/index.js'], { cwd: process.cwd(), env: { ...process.env, PORT: String(port) }, stdio: ['ignore', 'pipe', 'pipe'] }); function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function request(path, options = {}) { const response = await fetch(`${baseUrl}${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...(options.headers || {}) } }); const body = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(`${path} failed: ${response.status} ${JSON.stringify(body)}`); } return body; } async function waitForServer() { for (let attempt = 0; attempt < 60; attempt += 1) { try { await request('/api/health'); return; } catch { await wait(250); } } throw new Error('Server did not start'); } async function main() { await waitForServer(); const login = await request('/api/auth/login', { method: 'POST', body: JSON.stringify({ email: 'admin@mixedassets.local', password: process.env.MIXEDASSETS_ADMIN_PASSWORD || 'ChangeMe123!' }) }); const auth = { Authorization: `Bearer ${login.token}` }; const profile = await request('/api/profile', { headers: auth }); if (profile.preferences.theme !== 'mixedAssetsDark') { throw new Error(`Expected default dark theme, found ${profile.preferences.theme}`); } const updatedProfile = await request('/api/profile', { method: 'PUT', headers: auth, body: JSON.stringify({ preferences: { theme: 'mixedAssetsDark' } }) }); if (updatedProfile.preferences.theme !== 'mixedAssetsDark') { throw new Error('Profile theme preference did not persist'); } const taxRules = await request('/api/tax-rules', { headers: auth }); const activeRule = taxRules.ruleSets.find((rule) => rule.active) || taxRules.ruleSets[0]; const methodCount = activeRule?.rules_json?.methods?.length || 0; if (methodCount !== 68) { throw new Error(`Expected 68 depreciation methods, found ${methodCount}`); } const suffix = Date.now().toString().slice(-6); const accessControl = await request('/api/access-control', { headers: auth }); if (!accessControl.roles.includes('admin') || !accessControl.roleCapabilities.some((capability) => capability.key === 'users')) { throw new Error('Access control role matrix was not returned correctly'); } const teamResult = await request('/api/teams', { method: 'POST', headers: auth, body: JSON.stringify({ name: `Smoke Team ${suffix}`, description: 'Smoke test RBAC team' }) }); const userResult = await request('/api/users', { method: 'POST', headers: auth, body: JSON.stringify({ team_id: teamResult.team.id, name: 'Smoke Test Viewer', email: `viewer-${suffix}@example.com`, password: 'ChangeMe123!', role: 'viewer', status: 'active' }) }); const updatedUser = await request(`/api/users/${userResult.user.id}`, { method: 'PUT', headers: auth, body: JSON.stringify({ team_id: teamResult.team.id, name: 'Smoke Test Operator', email: `operator-${suffix}@example.com`, role: 'operations', status: 'active' }) }); if (updatedUser.user.role !== 'operations' || updatedUser.user.team_id !== teamResult.team.id) { throw new Error('User role or team update did not persist'); } await request('/api/templates', { method: 'POST', headers: auth, body: JSON.stringify({ name: `Smoke template ${suffix}`, description: 'Template custom field smoke test', defaults: { category: 'Computer Equipment', useful_life_months: 60, depreciation_method: 'macrs_gds_200db_5' }, custom_fields: [ { key: 'employee_id', label: 'Employee ID', type: 'text', required: true }, { key: 'calibration_due', label: 'Calibration due', type: 'date', required: false } ] }) }); const templates = await request('/api/templates', { headers: auth }); const smokeTemplate = templates.templates.find((template) => template.name === `Smoke template ${suffix}`); if (!smokeTemplate || smokeTemplate.custom_fields.length !== 2) { throw new Error('Template custom fields were not saved or returned correctly'); } const assetResult = await request('/api/assets', { method: 'POST', headers: auth, body: JSON.stringify({ asset_id: `T-${suffix}`, description: 'Smoke test workstation', category: 'Computer Equipment', acquisition_cost: 2400, acquired_date: '2026-01-05', in_service_date: '2026-01-05', useful_life_months: 60, depreciation_method: 'macrs_gds_200db_5', books: [ { book_type: 'GAAP', active: true, depreciation_method: 'straight_line', convention: 'half_year', useful_life_months: 60, cost: 2400 }, { book_type: 'FEDERAL', active: true, depreciation_method: 'macrs_gds_200db_5', convention: 'half_year', useful_life_months: 60, section_179_amount: 500, bonus_percent: 0, cost: 2400 } ] }) }); const assetId = assetResult.asset.id; // Per-book persistence: GAAP and FEDERAL must keep distinct methods. const hydrated = await request(`/api/assets/${assetId}`, { headers: auth }); const gaapBook = hydrated.asset.books.find((book) => book.book_type === 'GAAP'); const federalBook = hydrated.asset.books.find((book) => book.book_type === 'FEDERAL'); if (gaapBook?.depreciation_method !== 'straight_line' || federalBook?.depreciation_method !== 'macrs_gds_200db_5') { throw new Error('Per-book depreciation methods did not persist independently'); } if (Number(federalBook.section_179_amount) !== 500) { throw new Error('Per-book Section 179 amount did not persist'); } // File cabinet (multipart upload bypasses the JSON request helper) const uploadForm = new FormData(); uploadForm.append('file', new Blob(['invoice contents'], { type: 'text/plain' }), 'invoice.txt'); const uploadResponse = await fetch(`${baseUrl}/api/assets/${assetId}/files`, { method: 'POST', headers: auth, body: uploadForm }); const fileResult = await uploadResponse.json(); if (!uploadResponse.ok || !fileResult.file?.id) throw new Error('Asset file upload did not return a file'); const fileDownload = await fetch(`${baseUrl}/api/assets/${assetId}/files/${fileResult.file.id}`, { headers: auth }); if (!fileDownload.ok || (await fileDownload.text()) !== 'invoice contents') { throw new Error('Asset file download did not return stored contents'); } // Warranty const warrantyResult = await request(`/api/assets/${assetId}/warranties`, { method: 'POST', headers: auth, body: JSON.stringify({ provider: 'Acme Care', start_date: '2026-01-05', expiration_date: '2029-01-05', coverage_details: 'Parts and labor' }) }); if (!warrantyResult.warranty?.id) throw new Error('Warranty was not created'); // Task lifecycle const taskResult = await request(`/api/assets/${assetId}/tasks`, { method: 'POST', headers: auth, body: JSON.stringify({ title: 'Calibrate device', type: 'calibration', due_date: '2026-03-01' }) }); const completedTask = await request(`/api/assets/${assetId}/tasks/${taskResult.task.id}`, { method: 'PUT', headers: auth, body: JSON.stringify({ status: 'complete' }) }); if (completedTask.task.status !== 'complete') throw new Error('Task status update did not persist'); // Notes await request(`/api/assets/${assetId}/notes`, { method: 'POST', headers: auth, body: JSON.stringify({ body: 'Received and tagged in receiving dock.' }) }); const detailed = await request(`/api/assets/${assetId}`, { headers: auth }); if (!detailed.asset.files.length || !detailed.asset.warranties.length || !detailed.asset.notes_log.length) { throw new Error('Asset detail collections (files/warranties/notes) were not returned'); } const employeeResult = await request('/api/employees', { method: 'POST', headers: auth, body: JSON.stringify({ employee_number: `EMP-${suffix}`, name: 'Smoke Test Employee', email: `employee-${suffix}@example.com`, department: 'Operations', location: 'Headquarters' }) }); await request('/api/assignments', { method: 'POST', headers: auth, body: JSON.stringify({ asset_id: assetResult.asset.id, employee_id: employeeResult.employee.id, notes: 'Smoke assignment' }) }); const assignments = await request('/api/assignments?active=true', { headers: auth }); if (!assignments.assignments.some((assignment) => assignment.asset_id === assetResult.asset.id && assignment.employee_id === employeeResult.employee.id)) { throw new Error('Asset assignment was not saved or returned correctly'); } const workday = await request('/api/workday/connection', { method: 'PUT', headers: auth, body: JSON.stringify({ name: 'Smoke Workday', tenant: 'smoke', base_url: 'https://example.workday.com', token_url: 'https://example.workday.com/oauth2/token', client_id: 'client', client_secret: 'secret', workers_path: '/workers', enabled: false }) }); if (!workday.connection.configured) { throw new Error('Workday connection configuration did not persist'); } const report = await request('/api/reports/depreciation?year=2026&book=GAAP', { headers: auth }); if (!Array.isArray(report.rows) || !report.rows.length) { throw new Error('Depreciation report returned no rows'); } console.log('Smoke test passed'); } main() .catch((error) => { console.error(error.message); process.exitCode = 1; }) .finally(() => { child.kill('SIGTERM'); });