/** * Tests for electron/aiturk-login.ts — the one-click TurkServis sign-in. * Everything is injected (fake fetch, fake backend, fake CLI, fake clock), so * no network, browser or real CLI is touched. The fixture "keys" below are * obviously fake placeholders in the right shape. */ import { EventEmitter } from 'node:events' import { describe, expect, it, vi } from 'vitest' import { AITURK_CLI_KEY_HOLD_MS, AITURK_LOGIN_START_RETRY_DELAY_MS, type AiturkCliExit, AiturkLoginError, createAiturkCliRunner, createAiturkLogin, resolveAiturkCliPath } from './aiturk-login' const DEVICE_CODE = `tsdl_${'d'.repeat(43)}` const IDE_KEY = 'tsk_FAKE-ide-key-for-tests' const CLI_KEY = 'tsk_FAKE-cli-key-for-tests' const URI = 'https://turkservis.online/account/aiturk/device-login/0f8fad5b-d9cb-469f-a165-70867728950e' const startBody = (over: Record = {}) => ({ loginId: '0f8fad5b-d9cb-469f-a165-70867728950e', deviceCode: DEVICE_CODE, userCode: 'ABCD2345', expiresIn: 600, interval: 3, verificationUri: URI, ...over }) const approved = { state: 'approved', key: IDE_KEY, cliKey: CLI_KEY, account: 'acct_test' } function harness( options: { start?: { status: number; body?: unknown } /** Thrown by successive `device-auth/start` calls before any answer is given. */ startErrors?: Error[] polls?: ({ status: number; body?: unknown } | Error)[] cliExits?: AiturkCliExit[] validate?: unknown save?: unknown openExternal?: () => boolean } = {} ) { const calls: { url: string; init: any }[] = [] const polls = [...(options.polls ?? [])] const startErrors = [...(options.startErrors ?? [])] const cliExits = [...(options.cliExits ?? [0])] const clock = { now: 1_000_000 } const timers: { fn: () => void; ms: number; cleared: boolean }[] = [] const fetch = vi.fn(async (url: string, init: any) => { calls.push({ url, init }) if (url.endsWith('/start')) { const failure = startErrors.shift() if (failure) { throw failure } const r = options.start ?? { status: 201, body: startBody() } return { status: r.status, json: async () => r.body } } if (url.endsWith('/poll')) { const r = polls.shift() ?? { status: 200, body: { state: 'pending' } } if (r instanceof Error) { throw r } return { status: r.status, json: async () => r.body } } return { status: 200, json: async () => ({}) } }) const backendRequest = vi.fn(async (request: any) => { if (request.path.endsWith('/validate')) { return options.validate ?? { ok: true, reachable: true, models: ['account/model-a', 'account/model-b'], message: '' } } return options.save ?? { ok: true, id: 'turkservis', endpoints: [] } }) const runCli = vi.fn(async (_key: string, _options: { keepOtherAccount: boolean }) => cliExits.length ? (cliExits.shift() as AiturkCliExit) : 0 ) const openExternal = vi.fn(options.openExternal ?? (() => true)) const login = createAiturkLogin({ fetch, openExternal, hostname: () => 'TEST-PC', backendRequest, runCli, now: () => clock.now, setTimer: (fn, ms) => { const timer = { fn, ms, cleared: false } timers.push(timer) return timer }, clearTimer: handle => { ;(handle as { cleared: boolean }).cleared = true }, timeoutSignal: () => undefined }) return { login, fetch, calls, backendRequest, runCli, openExternal, clock, timers } } const leaks = (value: unknown) => /tsk_|tsdl_/.test(typeof value === 'string' ? value : JSON.stringify(value) ?? '') describe('aiturk login: start', () => { it('asks the server for a code, opens the approval page and returns no secret', async () => { const h = harness() const started = await h.login.start({ profile: 'work' }) expect(started).toEqual({ userCode: 'ABCD2345', verificationUri: URI, expiresAt: 1_000_000 + 600_000 }) expect(leaks(started)).toBe(false) expect(h.openExternal).toHaveBeenCalledExactlyOnceWith(URI) const { url, init } = h.calls[0] expect(url).toBe('https://turkservis.online/v1/aiturk/device-auth/start') expect(init.method).toBe('POST') expect(init.redirect).toBe('error') expect(JSON.parse(init.body)).toEqual({ client: 'ide', device_name: 'TEST-PC', also_cli: true }) expect(init.headers.Authorization).toBeUndefined() }) it.each([ 'https://turkservis.online.evil.example/account/aiturk/device-login/0f8fad5b-d9cb-469f-a165-70867728950e', 'http://turkservis.online/account/aiturk/device-login/0f8fad5b-d9cb-469f-a165-70867728950e', 'https://turkservis.online/account/aiturk/device-login/0f8fad5b-d9cb-469f-a165-70867728950e?next=//evil', 'https://turkservis.online/account/aiturk/device-login/../../x', 'https://evil.example/account/aiturk/device-login/0f8fad5b-d9cb-469f-a165-70867728950e', '' ])('refuses a verification URL that is not exactly the approval page: %s', async verificationUri => { const h = harness({ start: { status: 201, body: startBody({ verificationUri }) } }) const error = await h.login.start().catch(e => e) expect(error).toBeInstanceOf(AiturkLoginError) expect(error.message).toBe('aiturk-login:invalid-response') expect(leaks(error.message)).toBe(false) expect(h.openExternal).not.toHaveBeenCalled() // The issued code is revoked instead of being left approvable. expect(h.calls.at(-1)?.url).toBe('https://turkservis.online/v1/aiturk/device-auth/cancel') expect(await h.login.poll()).toEqual({ state: 'expired' }) }) it('maps 429 to rate-limited and other failures to unavailable', async () => { await expect(harness({ start: { status: 429 } }).login.start()).rejects.toThrow('aiturk-login:rate-limited') await expect(harness({ start: { status: 422 } }).login.start()).rejects.toThrow('aiturk-login:unavailable') await expect(harness({ start: { status: 500 } }).login.start()).rejects.toThrow('aiturk-login:unavailable') }) it('tries start once more after a thrown network error (first request of a long-idle process)', async () => { const h = harness({ startErrors: [new TypeError('fetch failed')] }) const pending = h.login.start() await vi.waitFor(() => expect(h.timers.some(timer => timer.ms === AITURK_LOGIN_START_RETRY_DELAY_MS)).toBe(true)) expect(AITURK_LOGIN_START_RETRY_DELAY_MS).toBe(500) expect(h.calls.filter(call => call.url.endsWith('/start'))).toHaveLength(1) expect(h.openExternal).not.toHaveBeenCalled() h.timers.find(timer => timer.ms === AITURK_LOGIN_START_RETRY_DELAY_MS)!.fn() expect(await pending).toEqual({ userCode: 'ABCD2345', verificationUri: URI, expiresAt: 1_000_000 + 600_000 }) expect(h.calls.filter(call => call.url.endsWith('/start'))).toHaveLength(2) // One code only: the approval page is opened once. expect(h.openExternal).toHaveBeenCalledExactlyOnceWith(URI) }) it('gives up after the second network error, and never retries an HTTP answer or a timeout', async () => { const twice = harness({ startErrors: [new TypeError('fetch failed'), new TypeError('fetch failed')] }) const failing = twice.login.start().catch(e => e) await vi.waitFor(() => expect(twice.timers.some(timer => timer.ms === AITURK_LOGIN_START_RETRY_DELAY_MS)).toBe(true)) twice.timers.find(timer => timer.ms === AITURK_LOGIN_START_RETRY_DELAY_MS)!.fn() expect((await failing).message).toBe('aiturk-login:unavailable') expect(twice.calls.filter(call => call.url.endsWith('/start'))).toHaveLength(2) expect(twice.timers.filter(timer => timer.ms === AITURK_LOGIN_START_RETRY_DELAY_MS)).toHaveLength(1) for (const status of [500, 503, 429, 422]) { const answered = harness({ start: { status } }) await expect(answered.login.start()).rejects.toThrow(/^aiturk-login:(?:unavailable|rate-limited)$/) expect(answered.calls.filter(call => call.url.endsWith('/start'))).toHaveLength(1) expect(answered.timers).toHaveLength(0) } const timedOut = harness({ startErrors: [new DOMException('The operation was aborted due to timeout', 'TimeoutError')] }) await expect(timedOut.login.start()).rejects.toThrow('aiturk-login:unavailable') expect(timedOut.calls.filter(call => call.url.endsWith('/start'))).toHaveLength(1) expect(timedOut.timers).toHaveLength(0) }) it('rejects malformed codes', async () => { await expect( harness({ start: { status: 201, body: startBody({ userCode: 'abcd2345' }) } }).login.start() ).rejects.toThrow('aiturk-login:invalid-response') await expect( harness({ start: { status: 201, body: startBody({ deviceCode: 'nope' }) } }).login.start() ).rejects.toThrow('aiturk-login:invalid-response') }) it('keeps a single login: a second start revokes the first code', async () => { const h = harness() await h.login.start() await h.login.start() const urls = h.calls.map(c => c.url.split('/').pop()) expect(urls).toEqual(['start', 'cancel', 'start']) expect(h.calls[1].init.headers.Authorization).toBe(`Bearer ${DEVICE_CODE}`) }) it('cancels the code when the browser cannot be opened', async () => { const h = harness({ openExternal: () => false }) await expect(h.login.start()).rejects.toThrow('aiturk-login:browser-failed') expect(h.calls.at(-1)?.url).toMatch(/\/cancel$/) }) }) describe('aiturk login: poll', () => { it('reports pending while the user has not approved', async () => { const h = harness({ polls: [{ status: 200, body: { state: 'pending' } }] }) await h.login.start() expect(await h.login.poll()).toEqual({ state: 'pending' }) expect(h.calls[1].url).toBe('https://turkservis.online/v1/aiturk/device-auth/poll') expect(h.calls[1].init.headers.Authorization).toBe(`Bearer ${DEVICE_CODE}`) expect(h.calls[1].init.redirect).toBe('error') expect(h.backendRequest).not.toHaveBeenCalled() expect(h.runCli).not.toHaveBeenCalled() }) it('keeps waiting through a transient network error', async () => { const h = harness({ polls: [new Error(`boom ${DEVICE_CODE}`), { status: 503 }] }) await h.login.start() expect(await h.login.poll()).toEqual({ state: 'pending' }) expect(await h.login.poll()).toEqual({ state: 'pending' }) }) it('reports expired on 401 and stops asking the server', async () => { const h = harness({ polls: [{ status: 401, body: { detail: 'expired' } }] }) await h.login.start() expect(await h.login.poll()).toEqual({ state: 'expired' }) const count = h.fetch.mock.calls.length expect(await h.login.poll()).toEqual({ state: 'expired' }) expect(h.fetch.mock.calls.length).toBe(count) }) it('expires by the clock without a request', async () => { const h = harness() await h.login.start() h.clock.now += 600_000 expect(await h.login.poll()).toEqual({ state: 'expired' }) expect(h.calls.filter(c => c.url.endsWith('/poll'))).toHaveLength(0) }) it('on approval saves the provider and pipes ONLY the CLI key to the CLI', async () => { const h = harness({ polls: [{ status: 200, body: approved }] }) await h.login.start({ profile: 'work', connectionId: 'conn-1' }) const result = await h.login.poll() expect(result).toEqual({ state: 'connected', cli: 'ok' }) expect(leaks(result)).toBe(false) expect(h.backendRequest).toHaveBeenCalledTimes(2) expect(h.backendRequest.mock.calls[0][0]).toEqual({ connectionId: 'conn-1', path: '/api/providers/custom-endpoints/validate', method: 'POST', body: { id: 'turkservis', name: 'TurkServis · AITURK', base_url: 'https://ai.turkservis.online/v1', api_key: IDE_KEY, model: '', discover_models: true, make_default: true } }) expect(h.backendRequest.mock.calls[1][0]).toEqual({ profile: 'work', connectionId: 'conn-1', path: '/api/providers/custom-endpoints', method: 'POST', body: { id: 'turkservis', name: 'TurkServis · AITURK', base_url: 'https://ai.turkservis.online/v1', api_key: IDE_KEY, model: 'account/model-a', models: ['account/model-a', 'account/model-b'], discover_models: true, make_default: true } }) expect(h.runCli).toHaveBeenCalledExactlyOnceWith(CLI_KEY, { keepOtherAccount: true }) // No key ever rides a URL. expect(h.calls.some(c => leaks(c.url))).toBe(false) }) it('does not ask for the one-time keys again on later polls', async () => { const h = harness({ polls: [{ status: 200, body: approved }] }) await h.login.start() const first = await h.login.poll() const fetches = h.fetch.mock.calls.length const second = await h.login.poll() expect(second).toEqual(first) expect(h.fetch.mock.calls.length).toBe(fetches) expect(h.backendRequest).toHaveBeenCalledTimes(2) expect(h.runCli).toHaveBeenCalledTimes(1) }) it('shares one completion between overlapping polls', async () => { const h = harness({ polls: [{ status: 200, body: approved }] }) await h.login.start() const [a, b] = await Promise.all([h.login.poll(), h.login.poll()]) expect(a).toEqual({ state: 'connected', cli: 'ok' }) expect(b.state === 'connected' || b.state === 'pending').toBe(true) expect(h.runCli).toHaveBeenCalledTimes(1) }) it.each([ [0, 'ok'], [2, 'outdated'], [3, 'other-account'], [4, 'unknown-account'], [1, 'failed'], [null, 'failed'], ['missing', 'missing'] ] as const)('maps CLI exit %s to cli=%s', async (exit, cli) => { const h = harness({ polls: [{ status: 200, body: approved }], cliExits: [exit] }) await h.login.start() expect(await h.login.poll()).toEqual({ state: 'connected', cli }) }) it('treats a throwing CLI runner as failed without leaking', async () => { const h = harness({ polls: [{ status: 200, body: approved }] }) h.runCli.mockRejectedValueOnce(new Error(`spawn failed ${CLI_KEY}`)) await h.login.start() const result = await h.login.poll() expect(result).toEqual({ state: 'connected', cli: 'failed' }) }) it('does not save or touch the CLI when the key is rejected by the backend', async () => { const h = harness({ polls: [{ status: 200, body: approved }], validate: { ok: false, reachable: true, models: [], message: `bad key ${IDE_KEY}` } }) await h.login.start() const result = await h.login.poll() expect(result).toEqual({ state: 'error', error: 'provider-failed' }) expect(leaks(result)).toBe(false) expect(h.backendRequest).toHaveBeenCalledTimes(1) expect(h.runCli).not.toHaveBeenCalled() }) it('does not invent a model when the catalog is empty', async () => { const h = harness({ polls: [{ status: 200, body: approved }], validate: { ok: true, reachable: true, models: [], message: '' } }) await h.login.start() expect(await h.login.poll()).toEqual({ state: 'error', error: 'no-models' }) expect(h.backendRequest).toHaveBeenCalledTimes(1) }) it('never surfaces backend error text (which could echo the key)', async () => { const h = harness({ polls: [{ status: 200, body: approved }] }) h.backendRequest.mockRejectedValueOnce(new Error(`500: echoed ${IDE_KEY}`)) await h.login.start() const result = await h.login.poll() expect(result).toEqual({ state: 'error', error: 'provider-failed' }) expect(leaks(result)).toBe(false) }) it('rejects an approval whose key is malformed', async () => { const h = harness({ polls: [{ status: 200, body: { state: 'approved', key: 'not a key', cliKey: CLI_KEY } }] }) await h.login.start() expect(await h.login.poll()).toEqual({ state: 'error', error: 'invalid-response' }) expect(h.backendRequest).not.toHaveBeenCalled() expect(h.runCli).not.toHaveBeenCalled() }) }) describe('aiturk login: other CLI account', () => { async function otherAccount(overwriteExit: AiturkCliExit = 0) { const h = harness({ polls: [{ status: 200, body: approved }], cliExits: [3, overwriteExit] }) await h.login.start() expect(await h.login.poll()).toEqual({ state: 'connected', cli: 'other-account' }) return h } it('overwrites only after the user confirms, without the keep flag', async () => { const h = await otherAccount() const result = await h.login.overwriteCli() expect(result).toEqual({ cli: 'ok' }) expect(leaks(result)).toBe(false) expect(h.runCli).toHaveBeenLastCalledWith(CLI_KEY, { keepOtherAccount: false }) expect(await h.login.poll()).toEqual({ state: 'connected', cli: 'ok' }) // Single use: the held key is gone. await expect(h.login.overwriteCli()).rejects.toThrow('aiturk-login:nothing-to-overwrite') expect(h.runCli).toHaveBeenCalledTimes(2) }) it('reports an overwrite that still fails', async () => { const h = await otherAccount(4) expect(await h.login.overwriteCli()).toEqual({ cli: 'unknown-account' }) }) it('drops the held CLI key after five minutes', async () => { const h = await otherAccount() const hold = h.timers.find(t => t.ms === AITURK_CLI_KEY_HOLD_MS) expect(hold).toBeDefined() h.clock.now += AITURK_CLI_KEY_HOLD_MS hold?.fn() await expect(h.login.overwriteCli()).rejects.toThrow('aiturk-login:nothing-to-overwrite') expect(h.runCli).toHaveBeenCalledTimes(1) }) it('refuses a stale held key even if the timer never fired', async () => { const h = await otherAccount() h.clock.now += AITURK_CLI_KEY_HOLD_MS await expect(h.login.overwriteCli()).rejects.toThrow('aiturk-login:nothing-to-overwrite') expect(h.runCli).toHaveBeenCalledTimes(1) }) it('drops the held CLI key on cancel', async () => { const h = await otherAccount() await h.login.cancel() const error = await h.login.overwriteCli().catch(e => e) expect(error.message).toBe('aiturk-login:nothing-to-overwrite') expect(leaks(error.message)).toBe(false) expect(h.runCli).toHaveBeenCalledTimes(1) }) it('has nothing to overwrite when the CLI simply succeeded', async () => { const h = harness({ polls: [{ status: 200, body: approved }] }) await h.login.start() await h.login.poll() await expect(h.login.overwriteCli()).rejects.toThrow('aiturk-login:nothing-to-overwrite') }) }) describe('aiturk login: cancel', () => { it('revokes the code on the server and clears the session', async () => { const h = harness() await h.login.start() expect(await h.login.cancel()).toEqual({ ok: true }) const last = h.calls.at(-1) expect(last?.url).toBe('https://turkservis.online/v1/aiturk/device-auth/cancel') expect(last?.init.headers.Authorization).toBe(`Bearer ${DEVICE_CODE}`) const count = h.fetch.mock.calls.length expect(await h.login.poll()).toEqual({ state: 'expired' }) expect(h.fetch.mock.calls.length).toBe(count) }) it('is a no-op without a login and survives a failing server', async () => { const h = harness() expect(await h.login.cancel()).toEqual({ ok: true }) expect(h.fetch).not.toHaveBeenCalled() await h.login.start() h.fetch.mockRejectedValueOnce(new Error('offline')) expect(await h.login.cancel()).toEqual({ ok: true }) }) }) describe('aiturk CLI runner', () => { const CLI_PATH = 'C:\\Users\\Test User\\AppData\\Local\\TurkServis\\AITURK\\program\\bin\\aiturk.cmd' const env = { LOCALAPPDATA: 'C:\\Users\\Test User\\AppData\\Local', SystemRoot: 'C:\\Windows' } function fakeSpawn(behaviour: (child: any) => void = child => child.emit('close', 0)) { const state: any = { calls: [], stdin: [] as string[], killed: false } const spawn = vi.fn((command: string, args: string[], options: any) => { const child: any = new EventEmitter() child.stdin = new EventEmitter() child.stdin.end = (chunk: string) => { state.stdin.push(chunk) queueMicrotask(() => behaviour(child)) } child.kill = () => { state.killed = true } state.calls.push({ command, args, options }) return child }) return { spawn, state } } it('finds the CLI only at the fixed install path', () => { expect(resolveAiturkCliPath({ env, existsSync: p => p === CLI_PATH, platform: 'win32' })).toBe(CLI_PATH) expect(resolveAiturkCliPath({ env, existsSync: () => false, platform: 'win32' })).toBeNull() expect(resolveAiturkCliPath({ env: {}, existsSync: () => true, platform: 'win32' })).toBeNull() expect(resolveAiturkCliPath({ env: { LOCALAPPDATA: 'relative' }, existsSync: () => true, platform: 'win32' })).toBeNull() expect(resolveAiturkCliPath({ env, existsSync: () => true, platform: 'linux' })).toBeNull() }) it('reports missing without spawning anything', async () => { const { spawn } = fakeSpawn() const run = createAiturkCliRunner({ spawn, existsSync: () => false, env, platform: 'win32' }) expect(await run(CLI_KEY, { keepOtherAccount: true })).toBe('missing') expect(spawn).not.toHaveBeenCalled() }) it('sends the key on stdin only, through an absolute cmd.exe, without a shell', async () => { const { spawn, state } = fakeSpawn() const run = createAiturkCliRunner({ spawn, existsSync: () => true, env, platform: 'win32' }) expect(await run(CLI_KEY, { keepOtherAccount: true })).toBe(0) const { command, args, options } = state.calls[0] expect(command).toBe('C:\\Windows\\System32\\cmd.exe') expect(args).toEqual(['/d', '/s', '/c', `""${CLI_PATH}" login --key-stdin --keep-other-account"`]) expect(options).toEqual({ shell: false, windowsHide: true, windowsVerbatimArguments: true, stdio: ['pipe', 'ignore', 'ignore'] }) expect(leaks([command, args, options])).toBe(false) expect(state.stdin).toEqual([`${CLI_KEY}\n`]) }) it('drops the keep flag for a confirmed overwrite', async () => { const { spawn, state } = fakeSpawn() const run = createAiturkCliRunner({ spawn, existsSync: () => true, env, platform: 'win32' }) await run(CLI_KEY, { keepOtherAccount: false }) expect(state.calls[0].args[3]).toBe(`""${CLI_PATH}" login --key-stdin"`) }) it.each([0, 1, 2, 3, 4])('returns exit code %s', async code => { const { spawn } = fakeSpawn(child => child.emit('close', code)) const run = createAiturkCliRunner({ spawn, existsSync: () => true, env, platform: 'win32' }) expect(await run(CLI_KEY, { keepOtherAccount: true })).toBe(code) }) it('returns null on spawn error, thrown spawn and stdin EPIPE', async () => { const errored = fakeSpawn(child => child.emit('error', new Error('ENOENT'))) expect( await createAiturkCliRunner({ spawn: errored.spawn, existsSync: () => true, env, platform: 'win32' })(CLI_KEY, { keepOtherAccount: true }) ).toBeNull() const thrower = vi.fn(() => { throw new Error('spawn EINVAL') }) expect( await createAiturkCliRunner({ spawn: thrower, existsSync: () => true, env, platform: 'win32' })(CLI_KEY, { keepOtherAccount: true }) ).toBeNull() const epipe = fakeSpawn(child => { child.stdin.emit('error', new Error('EPIPE')) child.emit('close', 2) }) expect( await createAiturkCliRunner({ spawn: epipe.spawn, existsSync: () => true, env, platform: 'win32' })(CLI_KEY, { keepOtherAccount: true }) ).toBe(2) }) it('kills a CLI that hangs past the timeout', async () => { const { spawn, state } = fakeSpawn(() => {}) let fire = () => {} const run = createAiturkCliRunner({ spawn, existsSync: () => true, env, platform: 'win32', setTimer: (fn, ms) => { expect(ms).toBe(60_000) fire = fn return 1 }, clearTimer: () => {} }) const pending = run(CLI_KEY, { keepOtherAccount: true }) fire() expect(await pending).toBeNull() expect(state.killed).toBe(true) }) it('refuses an install path cmd.exe could reinterpret', async () => { const { spawn } = fakeSpawn() const run = createAiturkCliRunner({ spawn, existsSync: () => true, env: { LOCALAPPDATA: 'C:\\Users\\a%PATH%b\\AppData\\Local', SystemRoot: 'C:\\Windows' }, platform: 'win32' }) expect(await run(CLI_KEY, { keepOtherAccount: true })).toBeNull() expect(spawn).not.toHaveBeenCalled() }) })