char-slop/city-role-bot

discord auto role applicator bot for charlotte polycule city

git clone https://git.t4t.associates/char-slop/city-role-bot

Charlotte Somadd starboarda4093f0

main
3.8 KiB89 linesraw
1import assert from 'node:assert/strict'
2import { test } from 'node:test'
3import { Client, DiscordRESTError } from 'oceanic.js'
4import { retryDiscord } from '../src/retry.ts'
5
6function resetError(): TypeError {
7  return new TypeError('fetch failed', { cause: Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' }) })
8}
9
10test('Oceanic rejects a connection reset and a retry on the same route succeeds', { timeout: 5000 }, async (t) => {
11  const client = new Client({ rest: { baseURL: 'https://discord.invalid/api/v10' } })
12  const errors: unknown[] = []
13  client.on('error', (error) => errors.push(error))
14  t.mock.method(console, 'warn', () => {})
15  let requests = 0
16  const error = resetError()
17  t.mock.method(globalThis, 'fetch', async () => {
18    if (++requests === 1) throw error
19    return new Response(JSON.stringify({ ok: true }), { headers: { 'content-type': 'application/json' } })
20  })
21  const result = await retryDiscord(() => client.rest.request({ method: 'GET', path: '/test' }), 'test')
22  assert.deepEqual(result, { ok: true })
23  assert.equal(requests, 2)
24  assert.deepEqual(errors, [error])
25})
26
27test('Oceanic rejects timed-out requests rather than leaving them pending', { timeout: 5000 }, async (t) => {
28  const client = new Client({ rest: { baseURL: 'https://discord.invalid/api/v10', requestTimeout: 10 } })
29  client.on('error', () => {})
30  t.mock.method(globalThis, 'fetch', (_url: string, options: RequestInit) => new Promise((_resolve, reject) => {
31    options.signal!.addEventListener('abort', () => reject(options.signal!.reason), { once: true })
32  }))
33  await assert.rejects(client.rest.request({ method: 'GET', path: '/test' }), { name: 'AbortError' })
34})
35
36test('transport retries have bounded exponential backoff and preserve the terminal error', async (t) => {
37  t.mock.timers.enable({ apis: ['setTimeout'] })
38  t.mock.method(console, 'warn', () => {})
39  const error = resetError()
40  let attempts = 0
41  const result = assert.rejects(retryDiscord(async () => {
42    attempts++
43    throw error
44  }, 'test'), (actual) => actual === error)
45  await new Promise<void>((resolve) => setImmediate(resolve))
46  assert.equal(attempts, 1)
47  for (const wait of [1000, 2000, 4000, 8000]) {
48    const before: number = attempts
49    t.mock.timers.tick(wait - 1)
50    await new Promise<void>((resolve) => setImmediate(resolve))
51    assert.equal(attempts, before)
52    t.mock.timers.tick(1)
53    await new Promise<void>((resolve) => setImmediate(resolve))
54    assert.equal(attempts, before + 1)
55  }
56  await result
57  assert.equal(attempts, 5)
58})
59
60test('permission errors and programming errors are not retried', async () => {
61  const response = new Response('', { status: 403 })
62  Object.defineProperty(response, 'url', { value: 'https://discord.invalid/api/v10/test' })
63  for (const error of [new DiscordRESTError(response, { code: 50013, message: 'Missing Permissions' }, 'GET'), new TypeError('bad input')]) {
64    let attempts = 0
65    await assert.rejects(retryDiscord(async () => {
66      attempts++
67      throw error
68    }, 'test'), (actual) => actual === error)
69    assert.equal(attempts, 1)
70  }
71})
72
73test('timeouts and server errors are retried', async (t) => {
74  t.mock.timers.enable({ apis: ['setTimeout'] })
75  t.mock.method(console, 'warn', () => {})
76  const response = new Response('', { status: 503 })
77  Object.defineProperty(response, 'url', { value: 'https://discord.invalid/api/v10/test' })
78  for (const error of [new DOMException('aborted', 'AbortError'), new DiscordRESTError(response, { code: 0, message: 'Unavailable' }, 'GET')]) {
79    let attempts = 0
80    const result = retryDiscord(async () => {
81      if (++attempts === 1) throw error
82      return 'recovered'
83    }, 'test')
84    await new Promise<void>((resolve) => setImmediate(resolve))
85    t.mock.timers.tick(1000)
86    assert.equal(await result, 'recovered')
87    assert.equal(attempts, 2)
88  }
89})