205 lines
6.3 KiB
TypeScript
205 lines
6.3 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
extractNimbusApiKey,
|
|
fetchUbisoftNewsThumbnail,
|
|
fetchUbisoftNews,
|
|
UbisoftNewsSourceError,
|
|
} from './ubisoft-news-client';
|
|
|
|
const externalIdOne = '64f1234567890abcdef12345';
|
|
const externalIdTwo = '64f1234567890abcdef67890';
|
|
|
|
function landing() {
|
|
return new Response(
|
|
`<script>window.__PRELOADED_STATE__ = ${JSON.stringify({ configuration: { nimbusApiKey: 'public-nimbus-key' } })};</script>`,
|
|
{ status: 200, headers: { 'content-type': 'text/html' } },
|
|
);
|
|
}
|
|
|
|
function page(total: number, items: unknown[]) {
|
|
return new Response(JSON.stringify({ total, items }), {
|
|
status: 200,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
function item(id: string, overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
id,
|
|
title: 'Division Resurgence Update',
|
|
abstract: 'Read the latest official update.',
|
|
date: '2024-03-11T15:20:00.000Z',
|
|
button: { buttonUrl: `/${id}/division-resurgence-update` },
|
|
thumbnail: {
|
|
url: `https://staticctf.ubisoft.com/news/${id}/cover.jpg`,
|
|
description: 'Division Resurgence key art',
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('Ubisoft News client', () => {
|
|
it('extracts the Nimbus key from the browser configuration', () => {
|
|
expect(
|
|
extractNimbusApiKey(
|
|
`<script>window.__PRELOADED_STATE__ = ${JSON.stringify({ configuration: { nimbusApiKey: 'public-nimbus-key' } })};</script>`,
|
|
),
|
|
).toBe('public-nimbus-key');
|
|
});
|
|
|
|
it('paginates, deduplicates overlapping items, and normalizes source dates', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(landing())
|
|
.mockResolvedValueOnce(page(2, [item(externalIdTwo)]))
|
|
.mockResolvedValueOnce(
|
|
page(2, [
|
|
item(externalIdTwo),
|
|
item(externalIdOne, {
|
|
title: 'Earlier Update',
|
|
date: '2024-01-05T08:00:00Z',
|
|
button: { buttonUrl: `/${externalIdOne}/earlier-update` },
|
|
}),
|
|
]),
|
|
);
|
|
|
|
const result = await fetchUbisoftNews(fetchImpl);
|
|
|
|
expect(result.map((entry) => entry.externalId)).toEqual([
|
|
externalIdOne,
|
|
externalIdTwo,
|
|
]);
|
|
expect(result[0].publishedAt).toBe('2024-01-05T08:00:00.000Z');
|
|
expect(result[0].canonicalUrl).toBe(
|
|
`https://www.ubisoft.com/en-us/game/the-division/resurgence/news-updates/${externalIdOne}/earlier-update`,
|
|
);
|
|
expect(result[0].thumbnailUrl).toBe(
|
|
`https://staticctf.ubisoft.com/news/${externalIdOne}/cover.jpg`,
|
|
);
|
|
expect(result[0].thumbnailAlt).toBe('Division Resurgence key art');
|
|
expect(fetchImpl).toHaveBeenCalledTimes(3);
|
|
const secondRequest = fetchImpl.mock.calls[1];
|
|
expect(String(secondRequest[0])).toContain('locale=en-us');
|
|
expect(secondRequest[1]?.headers).toMatchObject({
|
|
authorization: 'public-nimbus-key',
|
|
});
|
|
expect(secondRequest[1]?.redirect).toBe('error');
|
|
});
|
|
|
|
it('rejects article links outside the approved Ubisoft path', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(landing())
|
|
.mockResolvedValueOnce(
|
|
page(1, [
|
|
item(externalIdOne, {
|
|
button: {
|
|
buttonUrl: `/${externalIdOne}-lookalike/not-the-same-item`,
|
|
},
|
|
}),
|
|
]),
|
|
);
|
|
|
|
await expect(fetchUbisoftNews(fetchImpl)).rejects.toBeInstanceOf(
|
|
UbisoftNewsSourceError,
|
|
);
|
|
});
|
|
|
|
it('rejects malformed source dates', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(landing())
|
|
.mockResolvedValueOnce(
|
|
page(1, [item(externalIdOne, { date: 'unknown' })]),
|
|
);
|
|
|
|
await expect(fetchUbisoftNews(fetchImpl)).rejects.toThrow(
|
|
'Ubisoft returned an invalid news item.',
|
|
);
|
|
});
|
|
|
|
it('rejects thumbnails outside Ubisoft image hosting', async () => {
|
|
const fetchImpl = vi
|
|
.fn<typeof fetch>()
|
|
.mockResolvedValueOnce(landing())
|
|
.mockResolvedValueOnce(
|
|
page(1, [
|
|
item(externalIdOne, {
|
|
thumbnail: {
|
|
url: 'https://example.com/cover.jpg',
|
|
description: 'Untrusted image',
|
|
},
|
|
}),
|
|
]),
|
|
);
|
|
|
|
await expect(fetchUbisoftNews(fetchImpl)).rejects.toThrow(
|
|
'Ubisoft news thumbnail was outside the approved image host.',
|
|
);
|
|
});
|
|
|
|
it('does not follow provider redirects', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValueOnce(
|
|
new Response(null, {
|
|
status: 302,
|
|
headers: { location: 'http://127.0.0.1/internal' },
|
|
}),
|
|
);
|
|
|
|
await expect(fetchUbisoftNews(fetchImpl)).rejects.toThrow(
|
|
'Ubisoft news request returned HTTP 302.',
|
|
);
|
|
expect(fetchImpl.mock.calls[0][1]?.redirect).toBe('error');
|
|
});
|
|
|
|
it('fetches bounded raster thumbnails without following redirects', async () => {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValueOnce(
|
|
new Response(bytes, {
|
|
headers: { 'content-type': 'image/jpeg; charset=binary' },
|
|
}),
|
|
);
|
|
|
|
const image = await fetchUbisoftNewsThumbnail(
|
|
'https://staticctf.ubisoft.com/news/cover.jpg',
|
|
fetchImpl,
|
|
);
|
|
expect(image.contentType).toBe('image/jpeg');
|
|
expect(image.body).toEqual(bytes);
|
|
expect(fetchImpl.mock.calls[0][1]?.redirect).toBe('error');
|
|
});
|
|
|
|
it('rejects thumbnail responses that are not raster images', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValueOnce(
|
|
new Response('<svg/>', {
|
|
headers: { 'content-type': 'image/svg+xml' },
|
|
}),
|
|
);
|
|
|
|
await expect(
|
|
fetchUbisoftNewsThumbnail(
|
|
'https://staticctf.ubisoft.com/news/cover.svg',
|
|
fetchImpl,
|
|
),
|
|
).rejects.toThrow('not a supported image');
|
|
});
|
|
|
|
it('rejects thumbnails larger than the response bound', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValueOnce(
|
|
new Response(new Uint8Array([1]), {
|
|
headers: {
|
|
'content-type': 'image/jpeg',
|
|
'content-length': String(5 * 1024 * 1024 + 1),
|
|
},
|
|
}),
|
|
);
|
|
|
|
await expect(
|
|
fetchUbisoftNewsThumbnail(
|
|
'https://staticctf.ubisoft.com/news/large-cover.jpg',
|
|
fetchImpl,
|
|
),
|
|
).rejects.toThrow('exceeded the size limit');
|
|
});
|
|
});
|