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'], 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 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 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 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 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: [] }; }