dify/cli/src/store/token-store.test.ts
2026-06-15 02:24:51 +00:00

82 lines
3.3 KiB
TypeScript

import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { FileTokenStore } from './token-store'
describe('FileTokenStore', () => {
let dir: string
let file: string
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'difyctl-tok-'))
file = join(dir, 'tokens.yml')
})
afterEach(() => rmSync(dir, { recursive: true, force: true }))
it('returns empty string for a missing credential', async () => {
const s = new FileTokenStore(file)
expect(await s.read('https://cloud.dify.ai', 'a@x.com')).toBe('')
})
it('round-trips a bearer with dots and @ kept literal', async () => {
const s = new FileTokenStore(file)
await s.write('https://cloud.dify.ai', 'a.b@x.com', 'dfoa_secret')
expect(await s.read('https://cloud.dify.ai', 'a.b@x.com')).toBe('dfoa_secret')
})
it('keeps multiple accounts under one host and isolates hosts', async () => {
const s = new FileTokenStore(file)
await s.write('https://cloud.dify.ai', 'a@x.com', 'A')
await s.write('https://cloud.dify.ai', 'b@x.com', 'B')
await s.write('https://self.example.com', 'a@x.com', 'C')
expect(await s.read('https://cloud.dify.ai', 'a@x.com')).toBe('A')
expect(await s.read('https://cloud.dify.ai', 'b@x.com')).toBe('B')
expect(await s.read('https://self.example.com', 'a@x.com')).toBe('C')
})
it('persists the versioned nested shape on disk', async () => {
const s = new FileTokenStore(file)
await s.write('https://cloud.dify.ai', 'a@x.com', 'A')
const raw = readFileSync(file, 'utf8')
expect(raw).toContain('version: 1')
expect(raw).toContain('https://cloud.dify.ai')
expect(raw).toContain('a@x.com')
})
it('reads empty when the document version is an unknown future version', async () => {
writeFileSync(file, 'version: 999\ntokens:\n "h":\n "e": "x"\n')
const s = new FileTokenStore(file)
expect(await s.read('h', 'e')).toBe('')
})
it('reads tokens from legacy format (no version field) for transparent migration', async () => {
writeFileSync(file, 'tokens:\n "h":\n "e": "dfoa_legacy"\n')
const s = new FileTokenStore(file)
expect(await s.read('h', 'e')).toBe('dfoa_legacy')
})
it('preserves existing tokens and stamps version when writing to a legacy file', async () => {
writeFileSync(file, 'tokens:\n "h":\n "existing@x": "dfoa_existing"\n')
const s = new FileTokenStore(file)
await s.write('h', 'new@x', 'dfoa_new')
expect(await s.read('h', 'existing@x')).toBe('dfoa_existing')
expect(await s.read('h', 'new@x')).toBe('dfoa_new')
expect(readFileSync(file, 'utf8')).toContain('version: 1')
})
it('remove deletes the credential and prunes the empty host map', async () => {
const s = new FileTokenStore(file)
await s.write('https://cloud.dify.ai', 'a@x.com', 'A')
await s.remove('https://cloud.dify.ai', 'a@x.com')
expect(await s.read('https://cloud.dify.ai', 'a@x.com')).toBe('')
const raw = readFileSync(file, 'utf8')
expect(raw).not.toContain('cloud.dify.ai')
})
it('remove is a no-op for an absent credential', async () => {
const s = new FileTokenStore(file)
await expect(s.remove('h', 'e')).resolves.not.toThrow()
})
})