|
|
@@ -0,0 +1,119 @@
|
|
|
+import assert from 'node:assert/strict';
|
|
|
+import type { AddressInfo } from 'node:net';
|
|
|
+import test from 'node:test';
|
|
|
+import { createLocalDemoApp } from '../src/local-app.js';
|
|
|
+import type { DomesticDataset, DomesticMetricSummary } 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,
|
|
|
+};
|
|
|
+
|
|
|
+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: 1,
|
|
|
+ mappingRows: 0,
|
|
|
+ relations: 0,
|
|
|
+ uniqueCompetitorProducts: 0,
|
|
|
+ category2Count: 0,
|
|
|
+ category3Count: 0,
|
|
|
+ reviewCount: 0,
|
|
|
+ },
|
|
|
+ dailyTotals: [],
|
|
|
+ products: [{
|
|
|
+ platform: 'jd',
|
|
|
+ productId: '11266507445',
|
|
|
+ productKey: 'jd:11266507445',
|
|
|
+ asin: '11266507445',
|
|
|
+ role: 'own',
|
|
|
+ brand: 'Demashi',
|
|
|
+ title: 'Local demo product',
|
|
|
+ model: '',
|
|
|
+ category1: '',
|
|
|
+ category2: '',
|
|
|
+ category3: '',
|
|
|
+ source: 'excel',
|
|
|
+ relationCount: 0,
|
|
|
+ summary: emptyMetrics,
|
|
|
+ trend: [],
|
|
|
+ }],
|
|
|
+ mappingGroups: [],
|
|
|
+ relations: [],
|
|
|
+ reviews: [],
|
|
|
+ quality: {
|
|
|
+ orphanMappings: [],
|
|
|
+ mappingsWithoutCompetitor: [],
|
|
|
+ brandWithoutProductId: [],
|
|
|
+ },
|
|
|
+};
|
|
|
+
|
|
|
+test('local demo serves a snapshot and queryable completed sync jobs without a database', async () => {
|
|
|
+ const app = createLocalDemoApp({ dataset, corsOrigins: ['http://localhost:4200'] });
|
|
|
+ const server = await new Promise<ReturnType<typeof app.listen>>((resolve) => {
|
|
|
+ const listening = app.listen(0, '127.0.0.1', () => resolve(listening));
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ const address = server.address() as AddressInfo;
|
|
|
+ const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
|
+
|
|
|
+ const health = await fetch(`${baseUrl}/health`);
|
|
|
+ assert.equal(health.status, 200);
|
|
|
+ const healthBody = await health.json() as {
|
|
|
+ mode: string;
|
|
|
+ database: string;
|
|
|
+ dataset: { products: number; reviews: number };
|
|
|
+ };
|
|
|
+ assert.equal(healthBody.mode, 'local-demo');
|
|
|
+ assert.equal(healthBody.database, 'deferred');
|
|
|
+ assert.deepEqual(healthBody.dataset, { caseName: 'Demashi', platform: 'jd', products: 1, reviews: 0 });
|
|
|
+
|
|
|
+ const snapshot = await fetch(`${baseUrl}/api/domestic-voc/snapshot?workspaceId=demashi&platform=jd`);
|
|
|
+ assert.equal(snapshot.status, 200);
|
|
|
+ assert.equal((await snapshot.json() as DomesticDataset).products[0]?.productId, '11266507445');
|
|
|
+
|
|
|
+ const syncResponse = await fetch(`${baseUrl}/api/domestic-voc/sync`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'local-demo-sync-11266507445' },
|
|
|
+ body: JSON.stringify({ productIds: ['11266507445'] }),
|
|
|
+ });
|
|
|
+ assert.equal(syncResponse.status, 202);
|
|
|
+ const syncBody = await syncResponse.json() as { job: { id: string; status: string; progress: number } };
|
|
|
+ assert.equal(syncBody.job.status, 'completed');
|
|
|
+ assert.equal(syncBody.job.progress, 100);
|
|
|
+
|
|
|
+ const jobResponse = await fetch(`${baseUrl}/api/domestic-voc/jobs/${syncBody.job.id}`);
|
|
|
+ assert.equal(jobResponse.status, 200);
|
|
|
+ assert.equal((await jobResponse.json() as { job: { status: string } }).job.status, 'completed');
|
|
|
+ } finally {
|
|
|
+ await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
|
+ }
|
|
|
+});
|