Files
qipai/backend/tests/store-room.test.mjs
T
2026-08-10 11:40:09 +08:00

148 lines
6.2 KiB
JavaScript

import assert from 'node:assert/strict';
import { buildApp } from '../dist/app.js';
import { signAccessToken } from '../dist/auth/jwt.js';
import { StoreRoomError, StoreRoomRepository } from '../dist/stores/store-room-repository.js';
const tenantActor = {
tenantId: '7', userId: '21',
access: { roles: ['TENANT_ADMIN'], capabilities: ['tenant.manage'], storeIds: [] },
traceId: 'trace', ip: '127.0.0.1', userAgent: 'test'
};
const storeActor = {
tenantId: '7', userId: '22',
access: {
roles: ['STORE_ADMIN'], capabilities: ['store.operation.read', 'store.operation.write'],
storeIds: ['11']
},
traceId: 'trace', ip: '127.0.0.1', userAgent: 'test'
};
const repository = new StoreRoomRepository({
async execute(sql) {
if (sql.includes('FROM qipai_stores')) return [[{
id: 11, name: 'A', longitude: null, latitude: null, wifiConfigured: 1
}], []];
if (sql.includes('FROM qipai_store_business_hours')) return [[{
storeId: 11, weekday: 1, openMinute: 540, closeMinute: 1320, isClosed: 0
}], []];
return [[], []];
}
});
assert.equal((await repository.listStores(storeActor))[0].id, '11');
assert.equal((await repository.listStores(storeActor))[0].wifiConfigured, true);
assert.equal((await repository.listStores(storeActor))[0].businessHours[0].openMinute, 540);
assert.deepEqual(await repository.listRooms({
...storeActor,
access: { roles: ['STAFF'], capabilities: ['store.operation.read'], storeIds: ['11'] }
}, '11'), []);
await assert.rejects(
() => repository.listRooms({ ...storeActor, access: { ...storeActor.access, storeIds: ['12'] } }, '11'),
(error) => error instanceof StoreRoomError && error.code === 'STORE_SCOPE_FORBIDDEN'
);
const secret = 'test-only-jwt-secret-with-at-least-32-characters';
const token = signAccessToken({
sub: '21', sid: '5c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
tid: '7', aid: '9', rv: 1
}, secret, 900);
let roomInput;
let roomStatusInput;
let storeInput;
let currentAccess = tenantActor.access;
const app = await buildApp({
storeRoom: {
jwtSecret: secret,
authRepository: {
async validateSession() {
return {
id: '5c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
tenantId: '7', platformAppId: '9', expiresAt: new Date(Date.now() + 60000),
user: {
id: '21', tenantId: '7', userType: 'STAFF', status: 'ACTIVE',
roleVersion: 1, nickname: '', avatarUrl: '', phone: ''
}
};
}
},
accessControl: { async getAccessProfile() { return currentAccess; } },
repository: {
async listStores() { return []; },
async createStore() { return { storeId: '11' }; },
async updateStore(_actor, _storeId, input) { storeInput = input; return { storeId: '11' }; },
async archiveStore() { return { storeId: '11', archived: true }; },
async listRooms() { return []; },
async createRoom(_actor, input) { roomInput = input; return { roomId: '31' }; },
async updateRoom() { return { roomId: '31' }; },
async updateRoomStatus(actor, _roomId, input) {
if (!actor.access.capabilities.includes('store.operation.write')
&& !actor.access.capabilities.includes('tenant.manage')) {
throw new StoreRoomError('STORE_SCOPE_FORBIDDEN');
}
roomStatusInput = input;
return { roomId: '31', ...input };
},
async archiveRoom() { return { roomId: '31', archived: true }; },
async addDisabledPeriod() { return { disabledPeriodId: '41' }; }
}
}
});
const created = await app.inject({
method: 'POST', url: '/admin-api/rooms',
headers: { authorization: `Bearer ${token}` },
payload: {
storeId: '11', categoryName: '标准间', name: 'A01', roomNo: 'A01', capacity: 4,
basePriceCents: 3000, weekdayPriceCents: 2800, holidayPriceCents: 3500,
overnightPriceCents: 12000, depositCents: 0, minimumMinutes: 60,
maxAdvanceStartMinutes: 30, maxAdvanceDays: 30,
configurationStatus: 'ENABLED', operationalStatus: 'AVAILABLE',
tags: ['麻将'], images: [], sortOrder: 1
}
});
assert.equal(created.statusCode, 201);
assert.equal(created.json().data.roomId, '31');
assert.equal(roomInput.weekdayPriceCents, 2800);
const managementStores = await app.inject({
method: 'GET', url: '/app-api/management/stores',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(managementStores.statusCode, 200);
const updatedStore = await app.inject({
method: 'PUT', url: '/admin-api/stores/11',
headers: { authorization: `Bearer ${token}` },
payload: {
name: 'A 店', address: '测试路 1 号', city: '上海', district: '浦东新区',
contactPhone: '13800000000', timezone: 'Asia/Shanghai', businessStatus: 'OPEN',
wifiSsid: 'qipai-guest', notificationUrl: '', sortOrder: 1,
businessHours: [{ weekday: 1, openMinute: 540, closeMinute: 1320, isClosed: false }]
}
});
assert.equal(updatedStore.statusCode, 200);
assert.equal(storeInput.wifiPassword, undefined);
const statusUpdated = await app.inject({
method: 'PATCH', url: '/app-api/management/rooms/31/status',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', operationalStatus: 'MAINTENANCE', reason: '现场维护' }
});
assert.equal(statusUpdated.statusCode, 200);
assert.equal(roomStatusInput.operationalStatus, 'MAINTENANCE');
currentAccess = { roles: ['STAFF'], capabilities: ['store.operation.read'], storeIds: ['11'] };
const staffRooms = await app.inject({
method: 'GET', url: '/app-api/management/stores/11/rooms',
headers: { authorization: `Bearer ${token}` }
});
assert.equal(staffRooms.statusCode, 200);
const staffWrite = await app.inject({
method: 'PATCH', url: '/app-api/management/rooms/31/status',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', operationalStatus: 'AVAILABLE', reason: '越权尝试' }
});
assert.equal(staffWrite.statusCode, 403);
const invalid = await app.inject({
method: 'POST', url: '/admin-api/rooms/31/disabled-periods',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', startsAt: '2026-06-20T12:00:00Z', endsAt: '2026-06-20T11:00:00Z' }
});
assert.equal(invalid.statusCode, 400);
await app.close();
console.log('PASS: M03-A store scope, room pricing/status validation and management routes are present.');