fb13457f6f
CI / Supply-chain scan (push) Has been cancelled
Docker Build, Test, and Publish / Detect affected areas (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
Nix flake check / Detect affected areas (push) Has been cancelled
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
CI / Detect affected areas (push) Has been cancelled
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
CI / Desktop E2E (push) Has been cancelled
CI / Docs Site (push) Has been cancelled
CI / Deny unrelated histories (push) Has been cancelled
CI / Check contributors (push) Has been cancelled
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
CI / Check uv.lock (push) Has been cancelled
CI / Check no committed infographics (push) Has been cancelled
CI / Profile artifact check (push) Has been cancelled
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Docker Build, Test, and Publish / merge (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
CI / Check no case-colliding filenames (push) Has been cancelled
CI / package-lock.json diff (push) Has been cancelled
CI / Lint Docker scripts (push) Has been cancelled
CI / Review label gate (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
CI / CI timing report (push) Has been cancelled
CI / Python tests (push) Has been cancelled
CI / OS-specific tests (push) Has been cancelled
CI / Python lints (push) Has been cancelled
CI / JS & TS checks (push) Has been cancelled
CI / Installer tests (push) Has been cancelled
CI / Rust tests (push) Has been cancelled
Nix flake check / nix flake check (push) Has been cancelled
158 lines
7.2 KiB
TypeScript
158 lines
7.2 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
|
|
import { test } from 'vitest'
|
|
|
|
import { registerFreeModel2ApiIpc } from './freemodel2api-ipc'
|
|
import type { FetchLike, FreeModel2ApiStoreIo } from './freemodel2api'
|
|
|
|
function fakeIo() {
|
|
let fileText: string | null = null
|
|
const io: FreeModel2ApiStoreIo = {
|
|
encrypt: plaintext => ({ encoding: 'safeStorage', value: Buffer.from(plaintext, 'utf8').toString('base64') }),
|
|
decrypt: secret => (secret?.encoding === 'safeStorage' ? Buffer.from(String(secret.value), 'base64').toString('utf8') : ''),
|
|
readStoreText: () => {
|
|
if (fileText === null) throw new Error('ENOENT')
|
|
return fileText
|
|
},
|
|
writeStoreText: text => {
|
|
fileText = text
|
|
}
|
|
}
|
|
return { io, fileText: () => fileText }
|
|
}
|
|
|
|
function fakeIpcMain() {
|
|
const handlers = new Map<string, (event: any, payload?: any) => Promise<any>>()
|
|
return {
|
|
handlers,
|
|
ipcMain: { handle: (channel: string, fn: any) => handlers.set(channel, fn) }
|
|
}
|
|
}
|
|
|
|
function jsonResponse(status: number, body: any) {
|
|
return { ok: status >= 200 && status < 300, status, json: async () => body }
|
|
}
|
|
|
|
function makeFetcher(behavior: (url: string, init?: any) => { status: number; body: any }) {
|
|
const calls: { url: string; init?: any }[] = []
|
|
const fetcher: FetchLike = async (url, init) => {
|
|
calls.push({ url, init })
|
|
const { status, body } = behavior(url, init)
|
|
return jsonResponse(status, body)
|
|
}
|
|
return { fetcher, calls }
|
|
}
|
|
|
|
const LOGIN_OK = {
|
|
code: 0,
|
|
data: { access_token: 'AT-1', expires_in: 3600, token_type: 'Bearer', user: { email: 'u@example.com', username: 'yb', balance: 9.5, frozen_balance: 12.5 } }
|
|
}
|
|
|
|
test('login persists credentials and returns balance; status reuses in-memory session', async () => {
|
|
const { handlers, ipcMain } = fakeIpcMain()
|
|
const { io } = fakeIo()
|
|
const { fetcher, calls } = makeFetcher(url => (url.endsWith('/auth/login') ? { status: 200, body: LOGIN_OK } : { status: 200, body: { code: 0, data: { email: 'u@example.com', username: 'yb', balance: 9.5, frozen_balance: 12.5 } } }))
|
|
registerFreeModel2ApiIpc({ ipcMain, io, fetcher })
|
|
|
|
const login = await handlers.get('hermes:freemodel2api:login')!(null, { baseUrl: 'https://sub.example.com/', email: 'u@example.com', password: 'pw' })
|
|
assert.deepEqual(login, { ok: true, email: 'u@example.com', balance: 9.5, username: 'yb', frozenBalance: 12.5 })
|
|
assert.deepEqual(JSON.parse(io.decrypt(JSON.parse(io.readStoreText()).freemodel2api)), {
|
|
baseUrl: 'https://sub.example.com', email: 'u@example.com', password: 'pw'
|
|
})
|
|
|
|
const status = await handlers.get('hermes:freemodel2api:status')!(null)
|
|
assert.deepEqual(status, { configured: true, baseUrl: 'https://sub.example.com', email: 'u@example.com', balance: 9.5, username: 'yb', frozenBalance: 12.5 })
|
|
// status 用内存里的 session,没有第二次 login 请求
|
|
assert.equal(calls.filter(c => c.url.endsWith('/auth/login')).length, 1)
|
|
})
|
|
|
|
test('status with no stored credentials returns configured:false', async () => {
|
|
const { handlers, ipcMain } = fakeIpcMain()
|
|
const { io } = fakeIo()
|
|
const { fetcher } = makeFetcher(() => ({ status: 200, body: LOGIN_OK }))
|
|
registerFreeModel2ApiIpc({ ipcMain, io, fetcher })
|
|
assert.deepEqual(await handlers.get('hermes:freemodel2api:status')!(null), { configured: false })
|
|
})
|
|
|
|
test('key-balance queries by api key without a session; non-sub2api answers null', async () => {
|
|
const { handlers, ipcMain } = fakeIpcMain()
|
|
const { io } = fakeIo()
|
|
const { fetcher, calls } = makeFetcher(url =>
|
|
url.endsWith('/v1/sub2api/balance')
|
|
? { status: 200, body: { object: 'sub2api.key_balance', billing_mode: 'balance', balance: 9181.34, unit: 'USD' } }
|
|
: { status: 200, body: LOGIN_OK })
|
|
registerFreeModel2ApiIpc({ ipcMain, io, fetcher })
|
|
|
|
// baseUrl 带旧 /v1 也要归到同一个端点;凭 key,不 login
|
|
assert.deepEqual(
|
|
await handlers.get('hermes:freemodel2api:key-balance')!(null, { baseUrl: 'http://h:3020/v1', apiKey: 'sk-k' }),
|
|
{ balance: 9181.34 }
|
|
)
|
|
assert.equal(calls[0].url, 'http://h:3020/v1/sub2api/balance')
|
|
assert.equal(calls[0].init?.headers?.Authorization, 'Bearer sk-k')
|
|
assert.equal(calls.filter(c => c.url.endsWith('/auth/login')).length, 0)
|
|
|
|
// 非 sub2api 端点 404:不是错误,只是查不到
|
|
const { fetcher: fetcher404 } = makeFetcher(() => ({ status: 404, body: { message: 'nope' } }))
|
|
const second = fakeIpcMain()
|
|
registerFreeModel2ApiIpc({ ipcMain: second.ipcMain, io, fetcher: fetcher404 })
|
|
assert.deepEqual(
|
|
await second.handlers.get('hermes:freemodel2api:key-balance')!(null, { baseUrl: 'http://h:3020', apiKey: 'sk-k' }),
|
|
{ balance: null }
|
|
)
|
|
})
|
|
|
|
test('keys re-login once on 401 then succeed', async () => {
|
|
const { handlers, ipcMain } = fakeIpcMain()
|
|
const { io } = fakeIo()
|
|
let keysCalls = 0
|
|
const { fetcher, calls } = makeFetcher(url => {
|
|
if (url.endsWith('/auth/login')) return { status: 200, body: LOGIN_OK }
|
|
if (url.endsWith('/keys')) {
|
|
keysCalls += 1
|
|
if (keysCalls === 1) return { status: 401, body: { code: 401, message: 'expired', data: null } }
|
|
return { status: 200, body: { code: 0, data: { items: [], total: 0, page: 1, page_size: 20, pages: 1 } } }
|
|
}
|
|
return { status: 200, body: { code: 0, data: {} } }
|
|
})
|
|
registerFreeModel2ApiIpc({ ipcMain, io, fetcher })
|
|
await handlers.get('hermes:freemodel2api:login')!(null, { baseUrl: 'https://sub.example.com', email: 'u@example.com', password: 'pw' })
|
|
const result = await handlers.get('hermes:freemodel2api:keys')!(null)
|
|
assert.deepEqual(result, { keys: [] })
|
|
assert.equal(calls.filter(c => c.url.endsWith('/auth/login')).length, 2)
|
|
})
|
|
|
|
test('logout clears stored credentials and session', async () => {
|
|
const { handlers, ipcMain } = fakeIpcMain()
|
|
const { io } = fakeIo()
|
|
const { fetcher } = makeFetcher(() => ({ status: 200, body: LOGIN_OK }))
|
|
registerFreeModel2ApiIpc({ ipcMain, io, fetcher })
|
|
await handlers.get('hermes:freemodel2api:login')!(null, { baseUrl: 'https://sub.example.com', email: 'u@example.com', password: 'pw' })
|
|
await handlers.get('hermes:freemodel2api:logout')!(null)
|
|
assert.deepEqual(await handlers.get('hermes:freemodel2api:status')!(null), { configured: false })
|
|
})
|
|
|
|
test('remembered save/load round trips and survives logout', async () => {
|
|
const { handlers, ipcMain } = fakeIpcMain()
|
|
const { io, fileText } = fakeIo()
|
|
const { fetcher } = makeFetcher(() => ({ status: 200, body: LOGIN_OK }))
|
|
registerFreeModel2ApiIpc({ ipcMain, io, fetcher })
|
|
|
|
assert.equal(await handlers.get('hermes:freemodel2api:remembered:load')!(null), null)
|
|
|
|
const saved = await handlers.get('hermes:freemodel2api:remembered:save')!(null, {
|
|
baseUrl: 'https://sub.example.com/', email: 'u@example.com', password: 'pw-123'
|
|
})
|
|
assert.deepEqual(saved, { ok: true })
|
|
assert.ok(!fileText()!.includes('pw-123'))
|
|
|
|
await handlers.get('hermes:freemodel2api:login')!(null, { baseUrl: 'https://sub.example.com', email: 'u@example.com', password: 'pw' })
|
|
await handlers.get('hermes:freemodel2api:logout')!(null)
|
|
assert.deepEqual(await handlers.get('hermes:freemodel2api:remembered:load')!(null), {
|
|
baseUrl: 'https://sub.example.com', email: 'u@example.com', password: 'pw-123'
|
|
})
|
|
|
|
await handlers.get('hermes:freemodel2api:remembered:save')!(null, null)
|
|
assert.equal(await handlers.get('hermes:freemodel2api:remembered:load')!(null), null)
|
|
})
|