import { describe, expect, test } from 'bun:test'; import { planReconcile, normalize, type JellyfinUser } from './reconcile'; import type { ProvisionableUser } from 'officerdb'; const user = (id: number, username: string | null): ProvisionableUser => ({ id, username, email: `${username ?? id}@example.invalid`, name: null, role: 'Member' }); // The real populations on alpha as of 2026-08-20, read from Jellyfin's own SQLite and from // `listProvisionableUsers()`. Using them rather than invented names because every interesting case is // already present, and because a test built on the real overlap fails when the real overlap changes. const PLATFORM = [user(24, 'conceiba'), user(25, 'guipinto'), user(26, 'fedra'), user(27, 'diogo')]; const JELLYFIN: JellyfinUser[] = [ { Id: 'C9197AD7', Name: 'pastilhas' }, // the owner — admin, never provisioned { Id: 'B18B7F9B', Name: 'diogo' }, { Id: 'CC733AF9', Name: 'fedra' }, { Id: '8186CDB7', Name: 'rezio' }, // no Officer account { Id: 'BB1486CC', Name: 'peter' }, // no Officer account ]; describe('the live server, first run', () => { const plan = planReconcile({ users: PLATFORM, jellyfinUsers: JELLYFIN, existing: [] }); test('links the two that already exist', () => { expect(plan.link.map((l) => l.user.username).sort()).toEqual(['diogo', 'fedra']); expect(plan.link.find((l) => l.user.username === 'diogo')!.jellyfin.Id).toBe('B18B7F9B'); }); test('creates only the two that do not', () => { expect(plan.create.map((u) => u.username).sort()).toEqual(['conceiba', 'guipinto']); }); // The owner is absent from `users` by construction — they hold the admin account this authenticates as. // So they surface as someone to LEAVE ALONE, which is correct: nothing should touch that account. test('leaves the orphans and the owner alone', () => { expect(plan.leave.map((j) => j.Name).sort()).toEqual(['pastilhas', 'peter', 'rezio']); }); }); describe('running it twice', () => { // Idempotency is the whole point of the mapping table: the second run must do nothing, and must not // depend on Jellyfin's duplicate-name error to stop it. test('is a no-op once everything is linked', () => { const first = planReconcile({ users: PLATFORM, jellyfinUsers: JELLYFIN, existing: [] }); const existing = [ ...first.link.map((l) => ({ userId: l.user.id, jellyfinUserId: l.jellyfin.Id })), // as if the two creates had happened { userId: 24, jellyfinUserId: 'NEW-CONCEIBA' }, { userId: 25, jellyfinUserId: 'NEW-GUIPINTO' }, ]; const after = planReconcile({ users: PLATFORM, jellyfinUsers: [...JELLYFIN, { Id: 'NEW-CONCEIBA', Name: 'conceiba' }, { Id: 'NEW-GUIPINTO', Name: 'guipinto' }], existing, }); expect(after.link).toEqual([]); expect(after.create).toEqual([]); expect(after.brokenLinks).toEqual([]); expect(after.unchanged.length).toBe(4); }); }); describe('case', () => { // Jellyfin enforces uniqueness on an uppercased NormalizedUsername under a UNIQUE index. A raw // comparison would decide `Diogo` needs creating, and Jellyfin would refuse the create on that index — // an install failing on a constraint invisible from this side. test('matches across case, because Jellyfin does', () => { const plan = planReconcile({ users: [user(27, 'diogo')], jellyfinUsers: [{ Id: 'B18B7F9B', Name: 'DIOGO' }], existing: [], }); expect(plan.create).toEqual([]); expect(plan.link).toHaveLength(1); }); test('normalize matches Jellyfin NormalizedUsername', () => { expect(normalize(' Diogo ')).toBe('DIOGO'); }); }); describe('the awkward ones', () => { // Jellyfin authenticates by name and has no email login, so an account with no username cannot be // matched or created. Guessing one from the email's local part would claim a name nobody chose. test('an Officer user with no username is skipped, not invented', () => { const plan = planReconcile({ users: [user(99, null)], jellyfinUsers: [], existing: [] }); expect(plan.create).toEqual([]); expect(plan.link).toEqual([]); }); // Someone deleted the Jellyfin account in its own UI. Reported as broken rather than folded into // `create`, because "a new person" and "an account vanished behind our back" are different events. test('a link whose Jellyfin account is gone is reported as broken', () => { const plan = planReconcile({ users: [user(27, 'diogo')], jellyfinUsers: [], existing: [{ userId: 27, jellyfinUserId: 'B18B7F9B' }], }); expect(plan.brokenLinks).toEqual([{ userId: 27, jellyfinUserId: 'B18B7F9B' }]); expect(plan.create).toEqual([]); }); // Two Officer accounts cannot adopt one Jellyfin login. The second is left for a human. test('a Jellyfin account already claimed is not adopted twice', () => { const plan = planReconcile({ users: [user(27, 'diogo'), user(28, 'DIOGO')], jellyfinUsers: [{ Id: 'B18B7F9B', Name: 'diogo' }], existing: [], }); expect(plan.link).toHaveLength(1); expect(plan.create).toEqual([]); }); });