@thalorlabs/swapiinfo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +1 -0
- package/CLAUDE.md +33 -0
- package/dist/SwapiInfoClient.d.ts +23 -0
- package/dist/SwapiInfoClient.js +65 -0
- package/dist/SwapiInfoTypes.d.ts +35 -0
- package/dist/SwapiInfoTypes.js +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +9 -0
- package/eslint.config.mjs +2 -0
- package/package.json +42 -0
- package/src/SwapiInfoClient.ts +80 -0
- package/src/SwapiInfoTypes.ts +36 -0
- package/src/index.ts +6 -0
- package/tests/SwapiInfoClient.test.ts +133 -0
- package/tsconfig.json +8 -0
package/.prettierrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"@thalorlabs/eslint-plugin-dev-config/prettier"
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# CLAUDE.md — @thalorlabs/swapiinfo
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
Provider adapter for swapi.info (Star Wars API alternative). Returns `TLStarWarsFact`.
|
|
6
|
+
|
|
7
|
+
## External API
|
|
8
|
+
|
|
9
|
+
- swapi.info — free, no auth, no billing, no rate limits
|
|
10
|
+
- Returns planet data from the Star Wars universe
|
|
11
|
+
- Hosted on Vercel with CDN caching, ~50ms responses
|
|
12
|
+
|
|
13
|
+
## Build & publish
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run build # tsc → dist/
|
|
17
|
+
npm test # vitest
|
|
18
|
+
npm version patch # or minor/major
|
|
19
|
+
npm publish --access public
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What this package does
|
|
23
|
+
|
|
24
|
+
- HTTP adapter extending BaseHttpClient
|
|
25
|
+
- Normalises raw swapi.info planet response to TLStarWarsFact
|
|
26
|
+
- Generates deterministic MD5 hash ID from planet name
|
|
27
|
+
- Sets: type=PLANET, faction=UNKNOWN, isSafe=true
|
|
28
|
+
|
|
29
|
+
## What this package does NOT do
|
|
30
|
+
|
|
31
|
+
- No database, no caching, no logging — orchestrator handles that
|
|
32
|
+
- Raw types never exported
|
|
33
|
+
- No provider selection logic
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
2
|
+
import { TLStarWarsFact } from '@thalorlabs/types';
|
|
3
|
+
export interface SwapiInfoClientConfig {
|
|
4
|
+
baseURL?: string;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* HTTP adapter for swapi.info (Star Wars API).
|
|
9
|
+
*
|
|
10
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
11
|
+
* observability. Normalises the raw planet response to TLStarWarsFact.
|
|
12
|
+
* No DB, no cache, no logging — pure HTTP adapter.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SwapiInfoClient extends BaseHttpClient {
|
|
15
|
+
readonly serviceName = "swapiinfo";
|
|
16
|
+
private static readonly MAX_PLANET_ID;
|
|
17
|
+
private static readonly MAX_RETRIES_404;
|
|
18
|
+
constructor(config?: SwapiInfoClientConfig);
|
|
19
|
+
getFact(): Promise<TLStarWarsFact>;
|
|
20
|
+
private randomPlanetId;
|
|
21
|
+
private is404;
|
|
22
|
+
private normalise;
|
|
23
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SwapiInfoClient = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const api_1 = require("@thalorlabs/api");
|
|
6
|
+
const types_1 = require("@thalorlabs/types");
|
|
7
|
+
/**
|
|
8
|
+
* HTTP adapter for swapi.info (Star Wars API).
|
|
9
|
+
*
|
|
10
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
11
|
+
* observability. Normalises the raw planet response to TLStarWarsFact.
|
|
12
|
+
* No DB, no cache, no logging — pure HTTP adapter.
|
|
13
|
+
*/
|
|
14
|
+
class SwapiInfoClient extends api_1.BaseHttpClient {
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
super({
|
|
17
|
+
baseURL: config.baseURL ?? 'https://swapi.info/api',
|
|
18
|
+
serviceName: 'swapiinfo',
|
|
19
|
+
retries: 3,
|
|
20
|
+
timeout: config.timeout ?? 10000,
|
|
21
|
+
});
|
|
22
|
+
this.serviceName = 'swapiinfo';
|
|
23
|
+
}
|
|
24
|
+
async getFact() {
|
|
25
|
+
let lastError;
|
|
26
|
+
for (let attempt = 0; attempt < SwapiInfoClient.MAX_RETRIES_404; attempt++) {
|
|
27
|
+
const id = this.randomPlanetId();
|
|
28
|
+
try {
|
|
29
|
+
const { response } = await this.handleRequest({ method: 'GET', url: `/planets/${id}` }, { method: 'GET', url: `/planets/${id}`, requestId: (0, crypto_1.randomUUID)() });
|
|
30
|
+
return this.normalise(response.data);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
lastError = err;
|
|
34
|
+
if (this.is404(err))
|
|
35
|
+
continue;
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw lastError;
|
|
40
|
+
}
|
|
41
|
+
randomPlanetId() {
|
|
42
|
+
return Math.floor(Math.random() * SwapiInfoClient.MAX_PLANET_ID) + 1;
|
|
43
|
+
}
|
|
44
|
+
is404(err) {
|
|
45
|
+
if (err && typeof err === 'object' && 'response' in err) {
|
|
46
|
+
const resp = err.response;
|
|
47
|
+
return resp?.status === 404;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
normalise(raw) {
|
|
52
|
+
return types_1.TLStarWarsFact.parse({
|
|
53
|
+
id: (0, crypto_1.createHash)('md5').update(raw.name).digest('hex'),
|
|
54
|
+
type: types_1.EStarWarsFactType.PLANET,
|
|
55
|
+
content: `${raw.name} is a ${raw.climate} planet with ${raw.terrain} terrain and a population of ${raw.population}.`,
|
|
56
|
+
character: undefined,
|
|
57
|
+
faction: types_1.EStarWarsFaction.UNKNOWN,
|
|
58
|
+
film: undefined,
|
|
59
|
+
isSafe: true,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.SwapiInfoClient = SwapiInfoClient;
|
|
64
|
+
SwapiInfoClient.MAX_PLANET_ID = 60;
|
|
65
|
+
SwapiInfoClient.MAX_RETRIES_404 = 5;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw response from swapi.info planet endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Internal to this package only — never exported from index.ts,
|
|
5
|
+
* never added to @thalorlabs/types.
|
|
6
|
+
*/
|
|
7
|
+
export interface SwapiInfoPlanetRaw {
|
|
8
|
+
name: string;
|
|
9
|
+
rotation_period: string;
|
|
10
|
+
orbital_period: string;
|
|
11
|
+
diameter: string;
|
|
12
|
+
climate: string;
|
|
13
|
+
gravity: string;
|
|
14
|
+
terrain: string;
|
|
15
|
+
surface_water: string;
|
|
16
|
+
population: string;
|
|
17
|
+
residents: string[];
|
|
18
|
+
films: string[];
|
|
19
|
+
url: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Raw response from swapi.info film endpoint.
|
|
23
|
+
*
|
|
24
|
+
* Internal to this package only — never exported from index.ts,
|
|
25
|
+
* never added to @thalorlabs/types.
|
|
26
|
+
*/
|
|
27
|
+
export interface SwapiInfoFilmRaw {
|
|
28
|
+
title: string;
|
|
29
|
+
episode_id: number;
|
|
30
|
+
opening_crawl: string;
|
|
31
|
+
director: string;
|
|
32
|
+
producer: string;
|
|
33
|
+
release_date: string;
|
|
34
|
+
url: string;
|
|
35
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SWAPIINFO_CONFIG = exports.SwapiInfoClient = void 0;
|
|
4
|
+
var SwapiInfoClient_1 = require("./SwapiInfoClient");
|
|
5
|
+
Object.defineProperty(exports, "SwapiInfoClient", { enumerable: true, get: function () { return SwapiInfoClient_1.SwapiInfoClient; } });
|
|
6
|
+
exports.SWAPIINFO_CONFIG = {
|
|
7
|
+
cacheTtlMs: 1000 * 60 * 60 * 24 * 7, // 7 days — Star Wars data never changes
|
|
8
|
+
isBillable: false,
|
|
9
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thalorlabs/swapiinfo",
|
|
3
|
+
"author": "ThalorLabs",
|
|
4
|
+
"private": false,
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "Provider adapter for swapi.info — returns TLStarWarsFact",
|
|
7
|
+
"homepage": "https://github.com/ThalorLabs/swapiinfo#readme",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/ThalorLabs/swapiinfo/issues"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/ThalorLabs/swapiinfo.git"
|
|
14
|
+
},
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"clean": "rimraf dist",
|
|
23
|
+
"prebuild": "npm run clean",
|
|
24
|
+
"prepublishOnly": "npm run build",
|
|
25
|
+
"lint": "eslint src/",
|
|
26
|
+
"lint:fix": "eslint src/ --fix",
|
|
27
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
28
|
+
"format:check": "prettier --check \"src/**/*.ts\""
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@thalorlabs/eslint-plugin-dev-config": "^2.1.1",
|
|
32
|
+
"@types/node": "^25.5.2",
|
|
33
|
+
"prettier": "^3.8.1",
|
|
34
|
+
"rimraf": "^5.0.0",
|
|
35
|
+
"typescript": "^5.0.0",
|
|
36
|
+
"vitest": "^3.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@thalorlabs/api": "^1.0.0",
|
|
40
|
+
"@thalorlabs/types": "^1.9.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
|
|
3
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
4
|
+
import { EStarWarsFaction, EStarWarsFactType, TLStarWarsFact } from '@thalorlabs/types';
|
|
5
|
+
|
|
6
|
+
import { SwapiInfoPlanetRaw } from './SwapiInfoTypes';
|
|
7
|
+
|
|
8
|
+
export interface SwapiInfoClientConfig {
|
|
9
|
+
baseURL?: string;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* HTTP adapter for swapi.info (Star Wars API).
|
|
15
|
+
*
|
|
16
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
17
|
+
* observability. Normalises the raw planet response to TLStarWarsFact.
|
|
18
|
+
* No DB, no cache, no logging — pure HTTP adapter.
|
|
19
|
+
*/
|
|
20
|
+
export class SwapiInfoClient extends BaseHttpClient {
|
|
21
|
+
public readonly serviceName = 'swapiinfo';
|
|
22
|
+
|
|
23
|
+
private static readonly MAX_PLANET_ID = 60;
|
|
24
|
+
private static readonly MAX_RETRIES_404 = 5;
|
|
25
|
+
|
|
26
|
+
constructor(config: SwapiInfoClientConfig = {}) {
|
|
27
|
+
super({
|
|
28
|
+
baseURL: config.baseURL ?? 'https://swapi.info/api',
|
|
29
|
+
serviceName: 'swapiinfo',
|
|
30
|
+
retries: 3,
|
|
31
|
+
timeout: config.timeout ?? 10000,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getFact(): Promise<TLStarWarsFact> {
|
|
36
|
+
let lastError: unknown;
|
|
37
|
+
|
|
38
|
+
for (let attempt = 0; attempt < SwapiInfoClient.MAX_RETRIES_404; attempt++) {
|
|
39
|
+
const id = this.randomPlanetId();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const { response } = await this.handleRequest<SwapiInfoPlanetRaw>(
|
|
43
|
+
{ method: 'GET', url: `/planets/${id}` },
|
|
44
|
+
{ method: 'GET', url: `/planets/${id}`, requestId: randomUUID() }
|
|
45
|
+
);
|
|
46
|
+
return this.normalise(response.data);
|
|
47
|
+
} catch (err: unknown) {
|
|
48
|
+
lastError = err;
|
|
49
|
+
if (this.is404(err)) continue;
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw lastError;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private randomPlanetId(): number {
|
|
58
|
+
return Math.floor(Math.random() * SwapiInfoClient.MAX_PLANET_ID) + 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private is404(err: unknown): boolean {
|
|
62
|
+
if (err && typeof err === 'object' && 'response' in err) {
|
|
63
|
+
const resp = (err as { response?: { status?: number } }).response;
|
|
64
|
+
return resp?.status === 404;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private normalise(raw: SwapiInfoPlanetRaw): TLStarWarsFact {
|
|
70
|
+
return TLStarWarsFact.parse({
|
|
71
|
+
id: createHash('md5').update(raw.name).digest('hex'),
|
|
72
|
+
type: EStarWarsFactType.PLANET,
|
|
73
|
+
content: `${raw.name} is a ${raw.climate} planet with ${raw.terrain} terrain and a population of ${raw.population}.`,
|
|
74
|
+
character: undefined,
|
|
75
|
+
faction: EStarWarsFaction.UNKNOWN,
|
|
76
|
+
film: undefined,
|
|
77
|
+
isSafe: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw response from swapi.info planet endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Internal to this package only — never exported from index.ts,
|
|
5
|
+
* never added to @thalorlabs/types.
|
|
6
|
+
*/
|
|
7
|
+
export interface SwapiInfoPlanetRaw {
|
|
8
|
+
name: string;
|
|
9
|
+
rotation_period: string;
|
|
10
|
+
orbital_period: string;
|
|
11
|
+
diameter: string;
|
|
12
|
+
climate: string;
|
|
13
|
+
gravity: string;
|
|
14
|
+
terrain: string;
|
|
15
|
+
surface_water: string;
|
|
16
|
+
population: string;
|
|
17
|
+
residents: string[];
|
|
18
|
+
films: string[];
|
|
19
|
+
url: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Raw response from swapi.info film endpoint.
|
|
24
|
+
*
|
|
25
|
+
* Internal to this package only — never exported from index.ts,
|
|
26
|
+
* never added to @thalorlabs/types.
|
|
27
|
+
*/
|
|
28
|
+
export interface SwapiInfoFilmRaw {
|
|
29
|
+
title: string;
|
|
30
|
+
episode_id: number;
|
|
31
|
+
opening_crawl: string;
|
|
32
|
+
director: string;
|
|
33
|
+
producer: string;
|
|
34
|
+
release_date: string;
|
|
35
|
+
url: string;
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { SwapiInfoClient } from '../src/SwapiInfoClient';
|
|
3
|
+
import { EStarWarsFactType, EStarWarsFaction } from '@thalorlabs/types';
|
|
4
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
5
|
+
|
|
6
|
+
// Mock handleRequest on the prototype — no axios import needed in tests
|
|
7
|
+
const mockHandleRequest = vi.fn();
|
|
8
|
+
vi.spyOn(BaseHttpClient.prototype as any, 'handleRequest').mockImplementation(
|
|
9
|
+
mockHandleRequest
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const MOCK_PLANET = {
|
|
13
|
+
name: 'Tatooine',
|
|
14
|
+
rotation_period: '23',
|
|
15
|
+
orbital_period: '304',
|
|
16
|
+
diameter: '10465',
|
|
17
|
+
climate: 'arid',
|
|
18
|
+
gravity: '1 standard',
|
|
19
|
+
terrain: 'desert',
|
|
20
|
+
surface_water: '1',
|
|
21
|
+
population: '200000',
|
|
22
|
+
residents: ['https://swapi.info/api/people/1'],
|
|
23
|
+
films: ['https://swapi.info/api/films/1'],
|
|
24
|
+
url: 'https://swapi.info/api/planets/1',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe('SwapiInfoClient', () => {
|
|
28
|
+
let client: SwapiInfoClient;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
client = new SwapiInfoClient();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('constructor', () => {
|
|
36
|
+
it('sets serviceName to swapiinfo', () => {
|
|
37
|
+
expect(client.serviceName).toBe('swapiinfo');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('getFact', () => {
|
|
42
|
+
it('returns a valid TLStarWarsFact with PLANET type', async () => {
|
|
43
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
44
|
+
response: { data: MOCK_PLANET },
|
|
45
|
+
durationMs: 50,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const fact = await client.getFact();
|
|
49
|
+
|
|
50
|
+
expect(fact.type).toBe(EStarWarsFactType.PLANET);
|
|
51
|
+
expect(fact.faction).toBe(EStarWarsFaction.UNKNOWN);
|
|
52
|
+
expect(fact.isSafe).toBe(true);
|
|
53
|
+
expect(fact.character).toBeUndefined();
|
|
54
|
+
expect(fact.film).toBeUndefined();
|
|
55
|
+
expect(fact.id).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('formats content string correctly', async () => {
|
|
59
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
60
|
+
response: { data: MOCK_PLANET },
|
|
61
|
+
durationMs: 50,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const fact = await client.getFact();
|
|
65
|
+
|
|
66
|
+
expect(fact.content).toBe(
|
|
67
|
+
'Tatooine is a arid planet with desert terrain and a population of 200000.'
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('generates a deterministic id from planet name', async () => {
|
|
72
|
+
mockHandleRequest.mockResolvedValue({
|
|
73
|
+
response: { data: MOCK_PLANET },
|
|
74
|
+
durationMs: 10,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const fact1 = await client.getFact();
|
|
78
|
+
const fact2 = await client.getFact();
|
|
79
|
+
|
|
80
|
+
expect(fact1.id).toBe(fact2.id);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('calls handleRequest with GET /planets/{id}', async () => {
|
|
84
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
85
|
+
response: { data: MOCK_PLANET },
|
|
86
|
+
durationMs: 10,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await client.getFact();
|
|
90
|
+
|
|
91
|
+
expect(mockHandleRequest).toHaveBeenCalledWith(
|
|
92
|
+
expect.objectContaining({ method: 'GET' }),
|
|
93
|
+
expect.objectContaining({ method: 'GET' })
|
|
94
|
+
);
|
|
95
|
+
const callUrl = mockHandleRequest.mock.calls[0][0].url;
|
|
96
|
+
expect(callUrl).toMatch(/^\/planets\/\d+$/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('retries on 404 with a different planet ID', async () => {
|
|
100
|
+
const err404 = Object.assign(new Error('Not Found'), {
|
|
101
|
+
response: { status: 404 },
|
|
102
|
+
});
|
|
103
|
+
mockHandleRequest
|
|
104
|
+
.mockRejectedValueOnce(err404)
|
|
105
|
+
.mockResolvedValueOnce({
|
|
106
|
+
response: { data: MOCK_PLANET },
|
|
107
|
+
durationMs: 10,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const fact = await client.getFact();
|
|
111
|
+
|
|
112
|
+
expect(mockHandleRequest).toHaveBeenCalledTimes(2);
|
|
113
|
+
expect(fact.type).toBe(EStarWarsFactType.PLANET);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('propagates non-404 errors immediately', async () => {
|
|
117
|
+
mockHandleRequest.mockRejectedValueOnce(new Error('Network error'));
|
|
118
|
+
|
|
119
|
+
await expect(client.getFact()).rejects.toThrow('Network error');
|
|
120
|
+
expect(mockHandleRequest).toHaveBeenCalledTimes(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('throws after exhausting 404 retries', async () => {
|
|
124
|
+
const err404 = Object.assign(new Error('Not Found'), {
|
|
125
|
+
response: { status: 404 },
|
|
126
|
+
});
|
|
127
|
+
mockHandleRequest.mockRejectedValue(err404);
|
|
128
|
+
|
|
129
|
+
await expect(client.getFact()).rejects.toThrow('Not Found');
|
|
130
|
+
expect(mockHandleRequest).toHaveBeenCalledTimes(5);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|