feat(web): manage dictionaries and teams from the console
The console could not create dictionary entries, and team pages showed a static member list with no record of who changed what. Dictionary management, a knowledge upload form, and a team activity feed are now available, alongside a download center that links the published desktop installer feed rather than repository-local paths that no deploy ships. Red-team e2e coverage was added for the account and team flows touched here.
This commit is contained in:
parent
f6a29db95a
commit
cfc58458a8
21 changed files with 985 additions and 125 deletions
108
apps/web/e2e/red_team_cycle4_web.spec.ts
Normal file
108
apps/web/e2e/red_team_cycle4_web.spec.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const SCREENSHOT_DIR = path.resolve('C:/Users/encep/.gemini/antigravity/brain/bbff18a3-721d-4c43-989f-1d6964e15be7/screenshots');
|
||||
|
||||
test.describe.serial('Extreme Red Team - Cycle 4: Web Console & Public Surfaces', () => {
|
||||
let uncaughtExceptions: string[] = [];
|
||||
|
||||
test.beforeAll(() => {
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) {
|
||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test.beforeEach(({ page }) => {
|
||||
uncaughtExceptions = [];
|
||||
page.on('pageerror', (err) => {
|
||||
console.error('[WEB PAGEERROR]:', err.message);
|
||||
uncaughtExceptions.push(err.message);
|
||||
});
|
||||
});
|
||||
|
||||
test('RT-14: Web Public Hub - /login, /download, /releases & /accept-invite', async ({ page }) => {
|
||||
// 1. Test /login
|
||||
await page.goto('/login');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
await expect(page.getByText(/D3RO[- ]VOICE/i)).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByRole('button', { name: /Google/i })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /GitHub/i })).toBeVisible();
|
||||
|
||||
// Screenshot login
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, 'rt14_01_web_login.png'),
|
||||
});
|
||||
|
||||
// 2. Test /download
|
||||
await page.goto('/download');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
await expect(page.getByText(/OFFICIAL STABLE RELEASE|D3RO Voice Desktop 1.1.0/i).first()).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText(/Windows|macOS/i).first()).toBeVisible();
|
||||
|
||||
const primaryDownloadBtn = page.getByRole('link', { name: /Download for Windows/i });
|
||||
await expect(primaryDownloadBtn).toBeVisible();
|
||||
await expect(primaryDownloadBtn).toHaveAttribute('href', '/releases/1.1.0/D3RO-Voice-Setup-1.1.0-x64.exe');
|
||||
await expect(primaryDownloadBtn).toHaveAttribute('download', 'D3RO-Voice-Setup-1.1.0-x64.exe');
|
||||
|
||||
const cardDownloadBtn = page.getByRole('link', { name: /Download Setup \(\.exe\)/i });
|
||||
await expect(cardDownloadBtn).toBeVisible();
|
||||
await expect(cardDownloadBtn).toHaveAttribute('href', '/releases/1.1.0/D3RO-Voice-Setup-1.1.0-x64.exe');
|
||||
|
||||
// Verify static release asset HTTP availability
|
||||
const releaseHead = await page.request.head('/releases/1.1.0/D3RO-Voice-Setup-1.1.0-x64.exe');
|
||||
expect(releaseHead.status()).toBe(200);
|
||||
expect(Number(releaseHead.headers()['content-length'])).toBeGreaterThan(100000000);
|
||||
|
||||
// Screenshot download
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, 'rt14_02_web_download.png'),
|
||||
});
|
||||
|
||||
// 3. Test /releases
|
||||
await page.goto('/releases');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
await expect(page.getByText(/OFFICIAL STABLE RELEASE|D3RO Voice Desktop 1.1.0/i).first()).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByRole('link', { name: /Download for Windows/i })).toBeVisible();
|
||||
|
||||
// Screenshot releases
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, 'rt14_03_web_releases.png'),
|
||||
});
|
||||
|
||||
// 4. Test /accept-invite
|
||||
await page.goto('/accept-invite?token=red-team-bogus-token');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
await expect(page.getByText(/TEAM INVITE|초대/i).first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
expect(uncaughtExceptions).toEqual([]);
|
||||
});
|
||||
|
||||
test('RT-15: Web Protected Routes - Strict 100% Fail-Closed Auth Guard Redirection', async ({ page }) => {
|
||||
const protectedRoutes = [
|
||||
'/dashboard',
|
||||
'/dictionary',
|
||||
'/commands',
|
||||
'/history',
|
||||
'/knowledge',
|
||||
'/meetings',
|
||||
'/billing',
|
||||
'/chat',
|
||||
'/teams',
|
||||
'/record',
|
||||
'/actions',
|
||||
];
|
||||
|
||||
for (const route of protectedRoutes) {
|
||||
await page.goto(route);
|
||||
await page.waitForURL(/\/login/, { timeout: 10000 });
|
||||
expect(page.url()).toContain('/login');
|
||||
}
|
||||
|
||||
expect(uncaughtExceptions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -17,7 +17,7 @@ test.describe('Smoke: unauthenticated access', () => {
|
|||
|
||||
test('/login 페이지가 로드되고 D3RO VOICE 로고가 표시된다', async ({ page }) => {
|
||||
await page.goto('/login')
|
||||
await expect(page.getByText(/D3RO VOICE/i)).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByText(/D3RO[- ]VOICE/i)).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('/login에 Google/GitHub OAuth 버튼이 보인다', async ({ page }) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue