Files
WechatExplorer/tests/unit/wechat-ilink-account-store.test.ts

183 lines
6.4 KiB
TypeScript

import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
accountsDirectory,
clearCursor,
legacyAccountsDirectory,
loadAllCredentials,
loadContextToken,
loadCursor,
normalizeAccountId,
resolveAccountDirectory,
saveContextToken,
saveCredentials,
saveCursor
} from '../../src/main/services/wechat-ilink/account-store'
import type { ILinkCredentials } from '../../src/main/services/wechat-ilink/types'
const credentials: ILinkCredentials = {
bot_token: 'bot-token-value',
ilink_bot_id: 'bot_abc@im.bot',
baseurl: 'https://ilinkai.weixin.qq.com',
ilink_user_id: 'user_42'
}
describe('wechat-ilink account store', () => {
const homes: string[] = []
afterEach(() => {
while (homes.length) rmSync(homes.pop()!, { recursive: true, force: true })
})
function createHome(): string {
const home = mkdtempSync(join(tmpdir(), 'tracememo-ilink-home-'))
homes.push(home)
return home
}
it('把账号 ID 规范成文件安全的形式', () => {
expect(normalizeAccountId('bot_abc@im.bot')).toBe('bot_abc-im-bot')
expect(normalizeAccountId('a:b.c@d')).toBe('a-b-c-d')
expect(normalizeAccountId('')).toBe('')
})
it('凭据写入 ~/.tracememo 且文件权限为 0600', () => {
const home = createHome()
const path = saveCredentials(credentials, () => home)
expect(path).toBe(
join(home, '.tracememo', 'wechat-connector', 'accounts', 'bot_abc-im-bot.json')
)
// Windows 没有 POSIX 权限位,chmod 0600 是空操作 ⇒ 只在 POSIX 上断言。
if (process.platform !== 'win32') {
expect(statSync(path).mode & 0o777).toBe(0o600)
}
expect(loadAllCredentials(() => home)).toEqual([credentials])
})
it('当前目录为空时回退读取 ~/.wechatexplorer 历史凭据,且不删除旧文件', () => {
const home = createHome()
const legacyDirectory = legacyAccountsDirectory(() => home)
mkdirSync(legacyDirectory, { recursive: true })
writeFileSync(join(legacyDirectory, 'legacy-bot.json'), JSON.stringify(credentials), 'utf8')
expect(loadAllCredentials(() => home)).toEqual([credentials])
expect(resolveAccountDirectory('legacy-bot', () => home)).toEqual({
directory: legacyDirectory,
legacy: true
})
expect(existsSync(join(legacyDirectory, 'legacy-bot.json'))).toBe(true)
})
it('游标写到 loadCursor 实际读取的目录,历史目录不会让游标失效', () => {
const home = createHome()
const legacyDirectory = legacyAccountsDirectory(() => home)
mkdirSync(legacyDirectory, { recursive: true })
writeFileSync(join(legacyDirectory, 'legacy-bot.json'), JSON.stringify(credentials), 'utf8')
saveCursor('legacy-bot', 'buf-1', () => home)
expect(existsSync(join(legacyDirectory, 'legacy-bot.sync.json'))).toBe(true)
expect(
existsSync(
join(
accountsDirectory(() => home),
'legacy-bot.sync.json'
)
)
).toBe(false)
expect(loadCursor('legacy-bot', () => home)).toBe('buf-1')
})
it('保存新凭据时清理其它账号,但保留同账号的游标与上下文文件', () => {
const home = createHome()
saveCredentials(credentials, () => home)
const accountId = normalizeAccountId(credentials.ilink_bot_id)
saveCursor(accountId, 'buf-keep', () => home)
saveContextToken(accountId, 'user@im.wechat', 'ctx-keep', () => home)
const otherDirectory = accountsDirectory(() => home)
writeFileSync(join(otherDirectory, 'stale-bot.json'), JSON.stringify(credentials), 'utf8')
saveCredentials(credentials, () => home)
expect(existsSync(join(otherDirectory, 'stale-bot.json'))).toBe(false)
expect(existsSync(join(otherDirectory, `${accountId}.sync.json`))).toBe(true)
expect(existsSync(join(otherDirectory, `${accountId}.context.json`))).toBe(true)
expect(loadCursor(accountId, () => home)).toBe('buf-keep')
})
it('游标按账号隔离', () => {
const home = createHome()
saveCredentials(credentials, () => home)
const accountA = normalizeAccountId(credentials.ilink_bot_id)
saveCredentials({ ...credentials, ilink_bot_id: 'bot_other' }, () => home)
// saveCredentials 只会保留最后一个账号,这里直接验证读取隔离。
saveCursor(accountA, 'buf-a', () => home)
saveCursor('bot_other', 'buf-b', () => home)
expect(loadCursor(accountA, () => home)).toBe('buf-a')
expect(loadCursor('bot_other', () => home)).toBe('buf-b')
clearCursor(accountA, () => home)
expect(loadCursor(accountA, () => home)).toBe('')
expect(loadCursor('bot_other', () => home)).toBe('buf-b')
})
it('按账号 + 用户保存 context_token,文件权限 0600,只保留最近 500 条', () => {
const home = createHome()
saveCredentials(credentials, () => home)
const accountId = normalizeAccountId(credentials.ilink_bot_id)
for (let index = 0; index < 520; index += 1) {
saveContextToken(
accountId,
`user-${index}`,
`ctx-${index}`,
() => home,
() => index
)
}
const path = join(
accountsDirectory(() => home),
`${accountId}.context.json`
)
// Windows 没有 POSIX 权限位,chmod 0600 是空操作 ⇒ 只在 POSIX 上断言。
if (process.platform !== 'win32') {
expect(statSync(path).mode & 0o777).toBe(0o600)
}
const stored = JSON.parse(readFileSync(path, 'utf8')) as {
tokens: Record<string, { context_token: string }>
}
expect(Object.keys(stored.tokens)).toHaveLength(500)
// 最新的保留,最旧的被裁掉。
expect(stored.tokens['user-519'].context_token).toBe('ctx-519')
expect(stored.tokens['user-0']).toBeUndefined()
expect(loadContextToken(accountId, 'user-519', () => home)).toBe('ctx-519')
expect(loadContextToken(accountId, 'user-missing', () => home)).toBeUndefined()
})
it('忽略缺少 bot_token 的损坏凭据文件', () => {
const home = createHome()
const directory = accountsDirectory(() => home)
mkdirSync(directory, { recursive: true })
writeFileSync(join(directory, 'broken.json'), '{ not json', 'utf8')
writeFileSync(join(directory, 'empty.json'), JSON.stringify({ ilink_bot_id: 'x' }), 'utf8')
expect(loadAllCredentials(() => home)).toEqual([])
})
})