local-app.test.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import assert from 'node:assert/strict';
  2. import type { AddressInfo } from 'node:net';
  3. import test from 'node:test';
  4. import { createLocalDemoApp } from '../src/local-app.js';
  5. import type { DomesticDataset, DomesticMetricSummary } from '../src/types/domestic-dataset.js';
  6. const emptyMetrics: DomesticMetricSummary = {
  7. gmv: 0,
  8. soldUnits: 0,
  9. transactionOrders: 0,
  10. transactionCustomers: 0,
  11. impressions: 0,
  12. clicks: 0,
  13. views: 0,
  14. visitors: 0,
  15. cartUnits: 0,
  16. orderAmount: 0,
  17. orderUnits: 0,
  18. orderCount: 0,
  19. refundAmount: 0,
  20. refundUnits: 0,
  21. refundOrders: 0,
  22. conversionRate: 0,
  23. clickThroughRate: 0,
  24. averageUnitPrice: 0,
  25. refundToGmvRate: 0,
  26. };
  27. const dataset: DomesticDataset = {
  28. schemaVersion: 1,
  29. generatedAt: '2026-07-23T00:00:00.000Z',
  30. caseName: 'Demashi',
  31. platform: 'jd',
  32. source: {
  33. sourceFile: 'demashi-summary.json',
  34. sourceHash: 'test',
  35. sheets: [],
  36. dateRange: { start: '2026-07-01', end: '2026-07-23' },
  37. },
  38. summary: {
  39. metricRows: 0,
  40. metricProducts: 1,
  41. mappingRows: 0,
  42. relations: 0,
  43. uniqueCompetitorProducts: 0,
  44. category2Count: 0,
  45. category3Count: 0,
  46. reviewCount: 0,
  47. },
  48. dailyTotals: [],
  49. products: [{
  50. platform: 'jd',
  51. productId: '11266507445',
  52. productKey: 'jd:11266507445',
  53. asin: '11266507445',
  54. role: 'own',
  55. brand: 'Demashi',
  56. title: 'Local demo product',
  57. model: '',
  58. category1: '',
  59. category2: '',
  60. category3: '',
  61. source: 'excel',
  62. relationCount: 0,
  63. summary: emptyMetrics,
  64. trend: [],
  65. }],
  66. mappingGroups: [],
  67. relations: [],
  68. reviews: [],
  69. quality: {
  70. orphanMappings: [],
  71. mappingsWithoutCompetitor: [],
  72. brandWithoutProductId: [],
  73. },
  74. };
  75. test('local demo serves a snapshot and queryable completed sync jobs without a database', async () => {
  76. const app = createLocalDemoApp({ dataset, corsOrigins: ['http://localhost:4200'] });
  77. const server = await new Promise<ReturnType<typeof app.listen>>((resolve) => {
  78. const listening = app.listen(0, '127.0.0.1', () => resolve(listening));
  79. });
  80. try {
  81. const address = server.address() as AddressInfo;
  82. const baseUrl = `http://127.0.0.1:${address.port}`;
  83. const health = await fetch(`${baseUrl}/health`);
  84. assert.equal(health.status, 200);
  85. const healthBody = await health.json() as {
  86. mode: string;
  87. database: string;
  88. dataset: { products: number; reviews: number };
  89. };
  90. assert.equal(healthBody.mode, 'local-demo');
  91. assert.equal(healthBody.database, 'deferred');
  92. assert.deepEqual(healthBody.dataset, { caseName: 'Demashi', platform: 'jd', products: 1, reviews: 0 });
  93. const snapshot = await fetch(`${baseUrl}/api/domestic-voc/snapshot?workspaceId=demashi&platform=jd`);
  94. assert.equal(snapshot.status, 200);
  95. assert.equal((await snapshot.json() as DomesticDataset).products[0]?.productId, '11266507445');
  96. const syncResponse = await fetch(`${baseUrl}/api/domestic-voc/sync`, {
  97. method: 'POST',
  98. headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'local-demo-sync-11266507445' },
  99. body: JSON.stringify({ productIds: ['11266507445'] }),
  100. });
  101. assert.equal(syncResponse.status, 202);
  102. const syncBody = await syncResponse.json() as { job: { id: string; status: string; progress: number } };
  103. assert.equal(syncBody.job.status, 'completed');
  104. assert.equal(syncBody.job.progress, 100);
  105. const jobResponse = await fetch(`${baseUrl}/api/domestic-voc/jobs/${syncBody.job.id}`);
  106. assert.equal(jobResponse.status, 200);
  107. assert.equal((await jobResponse.json() as { job: { status: string } }).job.status, 'completed');
  108. } finally {
  109. await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
  110. }
  111. });