@axiapps/gw2-data 0.1.1 → 0.1.3
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/data/overrides.json +24 -0
- package/package.json +4 -1
- package/src/engine/attributes.js +102 -23
- package/src/engine/boons.js +19 -1
- package/src/engine/constants.js +27 -2
- package/src/engine/index.js +4 -4
- package/src/engine/modifiers.js +57 -21
- package/src/index.js +5 -0
- package/src/wiki/parser.js +17 -9
- package/scripts/generate-fixtures.js +0 -242
- package/tests/api-client.test.js +0 -138
- package/tests/cache.test.js +0 -108
- package/tests/engine/attributes.test.js +0 -252
- package/tests/engine/boons.test.js +0 -129
- package/tests/engine/combos.test.js +0 -76
- package/tests/engine/constants.test.js +0 -576
- package/tests/engine/fixtures/berserker-thief.json +0 -61
- package/tests/engine/fixtures/berserker-warrior.json +0 -113
- package/tests/engine/fixtures/celestial-firebrand-wvw.json +0 -94
- package/tests/engine/fixtures/harrier-druid.json +0 -119
- package/tests/engine/fixtures/viper-mirage.json +0 -104
- package/tests/engine/graph.test.js +0 -30
- package/tests/engine/integration.test.js +0 -111
- package/tests/engine/modifiers.test.js +0 -473
- package/tests/engine/overrides.test.js +0 -70
- package/tests/engine/snapshot.test.js +0 -53
- package/tests/engine/test-utils.js +0 -20
- package/tests/engine/tooltips.test.js +0 -62
- package/tests/fixtures/capture.js +0 -160
- package/tests/fixtures/fixtures.json +0 -839
- package/tests/integration.test.js +0 -100
- package/tests/match.test.js +0 -176
- package/tests/merge.test.js +0 -128
- package/tests/normalize.test.js +0 -78
- package/tests/parser.test.js +0 -506
- package/tests/real-data.test.js +0 -296
- package/tests/relations.test.js +0 -80
- package/tests/resolver.test.js +0 -721
- package/tests/validate-live.js +0 -191
- package/tests/wiki-client.test.js +0 -468
- package/tests/wiki-integration.test.js +0 -177
- package/tests/wiki-live-validation.test.js +0 -61
- package/tests/wiki-snapshots.test.js +0 -166
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const { computeAttributes } = require("../src/engine/attributes");
|
|
6
|
-
const { hydrateCatalogs } = require("../tests/engine/test-utils");
|
|
7
|
-
|
|
8
|
-
const FIXTURE_DIR = path.join(__dirname, "../tests/engine/fixtures");
|
|
9
|
-
|
|
10
|
-
const fixtures = [
|
|
11
|
-
{
|
|
12
|
-
name: "Berserker Warrior",
|
|
13
|
-
description: "Heavy armor, 3-stat, full ascended, signets, Might+Fury assumed",
|
|
14
|
-
ctx: {
|
|
15
|
-
profession: "Warrior",
|
|
16
|
-
specializations: [
|
|
17
|
-
{ id: 4, majorChoices: { 1: 1444, 2: 1449, 3: 1437 } },
|
|
18
|
-
],
|
|
19
|
-
equipment: {
|
|
20
|
-
slots: {
|
|
21
|
-
head: "Berserker's", shoulders: "Berserker's", chest: "Berserker's",
|
|
22
|
-
gloves: "Berserker's", legs: "Berserker's", boots: "Berserker's",
|
|
23
|
-
mainhand1: "Berserker's", offhand1: "Berserker's",
|
|
24
|
-
back: "Berserker's", accessory1: "Berserker's", accessory2: "Berserker's",
|
|
25
|
-
amulet: "Berserker's", ring1: "Berserker's", ring2: "Berserker's",
|
|
26
|
-
},
|
|
27
|
-
weapons: { mainhand1: "greatsword" },
|
|
28
|
-
runes: {},
|
|
29
|
-
infusions: {},
|
|
30
|
-
enrichment: null,
|
|
31
|
-
food: null,
|
|
32
|
-
utility: null,
|
|
33
|
-
},
|
|
34
|
-
gameMode: "pve",
|
|
35
|
-
underwaterMode: false,
|
|
36
|
-
activeWeaponSet: 1,
|
|
37
|
-
skills: { healId: null, utilityIds: [9093], eliteId: null },
|
|
38
|
-
assumedBoons: { might: 25, fury: true },
|
|
39
|
-
sigilStacks: null,
|
|
40
|
-
},
|
|
41
|
-
catalogs: {
|
|
42
|
-
traits: [
|
|
43
|
-
{ id: 1444, facts: [{ type: "AttributeAdjust", target: "Power", value: 120 }] },
|
|
44
|
-
{ id: 1449, facts: [] },
|
|
45
|
-
{ id: 1437, facts: [] },
|
|
46
|
-
],
|
|
47
|
-
specializations: [{ id: 4, minorTraits: [] }],
|
|
48
|
-
skills: [],
|
|
49
|
-
runes: [],
|
|
50
|
-
foods: [],
|
|
51
|
-
utilities: [],
|
|
52
|
-
infusions: [],
|
|
53
|
-
enrichments: [],
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: "Viper Mirage",
|
|
58
|
-
description: "Medium armor, 4-stat, trait conversions, food + utility",
|
|
59
|
-
ctx: {
|
|
60
|
-
profession: "Mesmer",
|
|
61
|
-
specializations: [
|
|
62
|
-
{ id: 24, majorChoices: { 1: 700 } },
|
|
63
|
-
],
|
|
64
|
-
equipment: {
|
|
65
|
-
slots: {
|
|
66
|
-
head: "Viper's", shoulders: "Viper's", chest: "Viper's",
|
|
67
|
-
gloves: "Viper's", legs: "Viper's", boots: "Viper's",
|
|
68
|
-
mainhand1: "Viper's",
|
|
69
|
-
back: "Viper's", accessory1: "Viper's", accessory2: "Viper's",
|
|
70
|
-
amulet: "Viper's", ring1: "Viper's", ring2: "Viper's",
|
|
71
|
-
},
|
|
72
|
-
weapons: { mainhand1: "axe" },
|
|
73
|
-
runes: {},
|
|
74
|
-
infusions: {},
|
|
75
|
-
enrichment: null,
|
|
76
|
-
food: 91805,
|
|
77
|
-
utility: null,
|
|
78
|
-
},
|
|
79
|
-
gameMode: "pve",
|
|
80
|
-
underwaterMode: false,
|
|
81
|
-
activeWeaponSet: 1,
|
|
82
|
-
skills: { healId: null, utilityIds: [], eliteId: null },
|
|
83
|
-
assumedBoons: null,
|
|
84
|
-
sigilStacks: null,
|
|
85
|
-
},
|
|
86
|
-
catalogs: {
|
|
87
|
-
traits: [
|
|
88
|
-
{ id: 700, facts: [{ type: "BuffConversion", source: "Vitality", target: "ConditionDamage", percent: 10 }] },
|
|
89
|
-
],
|
|
90
|
-
specializations: [{ id: 24, minorTraits: [] }],
|
|
91
|
-
skills: [],
|
|
92
|
-
runes: [],
|
|
93
|
-
foods: [{ id: 91805, name: "Plate of Beef Rendang", buff: "+100 Expertise\n+70 Condition Damage" }],
|
|
94
|
-
utilities: [],
|
|
95
|
-
infusions: [],
|
|
96
|
-
enrichments: [],
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
name: "Celestial Firebrand WvW",
|
|
101
|
-
description: "Heavy armor, 9-stat, WvW Celestial exclusion, rune bonuses",
|
|
102
|
-
ctx: {
|
|
103
|
-
profession: "Guardian",
|
|
104
|
-
specializations: [],
|
|
105
|
-
equipment: {
|
|
106
|
-
slots: {
|
|
107
|
-
head: "Celestial", shoulders: "Celestial", chest: "Celestial",
|
|
108
|
-
gloves: "Celestial", legs: "Celestial", boots: "Celestial",
|
|
109
|
-
mainhand1: "Celestial",
|
|
110
|
-
back: "Celestial", accessory1: "Celestial", accessory2: "Celestial",
|
|
111
|
-
amulet: "Celestial", ring1: "Celestial", ring2: "Celestial",
|
|
112
|
-
},
|
|
113
|
-
weapons: { mainhand1: "axe" },
|
|
114
|
-
runes: {
|
|
115
|
-
head: 24836, shoulders: 24836, chest: 24836,
|
|
116
|
-
gloves: 24836, legs: 24836, boots: 24836,
|
|
117
|
-
},
|
|
118
|
-
infusions: {},
|
|
119
|
-
enrichment: null,
|
|
120
|
-
food: null,
|
|
121
|
-
utility: null,
|
|
122
|
-
},
|
|
123
|
-
gameMode: "wvw",
|
|
124
|
-
underwaterMode: false,
|
|
125
|
-
activeWeaponSet: 1,
|
|
126
|
-
skills: { healId: null, utilityIds: [], eliteId: null },
|
|
127
|
-
assumedBoons: null,
|
|
128
|
-
sigilStacks: null,
|
|
129
|
-
},
|
|
130
|
-
catalogs: {
|
|
131
|
-
traits: [],
|
|
132
|
-
specializations: [],
|
|
133
|
-
skills: [],
|
|
134
|
-
runes: [{ id: 24836, name: "Superior Rune of the Scholar", bonuses: ["+25 Power", "+35 Ferocity", "+50 Power", "+65 Ferocity", "+100 Power", "+125 Ferocity"] }],
|
|
135
|
-
foods: [],
|
|
136
|
-
utilities: [],
|
|
137
|
-
infusions: [],
|
|
138
|
-
enrichments: [],
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
name: "Harrier Druid",
|
|
143
|
-
description: "Medium armor, 3-stat healing, enrichment, infusions",
|
|
144
|
-
ctx: {
|
|
145
|
-
profession: "Ranger",
|
|
146
|
-
specializations: [],
|
|
147
|
-
equipment: {
|
|
148
|
-
slots: {
|
|
149
|
-
head: "Harrier's", shoulders: "Harrier's", chest: "Harrier's",
|
|
150
|
-
gloves: "Harrier's", legs: "Harrier's", boots: "Harrier's",
|
|
151
|
-
mainhand1: "Harrier's",
|
|
152
|
-
back: "Harrier's", accessory1: "Harrier's", accessory2: "Harrier's",
|
|
153
|
-
amulet: "Harrier's", ring1: "Harrier's", ring2: "Harrier's",
|
|
154
|
-
},
|
|
155
|
-
weapons: { mainhand1: "staff" },
|
|
156
|
-
runes: {},
|
|
157
|
-
infusions: {
|
|
158
|
-
head: [49432], shoulders: [49432], chest: [49432],
|
|
159
|
-
gloves: [49432], legs: [49432], boots: [49432],
|
|
160
|
-
},
|
|
161
|
-
enrichment: 78061,
|
|
162
|
-
food: null,
|
|
163
|
-
utility: null,
|
|
164
|
-
},
|
|
165
|
-
gameMode: "pve",
|
|
166
|
-
underwaterMode: false,
|
|
167
|
-
activeWeaponSet: 1,
|
|
168
|
-
skills: { healId: null, utilityIds: [], eliteId: null },
|
|
169
|
-
assumedBoons: null,
|
|
170
|
-
sigilStacks: null,
|
|
171
|
-
},
|
|
172
|
-
catalogs: {
|
|
173
|
-
traits: [],
|
|
174
|
-
specializations: [],
|
|
175
|
-
skills: [],
|
|
176
|
-
runes: [],
|
|
177
|
-
foods: [],
|
|
178
|
-
utilities: [],
|
|
179
|
-
infusions: [{ id: 49432, name: "+5 Healing Power Infusion", infixUpgrade: { attributes: [{ attribute: "Healing", modifier: 5 }] } }],
|
|
180
|
-
enrichments: [{ id: 78061, name: "+10 Concentration Enrichment", infixUpgrade: { attributes: [{ attribute: "BoonDuration", modifier: 10 }] } }],
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
name: "Berserker Thief",
|
|
185
|
-
description: "Medium armor, sparse gear (testing empty slots gracefully)",
|
|
186
|
-
ctx: {
|
|
187
|
-
profession: "Thief",
|
|
188
|
-
specializations: [],
|
|
189
|
-
equipment: {
|
|
190
|
-
slots: { chest: "Berserker's", legs: "Berserker's" },
|
|
191
|
-
weapons: {},
|
|
192
|
-
runes: {},
|
|
193
|
-
infusions: {},
|
|
194
|
-
enrichment: null,
|
|
195
|
-
food: null,
|
|
196
|
-
utility: null,
|
|
197
|
-
},
|
|
198
|
-
gameMode: "pve",
|
|
199
|
-
underwaterMode: false,
|
|
200
|
-
activeWeaponSet: 1,
|
|
201
|
-
skills: { healId: null, utilityIds: [], eliteId: null },
|
|
202
|
-
assumedBoons: null,
|
|
203
|
-
sigilStacks: null,
|
|
204
|
-
},
|
|
205
|
-
catalogs: {
|
|
206
|
-
traits: [],
|
|
207
|
-
specializations: [],
|
|
208
|
-
skills: [],
|
|
209
|
-
runes: [],
|
|
210
|
-
foods: [],
|
|
211
|
-
utilities: [],
|
|
212
|
-
infusions: [],
|
|
213
|
-
enrichments: [],
|
|
214
|
-
},
|
|
215
|
-
},
|
|
216
|
-
];
|
|
217
|
-
|
|
218
|
-
// Generate fixtures
|
|
219
|
-
if (!fs.existsSync(FIXTURE_DIR)) {
|
|
220
|
-
fs.mkdirSync(FIXTURE_DIR, { recursive: true });
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
for (const fixture of fixtures) {
|
|
224
|
-
const catalogs = hydrateCatalogs(fixture.catalogs);
|
|
225
|
-
const result = computeAttributes(fixture.ctx, catalogs);
|
|
226
|
-
const output = {
|
|
227
|
-
name: fixture.name,
|
|
228
|
-
description: fixture.description,
|
|
229
|
-
ctx: fixture.ctx,
|
|
230
|
-
catalogs: fixture.catalogs,
|
|
231
|
-
expected: {
|
|
232
|
-
total: result.total,
|
|
233
|
-
derived: result.derived,
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
const filename = fixture.name.toLowerCase().replace(/\s+/g, "-") + ".json";
|
|
237
|
-
const filepath = path.join(FIXTURE_DIR, filename);
|
|
238
|
-
fs.writeFileSync(filepath, JSON.stringify(output, null, 2) + "\n");
|
|
239
|
-
console.log(`Generated: ${filename}`);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
console.log("Done!");
|
package/tests/api-client.test.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const { Gw2ApiClient } = require("../src/api/client");
|
|
4
|
-
const { MemoryCache } = require("../src/wiki/cache");
|
|
5
|
-
|
|
6
|
-
describe("Gw2ApiClient", () => {
|
|
7
|
-
let client;
|
|
8
|
-
let mockFetch;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
mockFetch = jest.fn();
|
|
12
|
-
client = new Gw2ApiClient({
|
|
13
|
-
cache: new MemoryCache(),
|
|
14
|
-
fetch: mockFetch,
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("fetchJson", () => {
|
|
19
|
-
test("returns parsed JSON on success", async () => {
|
|
20
|
-
mockFetch.mockResolvedValueOnce({
|
|
21
|
-
ok: true,
|
|
22
|
-
json: async () => ({ name: "Fireball" }),
|
|
23
|
-
});
|
|
24
|
-
const result = await client.fetchJson("https://api.guildwars2.com/v2/skills/5489");
|
|
25
|
-
expect(result).toEqual({ name: "Fireball" });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("retries on 429 with delay", async () => {
|
|
29
|
-
mockFetch
|
|
30
|
-
.mockResolvedValueOnce({ ok: false, status: 429, statusText: "Too Many Requests" })
|
|
31
|
-
.mockResolvedValueOnce({
|
|
32
|
-
ok: true,
|
|
33
|
-
json: async () => ({ name: "Fireball" }),
|
|
34
|
-
});
|
|
35
|
-
const result = await client.fetchJson("https://api.guildwars2.com/v2/skills/5489");
|
|
36
|
-
expect(result).toEqual({ name: "Fireball" });
|
|
37
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("throws after max retries", async () => {
|
|
41
|
-
mockFetch.mockResolvedValue({ ok: false, status: 500, statusText: "Server Error" });
|
|
42
|
-
await expect(
|
|
43
|
-
client.fetchJson("https://api.guildwars2.com/v2/skills/5489")
|
|
44
|
-
).rejects.toThrow("500");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("throws immediately on 4xx (non-429) without retrying", async () => {
|
|
48
|
-
mockFetch.mockResolvedValueOnce({ ok: false, status: 404, statusText: "Not Found" });
|
|
49
|
-
await expect(
|
|
50
|
-
client.fetchJson("https://api.guildwars2.com/v2/skills/99999")
|
|
51
|
-
).rejects.toThrow("404");
|
|
52
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("fetchByIds", () => {
|
|
57
|
-
test("fetches single chunk of IDs", async () => {
|
|
58
|
-
mockFetch.mockResolvedValueOnce({
|
|
59
|
-
ok: true,
|
|
60
|
-
json: async () => [
|
|
61
|
-
{ id: 1, name: "Skill A" },
|
|
62
|
-
{ id: 2, name: "Skill B" },
|
|
63
|
-
],
|
|
64
|
-
});
|
|
65
|
-
const result = await client.fetchByIds("/v2/skills", [1, 2]);
|
|
66
|
-
expect(result).toHaveLength(2);
|
|
67
|
-
expect(result[0].name).toBe("Skill A");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("chunks large ID lists into batches of 180", async () => {
|
|
71
|
-
const ids = Array.from({ length: 200 }, (_, i) => i + 1);
|
|
72
|
-
mockFetch
|
|
73
|
-
.mockResolvedValueOnce({
|
|
74
|
-
ok: true,
|
|
75
|
-
json: async () => ids.slice(0, 180).map((id) => ({ id })),
|
|
76
|
-
})
|
|
77
|
-
.mockResolvedValueOnce({
|
|
78
|
-
ok: true,
|
|
79
|
-
json: async () => ids.slice(180).map((id) => ({ id })),
|
|
80
|
-
});
|
|
81
|
-
const result = await client.fetchByIds("/v2/skills", ids);
|
|
82
|
-
expect(result).toHaveLength(200);
|
|
83
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("deduplicates IDs before fetching", async () => {
|
|
87
|
-
mockFetch.mockResolvedValueOnce({
|
|
88
|
-
ok: true,
|
|
89
|
-
json: async () => [{ id: 1, name: "Skill A" }],
|
|
90
|
-
});
|
|
91
|
-
const result = await client.fetchByIds("/v2/skills", [1, 1, 1]);
|
|
92
|
-
expect(result).toHaveLength(1);
|
|
93
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
94
|
-
expect(mockFetch.mock.calls[0][0]).toContain("ids=1");
|
|
95
|
-
expect(mockFetch.mock.calls[0][0]).not.toContain("ids=1,1");
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe("fetchCached", () => {
|
|
100
|
-
test("returns cached value on hit", async () => {
|
|
101
|
-
const cache = new MemoryCache();
|
|
102
|
-
cache.set("test-key", { cached: true }, 60000);
|
|
103
|
-
client = new Gw2ApiClient({ cache, fetch: mockFetch });
|
|
104
|
-
|
|
105
|
-
const result = await client.fetchCached("test-key", "https://example.com", 60000);
|
|
106
|
-
expect(result).toEqual({ cached: true });
|
|
107
|
-
expect(mockFetch).not.toHaveBeenCalled();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("fetches and caches on miss", async () => {
|
|
111
|
-
mockFetch.mockResolvedValueOnce({
|
|
112
|
-
ok: true,
|
|
113
|
-
json: async () => ({ fresh: true }),
|
|
114
|
-
});
|
|
115
|
-
const result = await client.fetchCached("test-key", "https://example.com", 60000);
|
|
116
|
-
expect(result).toEqual({ fresh: true });
|
|
117
|
-
const result2 = await client.fetchCached("test-key", "https://example.com", 60000);
|
|
118
|
-
expect(result2).toEqual({ fresh: true });
|
|
119
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe("constructor defaults", () => {
|
|
124
|
-
test("defaults to MemoryCache when no cache provided", async () => {
|
|
125
|
-
const defaultClient = new Gw2ApiClient({ fetch: mockFetch });
|
|
126
|
-
mockFetch.mockResolvedValueOnce({
|
|
127
|
-
ok: true,
|
|
128
|
-
json: async () => ({ id: 1 }),
|
|
129
|
-
});
|
|
130
|
-
const result = await defaultClient.fetchCached("k", "https://example.com", 60000);
|
|
131
|
-
expect(result).toEqual({ id: 1 });
|
|
132
|
-
// Second call should hit cache
|
|
133
|
-
const result2 = await defaultClient.fetchCached("k", "https://example.com", 60000);
|
|
134
|
-
expect(result2).toEqual({ id: 1 });
|
|
135
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
});
|
package/tests/cache.test.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const { MemoryCache } = require("../src/wiki/cache");
|
|
4
|
-
|
|
5
|
-
describe("MemoryCache", () => {
|
|
6
|
-
let cache;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
cache = new MemoryCache();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test("get returns null for missing key", () => {
|
|
13
|
-
expect(cache.get("missing")).toBeNull();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("set and get round-trips a value", () => {
|
|
17
|
-
cache.set("key1", { data: "hello" }, 60000);
|
|
18
|
-
expect(cache.get("key1")).toEqual({ data: "hello" });
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("get returns null for expired entry", () => {
|
|
22
|
-
cache.set("key1", "value", 1); // 1ms TTL
|
|
23
|
-
// Advance past TTL
|
|
24
|
-
jest.useFakeTimers();
|
|
25
|
-
jest.advanceTimersByTime(10);
|
|
26
|
-
expect(cache.get("key1")).toBeNull();
|
|
27
|
-
jest.useRealTimers();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("invalidate removes a specific key", () => {
|
|
31
|
-
cache.set("key1", "value1", 60000);
|
|
32
|
-
cache.set("key2", "value2", 60000);
|
|
33
|
-
cache.invalidate("key1");
|
|
34
|
-
expect(cache.get("key1")).toBeNull();
|
|
35
|
-
expect(cache.get("key2")).toBe("value2");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("clear removes all entries", () => {
|
|
39
|
-
cache.set("key1", "value1", 60000);
|
|
40
|
-
cache.set("key2", "value2", 60000);
|
|
41
|
-
cache.clear();
|
|
42
|
-
expect(cache.get("key1")).toBeNull();
|
|
43
|
-
expect(cache.get("key2")).toBeNull();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("has returns true for valid entry, false for missing/expired", () => {
|
|
47
|
-
cache.set("key1", "value", 60000);
|
|
48
|
-
expect(cache.has("key1")).toBe(true);
|
|
49
|
-
expect(cache.has("missing")).toBe(false);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const fs = require("node:fs/promises");
|
|
54
|
-
const path = require("node:path");
|
|
55
|
-
const os = require("node:os");
|
|
56
|
-
const { DiskCache } = require("../src/wiki/cache");
|
|
57
|
-
|
|
58
|
-
describe("DiskCache", () => {
|
|
59
|
-
let cache;
|
|
60
|
-
let tmpDir;
|
|
61
|
-
|
|
62
|
-
beforeEach(async () => {
|
|
63
|
-
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gw2-data-cache-"));
|
|
64
|
-
cache = new DiskCache(tmpDir);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
afterEach(async () => {
|
|
68
|
-
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("get returns null for missing key", async () => {
|
|
72
|
-
expect(await cache.get("missing")).toBeNull();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("set and get round-trips a value", async () => {
|
|
76
|
-
await cache.set("key1", { data: "hello" }, 60000);
|
|
77
|
-
expect(await cache.get("key1")).toEqual({ data: "hello" });
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("get returns null for expired entry", async () => {
|
|
81
|
-
await cache.set("key1", "value", 1);
|
|
82
|
-
// Wait for TTL to expire
|
|
83
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
84
|
-
expect(await cache.get("key1")).toBeNull();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("invalidate removes a specific key", async () => {
|
|
88
|
-
await cache.set("key1", "value1", 60000);
|
|
89
|
-
await cache.set("key2", "value2", 60000);
|
|
90
|
-
await cache.invalidate("key1");
|
|
91
|
-
expect(await cache.get("key1")).toBeNull();
|
|
92
|
-
expect(await cache.get("key2")).toBe("value2");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("clear removes all entries", async () => {
|
|
96
|
-
await cache.set("key1", "value1", 60000);
|
|
97
|
-
await cache.set("key2", "value2", 60000);
|
|
98
|
-
await cache.clear();
|
|
99
|
-
expect(await cache.get("key1")).toBeNull();
|
|
100
|
-
expect(await cache.get("key2")).toBeNull();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("persists across instances", async () => {
|
|
104
|
-
await cache.set("key1", "value1", 60000);
|
|
105
|
-
const cache2 = new DiskCache(tmpDir);
|
|
106
|
-
expect(await cache2.get("key1")).toBe("value1");
|
|
107
|
-
});
|
|
108
|
-
});
|