import assert from 'node:assert/strict'; import type { AddressInfo } from 'node:net'; import test from 'node:test'; import express, { type ErrorRequestHandler } from 'express'; import { ZodError } from 'zod'; import { ApiError } from '../src/http/api-error.js'; import { createLocalDemoApp } from '../src/local-app.js'; import { LocalSyncJobStore } from '../src/modules/domestic-voc/local/local-sync-job.store.js'; import { createAuthenticationMiddleware, type RequestAuthenticator, WorkspaceAccessService, } from '../src/modules/saas-platform/auth.js'; import { LocalPlatformRepository } from '../src/modules/saas-platform/local-platform.repository.js'; import { createSaasPlatformRouter } from '../src/modules/saas-platform/routes.js'; import type { DomesticDataset, DomesticMetricSummary, DomesticProduct } from '../src/types/domestic-dataset.js'; const emptyMetrics: DomesticMetricSummary = { gmv: 0, soldUnits: 0, transactionOrders: 0, transactionCustomers: 0, impressions: 0, clicks: 0, views: 0, visitors: 0, cartUnits: 0, orderAmount: 0, orderUnits: 0, orderCount: 0, refundAmount: 0, refundUnits: 0, refundOrders: 0, conversionRate: 0, clickThroughRate: 0, averageUnitPrice: 0, refundToGmvRate: 0, }; function product(productId: string, role: DomesticProduct['role']): DomesticProduct { return { platform: 'jd', productId, productKey: `jd:${productId}`, asin: productId, role, brand: role === 'own' ? 'Demashi' : 'Competitor', title: `Product ${productId}`, model: `M-${productId}`, category1: 'Commercial appliance', category2: 'Kitchen equipment', category3: 'Oven', source: 'test', relationCount: role === 'own' ? 2 : 0, summary: emptyMetrics, trend: [], }; } const dataset: DomesticDataset = { schemaVersion: 1, generatedAt: '2026-07-23T00:00:00.000Z', caseName: 'Demashi', platform: 'jd', source: { sourceFile: 'demashi-summary.json', sourceHash: 'test', sheets: [], dateRange: { start: '2026-07-01', end: '2026-07-23' }, }, summary: { metricRows: 0, metricProducts: 3, mappingRows: 2, relations: 2, uniqueCompetitorProducts: 2, category2Count: 1, category3Count: 1, reviewCount: 0, }, dailyTotals: [], products: [product('1001', 'own'), product('1002', 'competitor'), product('1003', 'competitor')], mappingGroups: [], relations: [ { relationKey: 'jd:1001:1002', ownProductKey: 'jd:1001', ownProductId: '1001', competitorProductKey: 'jd:1002', competitorProductId: '1002', competitorBrand: 'Competitor', category: 'Oven', }, { relationKey: 'jd:1001:1003', ownProductKey: 'jd:1001', ownProductId: '1001', competitorProductKey: 'jd:1003', competitorProductId: '1003', competitorBrand: 'Competitor', category: 'Oven', }, ], reviews: [], quality: { orphanMappings: [], mappingsWithoutCompetitor: [], brandWithoutProductId: [] }, }; async function listen(app: ReturnType) { const server = await new Promise>((resolve) => { const listening = app.listen(0, '127.0.0.1', () => resolve(listening)); }); const address = server.address() as AddressInfo; return { baseUrl: `http://127.0.0.1:${address.port}`, close: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())), }; } async function json(response: Response): Promise> { return await response.json() as Record; } test('local SaaS APIs cover context, cursor catalogs, workflows, and audit history', async () => { const app = createLocalDemoApp({ dataset, corsOrigins: ['http://localhost:4200'] }); const server = await listen(app); try { const contextResponse = await fetch(`${server.baseUrl}/api/saas/context`); assert.equal(contextResponse.status, 200); const context = await json(contextResponse); assert.equal(context.principal.userId, 'local-admin'); assert.equal(context.workspaces[0].role, 'owner'); const firstProductsResponse = await fetch(`${server.baseUrl}/api/domestic-voc/products?limit=2`); assert.equal(firstProductsResponse.status, 200); const firstProducts = await json(firstProductsResponse); assert.equal(firstProducts.items.length, 2); assert.equal(typeof firstProducts.nextCursor, 'string'); const secondProductsResponse = await fetch( `${server.baseUrl}/api/domestic-voc/products?limit=2&cursor=${encodeURIComponent(firstProducts.nextCursor)}`, ); assert.equal(secondProductsResponse.status, 200); const secondProducts = await json(secondProductsResponse); assert.equal(secondProducts.items.length, 1); assert.equal(secondProducts.nextCursor, null); const invalidCursor = await fetch(`${server.baseUrl}/api/domestic-voc/products?cursor=not-a-cursor`); assert.equal(invalidCursor.status, 400); assert.equal((await json(invalidCursor)).error, 'invalid_cursor'); const firstRelations = await json(await fetch(`${server.baseUrl}/api/domestic-voc/relations?limit=1`)); assert.equal(firstRelations.items.length, 1); assert.equal(typeof firstRelations.nextCursor, 'string'); const memberResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/members/viewer-user`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'viewer@example.test', displayName: 'Viewer', role: 'viewer', status: 'active', }), }); assert.equal(memberResponse.status, 200); assert.equal((await json(memberResponse)).member.role, 'viewer'); const analysisResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ analysisType: 'voice', targetKind: 'workspace', input: { platform: 'jd' } }), }); assert.equal(analysisResponse.status, 202); assert.equal((await json(analysisResponse)).analysis.status, 'pending'); const analyses = await json(await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses`)); assert.equal(analyses.items.length, 1); const actionResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'Verify review source', priority: 'high' }), }); assert.equal(actionResponse.status, 201); const action = (await json(actionResponse)).action; const completedActionResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions/${action.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'completed' }), }); assert.equal(completedActionResponse.status, 200); assert.equal((await json(completedActionResponse)).action.status, 'completed'); const completedActions = await json(await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions?status=completed`)); assert.equal(completedActions.items.length, 1); assert.equal(typeof completedActions.items[0].completedAt, 'string'); const alertResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/alerts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'Review evidence unavailable', alertType: 'data_quality', severity: 'high' }), }); assert.equal(alertResponse.status, 201); const alert = (await json(alertResponse)).alert; const resolvedAlertResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/alerts/${alert.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'resolved' }), }); assert.equal(resolvedAlertResponse.status, 200); assert.equal(typeof (await json(resolvedAlertResponse)).alert.resolvedAt, 'string'); const syncResponse = await fetch(`${server.baseUrl}/api/domestic-voc/sync`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'platform-audit-sync-1001' }, body: JSON.stringify({ productIds: ['1001'] }), }); assert.equal(syncResponse.status, 202); const auditResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/audit?limit=100`); assert.equal(auditResponse.status, 200); const auditActions = (await json(auditResponse)).items.map((item: { action: string }) => item.action); assert.ok(auditActions.includes('member.upserted')); assert.ok(auditActions.includes('analysis.created')); assert.ok(auditActions.includes('action.updated')); assert.ok(auditActions.includes('alert.updated')); assert.ok(auditActions.includes('sync.requested')); } finally { await server.close(); } }); test('viewer membership can read but cannot call write or member-management routes', async () => { const jobs = new LocalSyncJobStore(dataset); const repository = new LocalPlatformRepository(dataset, jobs, { userId: 'local-admin', email: 'local-admin@localhost', displayName: 'Local Admin', }); await repository.upsertMember({ workspaceId: 'demashi', userId: 'viewer-user', email: 'viewer@example.test', displayName: 'Viewer', role: 'viewer', status: 'active', }); const authenticator: RequestAuthenticator = { async authenticate(request) { const userId = request.header('X-Test-User') || 'local-admin'; return { userId, email: `${userId}@example.test`, displayName: userId, authMode: 'disabled' }; }, }; const app = express(); app.use(express.json()); app.use('/api', createAuthenticationMiddleware(authenticator)); app.use('/api/saas', createSaasPlatformRouter({ repository, access: new WorkspaceAccessService(repository), })); const errorHandler: ErrorRequestHandler = (error, _request, response, _next) => { if (error instanceof ApiError) { response.status(error.status).json({ error: error.code }); return; } if (error instanceof ZodError) { response.status(400).json({ error: 'invalid_request' }); return; } response.status(500).json({ error: 'internal_error' }); }; app.use(errorHandler); const server = await listen(app); const viewerHeaders = { 'Content-Type': 'application/json', 'X-Test-User': 'viewer-user' }; try { const readable = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { headers: viewerHeaders }); assert.equal(readable.status, 200); const writes = [ fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses`, { method: 'POST', headers: viewerHeaders, body: JSON.stringify({ analysisType: 'voice' }), }), fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers: viewerHeaders, body: JSON.stringify({ title: 'Forbidden' }), }), fetch(`${server.baseUrl}/api/saas/workspaces/demashi/alerts`, { method: 'POST', headers: viewerHeaders, body: JSON.stringify({ title: 'Forbidden' }), }), fetch(`${server.baseUrl}/api/saas/workspaces/demashi/members/new-user`, { method: 'PUT', headers: viewerHeaders, body: JSON.stringify({ role: 'viewer', status: 'active' }), }), ]; for (const response of await Promise.all(writes)) { assert.equal(response.status, 403); assert.equal((await json(response)).error, 'workspace_permission_denied'); } } finally { await server.close(); } });