269 lines
9.7 KiB
JavaScript
269 lines
9.7 KiB
JavaScript
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');
|
|
});
|