Files
MixedAssets/server/smoke-test.js
2026-06-05 04:15:24 -05:00

1209 lines
67 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { spawn } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const http = require('http');
const os = require('os');
const path = require('path');
const port = 3999;
const baseUrl = `http://127.0.0.1:${port}`;
// Run against an isolated, freshly-seeded database so the test is deterministic
// and never reads or mutates the developer's working data.
const dbPath = path.join(os.tmpdir(), `mixedassets-smoke-${Date.now()}.sqlite`);
const child = spawn(process.execPath, ['server/index.js'], {
cwd: process.cwd(),
env: { ...process.env, PORT: String(port), MIXEDASSETS_DB_PATH: dbPath },
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');
}
const pngDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
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}` };
if (!Array.isArray(login.capabilities) || !login.capabilities.includes('*')) {
throw new Error('Login did not return admin capabilities');
}
const profile = await request('/api/profile', { headers: auth });
if (profile.preferences.theme !== 'mixedAssetsDark') {
throw new Error(`Expected default dark theme, found ${profile.preferences.theme}`);
}
if (!Array.isArray(profile.capabilities) || !profile.capabilities.includes('*')) {
throw new Error('Profile did not return capabilities');
}
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 !== 74) {
throw new Error(`Expected 74 depreciation methods, found ${methodCount}`);
}
const suffix = Date.now().toString().slice(-6);
const accessControl = await request('/api/access-control', { headers: auth });
if (!accessControl.roles.some((r) => r.key === 'admin') || !accessControl.capabilities.some((c) => c.key === 'admin.users')) {
throw new Error('Access control did not return roles + capability catalog');
}
// Built-in PM roles exist with the expected split.
const pmAdmin = accessControl.roles.find((r) => r.key === 'pm_admin');
const pmUser = accessControl.roles.find((r) => r.key === 'pm_user');
if (!pmAdmin || !pmAdmin.capabilities.includes('pm.manage')) throw new Error('PM Admin role missing or lacks pm.manage');
if (!pmUser || pmUser.capabilities.includes('pm.manage') || !pmUser.capabilities.includes('pm.complete')) {
throw new Error('PM User role should complete PM but not manage plans');
}
// Custom role: create, assign to a user, enforce its capabilities, then guard deletion.
const customRole = await request('/api/roles', {
method: 'POST', headers: auth,
body: JSON.stringify({ name: `Inspector ${suffix}`, description: 'Read + PM completion', capabilities: ['assets.view', 'pm.view', 'pm.complete'] })
});
const customRoleKey = customRole.role.key;
if (!customRoleKey || customRole.role.capabilities.includes('assets.delete')) throw new Error('Custom role was not created with sanitized capabilities');
const roleUser = await request('/api/users', {
method: 'POST', headers: auth,
body: JSON.stringify({ name: `Inspector User ${suffix}`, email: `inspector-${suffix}@example.com`, password: 'ChangeMe123!', role: customRoleKey, status: 'active' })
});
if (roleUser.user.role !== customRoleKey) throw new Error('User was not assigned the custom role');
// Sign in as the custom-role user and verify enforcement.
const roleLogin = await request('/api/auth/login', {
method: 'POST', body: JSON.stringify({ email: `inspector-${suffix}@example.com`, password: 'ChangeMe123!' })
});
if (roleLogin.capabilities.includes('*') || !roleLogin.capabilities.includes('pm.complete')) throw new Error('Custom-role login capabilities incorrect');
const roleAuth = { Authorization: `Bearer ${roleLogin.token}` };
// Allowed: viewing assets. Denied: creating an asset (needs assets.edit) and managing settings.
await request('/api/assets', { headers: roleAuth });
const deniedCreate = await fetch(`${baseUrl}/api/assets`, {
method: 'POST', headers: { ...roleAuth, 'Content-Type': 'application/json' }, body: JSON.stringify({ description: 'nope', category: 'Computer Equipment' })
});
if (deniedCreate.status !== 403) throw new Error('Custom role without assets.edit should be denied asset creation');
const deniedSettings = await fetch(`${baseUrl}/api/settings`, { headers: roleAuth });
if (deniedSettings.status !== 403) throw new Error('Custom role without admin.settings should be denied settings');
// Editing the role grants a new capability that immediately takes effect on next login.
await request(`/api/roles/${customRoleKey}`, {
method: 'PUT', headers: auth, body: JSON.stringify({ capabilities: ['assets.view', 'assets.edit', 'pm.view', 'pm.complete'] })
});
const reLogin = await request('/api/auth/login', { method: 'POST', body: JSON.stringify({ email: `inspector-${suffix}@example.com`, password: 'ChangeMe123!' }) });
const reAuth = { Authorization: `Bearer ${reLogin.token}` };
const nowAllowed = await fetch(`${baseUrl}/api/assets`, {
method: 'POST', headers: { ...reAuth, 'Content-Type': 'application/json' }, body: JSON.stringify({ description: `role asset ${suffix}`, category: 'Computer Equipment' })
});
if (nowAllowed.status !== 201) throw new Error('Role capability change did not take effect');
// Deleting a role in use is blocked; built-in roles cannot be deleted.
const blockedRoleDelete = await fetch(`${baseUrl}/api/roles/${customRoleKey}`, { method: 'DELETE', headers: auth });
if (blockedRoleDelete.status !== 400) throw new Error('Deleting a role in use should be blocked');
const blockedSystemDelete = await fetch(`${baseUrl}/api/roles/admin`, { method: 'DELETE', headers: auth });
if (blockedSystemDelete.status !== 400) throw new Error('Built-in roles must not be deletable');
const teamResult = await request('/api/teams', {
method: 'POST',
headers: auth,
body: JSON.stringify({
name: `Smoke Team ${suffix}`,
description: 'Smoke test RBAC team'
})
});
const renamedTeam = await request(`/api/teams/${teamResult.team.id}`, {
method: 'PUT',
headers: auth,
body: JSON.stringify({ name: `Smoke Team ${suffix} (edited)`, description: 'edited' })
});
if (renamedTeam.team.name !== `Smoke Team ${suffix} (edited)`) throw new Error('Team update did not persist');
const tmpTeam = await request('/api/teams', { method: 'POST', headers: auth, body: JSON.stringify({ name: `Tmp Team ${suffix}` }) });
const okDelete = await fetch(`${baseUrl}/api/teams/${tmpTeam.team.id}`, { method: 'DELETE', headers: auth });
if (okDelete.status !== 204) throw new Error('Deleting an empty team failed');
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');
}
const blockedDelete = await fetch(`${baseUrl}/api/teams/${teamResult.team.id}`, { method: 'DELETE', headers: auth });
if (blockedDelete.status !== 400) throw new Error('Deleting a team with members should be blocked');
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');
}
if (smokeTemplate.asset_count !== 0) throw new Error('New template should report zero assigned assets');
// Update the template.
const updatedTemplate = await request(`/api/templates/${smokeTemplate.id}`, {
method: 'PUT',
headers: auth,
body: JSON.stringify({
name: `Smoke template ${suffix} (edited)`,
description: 'edited',
defaults: smokeTemplate.defaults,
custom_fields: [{ key: 'employee_id', label: 'Employee ID', type: 'text', required: true }]
})
});
if (updatedTemplate.template.name !== `Smoke template ${suffix} (edited)`) throw new Error('Template update did not persist');
// Delete a disposable template and confirm it reports unlinked-asset impact.
const disposable = await request('/api/templates', {
method: 'POST', headers: auth,
body: JSON.stringify({ name: `Disposable template ${suffix}`, defaults: {}, custom_fields: [] })
});
const tmplDelete = await request(`/api/templates/${disposable.template.id}`, { method: 'DELETE', headers: auth });
if (!tmplDelete.deleted || typeof tmplDelete.unlinked_assets !== 'number') throw new Error('Template delete did not report unlink impact');
const afterDelete = await request('/api/templates', { headers: auth });
if (afterDelete.templates.some((t) => t.id === disposable.template.id)) throw new Error('Deleted template still present');
// Asset categories: create, list, rename (cascades to assets), delete.
const newCategory = await request('/api/asset-categories', {
method: 'POST', headers: auth, body: JSON.stringify({ name: `Smoke Category ${suffix}`, code: 'SMK' })
});
if (!newCategory.category?.id || newCategory.category.asset_count !== 0) throw new Error('Asset category was not created');
const dupCategory = await fetch(`${baseUrl}/api/asset-categories`, {
method: 'POST', headers: { ...auth, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: `Smoke Category ${suffix}` })
});
if (dupCategory.status !== 400) throw new Error('Duplicate category should be rejected');
// An asset using the category, to prove rename cascades.
const catAsset = await request('/api/assets', {
method: 'POST', headers: auth,
body: JSON.stringify({ asset_id: `CAT-${suffix}`, description: 'Category test', category: `Smoke Category ${suffix}`, acquisition_cost: 100 })
});
const renamedCategory = await request(`/api/asset-categories/${newCategory.category.id}`, {
method: 'PUT', headers: auth, body: JSON.stringify({ name: `Smoke Category ${suffix} (renamed)` })
});
if (renamedCategory.renamed_assets < 1) throw new Error('Category rename did not cascade to assets');
const renamedAsset = await request(`/api/assets/${catAsset.asset.id}`, { headers: auth });
if (renamedAsset.asset.category !== `Smoke Category ${suffix} (renamed)`) throw new Error('Asset category was not updated by rename');
const categoryList = await request('/api/asset-categories', { headers: auth });
const listedCategory = categoryList.categories.find((c) => c.id === newCategory.category.id);
if (!listedCategory || listedCategory.asset_count !== 1) throw new Error('Category usage count was not reported');
const catDelete = await request(`/api/asset-categories/${newCategory.category.id}`, { method: 'DELETE', headers: auth });
if (!catDelete.deleted || catDelete.asset_count !== 1) throw new Error('Category delete did not report usage impact');
// IRS asset-class reference catalog is available for pre-populating class numbers.
const irs = await request('/api/asset-classes', { headers: auth });
if (!Array.isArray(irs.assetClasses) || irs.assetClasses.length < 280) throw new Error('IRS asset-class catalog was not returned');
const computers = irs.assetClasses.find((c) => c.description === 'Computers' && c.table === 'common');
if (!computers || computers.class_number !== '00.12' || computers.gds !== 5) throw new Error('IRS common catalog entry (Computers) was incorrect');
const semiconductor = irs.assetClasses.find((c) => c.description === 'Semiconductor Manufacturing Equipment' && c.table === 'industry');
if (!semiconductor || semiconductor.class_number !== '36.1') throw new Error('IRS manufacturing-industry catalog entry was missing');
const construction = irs.assetClasses.find((c) => c.description === 'Construction' && c.table === 'business');
if (!construction || construction.class_number !== '15.0') throw new Error('IRS business-activity catalog entry was missing');
if (!irs.assetClasses[0].id) throw new Error('Asset classes should be managed records with ids');
// Asset classes are now managed: create, edit, delete.
const newClass = await request('/api/asset-classes', {
method: 'POST', headers: auth,
body: JSON.stringify({ description: `Smoke Class ${suffix}`, class_number: `99.${suffix.slice(-2)}`, gds: 7, ads: 12, source: 'custom' })
});
if (!newClass.assetClass?.id || newClass.assetClass.source !== 'custom') throw new Error('Asset class was not created');
const editedClass = await request(`/api/asset-classes/${newClass.assetClass.id}`, {
method: 'PUT', headers: auth, body: JSON.stringify({ gds: 5 })
});
if (editedClass.assetClass.gds !== 5) throw new Error('Asset class update did not persist');
const classDelete = await fetch(`${baseUrl}/api/asset-classes/${newClass.assetClass.id}`, { method: 'DELETE', headers: auth });
if (classDelete.status !== 204) throw new Error('Asset class delete failed');
// Asset class number: shared by every asset in the category, and cascades on change.
const classCat = await request('/api/asset-categories', {
method: 'POST', headers: auth, body: JSON.stringify({ name: `Class Cat ${suffix}`, asset_class_number: 'AC-100' })
});
if (classCat.category.asset_class_number !== 'AC-100') throw new Error('Category asset class number was not stored');
const classAsset = await request('/api/assets', {
method: 'POST', headers: auth, body: JSON.stringify({ asset_id: `CLS-${suffix}`, description: 'Class test', category: `Class Cat ${suffix}`, acquisition_cost: 50 })
});
if (classAsset.asset.asset_class_number !== 'AC-100') throw new Error('Asset did not inherit the category class number');
// Changing the category's class number cascades to its assets.
await request(`/api/asset-categories/${classCat.category.id}`, {
method: 'PUT', headers: auth, body: JSON.stringify({ asset_class_number: 'AC-200' })
});
const reclassed = await request(`/api/assets/${classAsset.asset.id}`, { headers: auth });
if (reclassed.asset.asset_class_number !== 'AC-200') throw new Error('Class number change did not cascade to assets');
// Asset ID templates: pattern, preview, assign to a category, auto-increment on asset creation.
const idTemplate = await request('/api/asset-id-templates', {
method: 'POST', headers: auth, body: JSON.stringify({ name: `Tag ${suffix}`, pattern: 'T-A#####', next_number: 5 })
});
if (idTemplate.template.preview !== 'T-A00005') throw new Error('ID template preview was not formatted correctly');
const badPattern = await fetch(`${baseUrl}/api/asset-id-templates`, {
method: 'POST', headers: { ...auth, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'bad', pattern: 'NOHASH' })
});
if (badPattern.status !== 400) throw new Error('Pattern without # should be rejected');
// A category that uses the template.
const tagCategory = await request('/api/asset-categories', {
method: 'POST', headers: auth, body: JSON.stringify({ name: `Tagged Cat ${suffix}`, id_template_id: idTemplate.template.id })
});
if (tagCategory.category.id_template_id !== idTemplate.template.id) throw new Error('Category did not store its ID template assignment');
// New asset with no explicit asset_id should get the templated tag and advance the counter.
const auto1 = await request('/api/assets', {
method: 'POST', headers: auth, body: JSON.stringify({ description: 'Auto tag 1', category: `Tagged Cat ${suffix}`, acquisition_cost: 10 })
});
if (auto1.asset.asset_id !== 'T-A00005') throw new Error(`Expected auto tag T-A00005, got ${auto1.asset.asset_id}`);
const auto2 = await request('/api/assets', {
method: 'POST', headers: auth, body: JSON.stringify({ description: 'Auto tag 2', category: `Tagged Cat ${suffix}`, acquisition_cost: 10 })
});
if (auto2.asset.asset_id !== 'T-A00006') throw new Error(`Expected auto tag T-A00006, got ${auto2.asset.asset_id}`);
// An explicit asset_id still wins over the template.
const explicit = await request('/api/assets', {
method: 'POST', headers: auth, body: JSON.stringify({ asset_id: `EXP-${suffix}`, description: 'Explicit', category: `Tagged Cat ${suffix}`, acquisition_cost: 10 })
});
if (explicit.asset.asset_id !== `EXP-${suffix}`) throw new Error('Explicit asset_id should override the template');
// Deleting the template unassigns it from the category.
const idTplDelete = await request(`/api/asset-id-templates/${idTemplate.template.id}`, { method: 'DELETE', headers: auth });
if (!idTplDelete.deleted || idTplDelete.category_count !== 1) throw new Error('ID template delete did not unassign its category');
// Asset departments: create, rename (cascades to assets), usage count, delete.
const newDept = await request('/api/asset-departments', {
method: 'POST', headers: auth, body: JSON.stringify({ name: `Smoke Dept ${suffix}`, code: 'SMKD' })
});
if (!newDept.department?.id) throw new Error('Asset department was not created');
await request(`/api/assets/${catAsset.asset.id}`, {
method: 'PUT', headers: auth, body: JSON.stringify({ department: `Smoke Dept ${suffix}` })
});
const renamedDept = await request(`/api/asset-departments/${newDept.department.id}`, {
method: 'PUT', headers: auth, body: JSON.stringify({ name: `Smoke Dept ${suffix} (renamed)` })
});
if (renamedDept.renamed_assets < 1) throw new Error('Department rename did not cascade to assets');
const deptAsset = await request(`/api/assets/${catAsset.asset.id}`, { headers: auth });
if (deptAsset.asset.department !== `Smoke Dept ${suffix} (renamed)`) throw new Error('Asset department was not updated by rename');
const deptList = await request('/api/asset-departments', { headers: auth });
if (!deptList.departments.some((d) => d.id === newDept.department.id && d.asset_count === 1)) throw new Error('Department usage count was not reported');
const deptDelete = await request(`/api/asset-departments/${newDept.department.id}`, { method: 'DELETE', headers: auth });
if (!deptDelete.deleted || deptDelete.asset_count !== 1) throw new Error('Department delete did not report usage impact');
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.' })
});
// Identification photos (base64 + caption)
const photoResult = await request(`/api/assets/${assetId}/photos`, {
method: 'POST', headers: auth,
body: JSON.stringify({ name: 'front.png', mime: 'image/png', caption: 'Front nameplate', data: pngDataUrl })
});
if (!photoResult.photo?.id) throw new Error('Asset photo was not created');
const recaptioned = await request(`/api/assets/${assetId}/photos/${photoResult.photo.id}`, {
method: 'PUT', headers: auth, body: JSON.stringify({ caption: 'Serial label, left side' })
});
if (recaptioned.photo.caption !== 'Serial label, left side') throw new Error('Asset photo caption did not update');
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');
}
if (!detailed.asset.photos.length || !detailed.asset.photos[0].data || detailed.asset.photos[0].caption !== 'Serial label, left side') {
throw new Error('Asset photos (with caption + data) were not returned');
}
// Barcode lookup: set a barcode value, then resolve it via the scan endpoint.
await request(`/api/assets/${assetId}`, {
method: 'PUT',
headers: auth,
body: JSON.stringify({ barcode_value: `BC-${suffix}` })
});
const scanned = await request(`/api/assets/lookup?code=BC-${suffix}`, { headers: auth });
if (scanned.asset.id !== assetId) throw new Error('Barcode lookup did not resolve the scanned asset');
const byAssetId = await request(`/api/assets/lookup?code=${encodeURIComponent(assetResult.asset.asset_id)}`, { headers: auth });
if (byAssetId.asset.id !== assetId) throw new Error('Asset ID lookup fallback failed');
// Reporting suite: catalog, several report types, export, and saved reports.
const reportCatalog = await request('/api/reports/catalog', { headers: auth });
if (!reportCatalog.reports.length || !reportCatalog.params.book) {
throw new Error('Report catalog did not return reports and param specs');
}
const runReportApi = (type, options) => request('/api/reports/run', {
method: 'POST',
headers: auth,
body: JSON.stringify({ type, options })
});
const acq = await runReportApi('acquisitions', { year: 2026 });
if (!Array.isArray(acq.report.rows)) throw new Error('Acquisitions report did not return rows');
const disp = await runReportApi('disposals', { year: 2026 });
if (!Array.isArray(disp.report.rows)) throw new Error('Disposals report did not return rows');
const roll = await runReportApi('rollforward', { book: 'GAAP', year: 2026 });
if (!roll.report.columns.length) throw new Error('Rollforward report did not return columns');
const f4797 = await runReportApi('form_4797', { year: 2026 });
if (!Array.isArray(f4797.report.rows)) throw new Error('Form 4797 report did not return rows');
const custom = await runReportApi('custom', { columns: ['asset_id', 'acquisition_cost'] });
if (custom.report.columns.length !== 2) throw new Error('Report builder column projection failed');
for (const pmReport of ['pm_due', 'pm_uncompleted', 'pm_completed', 'alerts_since']) {
const r = await runReportApi(pmReport, { before_date: '2099-01-01', since_date: '2000-01-01' });
if (!Array.isArray(r.report.rows)) throw new Error(`${pmReport} report did not return rows`);
}
const exportResponse = await fetch(`${baseUrl}/api/reports/export`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'depreciation_expense', options: { book: 'GAAP', year: 2026 }, format: 'csv' })
});
if (!exportResponse.ok || !(await exportResponse.text()).includes('Asset ID')) {
throw new Error('Report CSV export failed');
}
const savedReport = await request('/api/saved-reports', {
method: 'POST',
headers: auth,
body: JSON.stringify({ name: `Saved ${suffix}`, report_type: 'net_book_value', options: { book: 'GAAP', year: 2026 } })
});
const savedList = await request('/api/saved-reports', { headers: auth });
if (!savedList.reports.some((report) => report.id === savedReport.report.id)) {
throw new Error('Saved report was not persisted/listed');
}
// Tax rule sets: expand (method catalog), create, edit, activate, delete.
const expanded = await request('/api/tax-rules/expand', {
method: 'POST',
headers: auth,
body: JSON.stringify({ rules_json: { jurisdiction: 'US-SMOKE', version: '1.0' } })
});
if ((expanded.rules_json.methods || []).length !== 74) {
throw new Error('Rule set expansion did not generate the 74-method catalog');
}
const createdRule = await request('/api/tax-rules', {
method: 'POST',
headers: auth,
body: JSON.stringify({
jurisdiction: `US-SMOKE-${suffix}`,
name: 'Smoke rule set',
version: '1.0',
effective_date: '2026-01-01',
active: false,
rules_json: { jurisdiction: `US-SMOKE-${suffix}`, version: '1.0', limits: { section179: { deductionLimit: 1000000 } } }
})
});
const editedRule = await request(`/api/tax-rules/${createdRule.ruleSet.id}`, {
method: 'PUT',
headers: auth,
body: JSON.stringify({ name: 'Smoke rule set (edited)', rules_json: { jurisdiction: `US-SMOKE-${suffix}`, version: '1.0', limits: { section179: { deductionLimit: 2000000 } } } })
});
if (editedRule.ruleSet.name !== 'Smoke rule set (edited)' || editedRule.ruleSet.rules_json.limits.section179.deductionLimit !== 2000000) {
throw new Error('Tax rule set edit did not persist');
}
const activated = await request(`/api/tax-rules/${createdRule.ruleSet.id}/activate`, { method: 'POST', headers: auth });
if (!activated.ruleSets.find((set) => set.id === createdRule.ruleSet.id)?.active) {
throw new Error('Tax rule set activation did not persist');
}
const deleteRule = await fetch(`${baseUrl}/api/tax-rules/${createdRule.ruleSet.id}`, { method: 'DELETE', headers: auth });
if (deleteRule.status !== 204) throw new Error('Tax rule set delete failed');
// Books registry: list, edit (assign rule set + default method), and GL ledger.
const booksList = await request('/api/books', { headers: auth });
if (booksList.books.length < 6 || !Array.isArray(booksList.ruleSets)) {
throw new Error('Books registry did not return the standard books and rule sets');
}
const gaapBookDef = booksList.books.find((book) => book.code === 'GAAP');
const federalRuleSet = booksList.ruleSets[0];
const editedBook = await request(`/api/books/${gaapBookDef.id}`, {
method: 'PUT',
headers: auth,
body: JSON.stringify({ default_method: 'straight_line', tax_rule_set_id: federalRuleSet.id, enabled: true })
});
if (editedBook.book.tax_rule_set_id !== federalRuleSet.id) {
throw new Error('Book tax-rule assignment did not persist');
}
// User-defined book: create, ensure it applies to a new asset, then guard + delete.
const newBook = await request('/api/books', {
method: 'POST',
headers: auth,
body: JSON.stringify({ code: `IFRS${suffix}`.slice(0, 12), name: 'IFRS book', book_type: 'financial', enabled: true })
});
const newBookCode = newBook.book.code;
const assetWithUserBook = await request('/api/assets', {
method: 'POST',
headers: auth,
body: JSON.stringify({ asset_id: `UB-${suffix}`, description: 'User-book asset', category: 'Computer Equipment', acquisition_cost: 1200 })
});
const hydratedUserBook = await request(`/api/assets/${assetWithUserBook.asset.id}`, { headers: auth });
if (!hydratedUserBook.asset.books.some((b) => b.book_type === newBookCode)) {
throw new Error('Newly created enabled book did not apply to a new asset');
}
const blockedBookDelete = await fetch(`${baseUrl}/api/books/${newBook.book.id}`, { method: 'DELETE', headers: auth });
if (blockedBookDelete.status !== 400) throw new Error('Deleting a book in use should be blocked');
const spareBook = await request('/api/books', {
method: 'POST', headers: auth, body: JSON.stringify({ code: `TMP${suffix}`.slice(0, 12), name: 'Temp book' })
});
const okBookDelete = await fetch(`${baseUrl}/api/books/${spareBook.book.id}`, { method: 'DELETE', headers: auth });
if (okBookDelete.status !== 204) throw new Error('Deleting an unused book failed');
const ledger = await request('/api/books/GAAP/ledger?year=2026', { headers: auth });
if (!ledger.ledger.accounts.length || typeof ledger.ledger.summary.nbv !== 'number') {
throw new Error('Book GL ledger did not return accounts and summary');
}
const ledgerExport = await fetch(`${baseUrl}/api/books/GAAP/ledger/export`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({ year: 2026, format: 'csv' })
});
if (!ledgerExport.ok || !(await ledgerExport.text()).includes('GL account')) {
throw new Error('Book GL ledger export failed');
}
// Alerts: a clearly-overdue task should raise a critical alert on scan.
await request(`/api/assets/${assetId}/tasks`, {
method: 'POST',
headers: auth,
body: JSON.stringify({ title: 'Annual inspection', type: 'inspection', due_date: '2020-01-01' })
});
const scan = await request('/api/alerts/scan', { method: 'POST', headers: auth });
const overdue = scan.alerts.find((a) => a.type === 'maintenance_overdue' && a.due_date === '2020-01-01');
if (!overdue || overdue.severity !== 'critical') throw new Error('Overdue maintenance alert was not raised');
const acked = await request(`/api/alerts/${overdue.id}/acknowledge`, { method: 'PUT', headers: auth });
if (acked.alert.status !== 'acknowledged') throw new Error('Alert acknowledge did not persist');
const dismissed = await request(`/api/alerts/${overdue.id}/dismiss`, { method: 'PUT', headers: auth });
if (dismissed.alert.status !== 'dismissed') throw new Error('Alert dismiss did not persist');
// Notification (SMTP) settings: password is write-only and never returned.
await request('/api/notifications/settings', {
method: 'PUT',
headers: auth,
body: JSON.stringify({
smtp_host: 'smtp.example.com', smtp_port: 587, smtp_secure: false,
smtp_user: 'mailer@example.com', smtp_password: 'secret', smtp_from: 'MixedAssets <no-reply@example.com>',
alerts_enabled: false, alerts_lead_days: 45, alerts_recipients: 'ops@example.com, finance@example.com'
})
});
const notif = await request('/api/notifications/settings', { headers: auth });
if (notif.settings.smtp_host !== 'smtp.example.com' || notif.settings.smtp_password_set !== true || notif.settings.smtp_password !== undefined) {
throw new Error('Notification settings did not persist or leaked the password');
}
if (notif.settings.alerts_lead_days !== 45) throw new Error('Alert lead days did not persist');
const adminSettings = await request('/api/settings', { headers: auth });
if ('smtp_password' in adminSettings.settings) throw new Error('Admin settings endpoint leaked smtp_password');
// Preventative maintenance: plan with steps, attach to asset, alert, complete, settings.
const pmPlan = await request('/api/pm-plans', {
method: 'POST',
headers: auth,
body: JSON.stringify({
name: `Lift PM ${suffix}`,
frequency_value: 3,
frequency_unit: 'months',
steps: [{ title: 'Tighten lug bolt A3', est_minutes: 15 }, { title: 'Inspect hydraulics', est_minutes: 30 }],
components: [
{ part_number: 'OIL-10', description: 'Hydraulic oil', supplier: 'Acme', cost: 40 },
{ part_number: 'FLT-2', description: 'Filter', supplier: 'Acme', cost: 12 }
],
photos: [{ name: 'diagram.png', mime: 'image/png', caption: 'Lug bolt locations', data: pngDataUrl }]
})
});
if (pmPlan.plan.steps.length !== 2) throw new Error('PM plan steps were not saved');
if (pmPlan.plan.estimated_minutes !== 45) throw new Error('Estimated minutes were not totaled from step times');
if (pmPlan.plan.components.length !== 2 || pmPlan.plan.components_total !== 52) throw new Error('PM components were not saved/totaled');
if (pmPlan.plan.photos.length !== 1 || !pmPlan.plan.photos[0].data) throw new Error('PM plan guidance photo was not saved');
// PM categories: create, rename (cascades to plans), usage count, delete — separate from asset categories.
const pmCat = await request('/api/pm-categories', {
method: 'POST', headers: auth, body: JSON.stringify({ name: `PM Cat ${suffix}`, code: 'PMC' })
});
if (!pmCat.category?.id) throw new Error('PM category was not created');
await request(`/api/pm-plans/${pmPlan.plan.id}`, { method: 'PUT', headers: auth, body: JSON.stringify({ category: `PM Cat ${suffix}` }) });
const renamedPmCat = await request(`/api/pm-categories/${pmCat.category.id}`, {
method: 'PUT', headers: auth, body: JSON.stringify({ name: `PM Cat ${suffix} (renamed)` })
});
if (renamedPmCat.renamed_plans < 1) throw new Error('PM category rename did not cascade to plans');
const renamedPlan = await request(`/api/pm-plans/${pmPlan.plan.id}`, { headers: auth });
if (renamedPlan.plan.category !== `PM Cat ${suffix} (renamed)`) throw new Error('PM plan category was not updated by rename');
const pmCatList = await request('/api/pm-categories', { headers: auth });
if (!pmCatList.categories.some((c) => c.id === pmCat.category.id && c.plan_count === 1)) throw new Error('PM category usage count was not reported');
// PM categories are distinct from asset categories.
const assetCats = await request('/api/asset-categories', { headers: auth });
if (assetCats.categories.some((c) => c.name === `PM Cat ${suffix} (renamed)`)) throw new Error('PM category leaked into asset categories');
const pmCatDelete = await request(`/api/pm-categories/${pmCat.category.id}`, { method: 'DELETE', headers: auth });
if (!pmCatDelete.deleted || pmCatDelete.plan_count !== 1) throw new Error('PM category delete did not report usage impact');
// The list view omits photo blobs; the single-plan fetch includes them.
const planList = await request('/api/pm-plans', { headers: auth });
const listed = planList.plans.find((p) => p.id === pmPlan.plan.id);
if (listed.photo_count !== 1 || (listed.photos[0] && listed.photos[0].data)) throw new Error('PM plan list should report photo_count without blobs');
const singlePlan = await request(`/api/pm-plans/${pmPlan.plan.id}`, { headers: auth });
if (!singlePlan.plan.photos[0].data) throw new Error('Single PM plan fetch should include photo data');
// Editing the plan keeps the existing photo (resent by id) and updates its caption.
const editedPlan = await request(`/api/pm-plans/${pmPlan.plan.id}`, {
method: 'PUT', headers: auth,
body: JSON.stringify({ photos: [{ id: pmPlan.plan.photos[0].id, caption: 'Updated caption' }] })
});
if (editedPlan.plan.photos.length !== 1 || editedPlan.plan.photos[0].caption !== 'Updated caption' || !editedPlan.plan.photos[0].data) {
throw new Error('Editing PM plan photo caption (resend by id) did not preserve the blob');
}
const planPrint = await request('/api/reports/run', { method: 'POST', headers: auth, body: JSON.stringify({ type: 'pm_plan', options: { pm_plan_id: pmPlan.plan.id } }) });
if (planPrint.report.rows.length !== 2 || !planPrint.report.meta.note.includes('Frequency')) {
throw new Error('Printable PM plan report was incomplete');
}
const attached = await request(`/api/assets/${assetId}/pm`, {
method: 'POST',
headers: auth,
body: JSON.stringify({ plan_id: pmPlan.plan.id, frequency_value: 1, frequency_unit: 'months', start_date: '2020-01-01', next_due_date: '2020-01-01', end_date: '2035-01-01' })
});
const pmScan = await request('/api/alerts/scan', { method: 'POST', headers: auth });
if (!pmScan.alerts.some((a) => a.type === 'pm_overdue' && a.reference_id === attached.pm.id)) {
throw new Error('Overdue PM alert was not raised');
}
// Guidance photos surface on the asset's PM schedule for the completion view.
const pmGuidance = await request(`/api/assets/${assetId}/pm`, { headers: auth });
const pmSchedule = pmGuidance.pm.find((p) => p.id === attached.pm.id);
if (!pmSchedule.plan_photos || !pmSchedule.plan_photos.length || !pmSchedule.plan_photos[0].data) {
throw new Error('PM plan guidance photos were not exposed on the asset schedule');
}
const completed = await request(`/api/assets/${assetId}/pm/${attached.pm.id}/complete`, {
method: 'POST',
headers: auth,
body: JSON.stringify({
steps: [{ title: 'Tighten lug bolt A3', done: true }],
notes_list: ['Topped up fluids', 'Replaced worn filter'],
ratings: { safety: 5, physical: 4, operating: 5 },
signature: pngDataUrl,
photos: [{ name: 'after.png', mime: 'image/png', data: pngDataUrl }]
})
});
if (!completed.pm.next_due_date || completed.pm.next_due_date <= '2020-01-01') {
throw new Error('Completing PM did not advance the next due date');
}
// Signature is required.
const missingSig = await fetch(`${baseUrl}/api/assets/${assetId}/pm/${attached.pm.id}/complete`, {
method: 'POST', headers: { ...auth, 'Content-Type': 'application/json' }, body: JSON.stringify({ ratings: { safety: 3 } })
});
if (missingSig.status !== 400) throw new Error('PM completion without a signature should be rejected');
const withPm = await request(`/api/assets/${assetId}`, { headers: auth });
if (!withPm.asset.pm.length || !withPm.asset.pm[0].completions.length) {
throw new Error('Asset PM schedule/completions were not returned');
}
if (withPm.asset.pm[0].total_cost !== 52) {
throw new Error('PM completion cost (from components) was not captured');
}
const firstCompletion = withPm.asset.pm[0].completions[0];
if (!firstCompletion.has_signature || firstCompletion.photo_count !== 1 || firstCompletion.ratings.safety !== 5) {
throw new Error('PM completion metadata (signature/photos/ratings) was not stored');
}
const completionList = await request(`/api/pm-completions?asset_id=${assetId}`, { headers: auth });
if (!completionList.completions.length) throw new Error('PM completions list was empty');
const completionDetail = await request(`/api/pm-completions/${completionList.completions[0].id}`, { headers: auth });
if (!completionDetail.completion.signature || !completionDetail.completion.photos.length || completionDetail.completion.notes.length !== 2) {
throw new Error('PM completion form detail was incomplete');
}
// PM book: actual maintenance spend aggregated per asset.
const booksWithPm = await request('/api/books', { headers: auth });
if (!booksWithPm.books.some((b) => b.code === 'PM')) throw new Error('PM book missing from the registry');
const yearNow = new Date().getFullYear();
const pmLedger = await request(`/api/books/PM/ledger?year=${yearNow}`, { headers: auth });
if (pmLedger.ledger.kind !== 'maintenance' || pmLedger.ledger.summary.cost < 52) {
throw new Error('PM book ledger did not aggregate maintenance cost');
}
await request('/api/pm-settings', {
method: 'PUT',
headers: auth,
body: JSON.stringify({ pm_default_frequency_value: 6, pm_default_frequency_unit: 'months', pm_lead_days: 14 })
});
const pmSettings = await request('/api/pm-settings', { headers: auth });
if (pmSettings.settings.pm_default_frequency_value !== 6 || pmSettings.settings.pm_lead_days !== 14) {
throw new Error('PM settings did not persist');
}
// Contacts directory: typed records, type-specific fields, filtering, notes.
const vendorContact = await request('/api/contacts', {
method: 'POST',
headers: auth,
body: JSON.stringify({
type: 'vendor', organization: `Acme Supply ${suffix}`, email: `sales-${suffix}@acme.test`,
phone: '+1 555-0100', rating: 4, credit_term: 'net_60', country: 'US'
})
});
if (vendorContact.contact.type !== 'vendor' || vendorContact.contact.rating !== 4 || vendorContact.contact.credit_term !== 'net_60') {
throw new Error('Vendor contact fields did not persist');
}
await request('/api/contacts', {
method: 'POST',
headers: auth,
body: JSON.stringify({
type: 'contractor', first_name: 'Dana', last_name: 'Cruz', email: `dana-${suffix}@contractor.test`,
tax_id: 'BL-99821', department: 'Facilities'
})
});
const vendorList = await request('/api/contacts?type=vendor', { headers: auth });
if (!vendorList.contacts.some((c) => c.id === vendorContact.contact.id) || vendorList.contacts.some((c) => c.type !== 'vendor')) {
throw new Error('Contact type filter failed');
}
const updatedContact = await request(`/api/contacts/${vendorContact.contact.id}`, {
method: 'PUT',
headers: auth,
body: JSON.stringify({ rating: 5 })
});
if (updatedContact.contact.rating !== 5) throw new Error('Contact update did not persist');
await request(`/api/contacts/${vendorContact.contact.id}/notes`, {
method: 'POST',
headers: auth,
body: JSON.stringify({ body: 'Negotiated faster lead times.' })
});
const contactDetail = await request(`/api/contacts/${vendorContact.contact.id}`, { headers: auth });
if (!contactDetail.contact.contact_notes.length) throw new Error('Contact 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,
sync_enabled: true,
sync_interval_hours: 12
})
});
if (!workday.connection.configured || workday.connection.sync_enabled !== true || workday.connection.sync_interval_hours !== 12) {
throw new Error('Workday connection configuration did not persist');
}
await request('/api/workday/import-workers', {
method: 'POST',
headers: auth,
body: JSON.stringify({
workers: [{ id: `WD-${suffix}`, firstName: 'Pat', lastName: 'Lee', email: `pat-${suffix}@corp.test`, department: 'IT', managerName: 'Sam Ray', hireDate: '2024-02-01' }]
})
});
const importedContacts = await request(`/api/contacts?type=employee&search=pat-${suffix}`, { headers: auth });
if (!importedContacts.contacts.some((c) => c.workday_worker_id === `WD-${suffix}` && c.start_date === '2024-02-01' && c.manager_last_name === 'Ray')) {
throw new Error('Workday import did not create an enriched employee contact');
}
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');
}
// 200% declining-balance methods. MF200 (MACRS) ignores salvage and reproduces the IRS
// 5-year table; DB200 (standard) honors salvage (recovers only cost salvage).
const round = (n) => Math.round(Number(n));
const macrsAsset = await request('/api/assets', {
method: 'POST', headers: auth,
body: JSON.stringify({
asset_id: `MF-${suffix}`, description: 'MF200 press', category: 'Computer Equipment',
acquisition_cost: 10000, salvage_value: 1000, in_service_date: '2026-01-01', useful_life_months: 60,
books: [{ book_type: 'FEDERAL', active: true, depreciation_method: 'MF200', convention: 'half_year', useful_life_months: 60, cost: 10000 }]
})
});
const mfSched = (await request(`/api/assets/${macrsAsset.asset.id}/depreciation?startYear=2026&endYear=2032`, { headers: auth }))
.schedules.filter((r) => r.book === 'FEDERAL');
const mfByYear = Object.fromEntries(mfSched.map((r) => [r.year, round(r.depreciation)]));
// IRS 5-year 200%DB half-year: 2000, 3200, 1920, 1152, 1152, 576 — salvage is ignored.
if (mfByYear[2026] !== 2000 || mfByYear[2027] !== 3200 || mfByYear[2028] !== 1920) {
throw new Error(`MF200 schedule incorrect: ${JSON.stringify(mfByYear)}`);
}
const mfTotal = round(mfSched.reduce((s, r) => s + r.depreciation, 0));
if (mfTotal !== 10000) throw new Error(`MF200 should recover full cost (MACRS ignores salvage), got ${mfTotal}`);
const dbAsset = await request('/api/assets', {
method: 'POST', headers: auth,
body: JSON.stringify({
asset_id: `DB-${suffix}`, description: 'DB200 press', category: 'Computer Equipment',
acquisition_cost: 10000, salvage_value: 1000, in_service_date: '2026-01-01', useful_life_months: 60,
books: [{ book_type: 'FEDERAL', active: true, depreciation_method: 'DB200', convention: 'half_year', useful_life_months: 60, cost: 10000 }]
})
});
const dbSched = (await request(`/api/assets/${dbAsset.asset.id}/depreciation?startYear=2026&endYear=2034`, { headers: auth }))
.schedules.filter((r) => r.book === 'FEDERAL');
const dbTotal = round(dbSched.reduce((s, r) => s + r.depreciation, 0));
if (dbTotal !== 9000) throw new Error(`DB200 should recover cost salvage (9000), got ${dbTotal}`);
if (dbSched.some((r) => r.netBookValue < 999)) throw new Error('DB200 depreciated below salvage value');
// New York Liberty Zone: seeded zone applies a 30% §168 allowance to the federal book in year 1.
const zoneList = await request('/api/depreciation-zones', { headers: auth });
const liberty = zoneList.zones.find((z) => z.code === 'ny_liberty');
if (!liberty || liberty.allowance_percent !== 30) throw new Error('New York Liberty Zone was not seeded');
const zoneAsset = await request('/api/assets', {
method: 'POST', headers: auth,
body: JSON.stringify({
asset_id: `LZ-${suffix}`, description: 'Liberty Zone equipment', category: 'Computer Equipment',
acquisition_cost: 10000, in_service_date: '2005-01-01', useful_life_months: 60, special_zone: 'ny_liberty',
books: [{ book_type: 'FEDERAL', active: true, depreciation_method: 'MF200', convention: 'half_year', useful_life_months: 60, cost: 10000 }]
})
});
const lzSched = (await request(`/api/assets/${zoneAsset.asset.id}/depreciation?startYear=2005&endYear=2011`, { headers: auth }))
.schedules.filter((r) => r.book === 'FEDERAL');
const lzByYear = Object.fromEntries(lzSched.map((r) => [r.year, round(r.depreciation)]));
// 30% allowance (3000) + first-year MACRS (20% half-year) on the remaining 7000 (1400) = 4400.
if (lzByYear[2005] !== 4400) throw new Error(`Liberty Zone year-1 allowance incorrect: ${JSON.stringify(lzByYear)}`);
// Outside the placed-in-service window (2008) the allowance must NOT apply.
const outAsset = await request('/api/assets', {
method: 'POST', headers: auth,
body: JSON.stringify({
asset_id: `LZX-${suffix}`, description: 'Out-of-window', category: 'Computer Equipment',
acquisition_cost: 10000, in_service_date: '2008-01-01', useful_life_months: 60, special_zone: 'ny_liberty',
books: [{ book_type: 'FEDERAL', active: true, depreciation_method: 'MF200', convention: 'half_year', useful_life_months: 60, cost: 10000 }]
})
});
const outYear1 = round((await request(`/api/assets/${outAsset.asset.id}/depreciation?startYear=2008&endYear=2008`, { headers: auth }))
.schedules.filter((r) => r.book === 'FEDERAL')[0].depreciation);
if (outYear1 !== 2000) throw new Error(`Out-of-window asset should get plain MACRS year-1 (2000), got ${outYear1}`);
// Disposal: preview gain/loss, then record a full disposal.
const disposalAsset = await request('/api/assets', {
method: 'POST',
headers: auth,
body: JSON.stringify({
asset_id: `D-${suffix}`,
description: 'Disposal test press',
category: 'Computer Equipment',
acquisition_cost: 10000,
acquired_date: '2022-01-05',
in_service_date: '2022-01-05',
useful_life_months: 60,
depreciation_method: 'straight_line'
})
});
const preview = await request(`/api/assets/${disposalAsset.asset.id}/disposal/preview`, {
method: 'POST',
headers: auth,
body: JSON.stringify({ book_type: 'GAAP', disposal_date: '2026-06-01', disposal_price: 3000, disposal_expense: 200 })
});
if (typeof preview.preview.gain_loss !== 'number' || typeof preview.preview.net_book_value !== 'number') {
throw new Error('Disposal preview did not return gain/loss figures');
}
const disposed = await request(`/api/assets/${disposalAsset.asset.id}/disposals`, {
method: 'POST',
headers: auth,
body: JSON.stringify({ book_type: 'GAAP', disposal_date: '2026-06-01', disposal_price: 3000, disposal_expense: 200, method: 'sale', property_type: '1245' })
});
if (disposed.asset.status !== 'disposed' || !disposed.disposal?.id) {
throw new Error('Full disposal did not mark the asset disposed');
}
// Impairment adjustment
const adjustment = await request(`/api/assets/${disposalAsset.asset.id}/adjustments`, {
method: 'POST',
headers: auth,
body: JSON.stringify({ type: 'impairment', amount: 500, book_type: 'GAAP', reason: 'Flood damage' })
});
if (!adjustment.adjustment?.id) throw new Error('Adjustment was not recorded');
// Lease accounting + amortization schedule
const lease = await request('/api/leases', {
method: 'POST',
headers: auth,
body: JSON.stringify({
asset_id: assetId,
lessor: 'Equip Leasing Co',
contract_value: 24000,
start_date: '2026-01-01',
end_date: '2028-01-01',
discount_rate: 6,
payment_frequency: 'monthly'
})
});
const schedule = await request(`/api/leases/${lease.lease.id}/schedule`, { headers: auth });
if (!schedule.schedule?.rows?.length || !(schedule.schedule.summary.present_value > 0)) {
throw new Error('Lease amortization schedule was not generated');
}
if (schedule.schedule.rows.length !== schedule.schedule.summary.periods) {
throw new Error('Lease schedule period count mismatch');
}
// Parts inventory: part record, stock locations (aisle/bin), reservations, movements, photos.
const workLocation = await request('/api/contacts', {
method: 'POST',
headers: auth,
body: JSON.stringify({ type: 'work_location', organization: `Main Warehouse ${suffix}` })
});
const partResult = await request('/api/parts', {
method: 'POST',
headers: auth,
body: JSON.stringify({
part_number: `HYD-${suffix}`, name: 'Hydraulic filter', category: 'Filters',
unit_of_measure: 'each', unit_cost: 18.5, reorder_point: 5, reorder_quantity: 20,
measurements: '120mm x 80mm', supplier_contact_id: vendorContact.contact.id
})
});
const partId = partResult.part.id;
if (partResult.part.supplier_name === null) throw new Error('Part supplier name was not resolved from contacts');
const withStock = await request(`/api/parts/${partId}/stock`, {
method: 'POST',
headers: auth,
body: JSON.stringify({ location_contact_id: workLocation.contact.id, aisle: 'A12', bin: 'B3', quantity: 10, reserved: 2 })
});
const stockRow = withStock.part.stock[0];
if (!stockRow || stockRow.aisle !== 'A12' || stockRow.bin !== 'B3' || stockRow.available !== 8) {
throw new Error('Part stock location (aisle/bin/available) was not stored correctly');
}
if (withStock.part.on_hand !== 10 || withStock.part.reserved !== 2) {
throw new Error('Part on-hand/reserved aggregation failed');
}
// Receive 5 more, then reserve 3, then issue 4.
const received = await request(`/api/parts/${partId}/transactions`, {
method: 'POST', headers: auth, body: JSON.stringify({ stock_id: stockRow.id, type: 'receive', quantity: 5, notes: 'PO-1' })
});
if (received.part.on_hand !== 15) throw new Error('Receive movement did not increase on-hand');
const reserved = await request(`/api/parts/${partId}/transactions`, {
method: 'POST', headers: auth, body: JSON.stringify({ stock_id: stockRow.id, type: 'reserve', quantity: 3, asset_id: assetId })
});
if (reserved.part.reserved !== 5) throw new Error('Reserve movement did not increase reserved');
const issued = await request(`/api/parts/${partId}/transactions`, {
method: 'POST', headers: auth, body: JSON.stringify({ stock_id: stockRow.id, type: 'issue', quantity: 4 })
});
if (issued.part.on_hand !== 11 || issued.part.available !== 6) throw new Error('Issue movement did not reduce on-hand/available');
if (issued.part.transactions.length !== 3) throw new Error('Part transaction history was not recorded');
// Photo attach
const photoPart = await request(`/api/parts/${partId}/photos`, {
method: 'POST', headers: auth, body: JSON.stringify({ name: 'filter.png', mime: 'image/png', data: pngDataUrl })
});
if (!photoPart.part.photos.length) throw new Error('Part photo was not attached');
// Low-stock filter: drop below reorder point and confirm it surfaces.
await request(`/api/parts/${partId}/transactions`, {
method: 'POST', headers: auth, body: JSON.stringify({ stock_id: stockRow.id, type: 'adjust', quantity: 3 })
});
const lowStock = await request('/api/parts?low_stock=true', { headers: auth });
if (!lowStock.parts.some((p) => p.id === partId && p.low_stock)) {
throw new Error('Low-stock filter did not surface the depleted part');
}
const partList = await request(`/api/parts?search=HYD-${suffix}`, { headers: auth });
if (!partList.parts.some((p) => p.id === partId)) throw new Error('Part search did not return the created part');
// Parts categories: seeded defaults present; create, rename (cascades to parts), usage count, delete.
const seededPartCats = await request('/api/part-categories', { headers: auth });
for (const expected of ['Electrical', 'Plumbing', 'Fire Safety', 'Other']) {
if (!seededPartCats.categories.some((c) => c.name === expected)) throw new Error(`Seeded part category "${expected}" missing`);
}
const newPartCat = await request('/api/part-categories', {
method: 'POST', headers: auth, body: JSON.stringify({ name: `Part Cat ${suffix}` })
});
if (!newPartCat.category?.id) throw new Error('Part category was not created');
await request(`/api/parts/${partId}`, { method: 'PUT', headers: auth, body: JSON.stringify({ category: `Part Cat ${suffix}` }) });
const renamedPartCat = await request(`/api/part-categories/${newPartCat.category.id}`, {
method: 'PUT', headers: auth, body: JSON.stringify({ name: `Part Cat ${suffix} (renamed)` })
});
if (renamedPartCat.renamed_parts < 1) throw new Error('Part category rename did not cascade to parts');
const renamedPartDetail = await request(`/api/parts/${partId}`, { headers: auth });
if (renamedPartDetail.part.category !== `Part Cat ${suffix} (renamed)`) throw new Error('Part category was not updated by rename');
const partCatList = await request('/api/part-categories', { headers: auth });
if (!partCatList.categories.some((c) => c.id === newPartCat.category.id && c.part_count === 1)) throw new Error('Part category usage count was not reported');
const partCatDelete = await request(`/api/part-categories/${newPartCat.category.id}`, { method: 'DELETE', headers: auth });
if (!partCatDelete.deleted || partCatDelete.part_count !== 1) throw new Error('Part category delete did not report usage impact');
// Alert webhook: each new alert is delivered to an external endpoint as a JSON POST.
const webhookHits = [];
const hookSecret = 'whsec_smoke';
const hookServer = http.createServer((req, res) => {
let raw = '';
req.on('data', (chunk) => { raw += chunk; });
req.on('end', () => {
webhookHits.push({ headers: req.headers, raw, body: JSON.parse(raw || '{}') });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end('{"ok":true}');
});
});
await new Promise((resolve) => hookServer.listen(4100, '127.0.0.1', resolve));
try {
const hookUrl = 'http://127.0.0.1:4100/hook';
const savedHook = await request('/api/notifications/settings', {
method: 'PUT', headers: auth,
body: JSON.stringify({ webhook_enabled: true, webhook_url: hookUrl, webhook_secret: hookSecret })
});
if (savedHook.settings.webhook_url !== hookUrl || savedHook.settings.webhook_secret_set !== true || savedHook.settings.webhook_secret !== undefined) {
throw new Error('Webhook settings did not persist or leaked the secret');
}
// Test event + HMAC signature verification.
const hookTest = await request('/api/notifications/webhook-test', { method: 'POST', headers: auth, body: JSON.stringify({}) });
if (!hookTest.ok) throw new Error('Webhook test event was not delivered');
const testHit = webhookHits.find((h) => h.body.event === 'test');
if (!testHit) throw new Error('Webhook receiver did not get the test event');
const expectedSig = `sha256=${crypto.createHmac('sha256', hookSecret).update(testHit.raw).digest('hex')}`;
if (testHit.headers['x-mixedassets-signature'] !== expectedSig) {
throw new Error('Webhook HMAC signature was missing or incorrect');
}
// A fresh overdue task guarantees a new open alert; the cycle should POST it.
await request(`/api/assets/${assetId}/tasks`, {
method: 'POST', headers: auth,
body: JSON.stringify({ title: `Webhook overdue ${suffix}`, type: 'inspection', due_date: '2019-01-01' })
});
const cycle = await request('/api/alerts/run', { method: 'POST', headers: auth });
if (!cycle.webhooked || !(cycle.delivered > 0)) throw new Error('Alert cycle did not deliver alerts to the webhook');
const alertHit = webhookHits.find((h) => h.body.event === 'alert');
if (!alertHit || !alertHit.body.type || !alertHit.body.severity) throw new Error('Webhook alert payload was incomplete');
} finally {
await new Promise((resolve) => hookServer.close(resolve));
}
// ServiceNow integration: incident push (Table API) + CMDB asset pull, against a mock instance.
const snHits = [];
const snServer = http.createServer((req, res) => {
let raw = '';
req.on('data', (chunk) => { raw += chunk; });
req.on('end', () => {
snHits.push({ method: req.method, url: req.url, auth: req.headers.authorization || '', raw });
res.setHeader('Content-Type', 'application/json');
if (req.url.startsWith('/api/now/table/incident') && req.method === 'POST') {
res.writeHead(201);
res.end(JSON.stringify({ result: { sys_id: 'sys-inc-123', number: 'INC0099999' } }));
} else if (req.url.includes('/api/now/table/cmdb_ci_hardware')) {
res.writeHead(200);
res.end(JSON.stringify({
result: [{
sys_id: 'ci-abc-1', name: 'Mock Server 01', asset_tag: `CMDB-${suffix}`, serial_number: `SN-${suffix}`,
sys_class_name: 'cmdb_ci_server', manufacturer: 'Dell', location: 'Data Center 1', cost: '2500', purchase_date: '2025-01-15'
}]
}));
} else {
res.writeHead(200);
res.end(JSON.stringify({ result: [] }));
}
});
});
await new Promise((resolve) => snServer.listen(4200, '127.0.0.1', resolve));
try {
const snUrl = 'http://127.0.0.1:4200';
const savedSn = await request('/api/servicenow/settings', {
method: 'PUT', headers: auth,
body: JSON.stringify({
servicenow_instance_url: snUrl, servicenow_username: 'svc_mixedassets', servicenow_password: 'snpass',
servicenow_ticket_enabled: true, servicenow_cmdb_table: 'cmdb_ci_hardware'
})
});
if (savedSn.settings.servicenow_instance_url !== snUrl || savedSn.settings.servicenow_password_set !== true || savedSn.settings.servicenow_password !== undefined) {
throw new Error('ServiceNow settings did not persist or leaked the password');
}
if (!savedSn.settings.servicenow_cmdb_map || !savedSn.settings.servicenow_cmdb_map.serial_number) {
throw new Error('ServiceNow default CMDB field map was not returned');
}
// Connection test (GET incident with a basic-auth header).
const snTest = await request('/api/servicenow/test', { method: 'POST', headers: auth, body: JSON.stringify({}) });
if (!snTest.ok) throw new Error('ServiceNow connection test failed');
if (!snHits.some((h) => h.method === 'GET' && h.auth.startsWith('Basic '))) {
throw new Error('ServiceNow request did not include basic auth');
}
// Submit an alert as an incident.
await request(`/api/assets/${assetId}/tasks`, {
method: 'POST', headers: auth,
body: JSON.stringify({ title: `ServiceNow overdue ${suffix}`, type: 'inspection', due_date: '2018-01-01' })
});
const snScan = await request('/api/alerts/scan', { method: 'POST', headers: auth });
const targetAlert = snScan.alerts.find((a) => a.status === 'open' && !a.external_ticket_number);
if (!targetAlert) throw new Error('No open alert available to ticket');
const ticketResult = await request(`/api/alerts/${targetAlert.id}/ticket`, { method: 'POST', headers: auth, body: JSON.stringify({}) });
if (ticketResult.ticket.number !== 'INC0099999') throw new Error('ServiceNow incident number was not returned');
if (!snHits.some((h) => h.method === 'POST' && h.url.startsWith('/api/now/table/incident'))) {
throw new Error('ServiceNow incident POST was not sent');
}
// Re-submitting returns the existing ticket rather than duplicating.
const dupTicket = await request(`/api/alerts/${targetAlert.id}/ticket`, { method: 'POST', headers: auth, body: JSON.stringify({}) });
if (!dupTicket.ticket.duplicate) throw new Error('Re-ticketing should return the existing incident');
const ticketedList = await request('/api/alerts', { headers: auth });
if (!ticketedList.alerts.some((a) => a.id === targetAlert.id && a.external_ticket_number === 'INC0099999')) {
throw new Error('Alert was not linked to its ServiceNow incident');
}
// CMDB pull: create/update assets from configuration items.
const cmdbSync = await request('/api/servicenow/cmdb-sync', { method: 'POST', headers: auth, body: JSON.stringify({}) });
if (cmdbSync.total !== 1 || (cmdbSync.created + cmdbSync.updated) < 1) throw new Error('CMDB sync did not import the configuration item');
const cmdbAssets = await request(`/api/assets?search=CMDB-${suffix}`, { headers: auth });
const cmdbAsset = cmdbAssets.assets.find((a) => a.asset_id === `CMDB-${suffix}`);
if (!cmdbAsset || cmdbAsset.serial_number !== `SN-${suffix}` || Number(cmdbAsset.acquisition_cost) !== 2500) {
throw new Error('CMDB-sourced asset fields were not populated');
}
// A second sync should update the same asset, not duplicate it.
const reSync = await request('/api/servicenow/cmdb-sync', { method: 'POST', headers: auth, body: JSON.stringify({}) });
if (reSync.created !== 0 || reSync.updated !== 1) throw new Error('Re-syncing a CMDB CI should update, not duplicate');
} finally {
await new Promise((resolve) => snServer.close(resolve));
}
console.log('Smoke test passed');
}
main()
.catch((error) => {
console.error(error.message);
process.exitCode = 1;
})
.finally(() => {
child.kill('SIGTERM');
for (const suffix of ['', '-wal', '-shm']) {
try {
fs.rmSync(`${dbPath}${suffix}`, { force: true });
} catch {
// best-effort cleanup of the temp database
}
}
});