Discovery AniList client: spec sorts, challenge on 5xx, strict list validation (BL-0051)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
461a37e924
commit
3e7b1ca49f
2 files changed
+86
-23
No files matched your search
@@ -51,6 +51,46 @@ test('recs keeps positive ratings and manga formats', async () => {
|
||||
assert.equal(out[0].record.title, 'Sky Town');
|
||||
});
|
||||
|
||||
test('recs query follows spec sort and page size', async () => {
|
||||
const { client, calls } = stub(() => json(fixture('recs')));
|
||||
await client.recs(101);
|
||||
assert.match(calls[0].body.query, /recommendations\(perPage: 25, sort: RATING_DESC\)/);
|
||||
});
|
||||
|
||||
test('creator query follows spec sort and page size', async () => {
|
||||
const { client, calls } = stub(() => json(fixture('creator')));
|
||||
await client.creator(900);
|
||||
assert.match(calls[0].body.query, /staffMedia\(type: MANGA, perPage: 25, sort: POPULARITY_DESC\)/);
|
||||
});
|
||||
|
||||
test('title on a novel resolves to null', async () => {
|
||||
assert.equal(await clientFor(() => json(fixture('novel'))).title(777), null);
|
||||
});
|
||||
|
||||
test('requests refuse redirects', async () => {
|
||||
const { client, calls } = stub(() => json(fixture('title')));
|
||||
await client.title(101);
|
||||
assert.equal(calls[0].init.redirect, 'error');
|
||||
const redirected = () => { throw new TypeError('fetch failed: unexpected redirect'); };
|
||||
await assert.rejects(clientFor(redirected).title(1), e => e instanceof AniListSkip && e.reason === 'upstream');
|
||||
});
|
||||
|
||||
test('one malformed item invalidates a list response', async () => {
|
||||
const invalid = e => e instanceof AniListSkip && e.reason === 'invalid';
|
||||
const recs = fixture('recs');
|
||||
recs.data.Media.recommendations.nodes.push(null);
|
||||
await assert.rejects(clientFor(() => json(recs)).recs(1), invalid);
|
||||
const tagtop = fixture('tagtop');
|
||||
tagtop.data.Page.media[0].id = 'x';
|
||||
await assert.rejects(clientFor(() => json(tagtop)).tagTop({ name: 'Mystery', kind: 'genre' }), invalid);
|
||||
for (const patch of [{ genres: 'Drama' }, { tags: [null] }, { title: 'x' }, { staff: { edges: {} } }, { staff: { edges: [null] } }]) {
|
||||
const fresh = fixture('fresh');
|
||||
Object.assign(fresh.data.Page.media[0], patch);
|
||||
await assert.rejects(clientFor(() => json(fresh)).fresh(), invalid, JSON.stringify(patch));
|
||||
}
|
||||
await assert.rejects(async () => normalizeMedia({ id: 1, format: 'MANGA', title: 5 }), invalid);
|
||||
});
|
||||
|
||||
test('creator returns roles and drops novels', async () => {
|
||||
const out = await clientFor(() => json(fixture('creator'))).creator(900);
|
||||
assert.deepEqual(out.map(e => [e.anilistId, e.role]), [[301, 'Story & Art']]);
|
||||
@@ -70,10 +110,12 @@ test('tagTop uses genre_in for genres and tag_in for tags', async () => {
|
||||
const out = await g.client.tagTop({ name: 'Mystery', kind: 'genre' });
|
||||
assert.deepEqual(g.calls[0].body.variables.genre_in, ['Mystery']);
|
||||
assert.equal(g.calls[0].body.variables.tag_in, undefined);
|
||||
assert.match(g.calls[0].body.query, /popularity_greater: 1000/);
|
||||
assert.match(g.calls[0].body.query, /genre_in: \$genre_in, popularity_greater: 1000, sort: SCORE_DESC/);
|
||||
assert.match(g.calls[0].body.query, /Page\(perPage: 25\)/);
|
||||
assert.deepEqual(out.map(e => e.anilistId), [401]);
|
||||
const t = stub(() => json(fixture('tagtop')));
|
||||
await t.client.tagTop({ name: 'Detective', kind: 'tag' });
|
||||
assert.match(t.calls[0].body.query, /tag_in: \$tag_in, popularity_greater: 1000, sort: SCORE_DESC/);
|
||||
assert.deepEqual(t.calls[0].body.variables.tag_in, ['Detective']);
|
||||
assert.equal(t.calls[0].body.variables.genre_in, undefined);
|
||||
});
|
||||
@@ -90,6 +132,11 @@ test('429 stops, 403 stops, html body is a challenge', async () => {
|
||||
|
||||
test('5xx and timeout skip', async () => {
|
||||
await assert.rejects(clientFor(() => json({}, 503)).title(1), e => e instanceof AniListSkip && e.reason === 'upstream');
|
||||
// A challenge page served with a 5xx is still a challenge, not an outage.
|
||||
const html503 = () => new Response('<html>just a moment</html>', { status: 503, headers: { 'content-type': 'text/html' } });
|
||||
await assert.rejects(clientFor(html503).title(1), e => e instanceof AniListStop && e.reason === 'challenge');
|
||||
const text502 = () => new Response('Bad Gateway', { status: 502 });
|
||||
await assert.rejects(clientFor(text502).title(1), e => e instanceof AniListStop && e.reason === 'challenge');
|
||||
const timeout = () => { throw Object.assign(new Error('timed out'), { name: 'TimeoutError' }); };
|
||||
await assert.rejects(clientFor(timeout).title(1), e => e instanceof AniListSkip && e.reason === 'timeout');
|
||||
const abort = () => { throw Object.assign(new Error('aborted'), { name: 'AbortError' }); };
|
||||
|
||||
Reference in new issue
Block a user