feat: link official news to Ubisoft
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

This commit is contained in:
jb
2026-08-01 22:26:52 -04:00
parent bc40a737a8
commit dc08eefce6
13 changed files with 618 additions and 42 deletions
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from 'vitest';
import {
extractNimbusApiKey,
fetchUbisoftNewsThumbnail,
fetchUbisoftNews,
UbisoftNewsSourceError,
} from './ubisoft-news-client';
@@ -29,6 +30,10 @@ function item(id: string, overrides: Record<string, unknown> = {}) {
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,
};
}
@@ -68,6 +73,10 @@ describe('Ubisoft News client', () => {
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');
@@ -109,6 +118,26 @@ describe('Ubisoft News client', () => {
);
});
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, {
@@ -122,4 +151,54 @@ describe('Ubisoft News client', () => {
);
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');
});
});