@thalorlabs/swquotes 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 +34 -0
- package/dist/SwQuotesClient.d.ts +21 -0
- package/dist/SwQuotesClient.js +73 -0
- package/dist/SwQuotesTypes.d.ts +11 -0
- package/dist/SwQuotesTypes.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/SwQuotesClient.ts +89 -0
- package/src/SwQuotesTypes.ts +11 -0
- package/src/index.ts +6 -0
- package/tests/SwQuotesClient.test.ts +185 -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,34 @@
|
|
|
1
|
+
# CLAUDE.md — @thalorlabs/swquotes
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
Provider adapter for swquotesapi.digitaljedi.dk (Star Wars Quotes API). Returns `TLStarWarsFact`.
|
|
6
|
+
|
|
7
|
+
## External API
|
|
8
|
+
|
|
9
|
+
- swquotesapi.digitaljedi.dk — free, no auth, no billing
|
|
10
|
+
- Returns random Star Wars quotes with character attribution
|
|
11
|
+
- HTTP only (not HTTPS)
|
|
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 quote response to TLStarWarsFact
|
|
26
|
+
- Parses character name from quote text
|
|
27
|
+
- Maps numeric faction codes to EStarWarsFaction enum
|
|
28
|
+
- Sets: type=QUOTE, isSafe=true
|
|
29
|
+
|
|
30
|
+
## What this package does NOT do
|
|
31
|
+
|
|
32
|
+
- No database, no caching, no logging — orchestrator handles that
|
|
33
|
+
- Raw types never exported
|
|
34
|
+
- No provider selection logic
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
2
|
+
import { TLStarWarsFact } from '@thalorlabs/types';
|
|
3
|
+
export interface SwQuotesClientConfig {
|
|
4
|
+
baseURL?: string;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* HTTP adapter for swquotesapi.digitaljedi.dk.
|
|
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 SwQuotesClient extends BaseHttpClient {
|
|
15
|
+
readonly serviceName = "swquotes";
|
|
16
|
+
constructor(config?: SwQuotesClientConfig);
|
|
17
|
+
getFact(): Promise<TLStarWarsFact>;
|
|
18
|
+
private normalise;
|
|
19
|
+
private parseQuote;
|
|
20
|
+
private mapFaction;
|
|
21
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SwQuotesClient = 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 swquotesapi.digitaljedi.dk.
|
|
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 SwQuotesClient extends api_1.BaseHttpClient {
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
super({
|
|
17
|
+
baseURL: config.baseURL ?? 'http://swquotesapi.digitaljedi.dk',
|
|
18
|
+
serviceName: 'swquotes',
|
|
19
|
+
retries: 3,
|
|
20
|
+
timeout: config.timeout ?? 10000,
|
|
21
|
+
});
|
|
22
|
+
this.serviceName = 'swquotes';
|
|
23
|
+
}
|
|
24
|
+
async getFact() {
|
|
25
|
+
const { response } = await this.handleRequest({ method: 'GET', url: '/api/SWQuote/RandomStarWarsQuote' }, { method: 'GET', url: '/api/SWQuote/RandomStarWarsQuote', requestId: (0, crypto_1.randomUUID)() });
|
|
26
|
+
return this.normalise(response.data);
|
|
27
|
+
}
|
|
28
|
+
normalise(raw) {
|
|
29
|
+
const { content, character } = this.parseQuote(raw.starWarsQuote);
|
|
30
|
+
return types_1.TLStarWarsFact.parse({
|
|
31
|
+
id: (0, crypto_1.createHash)('md5').update(raw.starWarsQuote).digest('hex'),
|
|
32
|
+
type: types_1.EStarWarsFactType.QUOTE,
|
|
33
|
+
content,
|
|
34
|
+
character,
|
|
35
|
+
faction: this.mapFaction(raw.faction),
|
|
36
|
+
film: undefined,
|
|
37
|
+
isSafe: true,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
parseQuote(quote) {
|
|
41
|
+
// Try " — " first (em-dash style), then " - " (hyphen style)
|
|
42
|
+
const emDashIndex = quote.lastIndexOf(' — ');
|
|
43
|
+
if (emDashIndex !== -1) {
|
|
44
|
+
return {
|
|
45
|
+
content: quote.substring(0, emDashIndex).trim(),
|
|
46
|
+
character: quote.substring(emDashIndex + 3).trim(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const hyphenIndex = quote.lastIndexOf(' - ');
|
|
50
|
+
if (hyphenIndex !== -1) {
|
|
51
|
+
return {
|
|
52
|
+
content: quote.substring(0, hyphenIndex).trim(),
|
|
53
|
+
character: quote.substring(hyphenIndex + 3).trim(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { content: quote };
|
|
57
|
+
}
|
|
58
|
+
mapFaction(faction) {
|
|
59
|
+
switch (faction) {
|
|
60
|
+
case 0:
|
|
61
|
+
return types_1.EStarWarsFaction.JEDI_ORDER;
|
|
62
|
+
case 1:
|
|
63
|
+
return types_1.EStarWarsFaction.SITH;
|
|
64
|
+
case 3:
|
|
65
|
+
return types_1.EStarWarsFaction.REBEL_ALLIANCE;
|
|
66
|
+
case 2:
|
|
67
|
+
case 4:
|
|
68
|
+
default:
|
|
69
|
+
return types_1.EStarWarsFaction.UNKNOWN;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.SwQuotesClient = SwQuotesClient;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw response from swquotesapi.digitaljedi.dk API.
|
|
3
|
+
*
|
|
4
|
+
* Internal to this package only — never exported from index.ts,
|
|
5
|
+
* never added to @thalorlabs/types.
|
|
6
|
+
*/
|
|
7
|
+
export interface SwQuotesRawResponse {
|
|
8
|
+
id: number;
|
|
9
|
+
starWarsQuote: string;
|
|
10
|
+
faction: number;
|
|
11
|
+
}
|
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.SWQUOTES_CONFIG = exports.SwQuotesClient = void 0;
|
|
4
|
+
var SwQuotesClient_1 = require("./SwQuotesClient");
|
|
5
|
+
Object.defineProperty(exports, "SwQuotesClient", { enumerable: true, get: function () { return SwQuotesClient_1.SwQuotesClient; } });
|
|
6
|
+
exports.SWQUOTES_CONFIG = {
|
|
7
|
+
cacheTtlMs: 0, // random endpoint — no caching
|
|
8
|
+
isBillable: false, // swquotesapi is free
|
|
9
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thalorlabs/swquotes",
|
|
3
|
+
"author": "ThalorLabs",
|
|
4
|
+
"private": false,
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "Provider adapter for swquotesapi.digitaljedi.dk — returns TLStarWarsFact",
|
|
7
|
+
"homepage": "https://github.com/ThalorLabs/swquotes#readme",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/ThalorLabs/swquotes/issues"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/ThalorLabs/swquotes.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,89 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
|
|
3
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
4
|
+
import { EStarWarsFaction, EStarWarsFactType, TLStarWarsFact } from '@thalorlabs/types';
|
|
5
|
+
|
|
6
|
+
import { SwQuotesRawResponse } from './SwQuotesTypes';
|
|
7
|
+
|
|
8
|
+
export interface SwQuotesClientConfig {
|
|
9
|
+
baseURL?: string;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* HTTP adapter for swquotesapi.digitaljedi.dk.
|
|
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 SwQuotesClient extends BaseHttpClient {
|
|
21
|
+
public readonly serviceName = 'swquotes';
|
|
22
|
+
|
|
23
|
+
constructor(config: SwQuotesClientConfig = {}) {
|
|
24
|
+
super({
|
|
25
|
+
baseURL: config.baseURL ?? 'http://swquotesapi.digitaljedi.dk',
|
|
26
|
+
serviceName: 'swquotes',
|
|
27
|
+
retries: 3,
|
|
28
|
+
timeout: config.timeout ?? 10000,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async getFact(): Promise<TLStarWarsFact> {
|
|
33
|
+
const { response } = await this.handleRequest<SwQuotesRawResponse>(
|
|
34
|
+
{ method: 'GET', url: '/api/SWQuote/RandomStarWarsQuote' },
|
|
35
|
+
{ method: 'GET', url: '/api/SWQuote/RandomStarWarsQuote', requestId: randomUUID() }
|
|
36
|
+
);
|
|
37
|
+
return this.normalise(response.data);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private normalise(raw: SwQuotesRawResponse): TLStarWarsFact {
|
|
41
|
+
const { content, character } = this.parseQuote(raw.starWarsQuote);
|
|
42
|
+
|
|
43
|
+
return TLStarWarsFact.parse({
|
|
44
|
+
id: createHash('md5').update(raw.starWarsQuote).digest('hex'),
|
|
45
|
+
type: EStarWarsFactType.QUOTE,
|
|
46
|
+
content,
|
|
47
|
+
character,
|
|
48
|
+
faction: this.mapFaction(raw.faction),
|
|
49
|
+
film: undefined,
|
|
50
|
+
isSafe: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private parseQuote(quote: string): { content: string; character?: string } {
|
|
55
|
+
// Try " — " first (em-dash style), then " - " (hyphen style)
|
|
56
|
+
const emDashIndex = quote.lastIndexOf(' — ');
|
|
57
|
+
if (emDashIndex !== -1) {
|
|
58
|
+
return {
|
|
59
|
+
content: quote.substring(0, emDashIndex).trim(),
|
|
60
|
+
character: quote.substring(emDashIndex + 3).trim(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const hyphenIndex = quote.lastIndexOf(' - ');
|
|
65
|
+
if (hyphenIndex !== -1) {
|
|
66
|
+
return {
|
|
67
|
+
content: quote.substring(0, hyphenIndex).trim(),
|
|
68
|
+
character: quote.substring(hyphenIndex + 3).trim(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { content: quote };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private mapFaction(faction: number): EStarWarsFaction {
|
|
76
|
+
switch (faction) {
|
|
77
|
+
case 0:
|
|
78
|
+
return EStarWarsFaction.JEDI_ORDER;
|
|
79
|
+
case 1:
|
|
80
|
+
return EStarWarsFaction.SITH;
|
|
81
|
+
case 3:
|
|
82
|
+
return EStarWarsFaction.REBEL_ALLIANCE;
|
|
83
|
+
case 2:
|
|
84
|
+
case 4:
|
|
85
|
+
default:
|
|
86
|
+
return EStarWarsFaction.UNKNOWN;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw response from swquotesapi.digitaljedi.dk API.
|
|
3
|
+
*
|
|
4
|
+
* Internal to this package only — never exported from index.ts,
|
|
5
|
+
* never added to @thalorlabs/types.
|
|
6
|
+
*/
|
|
7
|
+
export interface SwQuotesRawResponse {
|
|
8
|
+
id: number;
|
|
9
|
+
starWarsQuote: string;
|
|
10
|
+
faction: number;
|
|
11
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { SwQuotesClient } from '../src/SwQuotesClient';
|
|
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('SwQuotesClient', () => {
|
|
13
|
+
let client: SwQuotesClient;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
client = new SwQuotesClient();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('constructor', () => {
|
|
21
|
+
it('sets serviceName to swquotes', () => {
|
|
22
|
+
expect(client.serviceName).toBe('swquotes');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('getFact', () => {
|
|
27
|
+
it('returns a valid TLStarWarsFact with QUOTE type', async () => {
|
|
28
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
29
|
+
response: {
|
|
30
|
+
data: {
|
|
31
|
+
id: 7,
|
|
32
|
+
starWarsQuote: 'Never tell me the odds! — Han Solo',
|
|
33
|
+
faction: 4,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
durationMs: 50,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const fact = await client.getFact();
|
|
40
|
+
|
|
41
|
+
expect(fact.type).toBe(EStarWarsFactType.QUOTE);
|
|
42
|
+
expect(fact.content).toBe('Never tell me the odds!');
|
|
43
|
+
expect(fact.character).toBe('Han Solo');
|
|
44
|
+
expect(fact.isSafe).toBe(true);
|
|
45
|
+
expect(fact.id).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('parses character name from em-dash separator', async () => {
|
|
49
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
50
|
+
response: {
|
|
51
|
+
data: {
|
|
52
|
+
id: 1,
|
|
53
|
+
starWarsQuote: 'Do. Or do not. There is no try. — Yoda',
|
|
54
|
+
faction: 0,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
durationMs: 10,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const fact = await client.getFact();
|
|
61
|
+
|
|
62
|
+
expect(fact.content).toBe('Do. Or do not. There is no try.');
|
|
63
|
+
expect(fact.character).toBe('Yoda');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('parses character name from hyphen separator', async () => {
|
|
67
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
68
|
+
response: {
|
|
69
|
+
data: {
|
|
70
|
+
id: 2,
|
|
71
|
+
starWarsQuote: 'I am your father - Darth Vader',
|
|
72
|
+
faction: 1,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
durationMs: 10,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const fact = await client.getFact();
|
|
79
|
+
|
|
80
|
+
expect(fact.content).toBe('I am your father');
|
|
81
|
+
expect(fact.character).toBe('Darth Vader');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('handles quotes without character attribution', async () => {
|
|
85
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
86
|
+
response: {
|
|
87
|
+
data: {
|
|
88
|
+
id: 3,
|
|
89
|
+
starWarsQuote: 'A long time ago in a galaxy far, far away...',
|
|
90
|
+
faction: 2,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
durationMs: 10,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const fact = await client.getFact();
|
|
97
|
+
|
|
98
|
+
expect(fact.content).toBe('A long time ago in a galaxy far, far away...');
|
|
99
|
+
expect(fact.character).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('maps faction 0 to JEDI_ORDER', async () => {
|
|
103
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
104
|
+
response: {
|
|
105
|
+
data: { id: 10, starWarsQuote: 'The Force is strong — Yoda', faction: 0 },
|
|
106
|
+
},
|
|
107
|
+
durationMs: 10,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const fact = await client.getFact();
|
|
111
|
+
expect(fact.faction).toBe(EStarWarsFaction.JEDI_ORDER);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('maps faction 1 to SITH', async () => {
|
|
115
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
116
|
+
response: {
|
|
117
|
+
data: { id: 11, starWarsQuote: 'The dark side — Palpatine', faction: 1 },
|
|
118
|
+
},
|
|
119
|
+
durationMs: 10,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const fact = await client.getFact();
|
|
123
|
+
expect(fact.faction).toBe(EStarWarsFaction.SITH);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('maps faction 3 to REBEL_ALLIANCE', async () => {
|
|
127
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
128
|
+
response: {
|
|
129
|
+
data: { id: 12, starWarsQuote: 'May the Force be with you — Leia', faction: 3 },
|
|
130
|
+
},
|
|
131
|
+
durationMs: 10,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const fact = await client.getFact();
|
|
135
|
+
expect(fact.faction).toBe(EStarWarsFaction.REBEL_ALLIANCE);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('maps faction 2 and 4 to UNKNOWN', async () => {
|
|
139
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
140
|
+
response: {
|
|
141
|
+
data: { id: 13, starWarsQuote: 'Beep boop — R2-D2', faction: 2 },
|
|
142
|
+
},
|
|
143
|
+
durationMs: 10,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const fact = await client.getFact();
|
|
147
|
+
expect(fact.faction).toBe(EStarWarsFaction.UNKNOWN);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('generates a deterministic id from quote text', async () => {
|
|
151
|
+
const quoteText = 'Never tell me the odds! — Han Solo';
|
|
152
|
+
mockHandleRequest.mockResolvedValue({
|
|
153
|
+
response: { data: { id: 7, starWarsQuote: quoteText, faction: 4 } },
|
|
154
|
+
durationMs: 10,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const fact1 = await client.getFact();
|
|
158
|
+
const fact2 = await client.getFact();
|
|
159
|
+
|
|
160
|
+
expect(fact1.id).toBe(fact2.id);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('calls handleRequest with GET /api/SWQuote/RandomStarWarsQuote', async () => {
|
|
164
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
165
|
+
response: {
|
|
166
|
+
data: { id: 1, starWarsQuote: 'A quote — Someone', faction: 0 },
|
|
167
|
+
},
|
|
168
|
+
durationMs: 10,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await client.getFact();
|
|
172
|
+
|
|
173
|
+
expect(mockHandleRequest).toHaveBeenCalledWith(
|
|
174
|
+
expect.objectContaining({ method: 'GET', url: '/api/SWQuote/RandomStarWarsQuote' }),
|
|
175
|
+
expect.objectContaining({ method: 'GET', url: '/api/SWQuote/RandomStarWarsQuote' })
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('propagates errors from handleRequest', async () => {
|
|
180
|
+
mockHandleRequest.mockRejectedValueOnce(new Error('Network error'));
|
|
181
|
+
|
|
182
|
+
await expect(client.getFact()).rejects.toThrow('Network error');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|