jd-sync.service.test.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import assert from 'node:assert/strict';
  2. import test from 'node:test';
  3. import type { ClaimedSyncJob } from '../src/modules/domestic-voc/jobs/sync-worker.js';
  4. import type {
  5. SyncJobFinalStatus,
  6. SyncPersistence,
  7. } from '../src/modules/domestic-voc/repositories/voc-ingestion.repository.js';
  8. import { JdSyncService, type GatewayRequestClient } from '../src/modules/domestic-voc/services/jd-sync.service.js';
  9. import { FmodeRequestError } from '../src/modules/domestic-voc/upstream/fmode-client.js';
  10. class FakePersistence implements SyncPersistence {
  11. products = 0;
  12. reviews = 0;
  13. progress: number[] = [];
  14. finished: Array<{ status: SyncJobFinalStatus; errorSummary?: string }> = [];
  15. requeued: string[] = [];
  16. events: Array<{ eventType: string; message: string }> = [];
  17. async upsertProduct() { this.products += 1; }
  18. async ensureProductStub() {}
  19. async upsertReviews(_workspaceId: string, _platform: string, _productId: string, reviews: unknown[]) {
  20. this.reviews += reviews.length;
  21. return reviews.length;
  22. }
  23. async setJobProgress(_jobInternalId: string, progress: number) { this.progress.push(progress); }
  24. async finishJob(_jobInternalId: string, status: SyncJobFinalStatus, errorSummary?: string) {
  25. this.finished.push({ status, ...(errorSummary ? { errorSummary } : {}) });
  26. }
  27. async requeueJob(_jobInternalId: string, errorSummary: string) { this.requeued.push(errorSummary); }
  28. async addJobEvent(input: { eventType: string; message: string }) {
  29. this.events.push({ eventType: input.eventType, message: input.message });
  30. }
  31. }
  32. function job(overrides: Partial<ClaimedSyncJob> = {}): ClaimedSyncJob {
  33. return {
  34. internalId: '1',
  35. publicId: '11111111-1111-4111-8111-111111111111',
  36. workspaceId: 'demashi',
  37. platform: 'jd',
  38. scopes: ['product', 'reviews'],
  39. productIds: ['11266507445'],
  40. attempts: 1,
  41. maxAttempts: 3,
  42. ...overrides,
  43. };
  44. }
  45. test('JD sync calls only registered company gateway paths and completes', async () => {
  46. const calls: Array<{ path: string; params: Record<string, unknown> | undefined }> = [];
  47. const gateway: GatewayRequestClient = {
  48. async request(path, init) {
  49. calls.push({ path, params: init?.params });
  50. if (path === 'jd/get-item-detail/v1') {
  51. return { code: 200, data: { code: 200, data: { item: { itemId: '11266507445', itemName: 'Product' } } } } as never;
  52. }
  53. return {
  54. code: 200,
  55. data: { code: 200, data: { comments: [{ commentId: 'c1', score: 5, content: 'Useful evidence.' }] } },
  56. } as never;
  57. },
  58. };
  59. const persistence = new FakePersistence();
  60. await new JdSyncService(gateway, persistence, 1).process(job());
  61. assert.deepEqual(calls, [
  62. { path: 'jd/get-item-detail/v1', params: { itemId: '11266507445' } },
  63. { path: 'jd/get-item-comments/v1', params: { itemId: '11266507445', page: 1 } },
  64. ]);
  65. assert.equal(persistence.products, 1);
  66. assert.equal(persistence.reviews, 1);
  67. assert.deepEqual(persistence.finished, [{ status: 'completed' }]);
  68. assert.equal(persistence.progress.at(-1), 100);
  69. });
  70. test('JD sync returns a safe partial status when reviews are not permitted', async () => {
  71. const gateway: GatewayRequestClient = {
  72. async request(path) {
  73. if (path === 'jd/get-item-detail/v1') {
  74. return { code: 200, data: { item: { itemId: '11266507445', itemName: 'Product' } } } as never;
  75. }
  76. throw new FmodeRequestError('Bearer secret-value raw 403 response', 403, false);
  77. },
  78. };
  79. const persistence = new FakePersistence();
  80. await new JdSyncService(gateway, persistence).process(job());
  81. assert.deepEqual(persistence.finished, [{
  82. status: 'partial',
  83. errorSummary: 'Data gateway permission is unavailable',
  84. }]);
  85. assert.doesNotMatch(JSON.stringify(persistence.events), /secret-value|Bearer|raw 403/);
  86. });
  87. test('JD sync requeues temporary gateway failures while attempts remain', async () => {
  88. const gateway: GatewayRequestClient = {
  89. async request() {
  90. throw new FmodeRequestError('network internals', undefined, true);
  91. },
  92. };
  93. const persistence = new FakePersistence();
  94. await new JdSyncService(gateway, persistence).process(job({ scopes: ['product'] }));
  95. assert.deepEqual(persistence.finished, []);
  96. assert.deepEqual(persistence.requeued, ['Data gateway is temporarily unavailable']);
  97. });