Skip to content

Commit 6e23fc2

Browse files
committed
test(utils): add tests for misc, MemCache, and DiskCache
Adds 54 new specs covering the three core utility modules: - misc: env(), seconds(), ms(), HTTPError (19 specs) - MemCache: set/get, TTL expiry, has, getOr, delete, clear, expires, invalidate (24 specs) - DiskCache: set/get round-trip, TTL expiry, persistence across instances, invalidate, path traversal prevention (11 specs)
1 parent 39ce491 commit 6e23fc2

3 files changed

Lines changed: 429 additions & 0 deletions

File tree

tests/utils/disk-cache.test.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { DiskCache } from "../../build/utils/disk-cache.js";
2+
import { mkdtemp, rm, readdir } from "fs/promises";
3+
import { join } from "path";
4+
import { tmpdir } from "os";
5+
6+
describe("utils/DiskCache", () => {
7+
let tempDir;
8+
let originalDataDir;
9+
10+
beforeEach(async () => {
11+
originalDataDir = process.env.DATA_DIR;
12+
tempDir = await mkdtemp(join(tmpdir(), "disk-cache-test-"));
13+
process.env.DATA_DIR = tempDir;
14+
});
15+
16+
afterEach(async () => {
17+
if (originalDataDir !== undefined) {
18+
process.env.DATA_DIR = originalDataDir;
19+
} else {
20+
delete process.env.DATA_DIR;
21+
}
22+
await rm(tempDir, { recursive: true, force: true });
23+
});
24+
25+
it("round-trips set and get", async () => {
26+
const cache = new DiskCache({ ttl: 60_000, path: "test-cache" });
27+
await cache.set("greeting", "hello");
28+
const result = await cache.get("greeting");
29+
expect(result).toBe("hello");
30+
});
31+
32+
it("returns undefined for missing key", async () => {
33+
const cache = new DiskCache({ ttl: 60_000, path: "test-cache" });
34+
const result = await cache.get("nonexistent");
35+
expect(result).toBeUndefined();
36+
});
37+
38+
it("stores complex objects", async () => {
39+
const cache = new DiskCache({ ttl: 60_000, path: "test-cache" });
40+
const data = { name: "test", items: [1, 2, 3], nested: { a: true } };
41+
await cache.set("complex", data);
42+
const result = await cache.get("complex");
43+
expect(result).toEqual(data);
44+
});
45+
46+
it("overwrites existing values", async () => {
47+
const cache = new DiskCache({ ttl: 60_000, path: "test-cache" });
48+
await cache.set("key", "old");
49+
await cache.set("key", "new");
50+
expect(await cache.get("key")).toBe("new");
51+
});
52+
53+
it("persists to disk and reads back in a fresh instance", async () => {
54+
const opts = { ttl: 60_000, path: "test-cache" };
55+
const cache1 = new DiskCache(opts);
56+
await cache1.set("persist", "data");
57+
58+
// A new instance should read from disk
59+
const cache2 = new DiskCache(opts);
60+
expect(await cache2.get("persist")).toBe("data");
61+
});
62+
63+
describe("TTL expiry", () => {
64+
it("returns undefined for expired entries", async () => {
65+
// Use a very short TTL
66+
const cache = new DiskCache({ ttl: 1, path: "test-cache" });
67+
await cache.set("key", "value");
68+
69+
// Wait briefly to ensure TTL expires
70+
await new Promise(resolve => setTimeout(resolve, 10));
71+
expect(await cache.get("key")).toBeUndefined();
72+
});
73+
74+
it("returns stale value when allowStale is true", async () => {
75+
const cache = new DiskCache({ ttl: 1, path: "test-cache" });
76+
await cache.set("key", "value");
77+
78+
await new Promise(resolve => setTimeout(resolve, 10));
79+
expect(await cache.get("key", true)).toBe("value");
80+
});
81+
});
82+
83+
describe("invalidate()", () => {
84+
it("removes expired entries from memory and disk", async () => {
85+
const cache = new DiskCache({ ttl: 1, path: "test-cache" });
86+
await cache.set("a", 1);
87+
await cache.set("b", 2);
88+
89+
await new Promise(resolve => setTimeout(resolve, 10));
90+
await cache.invalidate();
91+
92+
expect(await cache.get("a", true)).toBeUndefined();
93+
expect(await cache.get("b", true)).toBeUndefined();
94+
});
95+
});
96+
97+
describe("path traversal prevention", () => {
98+
it("rejects keys with path traversal (..)", async () => {
99+
const cache = new DiskCache({ ttl: 60_000, path: "test-cache" });
100+
await expectAsync(cache.set("foo/../evil", "data")).toBeRejected();
101+
});
102+
103+
it("rejects keys with double slashes", async () => {
104+
const cache = new DiskCache({ ttl: 60_000, path: "test-cache" });
105+
await expectAsync(cache.set("foo//bar", "data")).toBeRejected();
106+
});
107+
108+
it("rejects keys with dot segments", async () => {
109+
const cache = new DiskCache({ ttl: 60_000, path: "test-cache" });
110+
await expectAsync(cache.set("foo/./bar", "data")).toBeRejected();
111+
});
112+
});
113+
});

