Files
shd.build/src/lib/server/ubisoft-news-client.test.ts
T
jb dc08eefce6
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 28s
Validate and deploy production / deploy (push) Successful in 49s
Validate and deploy staging / deploy (push) Successful in 28s
feat: link official news to Ubisoft
2026-08-01 22:26:52 -04:00

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');
});
});