Files
aiturk-hermes-ide/apps/desktop/src/components/onboarding/aiturk-provider.test.tsx
T
yilsemandClaude Opus 5.5 25d8c18c10 feat(aiturk): one-click sign-in for AITURK IDE (1.0.0-beta.14)
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
2026-10-03 15:13:23 +03:00

216 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
})
})