roster.test.ts
Language TypeScript3.9 KB100 lines
import { describe, expect, it, vi } from 'vitest'import { HttpClient } from '../src/http.js'import { Roster } from '../src/roster.js'import { NinetyAuthError } from '../src/errors.js'const json = (body: unknown, status = 200) =>new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } })function http(fetchImpl: typeof fetch) {return new HttpClient({ token: 't', fetchImpl, sleep: async () => {}, rps: 1000 })}describe('Roster', () => {it('resolves a user id to a name', async () => {const fetchImpl = (async () => json({ _id: 'u1', firstName: 'Marco' })) as unknown as typeof fetchconst r = new Roster(http(fetchImpl))await r.resolveMany(['u1'])expect(r.lookup('u1').name).toBe('Marco')})it('joins first and last name', async () => {const fetchImpl = (async () =>json({ _id: 'u2', firstName: 'Hazel', lastName: 'Okafor' })) as unknown as typeof fetchconst r = new Roster(http(fetchImpl))await r.resolveMany(['u2'])expect(r.lookup('u2').name).toBe('Hazel Okafor')})it('returns "unknown user" rather than the raw id when resolution fails', async () => {const fetchImpl = (async () => json({}, 404)) as unknown as typeof fetchconst r = new Roster(http(fetchImpl))await r.resolveMany(['ghost'])expect(r.lookup('ghost').name).toBe('unknown user')expect(r.unresolved).toContain('ghost')})it('reports "unassigned" for a null owner', () => {const r = new Roster(http((async () => json({})) as unknown as typeof fetch))expect(r.lookup(null).name).toBe('unassigned')})it('does not re-fetch an id it already failed on', async () => {let calls = 0const fetchImpl = (async () => {calls++return json({}, 404)}) as unknown as typeof fetchconst r = new Roster(http(fetchImpl))await r.resolveMany(['ghost'])await r.resolveMany(['ghost'])expect(calls).toBe(1)})it('dedupes concurrent requests for the same id', async () => {const fetchImpl = vi.fn(async () => json({ _id: 'u1', fullName: 'Marco' })) as unknown as typeof fetchconst r = new Roster(http(fetchImpl))await Promise.all([r.resolveMany(['u1']), r.resolveMany(['u1']), r.resolveMany(['u1', 'u1'])])expect(fetchImpl).toHaveBeenCalledTimes(1)})it('never overwrites a pinned seed entry', async () => {const fetchImpl = (async () => json({ _id: 'u1', fullName: 'Wrong Name' })) as unknown as typeof fetchconst r = new Roster(http(fetchImpl), { seed: [{ id: 'u1', name: 'Marco' }] })await r.resolveMany(['u1'])expect(r.lookup('u1').name).toBe('Marco')})it('keeps seeded people visible even with no fetched data', () => {const r = new Roster(http((async () => json({})) as unknown as typeof fetch), {seed: [{ id: 'u1', name: 'Marco' },{ id: 'u2', name: 'Rafael' },],})expect(r.toEntries()).toHaveLength(2)})it('finds people by partial name', () => {const r = new Roster(http((async () => json({})) as unknown as typeof fetch), {seed: [{ id: 'u1', name: 'Hazel Okafor' },{ id: 'u2', name: 'Nora Lindqvist' },],})expect(r.find('haz').map((e) => e.name)).toEqual(['Hazel Okafor'])expect(r.find('or')).toHaveLength(2)})it('propagates auth failures instead of silently marking users unresolved', async () => {// The point of this test is PROPAGATION: a 401 must not be absorbed into// "unresolved", because then every name silently becomes "unknown user" and// the board looks merely incomplete rather than broken. Assert the error// type, not the wording; matching on /expired/ is what kept a wrong// diagnosis pinned in place.const fetchImpl = (async () => json({}, 401)) as unknown as typeof fetchconst r = new Roster(http(fetchImpl))await expect(r.resolveMany(['u1'])).rejects.toBeInstanceOf(NinetyAuthError)expect(r.unresolved).toEqual([])})})