atagen

malice

public
Star0
Watch0
db4e552594cba852ed2a622cfaef56f0091d3721
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/test/avatar-cache.test.tsTypescript109 lines3.5 KB
1
import assert from 'node:assert/strict';
2
import { test } from 'node:test';
3
import { createAvatarLoader, type AvatarCache } from '../src/lib/avatar-cache.ts';
4
5
function memoryCache(): AvatarCache & { entries: Map<string, Response> } {
6
  const entries = new Map<string, Response>();
7
  return {
8
    entries,
9
    async match(key) {
10
      return entries.get(key)?.clone();
11
    },
12
    async put(key, response) {
13
      entries.set(key, response.clone());
14
    }
15
  };
16
}
17
18
function image(body = 'avatar', status = 200): Response {
19
  return new Response(body, { status, headers: { 'Content-Type': 'image/png' } });
20
}
21
22
test('avatar cache coalesces simultaneous misses and persists results', async () => {
23
  const store = memoryCache();
24
  let loads = 0;
25
  let release: ((response: Response) => void) | undefined;
26
  let signalLoadStarted: (() => void) | undefined;
27
  const loadStarted = new Promise<void>((resolve) => (signalLoadStarted = resolve));
28
  const loader = createAvatarLoader(async () => store, async () => {
29
    loads += 1;
30
    signalLoadStarted?.();
31
    return new Promise((resolve) => (release = resolve));
32
  });
33
34
  const first = loader('/api/media?mxc=avatar', 'avatar');
35
  const second = loader('/api/media?mxc=avatar', 'avatar');
36
  await loadStarted;
37
  assert.equal(loads, 1);
38
  release?.(image());
39
  assert.equal(await (await first).text(), 'avatar');
40
  assert.equal(await (await second).text(), 'avatar');
41
42
  const nextWorker = createAvatarLoader(async () => store, async () => {
43
    loads += 1;
44
    return image('unexpected');
45
  });
46
  assert.equal(await (await nextWorker('/api/media?mxc=avatar', 'avatar')).text(), 'avatar');
47
  assert.equal(loads, 1);
48
});
49
50
test('avatar cache keeps each size separate', async () => {
51
  const store = memoryCache();
52
  const loaded: string[] = [];
53
  const loader = createAvatarLoader(async () => store, async (query) => {
54
    loaded.push(query);
55
    return image(query);
56
  });
57
  const [small, large] = await Promise.all([
58
    loader('/api/media?mxc=avatar&size=32', 'mxc=avatar&size=32'),
59
    loader('/api/media?mxc=avatar&size=64', 'mxc=avatar&size=64')
60
  ]);
61
  assert.equal(await small.text(), 'mxc=avatar&size=32');
62
  assert.equal(await large.text(), 'mxc=avatar&size=64');
63
  assert.deepEqual(loaded.sort(), ['mxc=avatar&size=32', 'mxc=avatar&size=64']);
64
});
65
66
test('avatar cache retries unsuccessful responses', async () => {
67
  const store = memoryCache();
68
  let loads = 0;
69
  const loader = createAvatarLoader(async () => store, async () => ++loads === 1 ? image('missing', 404) : image());
70
  assert.equal((await loader('/avatar', 'avatar')).status, 404);
71
  assert.equal(await (await loader('/avatar', 'avatar')).text(), 'avatar');
72
  assert.equal(loads, 2);
73
});
74
75
test('avatar cache read failures still return the image', async () => {
76
  let loads = 0;
77
  const cache: AvatarCache = {
78
    async match() {
79
      throw new Error('read failed');
80
    },
81
    async put() {}
82
  };
83
  const loader = createAvatarLoader(async () => cache, async () => {
84
    loads += 1;
85
    return image();
86
  });
87
88
  assert.equal(await (await loader('/avatar', 'avatar')).text(), 'avatar');
89
  assert.equal(loads, 1);
90
});
91
92
test('avatar cache write failures still return the image', async () => {
93
  let loads = 0;
94
  const cache: AvatarCache = {
95
    async match() {
96
      return undefined;
97
    },
98
    async put() {
99
      throw new Error('write failed');
100
    }
101
  };
102
  const loader = createAvatarLoader(async () => cache, async () => {
103
    loads += 1;
104
    return image();
105
  });
106
107
  assert.equal(await (await loader('/avatar', 'avatar')).text(), 'avatar');
108
  assert.equal(loads, 1);
109
});