jd-adapters.test.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import assert from 'node:assert/strict';
  2. import test from 'node:test';
  3. import { adaptJdProductProfile, adaptJdProductResponse } from '../src/modules/domestic-voc/adapters/jd-product.adapter.js';
  4. import { adaptJdPriceResponse } from '../src/modules/domestic-voc/adapters/jd-price.adapter.js';
  5. import { GatewayPayloadError } from '../src/modules/domestic-voc/adapters/jd-response.js';
  6. import { adaptJdReviewResponse } from '../src/modules/domestic-voc/adapters/jd-review.adapter.js';
  7. test('JD product adapter unwraps the company gateway envelope', () => {
  8. const product = adaptJdProductResponse({
  9. code: 200,
  10. data: {
  11. code: 200,
  12. data: {
  13. item: {
  14. itemId: '11266507445',
  15. itemName: 'Demashi commercial cooker',
  16. brandName: 'Demashi',
  17. model: '35P6-CM1',
  18. firstCategoryName: 'Commercial appliances',
  19. thirdCategoryName: 'Commercial cooker',
  20. },
  21. },
  22. },
  23. }, '11266507445');
  24. assert.equal(product.productId, '11266507445');
  25. assert.equal(product.productKey, 'jd:11266507445');
  26. assert.equal(product.title, 'Demashi commercial cooker');
  27. assert.equal(product.brand, 'Demashi');
  28. assert.equal(product.model, '35P6-CM1');
  29. assert.equal(product.source, 'fmode_gateway');
  30. });
  31. test('JD product adapter preserves an explicit competitor role', () => {
  32. const product = adaptJdProductResponse({ data: { item: { itemId: '100119862001', itemName: 'Competitor' } } }, '100119862001', 'competitor');
  33. assert.equal(product.role, 'competitor');
  34. });
  35. test('JD product profile exposes normalized catalog fields without the raw response', () => {
  36. const profile = adaptJdProductProfile({
  37. mainImages: ['https://img.example/item.jpg'],
  38. color: 'Black',
  39. productArea: 'China',
  40. weight: '12.5',
  41. length: '300',
  42. width: '200',
  43. height: '100',
  44. saleDate: '2026-07-01 10:00:00',
  45. shopId: 'shop-1',
  46. venderId: 'seller-1',
  47. jdexpress: true,
  48. }, '2026-07-24T00:00:00.000Z');
  49. assert.equal(profile?.imageUrl, 'https://img.example/item.jpg');
  50. assert.equal(profile?.origin, 'China');
  51. assert.equal(profile?.dimensionsMm.length, '300');
  52. assert.equal(profile?.jdExpress, true);
  53. assert.equal(profile?.collectedAt, '2026-07-24T00:00:00.000Z');
  54. });
  55. test('JD price adapter maps exact product price minor units', () => {
  56. const priceCents = adaptJdPriceResponse({
  57. code: 200,
  58. data: {
  59. code: 0,
  60. data: {
  61. data: [
  62. { good_id: 'other-product', price: 999_900 },
  63. { good_id: '100119862001', price: 229_900 },
  64. ],
  65. },
  66. },
  67. }, '100119862001');
  68. assert.equal(priceCents, 229_900);
  69. });
  70. test('JD price adapter rejects a mismatched product identity', () => {
  71. assert.equal(adaptJdPriceResponse({
  72. data: { data: { data: [{ good_id: 'different-product', price: 229_900 }] } },
  73. }, '100119862001'), null);
  74. });
  75. test('JD review adapter maps evidence and pagination without reviewer identity', () => {
  76. const page = adaptJdReviewResponse({
  77. code: 200,
  78. data: {
  79. code: 200,
  80. data: {
  81. comments: [{
  82. commentId: 'comment-1',
  83. score: 5,
  84. content: 'Easy to clean and heats quickly.',
  85. creationTime: '2026-07-20T10:00:00+08:00',
  86. nickname: 'not-exposed-by-snapshot',
  87. }],
  88. pageCount: 2,
  89. },
  90. },
  91. }, '11266507445', 1);
  92. assert.equal(page.reviews.length, 1);
  93. assert.equal(page.reviews[0]?.reviewId, 'comment-1');
  94. assert.equal(page.reviews[0]?.rating, 5);
  95. assert.equal(page.reviews[0]?.reviewDate, '2026-07-20T02:00:00.000Z');
  96. assert.match(page.reviews[0]?.reviewKey ?? '', /^jd:11266507445:/);
  97. assert.equal(page.hasNextPage, true);
  98. assert.equal('nickname' in page.reviews[0]!.rawPayload, false);
  99. });
  100. test('JD review adapter maps the live commentScore and commentDate fields and removes identity fields', () => {
  101. const page = adaptJdReviewResponse({
  102. data: { data: { data: [{
  103. commentId: 'live-comment-1',
  104. commentData: 'Heating is fast.',
  105. commentScore: '4',
  106. commentDate: '2026-07-21 08:30:00',
  107. userNickName: 'private-name',
  108. userImgURL: 'https://private.example/avatar.jpg',
  109. guid: 'private-guid',
  110. }] } },
  111. }, '100095574033', 1);
  112. assert.equal(page.reviews[0]?.rating, 4);
  113. assert.equal(page.reviews[0]?.reviewDate, '2026-07-21T00:30:00.000Z');
  114. assert.equal('userNickName' in page.reviews[0]!.rawPayload, false);
  115. assert.equal('userImgURL' in page.reviews[0]!.rawPayload, false);
  116. assert.equal('guid' in page.reviews[0]!.rawPayload, false);
  117. });
  118. test('JD adapters classify pending collection as retryable without raw body leakage', () => {
  119. assert.throws(
  120. () => adaptJdProductResponse({ code: 301, message: 'COLLECT FAILED, SEND REQUEST AGAIN', data: null }, '1'),
  121. (error: unknown) => {
  122. assert.ok(error instanceof GatewayPayloadError);
  123. assert.equal(error.retryable, true);
  124. assert.equal(error.message, 'Gateway data is not ready');
  125. assert.doesNotMatch(error.message, /COLLECT FAILED/);
  126. return true;
  127. },
  128. );
  129. });