import { describe, expect, it } from '@jest/globals'; import { hashSyncPayload, imageFilesHash, isUnsyncedDraftListing, listingImageSyncHash, listingImagesUnchanged, listingSyncFingerprint, listingSyncHash, listingSyncUnchanged, payloadsEqual, } from '../syncFingerprint.js'; const listing = { title: 'Widget', description: 'A widget', condition: 'new', stockLocation: { _id: 'loc-1' }, courierServices: [{ _id: 'cs-2' }, { _id: 'cs-1' }], fulfillmentPolicy: 'ful-1', paymentPolicy: { _id: 'pay-1' }, returnPolicy: { _id: 'ret-1' }, listingImages: [{ _id: 'img-1' }], updatedAt: new Date('2026-01-01'), lastSyncedAt: new Date('2026-01-02'), state: { type: 'active' }, }; const variants = [ { _reference: 'SKU-RED', price: 9.99, currency: 'GBP', stockQuantity: 3, aspects: [ { name: 'Size', value: 'L' }, { name: 'Color', value: 'Red' }, ], listingImages: [], }, ]; describe('syncFingerprint', () => { it('hashes the same listing payload consistently and ignores timestamps and state', () => { const first = listingSyncHash(listing, variants); const second = listingSyncHash( { ...listing, updatedAt: new Date(), lastSyncedAt: new Date(), state: { type: 'syncing' } }, variants ); expect(first).toBe(second); expect(listingSyncFingerprint(listing, variants).v).toBe(1); }); it('changes the hash when price, images, or policy ids change', () => { const baseline = listingSyncHash(listing, variants); expect( listingSyncHash(listing, [{ ...variants[0], price: 12.5 }]) ).not.toBe(baseline); expect( listingSyncHash({ ...listing, listingImages: [{ _id: 'img-2' }] }, variants) ).not.toBe(baseline); expect( listingSyncHash({ ...listing, paymentPolicy: 'pay-2' }, variants) ).not.toBe(baseline); }); it('treats a stored syncHash match as unchanged', () => { const syncHash = listingSyncHash(listing, variants); expect(listingSyncUnchanged({ ...listing, syncHash }, variants)).toBe(true); expect(listingSyncUnchanged({ ...listing, syncHash: 'nope' }, variants)).toBe(false); expect(listingSyncUnchanged(listing, variants)).toBe(false); }); it('identifies unsynced draft listings', () => { expect(isUnsyncedDraftListing({ state: { type: 'draft' } })).toBe(true); expect( isUnsyncedDraftListing({ state: { type: 'draft' }, externalReference: '123' }) ).toBe(false); expect(isUnsyncedDraftListing({ state: { type: 'active' } })).toBe(false); }); it('hashes image file ids independently of listing copy', () => { const imageHash = listingImageSyncHash(listing, variants); expect( listingImageSyncHash({ ...listing, title: 'Renamed' }, variants) ).toBe(imageHash); expect( listingImageSyncHash({ ...listing, listingImages: [{ _id: 'img-2' }] }, variants) ).not.toBe(imageHash); expect( listingImagesUnchanged( { ...listing, syncImageHash: imageFilesHash(listing.listingImages), marketplaceImageUrls: ['https://i.ebayimg.com/1.jpg'], }, variants ) ).toBe(true); expect(imageFilesHash([{ _id: 'img-1' }])).toBe(imageFilesHash(['img-1'])); }); it('compares payloads by canonical hash', () => { expect(payloadsEqual({ b: 1, a: 2 }, { a: 2, b: 1 })).toBe(true); expect(payloadsEqual({ a: 1 }, { a: 2 })).toBe(false); expect(hashSyncPayload({ a: 1 })).toHaveLength(64); }); });