@thalorlabs/swapi 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/.env.example +0 -0
- package/.prettierrc +1 -0
- package/CLAUDE.md +33 -0
- package/dist/SwapiClient.d.ts +21 -0
- package/dist/SwapiClient.js +59 -0
- package/dist/SwapiTypes.d.ts +62 -0
- package/dist/SwapiTypes.js +8 -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/SwapiClient.ts +76 -0
- package/src/SwapiTypes.ts +66 -0
- package/src/index.ts +6 -0
- package/tests/SwapiClient.test.ts +134 -0
- package/tsconfig.json +8 -0
package/.env.example
ADDED
|
File without changes
|
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/swapi
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
Provider adapter for swapi.dev (Star Wars API). Returns `TLStarWarsFact`.
|
|
6
|
+
|
|
7
|
+
## External API
|
|
8
|
+
|
|
9
|
+
- swapi.dev — free, no auth, no billing
|
|
10
|
+
- Returns character data from the Star Wars universe
|
|
11
|
+
- No random endpoint — uses random ID selection
|
|
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.dev response to TLStarWarsFact
|
|
26
|
+
- Generates deterministic MD5 hash ID from character name
|
|
27
|
+
- Sets: type=CHARACTER, 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,21 @@
|
|
|
1
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
2
|
+
import { TLStarWarsFact } from '@thalorlabs/types';
|
|
3
|
+
export interface SwapiClientConfig {
|
|
4
|
+
baseURL?: string;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* HTTP adapter for swapi.dev (Star Wars API).
|
|
9
|
+
*
|
|
10
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
11
|
+
* observability. Normalises the raw response to TLStarWarsFact.
|
|
12
|
+
* No DB, no cache, no logging — pure HTTP adapter.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SwapiClient extends BaseHttpClient {
|
|
15
|
+
readonly serviceName = "swapi";
|
|
16
|
+
private static readonly MAX_PERSON_ID;
|
|
17
|
+
private static readonly MAX_RETRIES_FOR_GAPS;
|
|
18
|
+
constructor(config?: SwapiClientConfig);
|
|
19
|
+
getFact(): Promise<TLStarWarsFact>;
|
|
20
|
+
private normalise;
|
|
21
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SwapiClient = 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.dev (Star Wars API).
|
|
9
|
+
*
|
|
10
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
11
|
+
* observability. Normalises the raw response to TLStarWarsFact.
|
|
12
|
+
* No DB, no cache, no logging — pure HTTP adapter.
|
|
13
|
+
*/
|
|
14
|
+
class SwapiClient extends api_1.BaseHttpClient {
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
super({
|
|
17
|
+
baseURL: config.baseURL ?? 'https://swapi.dev/api',
|
|
18
|
+
serviceName: 'swapi',
|
|
19
|
+
retries: 3,
|
|
20
|
+
timeout: config.timeout ?? 10000,
|
|
21
|
+
});
|
|
22
|
+
this.serviceName = 'swapi';
|
|
23
|
+
}
|
|
24
|
+
async getFact() {
|
|
25
|
+
let lastError;
|
|
26
|
+
for (let attempt = 0; attempt < SwapiClient.MAX_RETRIES_FOR_GAPS; attempt++) {
|
|
27
|
+
const id = Math.floor(Math.random() * SwapiClient.MAX_PERSON_ID) + 1;
|
|
28
|
+
try {
|
|
29
|
+
const { response } = await this.handleRequest({ method: 'GET', url: `/people/${id}/` }, { method: 'GET', url: `/people/${id}/`, requestId: (0, crypto_1.randomUUID)() });
|
|
30
|
+
return this.normalise(response.data);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
lastError = error;
|
|
34
|
+
// If it's a 404 (gap in IDs), retry with a different ID
|
|
35
|
+
if (error?.response?.status === 404 || error?.status === 404) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
// For non-404 errors, throw immediately
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw lastError ?? new Error('Failed to fetch Star Wars character after maximum retries');
|
|
43
|
+
}
|
|
44
|
+
normalise(raw) {
|
|
45
|
+
const content = `${raw.name} is a ${raw.gender} character born in ${raw.birth_year}, standing ${raw.height}cm tall.`;
|
|
46
|
+
return types_1.TLStarWarsFact.parse({
|
|
47
|
+
id: (0, crypto_1.createHash)('md5').update(raw.name).digest('hex'),
|
|
48
|
+
type: types_1.EStarWarsFactType.CHARACTER,
|
|
49
|
+
content,
|
|
50
|
+
character: raw.name,
|
|
51
|
+
faction: types_1.EStarWarsFaction.UNKNOWN,
|
|
52
|
+
film: undefined,
|
|
53
|
+
isSafe: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.SwapiClient = SwapiClient;
|
|
58
|
+
SwapiClient.MAX_PERSON_ID = 82;
|
|
59
|
+
SwapiClient.MAX_RETRIES_FOR_GAPS = 5;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw response types from swapi.dev API.
|
|
3
|
+
*
|
|
4
|
+
* Internal to this package only — never exported from index.ts,
|
|
5
|
+
* never added to @thalorlabs/types.
|
|
6
|
+
*/
|
|
7
|
+
export interface SwapiPersonRaw {
|
|
8
|
+
name: string;
|
|
9
|
+
height: string;
|
|
10
|
+
mass: string;
|
|
11
|
+
hair_color: string;
|
|
12
|
+
skin_color: string;
|
|
13
|
+
eye_color: string;
|
|
14
|
+
birth_year: string;
|
|
15
|
+
gender: string;
|
|
16
|
+
homeworld: string;
|
|
17
|
+
films: string[];
|
|
18
|
+
species: string[];
|
|
19
|
+
vehicles: string[];
|
|
20
|
+
starships: string[];
|
|
21
|
+
created: string;
|
|
22
|
+
edited: string;
|
|
23
|
+
url: string;
|
|
24
|
+
}
|
|
25
|
+
export interface SwapiPlanetRaw {
|
|
26
|
+
name: string;
|
|
27
|
+
rotation_period: string;
|
|
28
|
+
orbital_period: string;
|
|
29
|
+
diameter: string;
|
|
30
|
+
climate: string;
|
|
31
|
+
gravity: string;
|
|
32
|
+
terrain: string;
|
|
33
|
+
surface_water: string;
|
|
34
|
+
population: string;
|
|
35
|
+
residents: string[];
|
|
36
|
+
films: string[];
|
|
37
|
+
created: string;
|
|
38
|
+
edited: string;
|
|
39
|
+
url: string;
|
|
40
|
+
}
|
|
41
|
+
export interface SwapiFilmRaw {
|
|
42
|
+
title: string;
|
|
43
|
+
episode_id: number;
|
|
44
|
+
opening_crawl: string;
|
|
45
|
+
director: string;
|
|
46
|
+
producer: string;
|
|
47
|
+
release_date: string;
|
|
48
|
+
characters: string[];
|
|
49
|
+
planets: string[];
|
|
50
|
+
starships: string[];
|
|
51
|
+
vehicles: string[];
|
|
52
|
+
species: string[];
|
|
53
|
+
created: string;
|
|
54
|
+
edited: string;
|
|
55
|
+
url: string;
|
|
56
|
+
}
|
|
57
|
+
export interface SwapiListResponse<T> {
|
|
58
|
+
count: number;
|
|
59
|
+
next: string | null;
|
|
60
|
+
previous: string | null;
|
|
61
|
+
results: T[];
|
|
62
|
+
}
|
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.SWAPI_CONFIG = exports.SwapiClient = void 0;
|
|
4
|
+
var SwapiClient_1 = require("./SwapiClient");
|
|
5
|
+
Object.defineProperty(exports, "SwapiClient", { enumerable: true, get: function () { return SwapiClient_1.SwapiClient; } });
|
|
6
|
+
exports.SWAPI_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/swapi",
|
|
3
|
+
"author": "ThalorLabs",
|
|
4
|
+
"private": false,
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "Provider adapter for swapi.dev — returns TLStarWarsFact",
|
|
7
|
+
"homepage": "https://github.com/ThalorLabs/swapi#readme",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/ThalorLabs/swapi/issues"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/ThalorLabs/swapi.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,76 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
|
|
3
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
4
|
+
import { EStarWarsFaction, EStarWarsFactType, TLStarWarsFact } from '@thalorlabs/types';
|
|
5
|
+
|
|
6
|
+
import { SwapiPersonRaw } from './SwapiTypes';
|
|
7
|
+
|
|
8
|
+
export interface SwapiClientConfig {
|
|
9
|
+
baseURL?: string;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* HTTP adapter for swapi.dev (Star Wars API).
|
|
15
|
+
*
|
|
16
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
17
|
+
* observability. Normalises the raw response to TLStarWarsFact.
|
|
18
|
+
* No DB, no cache, no logging — pure HTTP adapter.
|
|
19
|
+
*/
|
|
20
|
+
export class SwapiClient extends BaseHttpClient {
|
|
21
|
+
public readonly serviceName = 'swapi';
|
|
22
|
+
|
|
23
|
+
private static readonly MAX_PERSON_ID = 82;
|
|
24
|
+
private static readonly MAX_RETRIES_FOR_GAPS = 5;
|
|
25
|
+
|
|
26
|
+
constructor(config: SwapiClientConfig = {}) {
|
|
27
|
+
super({
|
|
28
|
+
baseURL: config.baseURL ?? 'https://swapi.dev/api',
|
|
29
|
+
serviceName: 'swapi',
|
|
30
|
+
retries: 3,
|
|
31
|
+
timeout: config.timeout ?? 10000,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getFact(): Promise<TLStarWarsFact> {
|
|
36
|
+
let lastError: Error | undefined;
|
|
37
|
+
|
|
38
|
+
for (let attempt = 0; attempt < SwapiClient.MAX_RETRIES_FOR_GAPS; attempt++) {
|
|
39
|
+
const id = Math.floor(Math.random() * SwapiClient.MAX_PERSON_ID) + 1;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const { response } = await this.handleRequest<SwapiPersonRaw>(
|
|
43
|
+
{ method: 'GET', url: `/people/${id}/` },
|
|
44
|
+
{ method: 'GET', url: `/people/${id}/`, requestId: randomUUID() }
|
|
45
|
+
);
|
|
46
|
+
return this.normalise(response.data);
|
|
47
|
+
} catch (error: any) {
|
|
48
|
+
lastError = error;
|
|
49
|
+
|
|
50
|
+
// If it's a 404 (gap in IDs), retry with a different ID
|
|
51
|
+
if (error?.response?.status === 404 || error?.status === 404) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// For non-404 errors, throw immediately
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw lastError ?? new Error('Failed to fetch Star Wars character after maximum retries');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private normalise(raw: SwapiPersonRaw): TLStarWarsFact {
|
|
64
|
+
const content = `${raw.name} is a ${raw.gender} character born in ${raw.birth_year}, standing ${raw.height}cm tall.`;
|
|
65
|
+
|
|
66
|
+
return TLStarWarsFact.parse({
|
|
67
|
+
id: createHash('md5').update(raw.name).digest('hex'),
|
|
68
|
+
type: EStarWarsFactType.CHARACTER,
|
|
69
|
+
content,
|
|
70
|
+
character: raw.name,
|
|
71
|
+
faction: EStarWarsFaction.UNKNOWN,
|
|
72
|
+
film: undefined,
|
|
73
|
+
isSafe: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw response types from swapi.dev API.
|
|
3
|
+
*
|
|
4
|
+
* Internal to this package only — never exported from index.ts,
|
|
5
|
+
* never added to @thalorlabs/types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface SwapiPersonRaw {
|
|
9
|
+
name: string;
|
|
10
|
+
height: string;
|
|
11
|
+
mass: string;
|
|
12
|
+
hair_color: string;
|
|
13
|
+
skin_color: string;
|
|
14
|
+
eye_color: string;
|
|
15
|
+
birth_year: string;
|
|
16
|
+
gender: string;
|
|
17
|
+
homeworld: string;
|
|
18
|
+
films: string[];
|
|
19
|
+
species: string[];
|
|
20
|
+
vehicles: string[];
|
|
21
|
+
starships: string[];
|
|
22
|
+
created: string;
|
|
23
|
+
edited: string;
|
|
24
|
+
url: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SwapiPlanetRaw {
|
|
28
|
+
name: string;
|
|
29
|
+
rotation_period: string;
|
|
30
|
+
orbital_period: string;
|
|
31
|
+
diameter: string;
|
|
32
|
+
climate: string;
|
|
33
|
+
gravity: string;
|
|
34
|
+
terrain: string;
|
|
35
|
+
surface_water: string;
|
|
36
|
+
population: string;
|
|
37
|
+
residents: string[];
|
|
38
|
+
films: string[];
|
|
39
|
+
created: string;
|
|
40
|
+
edited: string;
|
|
41
|
+
url: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SwapiFilmRaw {
|
|
45
|
+
title: string;
|
|
46
|
+
episode_id: number;
|
|
47
|
+
opening_crawl: string;
|
|
48
|
+
director: string;
|
|
49
|
+
producer: string;
|
|
50
|
+
release_date: string;
|
|
51
|
+
characters: string[];
|
|
52
|
+
planets: string[];
|
|
53
|
+
starships: string[];
|
|
54
|
+
vehicles: string[];
|
|
55
|
+
species: string[];
|
|
56
|
+
created: string;
|
|
57
|
+
edited: string;
|
|
58
|
+
url: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SwapiListResponse<T> {
|
|
62
|
+
count: number;
|
|
63
|
+
next: string | null;
|
|
64
|
+
previous: string | null;
|
|
65
|
+
results: T[];
|
|
66
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { SwapiClient } from '../src/SwapiClient';
|
|
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
|
+
describe('SwapiClient', () => {
|
|
13
|
+
let client: SwapiClient;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
client = new SwapiClient();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('constructor', () => {
|
|
21
|
+
it('sets serviceName to swapi', () => {
|
|
22
|
+
expect(client.serviceName).toBe('swapi');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('getFact', () => {
|
|
27
|
+
it('returns a valid TLStarWarsFact with CHARACTER type', async () => {
|
|
28
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
29
|
+
response: {
|
|
30
|
+
data: {
|
|
31
|
+
name: 'Luke Skywalker',
|
|
32
|
+
height: '172',
|
|
33
|
+
mass: '77',
|
|
34
|
+
hair_color: 'blond',
|
|
35
|
+
skin_color: 'fair',
|
|
36
|
+
eye_color: 'blue',
|
|
37
|
+
birth_year: '19BBY',
|
|
38
|
+
gender: 'male',
|
|
39
|
+
homeworld: 'https://swapi.dev/api/planets/1/',
|
|
40
|
+
films: ['https://swapi.dev/api/films/1/'],
|
|
41
|
+
species: [],
|
|
42
|
+
vehicles: [],
|
|
43
|
+
starships: [],
|
|
44
|
+
created: '2014-12-09T13:50:51.644000Z',
|
|
45
|
+
edited: '2014-12-20T21:17:56.891000Z',
|
|
46
|
+
url: 'https://swapi.dev/api/people/1/',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
durationMs: 50,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const fact = await client.getFact();
|
|
53
|
+
|
|
54
|
+
expect(fact.type).toBe(EStarWarsFactType.CHARACTER);
|
|
55
|
+
expect(fact.faction).toBe(EStarWarsFaction.UNKNOWN);
|
|
56
|
+
expect(fact.character).toBe('Luke Skywalker');
|
|
57
|
+
expect(fact.content).toBe(
|
|
58
|
+
'Luke Skywalker is a male character born in 19BBY, standing 172cm tall.'
|
|
59
|
+
);
|
|
60
|
+
expect(fact.isSafe).toBe(true);
|
|
61
|
+
expect(fact.id).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('generates a deterministic id from character name', async () => {
|
|
65
|
+
const personData = {
|
|
66
|
+
name: 'Darth Vader',
|
|
67
|
+
height: '202',
|
|
68
|
+
mass: '136',
|
|
69
|
+
hair_color: 'none',
|
|
70
|
+
skin_color: 'white',
|
|
71
|
+
eye_color: 'yellow',
|
|
72
|
+
birth_year: '41.9BBY',
|
|
73
|
+
gender: 'male',
|
|
74
|
+
homeworld: 'https://swapi.dev/api/planets/1/',
|
|
75
|
+
films: [],
|
|
76
|
+
species: [],
|
|
77
|
+
vehicles: [],
|
|
78
|
+
starships: [],
|
|
79
|
+
created: '2014-12-10T15:18:20.704000Z',
|
|
80
|
+
edited: '2014-12-20T21:17:50.313000Z',
|
|
81
|
+
url: 'https://swapi.dev/api/people/4/',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
mockHandleRequest.mockResolvedValue({
|
|
85
|
+
response: { data: personData },
|
|
86
|
+
durationMs: 10,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const fact1 = await client.getFact();
|
|
90
|
+
const fact2 = await client.getFact();
|
|
91
|
+
|
|
92
|
+
expect(fact1.id).toBe(fact2.id);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('calls handleRequest with GET /people/{id}/', async () => {
|
|
96
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
97
|
+
response: {
|
|
98
|
+
data: {
|
|
99
|
+
name: 'C-3PO',
|
|
100
|
+
height: '167',
|
|
101
|
+
mass: '75',
|
|
102
|
+
hair_color: 'n/a',
|
|
103
|
+
skin_color: 'gold',
|
|
104
|
+
eye_color: 'yellow',
|
|
105
|
+
birth_year: '112BBY',
|
|
106
|
+
gender: 'n/a',
|
|
107
|
+
homeworld: 'https://swapi.dev/api/planets/1/',
|
|
108
|
+
films: [],
|
|
109
|
+
species: [],
|
|
110
|
+
vehicles: [],
|
|
111
|
+
starships: [],
|
|
112
|
+
created: '2014-12-10T15:10:51.357000Z',
|
|
113
|
+
edited: '2014-12-20T21:17:50.309000Z',
|
|
114
|
+
url: 'https://swapi.dev/api/people/2/',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
durationMs: 10,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await client.getFact();
|
|
121
|
+
|
|
122
|
+
expect(mockHandleRequest).toHaveBeenCalledWith(
|
|
123
|
+
expect.objectContaining({ method: 'GET', url: expect.stringMatching(/^\/people\/\d+\/$/) }),
|
|
124
|
+
expect.objectContaining({ method: 'GET', url: expect.stringMatching(/^\/people\/\d+\/$/) })
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('propagates errors from handleRequest', async () => {
|
|
129
|
+
mockHandleRequest.mockRejectedValueOnce(new Error('Network error'));
|
|
130
|
+
|
|
131
|
+
await expect(client.getFact()).rejects.toThrow('Network error');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|