179 lines
12 KiB
JavaScript
179 lines
12 KiB
JavaScript
import { chromium } from 'playwright'
|
||
import { createServer } from 'node:http'
|
||
import { readFile, mkdir } from 'node:fs/promises'
|
||
import { extname, resolve } from 'node:path'
|
||
import assert from 'node:assert/strict'
|
||
|
||
const root = resolve('dist')
|
||
const server = createServer(async (request, response) => {
|
||
try {
|
||
const path = decodeURIComponent(new URL(request.url, 'http://localhost').pathname)
|
||
const file = resolve(root, '.' + (path === '/' ? '/index.html' : path))
|
||
if (!file.startsWith(root + '\\') && !file.startsWith(root + '/')) throw new Error('outside assets')
|
||
const data = await readFile(file)
|
||
response.writeHead(200, { 'Content-Type': ({ '.html': 'text/html', '.mjs': 'text/javascript', '.js': 'text/javascript', '.css': 'text/css' })[extname(file)] || 'application/octet-stream' }); response.end(data)
|
||
} catch { response.writeHead(404); response.end() }
|
||
})
|
||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
|
||
const browser = await chromium.launch({ channel: 'msedge', headless: true })
|
||
try {
|
||
const page = await browser.newPage({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 1 })
|
||
page.setDefaultTimeout(15000)
|
||
const errors = []; page.on('pageerror', error => errors.push(error.message))
|
||
await page.goto(`http://127.0.0.1:${server.address().port}`)
|
||
await page.getByRole('heading', { name: 'Fikrini konuş. Birlikte geliştir.' }).waitFor()
|
||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false)
|
||
assert.ok((await page.getByRole('button', { name: 'Eşleştirme kodu oluştur' }).boundingBox()).height >= 48)
|
||
// UI wiring only: real account authentication is covered by gateway HTTP/DB
|
||
// tests. Never submit a credential to the live portal from this smoke test.
|
||
const loginCalls = []
|
||
await page.route('https://turkservis.online/v1/aiturk/mobile-auth/**', async route => {
|
||
const action = new URL(route.request().url()).pathname.split('/').at(-1); loginCalls.push(action)
|
||
const result = action === 'start' ? { deviceCode: 'tsml_fixture', userCode: 'ABCDEFGH',
|
||
verificationUri: 'https://turkservis.online/account/aiturk/mobile-login/11111111-1111-4111-8111-111111111111' }
|
||
: action === 'poll' ? { state: 'approved', accountToken: 'tsml_fixture' } : { revoked: true }
|
||
await route.fulfill({ status: 200, contentType: 'application/json', headers: { 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify(result) })
|
||
})
|
||
await page.getByRole('button', { name: 'AITURK hesabımla giriş yap' }).click()
|
||
await page.getByText('Yalnız kendi hesabınızda bu kodu onaylayın: ABCDEFGH').waitFor()
|
||
await page.getByRole('button', { name: 'Girişi kontrol et' }).click()
|
||
await page.getByText('Hesaba giriş yapıldı.', { exact: true }).waitFor()
|
||
let claimed = false
|
||
await page.route('https://turkservis.online/v1/aiturk/mobile/invites/**/claim', async route => {
|
||
claimed = true
|
||
const body = route.request().postDataJSON(); assert.equal(body.secret, 'a'.repeat(43))
|
||
await route.fulfill({ status: 201, contentType: 'application/json', headers: { 'Access-Control-Allow-Origin': '*' },
|
||
body: JSON.stringify({ device_id: '33333333-3333-4333-8333-333333333333', device_token: 'fixture-device-secret', pairing_id: 'fixture-pair', user_code: 'QRSTUVWX' }) })
|
||
})
|
||
const qrImage = await page.evaluate(async () => {
|
||
const { drawQr } = await import('./qr.mjs'), canvas = document.createElement('canvas')
|
||
window.fixtureDesktopCertificate = await RTCPeerConnection.generateCertificate({ name: 'ECDSA', namedCurve: 'P-256' })
|
||
const desktopFingerprint = window.fixtureDesktopCertificate.getFingerprints().find(item => item.algorithm === 'sha-256').value
|
||
drawQr(canvas, JSON.stringify({ kind: 'aiturk-mobile-invite', version: 2, desktopFingerprint, inviteId: '11111111-1111-4111-8111-111111111111',
|
||
desktopId: '22222222-2222-4222-8222-222222222222', secret: 'a'.repeat(43), expiresAt: new Date(Date.now() + 600000).toISOString() }))
|
||
return canvas.toDataURL('image/png').split(',')[1]
|
||
})
|
||
await page.locator('#qr-file').setInputFiles({ name: 'invite.png', mimeType: 'image/png', buffer: Buffer.from(qrImage, 'base64') })
|
||
await page.getByText('Bilgisayarda bekleyen telefonu getirip şu kodu onaylayın: QRSTUVWX').waitFor()
|
||
assert.equal(claimed, true)
|
||
const phoneQr = await page.evaluate(() => {
|
||
const canvas = document.getElementById('phone-identity-qr')
|
||
const pixels = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height)
|
||
return { decoded: jsQR(pixels.data, pixels.width, pixels.height)?.data,
|
||
expected: document.getElementById('phone-identity').value, visible: !canvas.hidden }
|
||
})
|
||
assert.equal(phoneQr.visible, true)
|
||
assert.equal(phoneQr.decoded, phoneQr.expected)
|
||
// Run the actual app -> AccountClient -> WebRTC -> VoiceSession wiring.
|
||
// Portal, IDE responses and Google frames are synthetic; no paid/API traffic.
|
||
await page.route('https://turkservis.online/v1/aiturk/mobile/devices', route => route.fulfill({
|
||
json: { devices: [{ id: '22222222-2222-4222-8222-222222222222', name: 'Test bilgisayarı', platform: 'windows', self: false }] } }))
|
||
await page.route('https://turkservis.online/v1/aiturk/mobile/ice', route => route.fulfill({ json: { iceServers: [] } }))
|
||
await page.route('https://turkservis.online/v1/aiturk/mobile/channels**', async route => {
|
||
if (route.request().method() === 'POST' && !route.request().url().endsWith('/heartbeat')) {
|
||
const { sdp } = route.request().postDataJSON()
|
||
await page.evaluate(async sdp => {
|
||
const { completeIce, sendFrame } = await import('./transport.mjs')
|
||
const { assertRemoteIdentity } = await import('./peer-identity.mjs')
|
||
const trustedPhone = JSON.parse(document.getElementById('phone-identity').value)
|
||
assertRemoteIdentity(sdp, trustedPhone.fingerprint)
|
||
const pc = new RTCPeerConnection({ certificates: [window.fixtureDesktopCertificate] }); window.fixtureDesktop = pc; window.fixtureActions = []
|
||
pc.ondatachannel = event => { event.channel.onmessage = incoming => {
|
||
const request = JSON.parse(incoming.data); window.fixtureActions.push(request.action)
|
||
const results = {
|
||
'sessions.list': { sessions: [{ id: 'fixture-session', title: 'Test işi', model: 'fixture-model' }] },
|
||
'session.open': { model: 'fixture-model', messages: [], approval: null, running: false },
|
||
'voice.token': { token: 'fixture-only', model: 'gemini-3.1-flash-live-preview' },
|
||
}
|
||
sendFrame(event.channel, { id: request.id, result: results[request.action] || {} })
|
||
} }
|
||
await pc.setRemoteDescription({ type: 'offer', sdp })
|
||
await pc.setLocalDescription(await pc.createAnswer()); await completeIce(pc)
|
||
window.fixtureAnswer = pc.localDescription.sdp
|
||
}, sdp)
|
||
await route.fulfill({ json: {} })
|
||
} else await route.fulfill({ json: { sdp: await page.evaluate(() => window.fixtureAnswer) } })
|
||
})
|
||
await page.evaluate(() => {
|
||
window.fixtureVoiceSockets = []
|
||
window.WebSocket = class {
|
||
constructor(url) {
|
||
if (!url.startsWith('wss://generativelanguage.googleapis.com/')) throw new Error('Unexpected test socket')
|
||
this.readyState = 1; this.bufferedAmount = 0; window.fixtureVoiceSockets.push(this)
|
||
queueMicrotask(() => this.onopen?.())
|
||
}
|
||
send(raw) {
|
||
if (JSON.parse(raw).setup) queueMicrotask(() => this.onmessage?.({ data: JSON.stringify({ setupComplete: {} }) }))
|
||
}
|
||
close() { this.readyState = 3; this.onclose?.() }
|
||
}
|
||
})
|
||
await page.getByRole('button', { name: 'Onaydan sonra bilgisayarları bul' }).click()
|
||
await page.getByRole('button', { name: 'Bilgisayara bağlan', exact: true }).click()
|
||
await page.getByText('Bilgisayar bağlı. IDE konuşmasını seçin.', { exact: true }).waitFor()
|
||
await page.getByRole('button', { name: 'Konuşmaları yenile' }).click()
|
||
await page.getByRole('button', { name: 'Bu konuşmayı seç', exact: true }).click()
|
||
await page.getByText('IDE konuşması açıldı.', { exact: true }).waitFor()
|
||
await page.locator('#consent').check()
|
||
await page.locator('#voice-budget').fill('0.01')
|
||
await page.locator('#voice-minutes').fill('3')
|
||
await page.getByRole('button', { name: 'Asistanla yazış', exact: true }).click()
|
||
await page.getByText('Google asistanı bağlı.', { exact: true }).waitFor()
|
||
assert.match(await page.locator('#usage').textContent(), /henüz bilinmiyor/)
|
||
await page.evaluate(() => window.fixtureVoiceSockets[0].onmessage({ data: JSON.stringify({ usageMetadata: {
|
||
promptTokenCount: 1000, responseTokenCount: 1000, totalTokenCount: 2000,
|
||
promptTokensDetails: [{ modality: 'AUDIO', tokenCount: 1000 }],
|
||
responseTokensDetails: [{ modality: 'AUDIO', tokenCount: 1000 }],
|
||
} }) }))
|
||
await page.locator('#voice-status').filter({ hasText: 'maliyet sınırına ulaşıldı' }).waitFor()
|
||
assert.match(await page.locator('#usage').textContent(), /0\.0150–0\.0150 USD/)
|
||
assert.equal(await page.evaluate(() => window.fixtureVoiceSockets[0].readyState), 3)
|
||
assert.deepEqual(await page.evaluate(() => window.fixtureActions), ['sessions.list', 'session.open', 'voice.token'])
|
||
assert.deepEqual(await page.evaluate(() => JSON.parse(localStorage.getItem('aiturk.voice-limits'))), { minutes: 3, usd: 0.01 })
|
||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false)
|
||
await mkdir('test-results', { recursive: true })
|
||
await page.screenshot({ path: 'test-results/android-voice-limit.png', fullPage: true })
|
||
await page.evaluate(() => window.fixtureDesktop.close())
|
||
// A signaling service returning another machine's valid SDP must not replace
|
||
// the identity captured from the user's QR or receive any IDE requests.
|
||
await page.evaluate(async () => {
|
||
window.fixtureDesktopCertificate = await RTCPeerConnection.generateCertificate({ name: 'ECDSA', namedCurve: 'P-256' })
|
||
})
|
||
await page.getByRole('button', { name: 'Bilgisayara bağlan', exact: true }).click()
|
||
await page.locator('#status').filter({ hasText: 'Cihaz kimliği doğrulanamadı' }).waitFor()
|
||
assert.deepEqual(await page.evaluate(() => window.fixtureActions), [])
|
||
await page.evaluate(() => window.fixtureDesktop.close())
|
||
await page.getByRole('button', { name: 'Bu uygulamadaki hesaptan çık' }).click()
|
||
await page.getByText('Bu uygulamanın oturumu kapatıldı.', { exact: false }).waitFor()
|
||
assert.deepEqual(loginCalls, ['start', 'poll', 'logout'])
|
||
await mkdir('test-results', { recursive: true })
|
||
await page.screenshot({ path: 'test-results/android-onboarding.png', fullPage: true })
|
||
const result = await page.evaluate(async () => {
|
||
const { completeIce, DataRequests, sendFrame } = await import('./transport.mjs')
|
||
const phone = new RTCPeerConnection(), desktop = new RTCPeerConnection()
|
||
try {
|
||
const channel = phone.createDataChannel('aiturk-mobile-v1'), requests = new DataRequests(channel, () => {})
|
||
const remoteReady = new Promise(resolve => { desktop.ondatachannel = event => {
|
||
event.channel.onmessage = incoming => {
|
||
const request = JSON.parse(incoming.data)
|
||
sendFrame(event.channel, { id: request.id, result: { action: request.action, text: 'Türkçe 🟢 '.repeat(24000) } })
|
||
}
|
||
event.channel.onopen = resolve
|
||
} })
|
||
await phone.setLocalDescription(await phone.createOffer()); await completeIce(phone)
|
||
await desktop.setRemoteDescription(phone.localDescription)
|
||
await desktop.setLocalDescription(await desktop.createAnswer()); await completeIce(desktop)
|
||
await phone.setRemoteDescription(desktop.localDescription)
|
||
await Promise.race([remoteReady, new Promise((_, reject) => setTimeout(() => reject(new Error('WebRTC timeout')), 15000))])
|
||
if (channel.readyState !== 'open') await new Promise(resolve => channel.onopen = resolve)
|
||
const response = await requests.call('session.history', { sessionId: 'fixture' })
|
||
return { length: response.text.length, expected: 'Türkçe 🟢 '.repeat(24000).length, action: response.action,
|
||
connectionState: phone.connectionState, dtls: phone.sctp.transport.state }
|
||
} finally { phone.close(); desktop.close() }
|
||
})
|
||
assert.equal(result.length, result.expected); assert.equal(result.dtls, 'connected'); assert.equal(result.action, 'session.history')
|
||
assert.deepEqual(errors, [])
|
||
console.log(JSON.stringify({ onboarding: 'passed', mockedLoginUi: 'passed', realQrImageWithMockedClaim: 'passed',
|
||
appWebRTCWithSyntheticGoogleUsageLimit: 'passed', qrPinnedDesktopReplacement: 'rejected', realWebRTC: result, errors }))
|
||
} finally { await browser.close(); server.close() }
|