feat(M10-A): 完成统一通知中心与投递闭环
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
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.');
|
||||
Reference in New Issue
Block a user