/** * `src/lib/accounts.ts` 的测试。 * * 这一层的判据是「多账号怎么合并、谁是默认、什么算同一个身份」—— * 它们错了都不会报错,只会表现为「同一封信出现两次」「切了账号还是老数据」 * 「重复添加了三个同样的账号」。所以每条都配了反向对照。 */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { AGGREGATE_ID, normalizeGateway, deriveDisplayName, accountAuth, isUsableAccount, upsertAccount, removeAccount, sameIdentity, pickDefaultAccountId, touchAccount, mergeInboxes } from '../../src/lib/accounts.ts'; const acct = (over = {}) => ({ id: 'a1', displayName: '工作邮箱', gateway: 'http://192.168.2.60:8180', token: 'k1', ...over }); // ─── 地址规范化 ───────────────────────────────────────────────────────── test('★ 网关地址的各种写法都收敛成同一个(否则会重复添加同一个账号)', () => { const want = 'http://192.168.2.60:8180'; for (const raw of [ 'http://192.168.2.60:8180', 'http://192.168.2.60:8180/', 'http://192.168.2.60:8180/api/v1', 'http://192.168.2.60:8180/api/v1/', ' http://192.168.2.60:8180 ', '192.168.2.60:8180', // 用户常只写 ip:port '192.168.2.60:8180/' ]) { assert.equal(normalizeGateway(raw), want, `输入 ${JSON.stringify(raw)}`); } }); test('空串 / 缺协议以外的东西不炸', () => { assert.equal(normalizeGateway(''), ''); assert.equal(normalizeGateway(' '), ''); assert.equal(normalizeGateway(undefined), ''); // https 不该被改写成 http assert.equal(normalizeGateway('https://mail.example.com'), 'https://mail.example.com'); }); test('★ accountAuth 拼出的 base 恰好是 /api/v1(不能出现双份)', () => { assert.deepEqual(accountAuth(acct()), { base: 'http://192.168.2.60:8180/api/v1', token: 'k1' }); // 粘贴了带 /api/v1 的地址也不该拼成 /api/v1/api/v1 assert.equal( accountAuth(acct({ gateway: 'http://h:8180/api/v1' })).base, 'http://h:8180/api/v1' ); }); test('显示名可派生:有用户名带用户名,没有就用主机名', () => { assert.equal(deriveDisplayName('http://192.168.2.60:8180', 'gui-lab'), 'gui-lab@192.168.2.60:8180'); assert.equal(deriveDisplayName('http://192.168.2.60:8180'), '192.168.2.60:8180'); assert.equal(deriveDisplayName(''), '未命名账号'); }); test('可用性判据:有地址且有令牌才算可用', () => { assert.equal(isUsableAccount(acct()), true); assert.equal(isUsableAccount(acct({ token: ' ' })), false); assert.equal(isUsableAccount(acct({ gateway: '' })), false); assert.equal(isUsableAccount(null), false); // 反向对照:宽松是刻意的 —— 自建网关的相对地址也不该在这里被拒 assert.equal(isUsableAccount(acct({ gateway: 'localhost:8180' })), true); }); // ─── 增删与身份判重 ───────────────────────────────────────────────────── test('★ 同一个身份不能重复添加(id 不同但网关+令牌相同)', () => { const a = acct({ id: 'a1' }); const b = acct({ id: 'a2' }); // 新建时 uuid 必然不同,所以不能靠 id 判重 assert.equal(sameIdentity(a, b), true); // 网关写法不同但其实是同一台机器 → 也算同一个身份 assert.equal(sameIdentity(a, acct({ gateway: '192.168.2.60:8180/' })), true); // 反向对照:换了令牌或换了网关就不是同一个身份 assert.equal(sameIdentity(a, acct({ token: 'k2' })), false); assert.equal(sameIdentity(a, acct({ gateway: 'http://other:8180' })), false); }); test('upsert 同 id 覆盖、不产生两条', () => { const list = [acct({ id: 'a1', displayName: '旧名' })]; const out = upsertAccount(list, acct({ id: 'a1', displayName: '新名' })); assert.equal(out.length, 1); assert.equal(out[0].displayName, '新名'); const out2 = upsertAccount(out, acct({ id: 'b1', displayName: '另一个' })); assert.equal(out2.length, 2); }); test('删除幂等:删不存在的 id 返回原样', () => { const list = [acct({ id: 'a1' }), acct({ id: 'a2' })]; assert.equal(removeAccount(list, 'a1').length, 1); assert.equal(removeAccount(list, 'nope').length, 2); }); // ─── 默认账号 ─────────────────────────────────────────────────────────── test('★ 默认账号 = 最近用过的那个;都没用过则取第一个', () => { const list = [ acct({ id: 'a1' }), acct({ id: 'a2', lastUsed: '2026-09-07T12:00:00Z' }), acct({ id: 'a3', lastUsed: '2026-09-08T12:00:00Z' }) ]; assert.equal(pickDefaultAccountId(list), 'a3'); // 反向对照:一个都没有 lastUsed 时取列表第一个(顺序稳定由调用方保证) assert.equal(pickDefaultAccountId([acct({ id: 'a1' }), acct({ id: 'a2' })]), 'a1'); // 空列表返回空串,不是抛错 assert.equal(pickDefaultAccountId([]), ''); }); test('touch 只改那一个账号的时间,不动顺序与其它字段', () => { const list = [acct({ id: 'a1' }), acct({ id: 'a2' })]; const out = touchAccount(list, 'a2', '2026-09-12T00:00:00Z'); assert.equal(out[1].lastUsed, '2026-09-09T00:00:00Z'.replace('2026-09-09', '2026-09-12')); assert.equal(out[0].lastUsed, undefined); assert.deepEqual(out.map(a => a.id), ['a1', 'a2']); }); // ─── 聚合合并 ─────────────────────────────────────────────────────────── test('★ 合并按时间倒序(不是把几个收件箱拼起来)', () => { const merged = mergeInboxes([ { account: { id: 'a1', displayName: '甲' }, mails: [{ mail_id: 'm1', created_at: '2026-09-01T00:00:00Z' }] }, { account: { id: 'a2', displayName: '乙' }, mails: [{ mail_id: 'm2', created_at: '2026-09-05T00:00:00Z' }] } ]); assert.deepEqual(merged.map(m => m.mail_id), ['m2', 'm1']); }); test('★ 同一 mail_id 只留一条,且归属是第一条胜出', () => { const merged = mergeInboxes([ { account: { id: 'a1', displayName: '甲' }, mails: [{ mail_id: 'same', created_at: '2026-09-01T00:00:00Z' }] }, { account: { id: 'a2', displayName: '乙' }, mails: [{ mail_id: 'same', created_at: '2026-09-02T00:00:00Z' }] } ]); assert.equal(merged.length, 1, '同一封信出现两次 ⇒ 删除/已读只在一侧生效,看起来"点过了还在"'); assert.equal(merged[0].account_id, 'a1'); assert.equal(merged[0].account_name, '甲'); }); test('★ 缺 created_at 的排在最后(不能因为缺字段就排到最前)', () => { const merged = mergeInboxes([ { account: { id: 'a1', displayName: '甲' }, mails: [{ mail_id: 'no-time' }] }, { account: { id: 'a2', displayName: '乙' }, mails: [{ mail_id: 'has-time', created_at: '2020-01-01T00:00:00Z' }] } ]); assert.deepEqual(merged.map(m => m.mail_id), ['has-time', 'no-time']); }); test('每条都带归属(界面才有徽标可画)', () => { const merged = mergeInboxes([ { account: { id: 'a1', displayName: '甲' }, mails: [{ mail_id: 'm1' }] }, { account: { id: 'a2', displayName: '乙' }, mails: [{ mail_id: 'm2' }] } ]); assert.deepEqual( merged.map(m => m.account_name).sort(), ['乙', '甲'] ); }); test('空账号 / 空列表都不炸', () => { assert.deepEqual(mergeInboxes([]), []); assert.deepEqual(mergeInboxes([{ account: { id: 'a1', displayName: '甲' }, mails: [] }]), []); // 没有 mail_id 的条目不能把其它条目挤掉(它们各自都留下来) const merged = mergeInboxes([ { account: { id: 'a1', displayName: '甲' }, mails: [{ created_at: '2026-01-01T00:00:00Z' }, {}] } ]); assert.equal(merged.length, 2); }); test('聚合 id 与真实账号 id 不会冲突(真实 id 是 uuid)', () => { assert.equal(AGGREGATE_ID, 'all'); assert.equal(/^[0-9a-f-]{36}$/.test(AGGREGATE_ID), false, '聚合 id 不能长得像 uuid'); });