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

56 lines
5.0 KiB
JavaScript

import assert from 'node:assert/strict';
import Fastify from 'fastify';
import { signAccessToken } from '../dist/auth/jwt.js';
import { NotificationError } from '../dist/notifications/notification-service.js';
import { registerNotificationRoutes } from '../dist/routes/notifications.js';
const secret = 'test-only-notification-route-secret';
const sessionId = '3a6ab573-1105-4bf9-b75b-e88e49eb3b82';
const token = signAccessToken({ sub: '21', sid: sessionId, tid: '7', aid: '9', rv: 1 }, secret, 900);
const headers = { authorization: `Bearer ${token}`, 'x-trace-id': 'm10a-notification-route' };
let access = { roles: ['STORE_ADMIN'], capabilities: ['notification.read', 'notification.manage'], storeIds: ['11'] };
const calls = [];
const service = {
async listTemplates(actor, input) { calls.push(['listTemplates', actor, input]); return [{ id: '1' }]; },
async saveTemplate(actor, input) { calls.push(['saveTemplate', actor, input]); return { id: input.id ?? '1', version: 1 }; },
async listRoutes(actor, input) { calls.push(['listRoutes', actor, input]); return [{ id: '2' }]; },
async createRoute(actor, input) { calls.push(['createRoute', actor, input]); return { id: '2', version: 1 }; },
async listDeliveries(actor, input) { calls.push(['listDeliveries', actor, input]); return { items: [], total: 0, page: input.page, pageSize: input.pageSize }; },
async manualRetry(actor, deliveryId) { calls.push(['manualRetry', actor, deliveryId]); if (deliveryId === '999') throw new NotificationError('NOTIFICATION_RETRY_NOT_ALLOWED'); return { id: deliveryId, queued: true }; },
async setSubscription(tenantId, userId, input) { calls.push(['setSubscription', tenantId, userId, input]); return input; },
async listInbox(tenantId, userId, input) { calls.push(['listInbox', tenantId, userId, input]); return { items: [], total: 0, ...input }; },
async markInboxRead(tenantId, userId, deliveryId) { calls.push(['markInboxRead', tenantId, userId, deliveryId]); return { id: deliveryId, read: true }; }
};
const app = Fastify({ logger: false });
app.decorateRequest('traceId', '');
app.addHook('onRequest', async (request) => { request.traceId = request.headers['x-trace-id'] || request.id; });
await registerNotificationRoutes(app, {
service,
authRepository: { async validateSession() { return { id: sessionId, 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 access; } }, jwtSecret: secret
});
assert.equal((await app.inject({ method: 'GET', url: '/admin-api/notifications/templates' })).statusCode, 401);
const listed = await app.inject({ method: 'GET', url: '/admin-api/notifications/deliveries?storeId=11&page=2&pageSize=10', headers });
assert.equal(listed.statusCode, 200); assert.equal(listed.json().data.page, 2);
const saved = await app.inject({ method: 'PUT', url: '/app-api/management/notifications/templates', headers, payload: { templateCode: 'PRODUCT_ORDER_PAID_IN_APP', eventType: 'PRODUCT_ORDER_PAID', channel: 'IN_APP', titleTemplate: '新订单', bodyTemplate: '订单 {{orderNo}} 已支付' } });
assert.equal(saved.statusCode, 200);
const routed = await app.inject({ method: 'POST', url: '/admin-api/notifications/routes', headers, payload: { storeId: '11', eventType: 'PRODUCT_ORDER_PAID', templateId: '1', recipientType: 'ROLE', recipientValue: 'STORE_ADMIN', quietStart: '23:00', quietEnd: '07:00' } });
assert.equal(routed.statusCode, 201);
assert.equal((await app.inject({ method: 'POST', url: '/admin-api/notifications/deliveries/3/retry', headers })).statusCode, 200);
assert.equal((await app.inject({ method: 'POST', url: '/admin-api/notifications/deliveries/999/retry', headers })).statusCode, 409);
assert.equal(calls.find((call) => call[0] === 'createRoute')[1].traceId, 'm10a-notification-route');
assert.equal((await app.inject({ method: 'PUT', url: '/app-api/notifications/subscriptions', headers,
payload: { channel: 'WECHAT_SUBSCRIBE', templateCode: 'PRODUCT_ORDER_PAID_WECHAT', status: 'AUTHORIZED' } })).statusCode, 200);
assert.equal((await app.inject({ method: 'GET', url: '/app-api/notifications/inbox?page=2&pageSize=10', headers })).json().data.page, 2);
assert.equal((await app.inject({ method: 'POST', url: '/app-api/notifications/inbox/3/read', headers })).statusCode, 200);
assert.deepEqual(calls.find((call) => call[0] === 'setSubscription').slice(1, 3), ['7', '21']);
access = { roles: ['STAFF'], capabilities: [], storeIds: ['11'] };
assert.equal((await app.inject({ method: 'GET', url: '/admin-api/notifications/templates', headers })).statusCode, 403);
access = { roles: ['STAFF'], capabilities: ['platform.manage'], storeIds: [] };
assert.equal((await app.inject({ method: 'GET', url: '/admin-api/notifications/templates', headers })).statusCode, 200);
await app.close();
console.log('PASS: M10-A notification routes enforce auth, permissions, validation and manual retry boundaries.');