init
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
process.chdir(root);
|
||||
|
||||
const dbPath = path.join(root, 'data', 'api-check.db');
|
||||
for (const file of [dbPath, `${dbPath}-journal`, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
||||
if (fs.existsSync(file)) fs.unlinkSync(file);
|
||||
}
|
||||
|
||||
process.env.DATABASE_URL = 'file:../data/api-check.db';
|
||||
process.env.JWT_SECRET = 'api-check-secret';
|
||||
process.env.TEXTLK_API_TOKEN = '';
|
||||
process.env.SEED_ADMIN_NAME = 'Super Admin';
|
||||
process.env.SEED_ADMIN_PHONE = '94770000001';
|
||||
|
||||
execSync('npx prisma db push --skip-generate --accept-data-loss', {
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
const app = require('../src/app');
|
||||
const prisma = require('../src/db');
|
||||
const { ageBadge } = require('../src/lib/stockRules');
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
async function request(port, method, urlPath, { token, body } = {}) {
|
||||
const response = await fetch(`http://127.0.0.1:${port}${urlPath}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
if (text) {
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
} catch {
|
||||
json = { raw: text };
|
||||
}
|
||||
}
|
||||
return { status: response.status, json };
|
||||
}
|
||||
|
||||
function daysAgo(days) {
|
||||
return new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
||||
}
|
||||
|
||||
async function run(port) {
|
||||
assert(ageBadge(29) === 'green', '29 days is green');
|
||||
assert(ageBadge(30) === 'yellow', '30 days is yellow');
|
||||
assert(ageBadge(45) === 'yellow', '45 days is yellow');
|
||||
assert(ageBadge(46) === 'red', '46 days is red');
|
||||
|
||||
const unknown = await request(port, 'POST', '/api/auth/send-otp', {
|
||||
body: { phone: '94770000099' },
|
||||
});
|
||||
assert(unknown.status === 404, `unknown phone should be 404, got ${unknown.status}`);
|
||||
|
||||
await prisma.user.create({
|
||||
data: { name: 'Super Admin', phone: '94770000001', role: 'superAdmin' },
|
||||
});
|
||||
|
||||
const sent = await request(port, 'POST', '/api/auth/send-otp', {
|
||||
body: { phone: '94770000001' },
|
||||
});
|
||||
assert(sent.status === 200 && sent.json.success === true, 'OTP send failed');
|
||||
|
||||
const adminRow = await prisma.user.findUnique({ where: { phone: '94770000001' } });
|
||||
const wrongCode = adminRow.currentOtp === '000000' ? '111111' : '000000';
|
||||
const badOtp = await request(port, 'POST', '/api/auth/verify-otp', {
|
||||
body: { phone: '94770000001', otp: wrongCode },
|
||||
});
|
||||
assert(badOtp.status === 401, 'wrong OTP should be rejected');
|
||||
|
||||
const verified = await request(port, 'POST', '/api/auth/verify-otp', {
|
||||
body: { phone: '94770000001', otp: adminRow.currentOtp },
|
||||
});
|
||||
assert(verified.status === 200 && verified.json.token, 'OTP verify failed');
|
||||
const adminToken = verified.json.token;
|
||||
|
||||
const me = await request(port, 'GET', '/api/auth/me', { token: adminToken });
|
||||
assert(me.status === 200 && me.json.role === 'superAdmin', '/me failed');
|
||||
const missing = await request(port, 'GET', '/api/auth/me');
|
||||
assert(missing.status === 401, 'missing token should be 401');
|
||||
|
||||
const dealerA = await request(port, 'POST', '/api/admin/dealers', {
|
||||
token: adminToken,
|
||||
body: { name: 'Kandy Motors', code: 'KDY', district: 'Kandy' },
|
||||
});
|
||||
const dealerB = await request(port, 'POST', '/api/admin/dealers', {
|
||||
token: adminToken,
|
||||
body: { name: 'Galle Motors', code: 'GAL', district: 'Galle' },
|
||||
});
|
||||
const dealerC = await request(port, 'POST', '/api/admin/dealers', {
|
||||
token: adminToken,
|
||||
body: { name: 'Jaffna Motors', code: 'JAF', district: 'Jaffna' },
|
||||
});
|
||||
assert(dealerA.status === 201 && dealerB.status === 201 && dealerC.status === 201, 'dealer create failed');
|
||||
|
||||
const rep = await request(port, 'POST', '/api/admin/sales-reps', {
|
||||
token: adminToken,
|
||||
body: {
|
||||
name: 'Nimal Perera',
|
||||
phone: '94771111111',
|
||||
dealerIds: [dealerA.json.data.id, dealerB.json.data.id],
|
||||
},
|
||||
});
|
||||
assert(rep.status === 201, `sales rep create failed ${rep.status}`);
|
||||
|
||||
const dealerUser = await request(port, 'POST', '/api/admin/dealer-users', {
|
||||
token: adminToken,
|
||||
body: { name: 'Kandy Desk', phone: '94772222222', dealerId: dealerA.json.data.id },
|
||||
});
|
||||
assert(dealerUser.status === 201, 'dealer user create failed');
|
||||
|
||||
async function login(phone) {
|
||||
const otpSend = await request(port, 'POST', '/api/auth/send-otp', { body: { phone } });
|
||||
assert(otpSend.status === 200, `otp send failed for ${phone}`);
|
||||
const row = await prisma.user.findUnique({ where: { phone } });
|
||||
const otpVerify = await request(port, 'POST', '/api/auth/verify-otp', {
|
||||
body: { phone, otp: row.currentOtp },
|
||||
});
|
||||
assert(otpVerify.status === 200, `otp verify failed for ${phone}`);
|
||||
return otpVerify.json.token;
|
||||
}
|
||||
|
||||
const repToken = await login('94771111111');
|
||||
const dealerToken = await login('94772222222');
|
||||
|
||||
const forbiddenStock = await request(port, 'POST', '/api/stocks', {
|
||||
token: dealerToken,
|
||||
body: {
|
||||
chassisNumber: 'NOPE1',
|
||||
engineNumber: 'ENG-NOPE',
|
||||
model: 'gn125',
|
||||
color: 'Red',
|
||||
currentDealerId: dealerA.json.data.id,
|
||||
},
|
||||
});
|
||||
assert(forbiddenStock.status === 403, 'dealer should not add stock');
|
||||
|
||||
async function addStock(chassis, engine, days, dealerId) {
|
||||
const created = await request(port, 'POST', '/api/stocks', {
|
||||
token: adminToken,
|
||||
body: {
|
||||
chassisNumber: chassis,
|
||||
engineNumber: engine,
|
||||
model: 'gn125',
|
||||
color: 'Black',
|
||||
currentDealerId: dealerId,
|
||||
dateReceived: daysAgo(days),
|
||||
},
|
||||
});
|
||||
assert(created.status === 201, `stock create failed for ${chassis}: ${created.status} ${JSON.stringify(created.json)}`);
|
||||
return created.json.data;
|
||||
}
|
||||
|
||||
const green = await addStock('CH-GREEN', 'EN-GREEN', 10, dealerA.json.data.id);
|
||||
const yellow = await addStock('CH-YELLOW', 'EN-YELLOW', 35, dealerA.json.data.id);
|
||||
const red = await addStock('CH-RED', 'EN-RED', 50, dealerA.json.data.id);
|
||||
assert(green.badge === 'green' && green.ageInDays === 10, `green badge mismatch ${JSON.stringify(green)}`);
|
||||
assert(yellow.badge === 'yellow' && yellow.ageInDays === 35, 'yellow badge mismatch');
|
||||
assert(red.badge === 'red' && red.ageInDays === 50, 'red badge mismatch');
|
||||
assert(green.stageLogDates.received, 'received stage was not logged');
|
||||
|
||||
const skip = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
|
||||
token: adminToken,
|
||||
body: { stage: 'plateReceived' },
|
||||
});
|
||||
assert(skip.status === 400, 'skipping stages should fail');
|
||||
|
||||
const ordered = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
|
||||
token: dealerToken,
|
||||
body: { stage: 'orderedByCustomer' },
|
||||
});
|
||||
assert(ordered.status === 200, 'dealer stage update failed');
|
||||
|
||||
const docs = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
|
||||
token: dealerToken,
|
||||
body: { stage: 'documentsProcessed' },
|
||||
});
|
||||
assert(docs.status === 200, 'documents stage failed');
|
||||
|
||||
const payMissing = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
|
||||
token: dealerToken,
|
||||
body: { stage: 'paymentApproved' },
|
||||
});
|
||||
assert(payMissing.status === 400, 'payment without type should fail');
|
||||
|
||||
const paid = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
|
||||
token: dealerToken,
|
||||
body: {
|
||||
stage: 'paymentApproved',
|
||||
paymentType: 'lease',
|
||||
leaseProviderType: 'thirdPartyFinance',
|
||||
financeCompanyName: 'Sampath Finance',
|
||||
},
|
||||
});
|
||||
assert(paid.status === 200 && paid.json.data.paymentType === 'lease', 'lease payment failed');
|
||||
assert(paid.json.data.stageLogDates.paymentApproved, 'payment stage log missing');
|
||||
|
||||
const outside = await request(port, 'POST', '/api/stocks/CH-YELLOW/transfer', {
|
||||
token: repToken,
|
||||
body: { dealerId: dealerC.json.data.id },
|
||||
});
|
||||
assert(outside.status === 403, 'rep should not transfer outside assignment');
|
||||
|
||||
const moved = await request(port, 'POST', '/api/stocks/CH-YELLOW/transfer', {
|
||||
token: repToken,
|
||||
body: { dealerId: dealerB.json.data.id },
|
||||
});
|
||||
assert(moved.status === 200 && moved.json.data.currentDealerId === dealerB.json.data.id, 'transfer failed');
|
||||
|
||||
const eventBad = await request(port, 'POST', '/api/events', {
|
||||
token: dealerToken,
|
||||
body: {
|
||||
dealerId: dealerA.json.data.id,
|
||||
planType: 'monthlySpecial',
|
||||
title: 'April push',
|
||||
description: 'Showroom weekend',
|
||||
expense: 15000,
|
||||
imageUrls: [],
|
||||
},
|
||||
});
|
||||
assert(eventBad.status === 400, 'monthly plan without month should fail');
|
||||
|
||||
const eventOk = await request(port, 'POST', '/api/events', {
|
||||
token: dealerToken,
|
||||
body: {
|
||||
dealerId: dealerA.json.data.id,
|
||||
planType: 'monthlySpecial',
|
||||
month: 4,
|
||||
year: 2026,
|
||||
title: 'April push',
|
||||
description: 'Showroom weekend',
|
||||
expense: 15000,
|
||||
imageUrls: ['https://example.com/poster.jpg'],
|
||||
},
|
||||
});
|
||||
assert(eventOk.status === 201, 'event create failed');
|
||||
|
||||
const otherDealerEvent = await request(port, 'POST', '/api/events', {
|
||||
token: dealerToken,
|
||||
body: {
|
||||
dealerId: dealerB.json.data.id,
|
||||
planType: 'general',
|
||||
title: 'Nope',
|
||||
description: 'Wrong dealer',
|
||||
expense: 1,
|
||||
imageUrls: [],
|
||||
},
|
||||
});
|
||||
assert(otherDealerEvent.status === 403, 'dealer should not post events for another dealer');
|
||||
|
||||
const pointForbidden = await request(port, 'POST', '/api/service-points', {
|
||||
token: dealerToken,
|
||||
body: { name: 'Hidden', district: 'Ratnapura' },
|
||||
});
|
||||
assert(pointForbidden.status === 403, 'dealer should not add service points');
|
||||
|
||||
const point = await request(port, 'POST', '/api/service-points', {
|
||||
token: repToken,
|
||||
body: {
|
||||
name: 'Ratnapura Cycle Repair',
|
||||
district: 'Ratnapura',
|
||||
address: 'Main street',
|
||||
notes: 'engine repair',
|
||||
},
|
||||
});
|
||||
assert(point.status === 201, 'service point create failed');
|
||||
|
||||
const found = await request(
|
||||
port,
|
||||
'GET',
|
||||
'/api/service-points?district=Ratnapura&search=repair',
|
||||
{ token: dealerToken }
|
||||
);
|
||||
assert(
|
||||
found.status === 200 && found.json.data.some((item) => item.name === 'Ratnapura Cycle Repair'),
|
||||
'service point search failed'
|
||||
);
|
||||
|
||||
const reassigned = await request(port, 'PATCH', '/api/admin/assign-dealer', {
|
||||
token: adminToken,
|
||||
body: { salesRepId: rep.json.data.id, dealerId: dealerC.json.data.id, assigned: true },
|
||||
});
|
||||
assert(
|
||||
reassigned.status === 200 && reassigned.json.data.assignedDealerIds.includes(dealerC.json.data.id),
|
||||
'assign dealer failed'
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const server = await new Promise((resolve) => {
|
||||
const listening = app.listen(0, '127.0.0.1', () => resolve(listening));
|
||||
});
|
||||
try {
|
||||
await run(server.address().port);
|
||||
console.log('API checks passed');
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
await prisma.$disconnect();
|
||||
for (const file of [dbPath, `${dbPath}-journal`, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
||||
if (fs.existsSync(file)) fs.unlinkSync(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user