예전 배포본(HEAD)의 상세 기능을 새 아키텍처(인증=.NET 백엔드, 데이터=Supabase) 위에 실데이터로 복원. 예전 HEAD는 서명 쿠키를 거부하고 위조 쿠키는 통과시키는 인증 결함이 있었고, 삭제된 페이지 다수는 백엔드 호출 0인 하드코딩 목업이었음. 인증/세션 - 로그인 이메일 전용화(username 폐지), 에러 키별 안내 메시지 - ADMIN_COOKIE_SECURE 옵션: TLS 없는 LAN HTTP 배포에서 Secure 쿠키 유실로 로그인이 유지되지 않던 문제 해결 (login/logout route, admin-session, compose, .env.example) - Supabase 미설정 시 우아한 저하: isSupabaseAdminConfigured + UnavailableAdminPanel 기능 복원 (실데이터) - Release Hub: Forgejo API 실데이터(다운로드 수/SHA-256 체크섬/릴리스 이력) - Ad Monetization: 데스크톱 미디에이션 10개 어댑터 로스터(fail-closed) + ad_reward_claims 통계 - License Issuer: 서버사이드 Ed25519 서명(/api/admin/license, super_admin 전용), 개인키는 ADMIN_LICENSE_PRIVATE_KEY env로만, 발급 감사를 .NET AdminAuditEntries에 기록 - Service Models: STT 7종/LLM 5종 프리셋 드롭다운 + 자동채움 - 대시보드 ARR/MRR KPI: Supabase 구독 실집계(티어 월단가 기반) - 사용자 상세 티어별 기능 배지(pro_plus 조건부) .NET - SuperAdminOnly 정책 추가, /api/admin/license-audit 엔드포인트, LicenseAuditDto
101 lines
5.5 KiB
JavaScript
101 lines
5.5 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { chromium } from 'playwright'
|
|
|
|
const baseUrl = process.env.D3RO_ADMIN_E2E_URL?.trim() ?? ''
|
|
if (!baseUrl.startsWith('https://127.0.0.1:')) throw new Error('Local HTTPS admin E2E URL is required')
|
|
|
|
const browser = await chromium.launch({ headless: true })
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, ignoreHTTPSErrors: true })
|
|
await page.addInitScript(() => {
|
|
window.alert = () => undefined
|
|
window.confirm = () => true
|
|
window.prompt = () => 'browser verified endpoint deletion'
|
|
})
|
|
const consoleErrors = []
|
|
const serverErrors = []
|
|
page.on('console', (message) => {
|
|
if (message.type() === 'error') consoleErrors.push(message.text())
|
|
})
|
|
page.on('response', (response) => {
|
|
if (response.status() >= 500) serverErrors.push(`${response.status()} ${response.url()}`)
|
|
})
|
|
|
|
try {
|
|
await page.goto(`${baseUrl}/`, { waitUntil: 'networkidle' })
|
|
await page.getByLabel('Admin Identifier (Username / Email)').fill('admin.browser.e2e@example.com')
|
|
await page.getByLabel('Password').fill('Admin-browser-e2e-strong-password!')
|
|
await Promise.all([
|
|
page.waitForURL(`${baseUrl}/`),
|
|
page.getByRole('button', { name: 'Sign In as Super Admin' }).click()
|
|
])
|
|
await page.getByText('BACKEND CONNECTED').waitFor()
|
|
|
|
const endpointPayload = await page.evaluate(async () => {
|
|
const response = await fetch('/api/admin/backend/endpoints', { cache: 'no-store' })
|
|
return { status: response.status, body: await response.json() }
|
|
})
|
|
assert.equal(endpointPayload.status, 200)
|
|
assert.ok(Array.isArray(endpointPayload.body))
|
|
assert.ok(endpointPayload.body.every((endpoint) => endpoint.apiKey === '' || endpoint.apiKey === '••••••••'))
|
|
|
|
await page.goto(`${baseUrl}/models`, { waitUntil: 'networkidle' })
|
|
await page.getByRole('tab', { name: /LLM & Reasoning Model Endpoints/ }).click()
|
|
const addModelButton = page.getByRole('button', { name: /Add LLM Model Endpoint/ })
|
|
if (await addModelButton.count() === 0) {
|
|
const cookies = await page.context().cookies()
|
|
const bodyText = (await page.locator('body').innerText()).slice(0, 1200)
|
|
throw new Error(`models diagnostic url=${page.url()} cookies=${cookies.map((cookie) => `${cookie.name}:${cookie.secure}:${cookie.sameSite}`).join(',')} body=${bodyText}`)
|
|
}
|
|
await addModelButton.click()
|
|
await page.getByLabel('Model ID').fill('browser-e2e-model')
|
|
await page.getByLabel('Display Name').fill('Browser E2E Model')
|
|
await page.getByLabel('Endpoint URL').fill('https://models.example.com/v1')
|
|
await page.getByLabel('Audit memo').last().fill('browser verified endpoint creation')
|
|
const createResponse = page.waitForResponse((response) => response.url().includes('/api/admin/backend/endpoints') && response.request().method() === 'POST')
|
|
await page.getByRole('button', { name: 'Save Endpoint' }).click()
|
|
const created = await createResponse
|
|
assert.equal(created.status(), 200, `model creation failed: ${await created.text()}`)
|
|
await page.getByText('Browser E2E Model').waitFor()
|
|
|
|
const modelRow = page.locator('tr').filter({ hasText: 'browser-e2e-model' })
|
|
const deleteResponse = page.waitForResponse((response) => response.url().includes('/api/admin/backend/endpoints/') && response.request().method() === 'DELETE')
|
|
await modelRow.getByRole('button', { name: 'Delete' }).click()
|
|
const deleted = await deleteResponse
|
|
assert.equal(deleted.status(), 200, `model deletion failed: ${await deleted.text()}`)
|
|
await page.getByText('Browser E2E Model').waitFor({ state: 'detached' })
|
|
|
|
await page.goto(`${baseUrl}/users`, { waitUntil: 'networkidle' })
|
|
const customerRow = page.locator('tr').filter({ hasText: 'customer.browser.e2e@example.com' })
|
|
await customerRow.waitFor()
|
|
const customerHref = await customerRow.getByRole('link').first().getAttribute('href')
|
|
assert.match(customerHref ?? '', /^\/users\/[0-9a-f-]{36}$/)
|
|
const customerId = customerHref.split('/').pop()
|
|
|
|
await page.goto(`${baseUrl}${customerHref}`, { waitUntil: 'networkidle' })
|
|
await page.getByRole('button', { name: 'Change Role' }).click()
|
|
const roleDialog = page.getByRole('dialog', { name: /CHANGE USER ROLE/ })
|
|
await roleDialog.waitFor()
|
|
await roleDialog.getByRole('combobox').click()
|
|
await page.getByRole('option', { name: 'manager' }).click()
|
|
await page.getByPlaceholder('Reason for role change (required)...').fill('browser verified support assignment')
|
|
await page.getByRole('button', { name: 'Change Role', exact: true }).last().click()
|
|
await page.getByText('MANAGER').first().waitFor()
|
|
|
|
await page.goto(`${baseUrl}/subscriptions/${customerId}`, { waitUntil: 'networkidle' })
|
|
await page.getByRole('combobox', { name: 'Tier' }).click()
|
|
await page.getByRole('option', { name: 'PRO', exact: true }).click()
|
|
await page.getByLabel('Memo (required for audit log)').fill('browser verified subscription update')
|
|
await page.getByRole('button', { name: 'Update' }).click()
|
|
await page.getByText('Operation successful').waitFor()
|
|
|
|
await page.goto(`${baseUrl}/audit-log`, { waitUntil: 'networkidle' })
|
|
await page.getByText('user.role_change').first().waitFor()
|
|
await page.getByText('subscription.update').first().waitFor()
|
|
await page.screenshot({ path: 'scratch/admin-browser-e2e.png', fullPage: true })
|
|
|
|
assert.deepEqual(serverErrors, [], `server errors: ${serverErrors.join(', ')}`)
|
|
assert.deepEqual(consoleErrors, [], `console errors: ${consoleErrors.join(', ')}`)
|
|
console.log('admin browser E2E: login, JWT proxy, model create/delete, users, role, subscription, audit GREEN')
|
|
} finally {
|
|
await browser.close()
|
|
}
|