Skip to content

Commit e350410

Browse files
authored
Merge pull request #92 from numbersprotocol/copilot/add-unit-tests-for-modules
Add unit tests for asset-service, interaction-tracker, media-viewer, and utils
2 parents 03a37cb + d13a87c commit e350410

4 files changed

Lines changed: 688 additions & 0 deletions

File tree

src/test/asset-service_test.ts

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { assert } from '@open-wc/testing';
2+
import sinon from 'sinon';
3+
import { fetchAsset, hasNftProduct } from '../asset/asset-service';
4+
import { Constant } from '../constant';
5+
6+
suite('asset-service', () => {
7+
let fetchStub: sinon.SinonStub;
8+
9+
setup(() => {
10+
fetchStub = sinon.stub(window, 'fetch');
11+
});
12+
13+
teardown(() => {
14+
fetchStub.restore();
15+
});
16+
17+
suite('fetchAsset', () => {
18+
test('maps API response fields correctly to AssetModel', async () => {
19+
const mockResponse = {
20+
nit_commit_custom: {
21+
assetCreator: 'Test Creator',
22+
creatorWallet: '0xabc123',
23+
assetLocationCreated: 'New York, USA',
24+
assetSourceType: 'upload',
25+
usedBy: 'test-user',
26+
captureEyeCustom: [
27+
{
28+
field: 'Custom Field',
29+
value: 'Custom Value',
30+
iconSource: 'https://example.com/icon.png',
31+
url: 'https://example.com',
32+
},
33+
],
34+
},
35+
signed_metadata: JSON.stringify({ created_at: 1704067200 }),
36+
integrity_info: [
37+
{
38+
search_id: '0x123abc',
39+
explorer_url: 'https://mainnet.num.network/tx/0x123abc',
40+
},
41+
],
42+
uploaded_at: '2024-01-01T00:00:00Z',
43+
asset_file_mime_type: 'image/jpeg',
44+
headline: 'Test Headline',
45+
caption: 'Test Abstract',
46+
asset_file_thumbnail: 'https://example.com/thumb.jpg',
47+
digital_source_type: 'original',
48+
owner_name: 'TestOwner',
49+
owner_profile_display_name: 'Owner Display Name',
50+
c2pa: true,
51+
};
52+
53+
fetchStub.resolves({
54+
ok: true,
55+
json: () => Promise.resolve(mockResponse),
56+
} as unknown as Response);
57+
58+
const result = await fetchAsset('test-nid');
59+
60+
assert.isDefined(result);
61+
assert.equal(result!.creator, 'Test Creator');
62+
assert.equal(result!.creatorWallet, '0xabc123');
63+
assert.equal(
64+
result!.createdTime,
65+
new Date('2024-01-01T00:00:00Z').toUTCString()
66+
);
67+
assert.equal(result!.encodingFormat, 'image/jpeg');
68+
assert.equal(result!.headline, 'Test Headline');
69+
assert.equal(result!.abstract, 'Test Abstract');
70+
assert.equal(result!.initialTransaction, '0x123abc');
71+
assert.equal(result!.thumbnailUrl, 'https://example.com/thumb.jpg');
72+
assert.equal(
73+
result!.explorerUrl,
74+
'https://mainnet.num.network/tx/0x123abc'
75+
);
76+
assert.equal(result!.assetSourceType, 'upload');
77+
assert.equal(
78+
result!.captureTime,
79+
new Date(1704067200 * 1000).toUTCString()
80+
);
81+
assert.equal(result!.captureLocation, 'New York, USA');
82+
assert.equal(result!.backendOwnerName, 'Owner Display Name');
83+
assert.equal(result!.digitalSourceType, 'original');
84+
assert.equal(result!.usedBy, 'test-user');
85+
assert.isTrue(result!.hasC2pa);
86+
assert.equal(
87+
result!.showcaseLink,
88+
`${Constant.url.showcase}/testowner`
89+
);
90+
assert.deepEqual(result!.captureEyeCustom, [
91+
{
92+
field: 'Custom Field',
93+
value: 'Custom Value',
94+
iconSource: 'https://example.com/icon.png',
95+
url: 'https://example.com',
96+
},
97+
]);
98+
});
99+
100+
test('sets explorerUrl to empty string when integrity_info is absent', async () => {
101+
fetchStub.resolves({
102+
ok: true,
103+
json: () =>
104+
Promise.resolve({
105+
creator_name: 'Creator',
106+
}),
107+
} as unknown as Response);
108+
109+
const result = await fetchAsset('test-nid');
110+
assert.isDefined(result);
111+
assert.equal(result!.explorerUrl, '');
112+
assert.isUndefined(result!.initialTransaction);
113+
});
114+
115+
test('falls back creator through priority chain', async () => {
116+
// No nit_commit_custom.assetCreator → fallback to creator_profile_display_name
117+
fetchStub.resolves({
118+
ok: true,
119+
json: () =>
120+
Promise.resolve({
121+
creator_profile_display_name: 'Display Name',
122+
creator_name: 'Username',
123+
}),
124+
} as unknown as Response);
125+
126+
const result = await fetchAsset('test-nid');
127+
assert.isDefined(result);
128+
assert.equal(result!.creator, 'Display Name');
129+
});
130+
131+
test('returns hasC2pa and showcaseLink fields', async () => {
132+
fetchStub.resolves({
133+
ok: true,
134+
json: () =>
135+
Promise.resolve({
136+
c2pa: true,
137+
owner_name: 'TestOwner',
138+
owner_profile_display_name: 'Owner Display',
139+
}),
140+
} as unknown as Response);
141+
142+
const result = await fetchAsset('test-nid');
143+
assert.isDefined(result);
144+
assert.isTrue(result!.hasC2pa);
145+
assert.equal(
146+
result!.showcaseLink,
147+
`${Constant.url.showcase}/testowner`
148+
);
149+
assert.equal(result!.backendOwnerName, 'Owner Display');
150+
});
151+
152+
test('showcaseLink is undefined when owner_name is absent', async () => {
153+
fetchStub.resolves({
154+
ok: true,
155+
json: () =>
156+
Promise.resolve({
157+
c2pa: false,
158+
owner_profile_display_name: 'Owner Display',
159+
}),
160+
} as unknown as Response);
161+
162+
const result = await fetchAsset('test-nid');
163+
assert.isDefined(result);
164+
assert.isFalse(result!.hasC2pa);
165+
assert.isUndefined(result!.showcaseLink);
166+
});
167+
168+
test('returns undefined on non-OK HTTP response', async () => {
169+
fetchStub.resolves({
170+
ok: false,
171+
status: 404,
172+
json: () =>
173+
Promise.resolve({
174+
error: { type: 'NotFound', message: 'Not Found' },
175+
}),
176+
} as unknown as Response);
177+
178+
const result = await fetchAsset('test-nid');
179+
assert.isUndefined(result);
180+
});
181+
182+
test('returns undefined on network error', async () => {
183+
fetchStub.rejects(new Error('Network error'));
184+
185+
const result = await fetchAsset('test-nid');
186+
assert.isUndefined(result);
187+
});
188+
189+
test('handles non-JSON error response body gracefully', async () => {
190+
fetchStub.resolves({
191+
ok: false,
192+
status: 500,
193+
json: () => Promise.reject(new SyntaxError('Unexpected token')),
194+
} as unknown as Response);
195+
196+
const result = await fetchAsset('test-nid');
197+
assert.isUndefined(result);
198+
});
199+
200+
test('returns undefined when response body is null', async () => {
201+
fetchStub.resolves({
202+
ok: true,
203+
json: () => Promise.resolve(null),
204+
} as unknown as Response);
205+
206+
const result = await fetchAsset('test-nid');
207+
assert.isUndefined(result);
208+
});
209+
});
210+
211+
suite('hasNftProduct', () => {
212+
test('returns true when count is greater than zero', async () => {
213+
fetchStub.resolves({
214+
ok: true,
215+
json: () => Promise.resolve({ count: 3 }),
216+
} as unknown as Response);
217+
218+
const result = await hasNftProduct('test-nid');
219+
assert.isTrue(result);
220+
});
221+
222+
test('returns false when count is zero', async () => {
223+
fetchStub.resolves({
224+
ok: true,
225+
json: () => Promise.resolve({ count: 0 }),
226+
} as unknown as Response);
227+
228+
const result = await hasNftProduct('test-nid');
229+
assert.isFalse(result);
230+
});
231+
232+
test('returns false on non-OK HTTP response', async () => {
233+
fetchStub.resolves({
234+
ok: false,
235+
status: 404,
236+
json: () =>
237+
Promise.resolve({
238+
error: { type: 'NotFound', message: 'Not found' },
239+
}),
240+
} as unknown as Response);
241+
242+
const result = await hasNftProduct('test-nid');
243+
assert.isFalse(result);
244+
});
245+
246+
test('returns false on network error', async () => {
247+
fetchStub.rejects(new Error('Network error'));
248+
249+
const result = await hasNftProduct('test-nid');
250+
assert.isFalse(result);
251+
});
252+
});
253+
});
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { assert } from '@open-wc/testing';
2+
import sinon from 'sinon';
3+
import interactionTracker, { TrackerEvent } from '../modal/interaction-tracker';
4+
5+
suite('interaction-tracker', () => {
6+
let fetchStub: sinon.SinonStub;
7+
8+
setup(() => {
9+
fetchStub = sinon.stub(window, 'fetch');
10+
sessionStorage.clear();
11+
});
12+
13+
teardown(() => {
14+
fetchStub.restore();
15+
sessionStorage.clear();
16+
});
17+
18+
test('does not fire fetch when token is empty string', () => {
19+
(interactionTracker as any).token = '';
20+
21+
(interactionTracker as any).createEvent(
22+
TrackerEvent.CAPTURE_EYE,
23+
new Date().toISOString(),
24+
'nid',
25+
'subid'
26+
);
27+
28+
assert.isFalse(fetchStub.called);
29+
});
30+
31+
test('schedules retry when token is null', () => {
32+
const clock = sinon.useFakeTimers();
33+
(interactionTracker as any).token = null;
34+
35+
const createEventSpy = sinon.spy(
36+
interactionTracker as any,
37+
'createEvent'
38+
);
39+
40+
const datetime = new Date().toISOString();
41+
(interactionTracker as any).createEvent(
42+
TrackerEvent.CAPTURE_EYE,
43+
datetime,
44+
'nid',
45+
'subid'
46+
);
47+
48+
assert.isFalse(fetchStub.called, 'fetch should not be called immediately');
49+
50+
clock.tick(1000);
51+
52+
assert.isTrue(
53+
createEventSpy.calledTwice,
54+
'createEvent should be retried after 1 second'
55+
);
56+
57+
createEventSpy.restore();
58+
clock.restore();
59+
});
60+
61+
test('fires fetch with Bearer token header when token is valid', () => {
62+
(interactionTracker as any).token = 'valid-test-token';
63+
fetchStub.resolves({
64+
ok: true,
65+
} as unknown as Response);
66+
67+
(interactionTracker as any).createEvent(
68+
TrackerEvent.CAPTURE_EYE,
69+
new Date().toISOString(),
70+
'nid',
71+
'subid'
72+
);
73+
74+
assert.isTrue(fetchStub.calledOnce);
75+
const [, options] = fetchStub.firstCall.args as [string, RequestInit];
76+
const headers = options.headers as Record<string, string>;
77+
assert.equal(headers['Authorization'], 'Bearer valid-test-token');
78+
});
79+
80+
test('token fetch failure sets token to empty string', async () => {
81+
fetchStub.rejects(new Error('Network error'));
82+
83+
await (interactionTracker as any).getToken();
84+
85+
assert.equal((interactionTracker as any).token, '');
86+
});
87+
88+
test('uses cached key from sessionStorage without fetching', async () => {
89+
const decryptDataStub = sinon
90+
.stub(interactionTracker as any, 'decryptData')
91+
.resolves('cached-decrypted-token');
92+
93+
sessionStorage.setItem('tokenCryptoKey', 'fake-base64-key');
94+
95+
await (interactionTracker as any).getToken();
96+
97+
assert.isFalse(fetchStub.called, 'fetch should not be called when key is cached');
98+
assert.equal((interactionTracker as any).token, 'cached-decrypted-token');
99+
100+
decryptDataStub.restore();
101+
});
102+
103+
test('fetches key and stores it in sessionStorage when not cached', async () => {
104+
fetchStub.resolves({
105+
ok: true,
106+
text: () => Promise.resolve('fetched-base64-key'),
107+
} as unknown as Response);
108+
109+
const decryptDataStub = sinon
110+
.stub(interactionTracker as any, 'decryptData')
111+
.resolves('fetched-decrypted-token');
112+
113+
await (interactionTracker as any).getToken();
114+
115+
assert.isTrue(fetchStub.calledOnce);
116+
assert.equal(
117+
sessionStorage.getItem('tokenCryptoKey'),
118+
'fetched-base64-key'
119+
);
120+
assert.equal((interactionTracker as any).token, 'fetched-decrypted-token');
121+
122+
decryptDataStub.restore();
123+
});
124+
125+
test('createEvent truncates subid longer than 255 characters via trackInteraction', () => {
126+
const clock = sinon.useFakeTimers();
127+
(interactionTracker as any).token = 'valid-test-token';
128+
fetchStub.resolves({ ok: true } as unknown as Response);
129+
130+
const longSubid = 'a'.repeat(300);
131+
interactionTracker.trackInteraction(TrackerEvent.CAPTURE_EYE, 'nid', longSubid);
132+
133+
clock.tick(50);
134+
135+
assert.isTrue(fetchStub.calledOnce);
136+
const [, options] = fetchStub.firstCall.args as [string, RequestInit];
137+
const body = JSON.parse(options.body as string);
138+
assert.equal(body.subid.length, 255);
139+
140+
clock.restore();
141+
});
142+
});

0 commit comments

Comments
 (0)