app.test.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import assert from 'node:assert/strict';
  2. import type { AddressInfo } from 'node:net';
  3. import test from 'node:test';
  4. import type { Pool } from 'pg';
  5. import { createApp } from '../src/app.js';
  6. import { loadConfig } from '../src/config/env.js';
  7. const config = loadConfig({
  8. NODE_ENV: 'test',
  9. HOST: '127.0.0.1',
  10. PORT: '4400',
  11. DATABASE_URL: 'postgres://runtime:password@127.0.0.1:5432/saas_voc',
  12. PARSE_APP_ID: 'test-app-id',
  13. PARSE_MASTER_KEY: 'a'.repeat(32),
  14. PARSE_MAINTENANCE_KEY: 'b'.repeat(32),
  15. PARSE_SERVER_URL: 'http://127.0.0.1:4400/parse',
  16. API_AUTH_MODE: 'disabled',
  17. FMODE_BASE_URL: 'http://127.0.0.1:3000/api/voc-e-commerce',
  18. FMODE_API_KEY: 'test-key',
  19. CORS_ORIGINS: 'http://127.0.0.1:4300',
  20. });
  21. function createMockPool(): Pool {
  22. return {
  23. async query(text: string) {
  24. if (text.includes('to_regclass')) {
  25. return { rows: [{ schema_ready: 'voc.workspace' }], rowCount: 1 };
  26. }
  27. if (text.includes('WITH target_workspace')) {
  28. return {
  29. rows: [{
  30. public_id: '11111111-1111-4111-8111-111111111111',
  31. workspace_public_id: 'demashi',
  32. platform: 'jd',
  33. status: 'pending',
  34. scopes: ['product', 'reviews'],
  35. product_ids: ['11266507445'],
  36. progress: 0,
  37. attempts: 0,
  38. max_attempts: 3,
  39. error_summary: null,
  40. requested_at: '2026-07-23T00:00:00.000Z',
  41. started_at: null,
  42. completed_at: null,
  43. }],
  44. rowCount: 1,
  45. };
  46. }
  47. if (text.includes('FROM voc.workspace_member')) {
  48. return {
  49. rows: [{
  50. id: '1',
  51. public_id: 'demashi',
  52. user_external_id: 'local-admin',
  53. email: 'local-admin@localhost',
  54. display_name: 'Local Admin',
  55. role: 'owner',
  56. status: 'active',
  57. created_at: '2026-07-23T00:00:00.000Z',
  58. updated_at: '2026-07-23T00:00:00.000Z',
  59. }],
  60. rowCount: 1,
  61. };
  62. }
  63. if (text.includes('FROM voc.workspace')) {
  64. return { rows: [{ id: '1', public_id: 'demashi', case_name: 'Demashi' }], rowCount: 1 };
  65. }
  66. return { rows: [], rowCount: 0 };
  67. },
  68. } as unknown as Pool;
  69. }
  70. test('HTTP surface exposes health, empty snapshot, and sync queue contract', async () => {
  71. const app = createApp({ config, pool: createMockPool() });
  72. const server = await new Promise<ReturnType<typeof app.listen>>((resolve) => {
  73. const listening = app.listen(0, '127.0.0.1', () => resolve(listening));
  74. });
  75. try {
  76. const address = server.address() as AddressInfo;
  77. const baseUrl = `http://127.0.0.1:${address.port}`;
  78. const health = await fetch(`${baseUrl}/health`);
  79. assert.equal(health.status, 200);
  80. assert.equal((await health.json() as { database: string }).database, 'ready');
  81. const snapshot = await fetch(`${baseUrl}/api/domestic-voc/snapshot?workspaceId=demashi&platform=jd`);
  82. assert.equal(snapshot.status, 200);
  83. assert.deepEqual((await snapshot.json() as { products: unknown[] }).products, []);
  84. const sync = await fetch(`${baseUrl}/api/domestic-voc/sync`, {
  85. method: 'POST',
  86. headers: { 'Content-Type': 'application/json' },
  87. body: JSON.stringify({ productIds: ['11266507445'] }),
  88. });
  89. assert.equal(sync.status, 202);
  90. assert.equal((await sync.json() as { job: { status: string } }).job.status, 'pending');
  91. } finally {
  92. await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
  93. }
  94. });