The onboarding and provider settings get a Sign in button: the IDE shows a short code, opens the turkservis.online approval page, and after approval saves the TurkServis provider itself and hands a separate member key to the AITURK CLI over stdin (aiturk login --key-stdin --keep-other-account). Keys never reach the renderer, logs, process arguments or URLs. Pasting an API key remains as the fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8ppceaR5s6cvMnm7uGRnf
216 lines
9.4 KiB
TypeScript
216 lines
9.4 KiB
TypeScript
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||
|
||
import { saveCustomEndpoint, validateCustomEndpoint } from '@/hermes'
|
||
import { I18nProvider } from '@/i18n/context'
|
||
|
||
import { AiturkProvider } from './aiturk-provider'
|
||
|
||
vi.mock('@/hermes', () => ({ saveCustomEndpoint: vi.fn(), validateCustomEndpoint: vi.fn() }))
|
||
vi.mock('./flow', () => ({ DocsLink: ({ children, href }: { children: React.ReactNode; href: string }) => <a href={href}>{children}</a> }))
|
||
|
||
afterEach(() => {
|
||
cleanup()
|
||
vi.resetAllMocks()
|
||
vi.useRealTimers()
|
||
delete (window as { hermesDesktop?: unknown }).hermesDesktop
|
||
})
|
||
|
||
function setup() {
|
||
const onConnected = vi.fn().mockResolvedValue(undefined)
|
||
render(<I18nProvider configClient={null}><AiturkProvider onConnected={onConnected} /></I18nProvider>)
|
||
const input = screen.getByLabelText('TurkServis API anahtarı') as HTMLInputElement
|
||
fireEvent.change(input, { target: { value: 'test-key-never-real' } })
|
||
fireEvent.click(screen.getByRole('button', { name: 'Bağlan' }))
|
||
|
||
return { input, onConnected }
|
||
}
|
||
|
||
describe('TurkServis provider setup', () => {
|
||
it('discovers an authorized model before saving it as the default', async () => {
|
||
vi.mocked(validateCustomEndpoint).mockResolvedValue({ ok: true, reachable: true, models: ['account/model-a', 'account/model-b'], message: '' })
|
||
vi.mocked(saveCustomEndpoint).mockResolvedValue({ id: 'turkservis', endpoints: [], current: { base_url: 'https://ai.turkservis.online/v1', model: 'account/model-a', provider: 'turkservis' }, ok: true })
|
||
const { input, onConnected } = setup()
|
||
expect(input.type).toBe('password')
|
||
await waitFor(() => expect(onConnected).toHaveBeenCalledOnce())
|
||
expect(saveCustomEndpoint).toHaveBeenCalledWith(expect.objectContaining({
|
||
base_url: 'https://ai.turkservis.online/v1', api_key: 'test-key-never-real', model: 'account/model-a',
|
||
models: ['account/model-a', 'account/model-b'], make_default: true
|
||
}))
|
||
expect(input.value).toBe('')
|
||
})
|
||
it('does not persist rejected credentials or claim a successful connection', async () => {
|
||
vi.mocked(validateCustomEndpoint).mockResolvedValue({ ok: false, reachable: true, models: [], message: 'Anahtar geçersiz (401)' })
|
||
const { onConnected } = setup()
|
||
await waitFor(() => expect(screen.getByRole('alert').textContent).toContain('401'))
|
||
expect(saveCustomEndpoint).not.toHaveBeenCalled()
|
||
expect(onConnected).not.toHaveBeenCalled()
|
||
})
|
||
it('does not invent a model when the authenticated catalog is empty', async () => {
|
||
vi.mocked(validateCustomEndpoint).mockResolvedValue({ ok: true, reachable: true, models: [], message: '' })
|
||
setup()
|
||
await waitFor(() => expect(screen.getByRole('alert').textContent).toContain('model bulunamadı'))
|
||
expect(saveCustomEndpoint).not.toHaveBeenCalled()
|
||
})
|
||
})
|
||
|
||
const URI = 'https://turkservis.online/account/aiturk/device-login/0f8fad5b-d9cb-469f-a165-70867728950e'
|
||
|
||
function setupLogin(polls: unknown[]) {
|
||
vi.useFakeTimers()
|
||
|
||
const bridge = {
|
||
start: vi.fn().mockResolvedValue({ userCode: 'ABCD2345', verificationUri: URI, expiresAt: Date.now() + 600_000 }),
|
||
poll: vi.fn(async () => polls.shift() ?? { state: 'pending' }),
|
||
cancel: vi.fn().mockResolvedValue({ ok: true }),
|
||
overwriteCli: vi.fn().mockResolvedValue({ cli: 'ok' })
|
||
}
|
||
|
||
const openExternal = vi.fn().mockResolvedValue(undefined)
|
||
|
||
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = { aiturkLogin: bridge, openExternal }
|
||
const onConnected = vi.fn().mockResolvedValue(undefined)
|
||
render(<I18nProvider configClient={null}><AiturkProvider onConnected={onConnected} /></I18nProvider>)
|
||
|
||
return { bridge, onConnected, openExternal }
|
||
}
|
||
|
||
const click = async (name: string) => {
|
||
await act(async () => {
|
||
fireEvent.click(screen.getByRole('button', { name }))
|
||
await Promise.resolve()
|
||
})
|
||
}
|
||
|
||
const tick = () =>
|
||
act(async () => {
|
||
await vi.advanceTimersByTimeAsync(3000)
|
||
})
|
||
|
||
describe('TurkServis one-click sign-in', () => {
|
||
it('shows the code, lets the user reopen the page and polls every 3 seconds', async () => {
|
||
const { bridge, onConnected, openExternal } = setupLogin([{ state: 'pending' }, { state: 'pending' }])
|
||
await click('Giriş yap')
|
||
|
||
expect(bridge.start).toHaveBeenCalledOnce()
|
||
expect(screen.getByLabelText('Giriş kodu').textContent).toBe('ABCD2345')
|
||
expect(screen.getByText('Tarayıcıda açılan sayfadaki kod bununla aynıysa onaylayın.')).toBeTruthy()
|
||
expect(bridge.poll).not.toHaveBeenCalled()
|
||
|
||
await tick()
|
||
expect(bridge.poll).toHaveBeenCalledTimes(1)
|
||
await tick()
|
||
expect(bridge.poll).toHaveBeenCalledTimes(2)
|
||
|
||
await click('Sayfayı yeniden aç')
|
||
expect(openExternal).toHaveBeenCalledWith(URI)
|
||
expect(onConnected).not.toHaveBeenCalled()
|
||
// No key ever reaches the renderer through this flow.
|
||
expect(saveCustomEndpoint).not.toHaveBeenCalled()
|
||
expect(validateCustomEndpoint).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('calls onConnected once the main process reports the provider connected', async () => {
|
||
const { bridge, onConnected } = setupLogin([{ state: 'pending' }, { state: 'connected', cli: 'ok' }])
|
||
await click('Giriş yap')
|
||
await tick()
|
||
expect(onConnected).not.toHaveBeenCalled()
|
||
await tick()
|
||
|
||
expect(onConnected).toHaveBeenCalledOnce()
|
||
expect(screen.getByRole('status').textContent).toBe('AITURK CLI de bu hesapla hazır.')
|
||
expect(screen.queryByLabelText('Giriş kodu')).toBeNull()
|
||
await tick()
|
||
expect(bridge.poll).toHaveBeenCalledTimes(2)
|
||
})
|
||
|
||
it.each([
|
||
['missing', ''],
|
||
['outdated', 'AITURK CLI güncellenince kendiliğinden bağlanması için yeniden giriş yapın.'],
|
||
['failed', 'AITURK CLI bağlanamadı; terminalde `aiturk login` çalıştırın.'],
|
||
['unknown-account', 'AITURK CLI bağlanamadı; terminalde `aiturk login` çalıştırın.']
|
||
])('connects and explains CLI result %s', async (cli, text) => {
|
||
const { onConnected } = setupLogin([{ state: 'connected', cli }])
|
||
await click('Giriş yap')
|
||
await tick()
|
||
|
||
expect(onConnected).toHaveBeenCalledOnce()
|
||
expect(screen.queryByRole('status')?.textContent ?? '').toBe(text)
|
||
})
|
||
|
||
it('asks before replacing another CLI account, then replaces on confirm', async () => {
|
||
const { bridge, onConnected } = setupLogin([{ state: 'connected', cli: 'other-account' }])
|
||
await click('Giriş yap')
|
||
await tick()
|
||
|
||
expect(screen.getByText('AITURK CLI’de başka bir hesap kayıtlı. Bu hesapla değiştirilsin mi?')).toBeTruthy()
|
||
expect(onConnected).not.toHaveBeenCalled()
|
||
expect(bridge.overwriteCli).not.toHaveBeenCalled()
|
||
|
||
await click('Değiştir')
|
||
expect(bridge.overwriteCli).toHaveBeenCalledOnce()
|
||
expect(onConnected).toHaveBeenCalledOnce()
|
||
expect(screen.getByRole('status').textContent).toBe('AITURK CLI de bu hesapla hazır.')
|
||
})
|
||
|
||
it('leaves the other CLI account alone when the user says so', async () => {
|
||
const { bridge, onConnected } = setupLogin([{ state: 'connected', cli: 'other-account' }])
|
||
await click('Giriş yap')
|
||
await tick()
|
||
await click('Olduğu gibi bırak')
|
||
|
||
expect(bridge.overwriteCli).not.toHaveBeenCalled()
|
||
expect(bridge.cancel).toHaveBeenCalledOnce()
|
||
expect(onConnected).toHaveBeenCalledOnce()
|
||
})
|
||
|
||
it('cancels on Vazgeç and returns to the sign-in button', async () => {
|
||
const { bridge, onConnected } = setupLogin([])
|
||
await click('Giriş yap')
|
||
await click('Vazgeç')
|
||
|
||
expect(bridge.cancel).toHaveBeenCalledOnce()
|
||
expect(screen.getByRole('button', { name: 'Giriş yap' })).toBeTruthy()
|
||
await tick()
|
||
expect(bridge.poll).not.toHaveBeenCalled()
|
||
expect(onConnected).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('reports an expired code and a failed start without connecting', async () => {
|
||
const { bridge, onConnected } = setupLogin([{ state: 'expired' }])
|
||
await click('Giriş yap')
|
||
await tick()
|
||
expect(screen.getByRole('alert').textContent).toBe('Giriş kodunun süresi doldu. Yeniden deneyin.')
|
||
|
||
bridge.start.mockRejectedValueOnce(
|
||
new Error("Error invoking remote method 'aiturk:login:start': AiturkLoginError: aiturk-login:rate-limited")
|
||
)
|
||
await click('Giriş yap')
|
||
expect(screen.getByRole('alert').textContent).toBe('Çok fazla deneme yapıldı. Biraz sonra yeniden deneyin.')
|
||
expect(onConnected).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('revokes a pending login when the panel goes away', async () => {
|
||
const { bridge } = setupLogin([])
|
||
await click('Giriş yap')
|
||
cleanup()
|
||
|
||
expect(bridge.cancel).toHaveBeenCalledOnce()
|
||
})
|
||
|
||
it('still connects with a pasted API key', async () => {
|
||
vi.mocked(validateCustomEndpoint).mockResolvedValue({ ok: true, reachable: true, models: ['account/model-a'], message: '' })
|
||
vi.mocked(saveCustomEndpoint).mockResolvedValue({ id: 'turkservis', endpoints: [], current: { base_url: 'https://ai.turkservis.online/v1', model: 'account/model-a', provider: 'turkservis' }, ok: true })
|
||
const { bridge, onConnected } = setupLogin([])
|
||
vi.useRealTimers()
|
||
|
||
expect(screen.getByText('ya da API anahtarı yapıştırın')).toBeTruthy()
|
||
fireEvent.change(screen.getByLabelText('TurkServis API anahtarı'), { target: { value: 'test-key-never-real' } })
|
||
fireEvent.click(screen.getByRole('button', { name: 'Bağlan' }))
|
||
|
||
await waitFor(() => expect(onConnected).toHaveBeenCalledOnce())
|
||
expect(saveCustomEndpoint).toHaveBeenCalledWith(expect.objectContaining({ api_key: 'test-key-never-real', model: 'account/model-a' }))
|
||
expect(bridge.start).not.toHaveBeenCalled()
|
||
})
|
||
})
|