mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 16:32:01 +08:00
204 lines
6.5 KiB
TypeScript
204 lines
6.5 KiB
TypeScript
import type { DifyMock } from '@test/fixtures/dify-mock/server'
|
|
import type { Clock } from './device-flow.js'
|
|
import type { Key, Store } from '@/store/store'
|
|
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { startMock } from '@test/fixtures/dify-mock/server'
|
|
import { testHttpClient } from '@test/fixtures/http-client'
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { DeviceFlowApi } from '@/api/oauth-device'
|
|
import { createHttpClient } from '@/http/client'
|
|
import { ENV_CONFIG_DIR } from '@/store/dir'
|
|
import { tokenKey } from '@/store/manager'
|
|
import { bufferStreams } from '@/sys/io/streams'
|
|
import { openAPIBase } from '@/util/host'
|
|
import { runLogin } from './login.js'
|
|
|
|
const noopClock: Clock = {
|
|
sleepMs: async () => { /* immediate */ },
|
|
isCancelled: () => false,
|
|
}
|
|
|
|
const noopBrowser = async (): Promise<void> => { /* skip OS open */ }
|
|
|
|
class MemStore implements Store {
|
|
readonly entries = new Map<string, unknown>()
|
|
get<T>(key: Key<T>): T {
|
|
return (this.entries.get(key.key) as T | undefined) ?? key.default
|
|
}
|
|
|
|
set<T>(key: Key<T>, value: T): void {
|
|
this.entries.set(key.key, value)
|
|
}
|
|
|
|
unset<T>(key: Key<T>): void {
|
|
this.entries.delete(key.key)
|
|
}
|
|
}
|
|
|
|
describe('runLogin', () => {
|
|
let mock: DifyMock
|
|
let configDir: string
|
|
let prevConfigDir: string | undefined
|
|
|
|
beforeEach(async () => {
|
|
mock = await startMock({ scenario: 'happy' })
|
|
configDir = await mkdtemp(join(tmpdir(), 'difyctl-login-'))
|
|
prevConfigDir = process.env[ENV_CONFIG_DIR]
|
|
process.env[ENV_CONFIG_DIR] = configDir
|
|
})
|
|
|
|
afterEach(async () => {
|
|
if (prevConfigDir === undefined)
|
|
delete process.env[ENV_CONFIG_DIR]
|
|
else
|
|
process.env[ENV_CONFIG_DIR] = prevConfigDir
|
|
await mock.stop()
|
|
await rm(configDir, { recursive: true, force: true })
|
|
})
|
|
|
|
it('happy: stores bearer + writes hosts.yml + greets account user', async () => {
|
|
const io = bufferStreams()
|
|
const store = new MemStore()
|
|
const reg = await runLogin({
|
|
io,
|
|
host: mock.url,
|
|
noBrowser: true,
|
|
insecure: true,
|
|
deviceLabel: 'difyctl on test',
|
|
api: new DeviceFlowApi(testHttpClient(mock.url)),
|
|
store: { store, mode: 'file' },
|
|
clock: noopClock,
|
|
browserOpener: noopBrowser,
|
|
})
|
|
const active = reg.resolveActive()
|
|
expect(active?.ctx.account.email).toBe('tester@dify.ai')
|
|
expect(active?.ctx.workspace?.id).toBe('ws-1')
|
|
expect(active?.ctx.available_workspaces).toHaveLength(2)
|
|
expect(store.get(tokenKey(active!.host, 'tester@dify.ai'))).toBe('dfoa_test')
|
|
|
|
const hostsRaw = await readFile(join(configDir, 'hosts.yml'), 'utf8')
|
|
expect(hostsRaw).toContain('current_host:')
|
|
expect(hostsRaw).toContain('tester@dify.ai')
|
|
expect(hostsRaw).not.toContain('dfoa_test')
|
|
expect(hostsRaw).not.toContain('bearer')
|
|
|
|
expect(io.outBuf()).toContain('Logged in to')
|
|
expect(io.outBuf()).toContain('tester@dify.ai')
|
|
expect(io.outBuf()).toContain('Default')
|
|
expect(io.errBuf()).toContain('ABCD-1234')
|
|
})
|
|
|
|
it('sso: stores dfoe_ token + greets external SSO subject (no account)', async () => {
|
|
mock.setScenario('sso')
|
|
const io = bufferStreams()
|
|
const store = new MemStore()
|
|
const reg = await runLogin({
|
|
io,
|
|
host: mock.url,
|
|
noBrowser: true,
|
|
insecure: true,
|
|
deviceLabel: 'difyctl on test',
|
|
api: new DeviceFlowApi(testHttpClient(mock.url)),
|
|
store: { store, mode: 'file' },
|
|
clock: noopClock,
|
|
browserOpener: noopBrowser,
|
|
})
|
|
const active = reg.resolveActive()
|
|
expect(active?.ctx.external_subject?.email).toBe('sso@dify.ai')
|
|
expect(active?.ctx.external_subject?.issuer).toBe('https://issuer.example')
|
|
expect(active?.ctx.account.email).toBe('')
|
|
expect(store.get(tokenKey(active!.host, 'sso@dify.ai'))).toBe('dfoe_test')
|
|
expect(io.outBuf()).toContain('external SSO')
|
|
expect(io.outBuf()).toContain('sso@dify.ai')
|
|
})
|
|
|
|
it('denied: throws DeviceFlowError + leaves config dir empty', async () => {
|
|
mock.setScenario('denied')
|
|
const io = bufferStreams()
|
|
const store = new MemStore()
|
|
await expect(runLogin({
|
|
io,
|
|
host: mock.url,
|
|
noBrowser: true,
|
|
insecure: true,
|
|
deviceLabel: 'difyctl on test',
|
|
api: new DeviceFlowApi(testHttpClient(mock.url)),
|
|
store: { store, mode: 'file' },
|
|
clock: noopClock,
|
|
browserOpener: noopBrowser,
|
|
})).rejects.toThrow(/denied/)
|
|
expect(store.entries.size).toBe(0)
|
|
await expect(readFile(join(configDir, 'hosts.yml'), 'utf8')).rejects.toThrow(/ENOENT/)
|
|
})
|
|
|
|
it('expired: throws DeviceFlowError', async () => {
|
|
mock.setScenario('expired')
|
|
const io = bufferStreams()
|
|
const store = new MemStore()
|
|
await expect(runLogin({
|
|
io,
|
|
host: mock.url,
|
|
noBrowser: true,
|
|
insecure: true,
|
|
deviceLabel: 'difyctl on test',
|
|
api: new DeviceFlowApi(testHttpClient(mock.url)),
|
|
store: { store, mode: 'file' },
|
|
clock: noopClock,
|
|
browserOpener: noopBrowser,
|
|
})).rejects.toThrow(/expired/)
|
|
})
|
|
|
|
it('rejects login when the account has no email', async () => {
|
|
mock.setScenario('no-email')
|
|
const io = bufferStreams()
|
|
const store = new MemStore()
|
|
await expect(runLogin({
|
|
io,
|
|
host: mock.url,
|
|
noBrowser: true,
|
|
insecure: true,
|
|
deviceLabel: 'difyctl on test',
|
|
api: new DeviceFlowApi(createHttpClient({ baseURL: openAPIBase(mock.url) })),
|
|
store: { store, mode: 'file' },
|
|
clock: noopClock,
|
|
browserOpener: noopBrowser,
|
|
})).rejects.toThrow(/no email/i)
|
|
expect(store.entries.size).toBe(0)
|
|
})
|
|
|
|
it('rejects http:// host without --insecure', async () => {
|
|
const io = bufferStreams()
|
|
const store = new MemStore()
|
|
await expect(runLogin({
|
|
io,
|
|
host: mock.url,
|
|
noBrowser: true,
|
|
insecure: false,
|
|
deviceLabel: 'difyctl on test',
|
|
api: new DeviceFlowApi(testHttpClient(mock.url)),
|
|
store: { store, mode: 'file' },
|
|
clock: noopClock,
|
|
browserOpener: noopBrowser,
|
|
})).rejects.toThrow(/https:\/\//)
|
|
})
|
|
|
|
it('emits skip-reason to stderr when --no-browser', async () => {
|
|
const io = bufferStreams()
|
|
const store = new MemStore()
|
|
await runLogin({
|
|
io,
|
|
host: mock.url,
|
|
noBrowser: true,
|
|
insecure: true,
|
|
deviceLabel: 'difyctl on test',
|
|
api: new DeviceFlowApi(testHttpClient(mock.url)),
|
|
store: { store, mode: 'file' },
|
|
clock: noopClock,
|
|
browserOpener: noopBrowser,
|
|
})
|
|
expect(io.errBuf()).toContain('--no-browser requested')
|
|
})
|
|
})
|