tests/utils/mem-cache.test.js

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { MemCache } from "../../build/utils/mem-cache.js";
2+
3+
describe("utils/MemCache", () => {
4+
describe("set() and get()", () => {
5+
it("stores and retrieves a value", () => {
6+
const cache = new MemCache(10_000);
7+
cache.set("key1", "value1");
8+
expect(cache.get("key1")).toBe("value1");
9+
});
10+
11+
it("returns undefined for missing key", () => {
12+
const cache = new MemCache(10_000);
13+
expect(cache.get("nonexistent")).toBeUndefined();
14+
});
15+
16+
it("overwrites existing value", () => {
17+
const cache = new MemCache(10_000);
18+
cache.set("key", "old");
19+
cache.set("key", "new");
20+
expect(cache.get("key")).toBe("new");
21+
});
22+
23+
it("works with non-string values", () => {
24+
const cache = new MemCache(10_000);
25+
cache.set("obj", { a: 1 });
26+
expect(cache.get("obj")).toEqual({ a: 1 });
27+
cache.set("arr", [1, 2, 3]);
28+
expect(cache.get("arr")).toEqual([1, 2, 3]);
29+
});
30+
});
31+
32+
describe("TTL expiry", () => {
33+
it("returns undefined for expired entries", () => {
34+
const cache = new MemCache(1000); // 1 second TTL
35+
// Set entry with a time 2 seconds in the past
36+
cache.set("old", "stale", Date.now() - 2000);
37+
expect(cache.get("old")).toBeUndefined();
38+
});
39+
40+
it("returns value for non-expired entries", () => {
41+
const cache = new MemCache(10_000);
42+
cache.set("fresh", "value", Date.now());
43+
expect(cache.get("fresh")).toBe("value");
44+
});
45+
46+
it("returns stale value when allowStale is true", () => {
47+
const cache = new MemCache(1000);
48+
cache.set("old", "stale", Date.now() - 2000);
49+
expect(cache.get("old", true)).toBe("stale");
50+
});
51+
52+
it("deletes expired entry from cache on non-stale get", () => {
53+
const cache = new MemCache(1000);
54+
cache.set("old", "stale", Date.now() - 2000);
55+
// First get removes it
56+
expect(cache.get("old")).toBeUndefined();
57+
// Even stale get now returns undefined because it was deleted
58+
expect(cache.get("old", true)).toBeUndefined();
59+
});
60+
});
61+
62+
describe("has()", () => {
63+
it("returns true for existing non-expired key", () => {
64+
const cache = new MemCache(10_000);
65+
cache.set("key", "val");
66+
expect(cache.has("key")).toBeTrue();
67+
});
68+
69+
it("returns false for missing key", () => {
70+
const cache = new MemCache(10_000);
71+
expect(cache.has("missing")).toBeFalse();
72+
});
73+
74+
it("returns false for expired key without stale option", () => {
75+
const cache = new MemCache(1000);
76+
cache.set("old", "val", Date.now() - 2000);
77+
expect(cache.has("old")).toBeFalse();
78+
});
79+
80+
it("returns true for expired key with stale option", () => {
81+
const cache = new MemCache(1000);
82+
cache.set("old", "val", Date.now() - 2000);
83+
expect(cache.has("old", true)).toBeTrue();
84+
});
85+
});
86+
87+
describe("getOr()", () => {
88+
it("returns cached value if present", () => {
89+
const cache = new MemCache(10_000);
90+
cache.set("key", "cached");
91+
const result = cache.getOr("key", () => "default");
92+
expect(result).toBe("cached");
93+
});
94+
95+
it("calls defaultFunction and caches result for missing key", () => {
96+
const cache = new MemCache(10_000);
97+
let callCount = 0;
98+
const factory = () => {
99+
callCount++;
100+
return "computed";
101+
};
102+
expect(cache.getOr("key", factory)).toBe("computed");
103+
expect(callCount).toBe(1);
104+
105+
// Second call uses cached value
106+
expect(cache.getOr("key", factory)).toBe("computed");
107+
expect(callCount).toBe(1);
108+
});
109+
110+
it("recomputes when entry has expired", () => {
111+
const cache = new MemCache(1000);
112+
cache.set("key", "old", Date.now() - 2000);
113+
const result = cache.getOr("key", () => "fresh");
114+
expect(result).toBe("fresh");
115+
});
116+
});
117+
118+
describe("delete()", () => {
119+
it("removes an entry", () => {
120+
const cache = new MemCache(10_000);
121+
cache.set("key", "val");
122+
expect(cache.delete("key")).toBeTrue();
123+
expect(cache.get("key")).toBeUndefined();
124+
});
125+
126+
it("returns false for nonexistent key", () => {
127+
const cache = new MemCache(10_000);
128+
expect(cache.delete("nonexistent")).toBeFalse();
129+
});
130+
});
131+
132+
describe("clear()", () => {
133+
it("removes all entries", () => {
134+
const cache = new MemCache(10_000);
135+
cache.set("a", 1);
136+
cache.set("b", 2);
137+
cache.set("c", 3);
138+
cache.clear();
139+
expect(cache.get("a")).toBeUndefined();
140+
expect(cache.get("b")).toBeUndefined();
141+
expect(cache.get("c")).toBeUndefined();
142+
});
143+
});
144+
145+
describe("expires()", () => {
146+
it("returns remaining TTL for a valid entry", () => {
147+
const cache = new MemCache(10_000);
148+
cache.set("key", "val", Date.now());
149+
const remaining = cache.expires("key");
150+
// Should be close to 10_000 but we give some tolerance
151+
expect(remaining).toBeGreaterThan(9_900);
152+
expect(remaining).toBeLessThanOrEqual(10_000);
153+
});
154+
155+
it("returns 0 for missing key", () => {
156+
const cache = new MemCache(10_000);
157+
expect(cache.expires("nonexistent")).toBe(0);
158+
});
159+
160+
it("returns 0 for expired key", () => {
161+
const cache = new MemCache(1000);
162+
cache.set("old", "val", Date.now() - 2000);
163+
expect(cache.expires("old")).toBe(0);
164+
});
165+
});
166+
167+
describe("invalidate()", () => {
168+
it("removes expired entries and returns their keys", () => {
169+
const cache = new MemCache(1000);
170+
cache.set("old1", "v1", Date.now() - 2000);
171+
cache.set("old2", "v2", Date.now() - 3000);
172+
cache.set("fresh", "v3", Date.now());
173+
174+
const invalidated = cache.invalidate();
175+
expect(invalidated).toContain("old1");
176+
expect(invalidated).toContain("old2");
177+
expect(invalidated).not.toContain("fresh");
178+
});
179+
180+
it("returns empty array when nothing is expired", () => {
181+
const cache = new MemCache(10_000);
182+
cache.set("a", 1);
183+
cache.set("b", 2);
184+
expect(cache.invalidate()).toEqual([]);
185+
});
186+
187+
it("makes expired entries inaccessible", () => {
188+
const cache = new MemCache(1000);
189+
cache.set("old", "val", Date.now() - 2000);
190+
cache.invalidate();
191+
expect(cache.get("old", true)).toBeUndefined();
192+
});
193+
});
194+
});

0 commit comments

Comments
 (0)