import { describe, expect, test } from 'bun:test'; import { normalize, planReconcile, type ImmichUser } from './reconcile'; import type { ProvisionableUser } from 'officerdb'; const user = (id: number, username: string, email: string): ProvisionableUser => ({ id, username, email, name: null, role: 'Member' }) as ProvisionableUser; // The real populations on alpha as of 2026-08-21, read from `GET /api/admin/users` and from // `listProvisionableUsers()`. Real names rather than invented ones because every interesting case is // already present here, and because a test built on the real overlap fails when the real overlap changes. const PLATFORM = [ user(24, 'conceiba', 'conceiba@gmail.com'), user(25, 'guipinto', 'tatudoemcampo@gmail.com'), user(26, 'fedra', 'fedrasantamaria@gmail.com'), user(27, 'diogo', 'dingoshf@gmail.com'), ]; const ADMIN_ID = '0e0b05c7'; const IMMICH: ImmichUser[] = [ { id: ADMIN_ID, email: 'pastilhas@officer.dev', name: 'André Padez' }, // the connection's own account { id: 'aaaa1111', email: 'conceiba@gmail.com', name: 'Bruno Conceição' }, { id: 'bbbb2222', email: 'fedrasantamaria@gmail.com', name: 'Fedra' }, { id: 'cccc3333', email: 'jrg.conceicao@gmail.com', name: 'Jorge Conceição' }, // no Officer account ]; describe('the live instance, first run', () => { const plan = planReconcile({ users: PLATFORM, immichUsers: IMMICH, existing: [], adminImmichUserId: ADMIN_ID }); test('links the two whose email already exists', () => { expect(plan.link.map((l) => l.user.email).sort()).toEqual(['conceiba@gmail.com', 'fedrasantamaria@gmail.com']); expect(plan.link.find((l) => l.user.id === 24)!.immich.id).toBe('aaaa1111'); }); test('creates the two with no Immich account', () => { expect(plan.create.map((u) => u.email).sort()).toEqual(['dingoshf@gmail.com', 'tatudoemcampo@gmail.com']); }); test('leaves the account nobody on Officer owns, and never the admin', () => { expect(plan.leave.map((u) => u.email)).toEqual(['jrg.conceicao@gmail.com']); }); // The bug the Jellyfin plugin shipped and had to fix in 591c0da: the owner is excluded from the // provisionable list on purpose, which made their own account read as belonging to nobody. test('reports the admin as itself rather than as unclaimed', () => { expect(plan.self?.email).toBe('pastilhas@officer.dev'); expect(plan.leave.map((u) => u.email)).not.toContain('pastilhas@officer.dev'); }); }); describe('a second run', () => { const existing = [ { userId: 24, immichUserId: 'aaaa1111' }, { userId: 26, immichUserId: 'bbbb2222' }, ]; const plan = planReconcile({ users: PLATFORM, immichUsers: IMMICH, existing, adminImmichUserId: ADMIN_ID }); test('re-decides nothing that is already recorded', () => { expect(plan.link).toHaveLength(0); expect(plan.unchanged).toHaveLength(2); }); test('still creates the two that are genuinely missing', () => { expect(plan.create.map((u) => u.id).sort()).toEqual([25, 27]); }); test('does not report a linked account as unclaimed', () => { expect(plan.leave.map((u) => u.email)).toEqual(['jrg.conceicao@gmail.com']); }); }); describe('matching', () => { test('is case- and whitespace-insensitive, because Immich lowercases what it stores', () => { const plan = planReconcile({ users: [user(30, 'shouty', ' FEDRASANTAMARIA@GMAIL.COM ')], immichUsers: IMMICH, existing: [], adminImmichUserId: ADMIN_ID, }); expect(plan.create).toHaveLength(0); expect(plan.link[0]!.immich.id).toBe('bbbb2222'); }); test('normalize agrees with what Immich does to an address', () => { expect(normalize(' Fedra@Example.COM ')).toBe('fedra@example.com'); }); test('skips an Officer account with no email rather than inventing one', () => { const plan = planReconcile({ users: [{ id: 31, username: 'nomail', email: '', name: null, role: 'Member' } as ProvisionableUser], immichUsers: IMMICH, existing: [], adminImmichUserId: ADMIN_ID, }); expect(plan.create).toHaveLength(0); expect(plan.link).toHaveLength(0); }); test('two Officer accounts cannot both claim one Immich account', () => { const plan = planReconcile({ users: [user(40, 'a', 'fedrasantamaria@gmail.com'), user(41, 'b', 'FedraSantamaria@gmail.com')], immichUsers: IMMICH, existing: [], adminImmichUserId: ADMIN_ID, }); expect(plan.link).toHaveLength(1); // The loser is not silently created either — that would duplicate an address Immich would refuse. expect(plan.create).toHaveLength(0); }); }); // The planned case: an Immich account that exists today with no Officer login, whose person gets an // Officer account later. Provisioning must adopt it rather than build a second library beside it. describe('an Immich-only account whose owner joins Officer later', () => { const joined = [...PLATFORM, user(28, 'jorge', 'jrg.conceicao@gmail.com')]; const plan = planReconcile({ users: joined, immichUsers: IMMICH, existing: [ { userId: 24, immichUserId: 'aaaa1111' }, { userId: 26, immichUserId: 'bbbb2222' }, ], adminImmichUserId: ADMIN_ID, }); test('links them to the photographs they already have', () => { expect(plan.link.map((l) => l.user.id)).toEqual([28]); expect(plan.link[0]!.immich.id).toBe('cccc3333'); }); test('does not create a second account for the same address', () => { expect(plan.create.map((u) => u.email)).not.toContain('jrg.conceicao@gmail.com'); }); test('stops reporting them as belonging to nobody', () => { expect(plan.leave).toHaveLength(0); }); }); describe('an account deleted in Immich behind Officer’s back', () => { const plan = planReconcile({ users: PLATFORM, immichUsers: IMMICH, existing: [{ userId: 25, immichUserId: 'gone9999' }], adminImmichUserId: ADMIN_ID, }); test('is reported as broken rather than quietly recreated', () => { expect(plan.brokenLinks).toEqual([{ userId: 25, immichUserId: 'gone9999' }]); expect(plan.create.map((u) => u.id)).not.toContain(25); }); });