Files
qipai/backend/tests/cleaning-route.test.mjs
T

349 lines
11 KiB
JavaScript

import assert from 'node:assert/strict';
import { buildApp } from '../dist/app.js';
import { signAccessToken } from '../dist/auth/jwt.js';
const secret = 'test-only-cleaning-route-secret';
const token = signAccessToken({
sub: '31', sid: '9c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
tid: '7', aid: '9', rv: 1
}, secret, 900);
const forbiddenToken = signAccessToken({
sub: '32', sid: '8c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
tid: '7', aid: '9', rv: 1
}, secret, 900);
const calls = [];
const app = await buildApp({
cleaning: {
jwtSecret: secret,
authRepository: {
async validateSession(sessionId) {
return {
id: sessionId,
tenantId: '7',
platformAppId: '9',
expiresAt: new Date(Date.now() + 60000),
user: {
id: sessionId.startsWith('8') ? '32' : '31',
tenantId: '7',
userType: 'CUSTOMER',
status: 'ACTIVE',
roleVersion: 1,
nickname: '',
avatarUrl: '',
phone: ''
}
};
}
},
accessControl: {
async getAccessProfile(tenantId, userId) {
return userId === '31'
? {
roles: ['CLEANER'],
capabilities: [
'cleaning.task.read', 'cleaning.task.write', 'cleaning.statistics.read',
'cleaning.settlement.read', 'cleaning.settlement.write'
],
storeIds: ['11']
}
: { roles: ['CUSTOMER'], capabilities: ['profile.read'], storeIds: [] };
}
},
repository: {
async listHall(input) {
calls.push(['listHall', input]);
return { items: [task('WAITING')], total: 1, page: input.page, pageSize: input.pageSize };
},
async listMine(input) {
calls.push(['listMine', input]);
return { items: [task('CLAIMED')], total: 1, page: input.page, pageSize: input.pageSize };
},
async listManage(input) {
calls.push(['listManage', input]);
return { items: [task(input.status || 'SUBMITTED')], total: 1, page: input.page, pageSize: input.pageSize };
},
async claim(input) {
calls.push(['claim', input]);
return task('CLAIMED');
},
async start(input) {
calls.push(['start', input]);
return task('STARTED');
},
async rework(input) {
calls.push(['rework', input]);
return task('STARTED');
},
async assertCanUploadPhoto(input) {
calls.push(['assertCanUploadPhoto', input]);
},
async submit(input) {
calls.push(['submit', input]);
return { ...task('SUBMITTED'), photoUrls: input.photoUrls };
},
async assign(input) {
calls.push(['assign', input]);
return { ...task('CLAIMED'), cleanerUserId: input.cleanerUserId };
},
async complete(input) {
calls.push(['complete', input]);
return task('COMPLETED');
},
async reject(input) {
calls.push(['reject', input]);
return { ...task('REJECTED'), rejectReason: input.reason };
},
async settlementCandidates(input) {
calls.push(['settlementCandidates', input]);
return { items: [task('COMPLETED')], total: 1, page: input.page, pageSize: input.pageSize };
},
async listSettlements(input) {
calls.push(['listSettlements', input]);
return { items: [settlementResponse(input.status || 'DRAFT')], total: 1, page: input.page, pageSize: input.pageSize };
},
async generateSettlement(input) {
calls.push(['generateSettlement', input]);
return settlementResponse('DRAFT');
},
async confirmSettlement(input) {
calls.push(['confirmSettlement', input]);
return settlementResponse('CONFIRMED');
},
async reclaimTimeouts(input) {
calls.push(['reclaimTimeouts', input]);
return { reclaimed: 2, taskIds: ['101', '102'] };
},
async stats(input) {
calls.push(['stats', input]);
return { byStatus: { SUBMITTED: 2 }, pendingSettlementCents: 1200 };
}
},
mediaStorage: {
async storeImage(input) {
calls.push(['storeImage', input]);
return {
storagePath: 'tenants/7/shared/cleaning.webp',
publicUrl: 'https://api.txyundm.cn/uploads/tenants/7/shared/cleaning.webp',
mimeType: 'image/webp',
byteSize: input.body.length,
width: 640,
height: 480,
checksumSha256: 'abc'
};
}
}
}
});
const hall = await app.inject({
method: 'GET',
url: '/app-api/cleaning/tasks/hall?page=1&pageSize=10',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(hall.statusCode, 200);
assert.equal(hall.json().data.items[0].status, 'WAITING');
assert.equal(calls.at(-1)[1].tenantId, '7');
assert.equal(calls.at(-1)[1].userId, '31');
const mine = await app.inject({
method: 'GET',
url: '/app-api/cleaning/tasks/mine?status=CLAIMED',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(mine.statusCode, 200);
assert.equal(calls.at(-1)[0], 'listMine');
assert.equal(calls.at(-1)[1].status, 'CLAIMED');
const manageList = await app.inject({
method: 'GET',
url: '/admin-api/cleaning/tasks?status=SUBMITTED',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(manageList.statusCode, 200);
assert.equal(calls.at(-1)[0], 'listManage');
assert.equal(calls.at(-1)[1].status, 'SUBMITTED');
const claim = await app.inject({
method: 'POST',
url: '/app-api/cleaning/tasks/101/claim',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(claim.statusCode, 200);
assert.equal(calls.at(-1)[0], 'claim');
assert.equal(calls.at(-1)[1].taskId, '101');
const start = await app.inject({
method: 'POST',
url: '/app-api/cleaning/tasks/101/start',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(start.statusCode, 200);
assert.equal(calls.at(-1)[0], 'start');
const rework = await app.inject({
method: 'POST',
url: '/app-api/cleaning/tasks/101/rework',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(rework.statusCode, 200);
assert.equal(calls.at(-1)[0], 'rework');
const photo = await app.inject({
method: 'POST',
url: '/app-api/cleaning/tasks/101/photos',
headers: {
authorization: `Bearer ${token}`,
'content-type': 'application/octet-stream',
'x-file-name': 'cleaning.jpg',
'x-image-content-type': 'image/jpeg'
},
payload: Buffer.from('fake-image')
});
assert.equal(photo.statusCode, 201);
assert.equal(photo.json().data.publicUrl, 'https://api.txyundm.cn/uploads/tenants/7/shared/cleaning.webp');
assert.equal(calls.at(-2)[0], 'assertCanUploadPhoto');
assert.equal(calls.at(-1)[0], 'storeImage');
const submit = await app.inject({
method: 'POST',
url: '/app-api/cleaning/tasks/101/submit',
headers: { authorization: `Bearer ${token}` },
payload: { photoUrls: ['https://api.txyundm.cn/uploads/cleaning/101.jpg'], note: 'ok' }
});
assert.equal(submit.statusCode, 200);
assert.deepEqual(calls.at(-1)[1].photoUrls, ['https://api.txyundm.cn/uploads/cleaning/101.jpg']);
const assign = await app.inject({
method: 'POST',
url: '/admin-api/cleaning/tasks/101/assign',
headers: { authorization: `Bearer ${token}` },
payload: { cleanerUserId: '33', note: 'manual dispatch' }
});
assert.equal(assign.statusCode, 200);
assert.equal(calls.at(-1)[0], 'assign');
assert.equal(calls.at(-1)[1].cleanerUserId, '33');
const complete = await app.inject({
method: 'POST',
url: '/admin-api/cleaning/tasks/101/complete',
headers: { authorization: `Bearer ${token}` },
payload: { note: 'ok' }
});
assert.equal(complete.statusCode, 200);
assert.equal(calls.at(-1)[0], 'complete');
const reject = await app.inject({
method: 'POST',
url: '/admin-api/cleaning/tasks/101/reject',
headers: { authorization: `Bearer ${token}` },
payload: { reason: 'photo is unclear' }
});
assert.equal(reject.statusCode, 200);
assert.equal(calls.at(-1)[0], 'reject');
assert.equal(calls.at(-1)[1].reason, 'photo is unclear');
const settlement = await app.inject({
method: 'GET',
url: '/admin-api/cleaning/settlement-candidates?page=1&pageSize=10',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(settlement.statusCode, 200);
assert.equal(calls.at(-1)[0], 'settlementCandidates');
const settlementList = await app.inject({
method: 'GET',
url: '/admin-api/cleaning/settlements?status=DRAFT',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(settlementList.statusCode, 200);
assert.equal(calls.at(-1)[0], 'listSettlements');
assert.equal(calls.at(-1)[1].status, 'DRAFT');
const generatedSettlement = await app.inject({
method: 'POST',
url: '/admin-api/cleaning/settlements',
headers: { authorization: `Bearer ${token}` },
payload: { cleanerUserId: '31', storeId: '11', note: 'weekly settlement' }
});
assert.equal(generatedSettlement.statusCode, 201);
assert.equal(calls.at(-1)[0], 'generateSettlement');
assert.equal(calls.at(-1)[1].cleanerUserId, '31');
const confirmedSettlement = await app.inject({
method: 'POST',
url: '/admin-api/cleaning/settlements/501/confirm',
headers: { authorization: `Bearer ${token}` },
payload: { note: 'confirmed' }
});
assert.equal(confirmedSettlement.statusCode, 200);
assert.equal(calls.at(-1)[0], 'confirmSettlement');
assert.equal(calls.at(-1)[1].settlementId, '501');
const reclaimed = await app.inject({
method: 'POST',
url: '/admin-api/cleaning/reclaim-timeouts',
headers: { authorization: `Bearer ${token}` },
payload: { olderThanMinutes: 30, limit: 10 }
});
assert.equal(reclaimed.statusCode, 200);
assert.equal(calls.at(-1)[0], 'reclaimTimeouts');
assert.equal(calls.at(-1)[1].olderThanMinutes, 30);
const stats = await app.inject({
method: 'GET',
url: '/app-api/cleaning/stats',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(stats.statusCode, 200);
assert.equal(stats.json().data.pendingSettlementCents, 1200);
const unauthorized = await app.inject({ method: 'GET', url: '/app-api/cleaning/tasks/hall' });
assert.equal(unauthorized.statusCode, 401);
const forbidden = await app.inject({
method: 'GET',
url: '/app-api/cleaning/tasks/hall',
headers: { authorization: `Bearer ${forbiddenToken}` }
});
assert.equal(forbidden.statusCode, 403);
await app.close();
console.log('PASS: M08-B cleaning routes authenticate and forward cleaner task workflows.');
function task(status) {
return {
id: '101',
taskNo: 'CLN-20260625-0001',
storeId: '11',
storeName: 'Test Store',
roomId: '21',
roomName: 'A Room',
roomNo: 'A01',
orderId: '301',
orderNo: 'O301',
status,
rewardCents: 600,
photoUrls: []
};
}
function settlementResponse(status) {
return {
id: '501',
settlementNo: 'CLS-20260626-0001',
cleanerUserId: '31',
cleanerName: 'Cleaner',
storeId: '11',
storeName: 'Test Store',
status,
taskCount: 2,
totalRewardCents: 1200,
periodStart: new Date(),
periodEnd: new Date(),
confirmedAt: status === 'CONFIRMED' ? new Date() : null,
paidAt: null,
note: ''
};
}