Align capacity units and scope administrator network edits
This commit is contained in:
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { resolve } from 'node:path';
|
||||
import { chromium } from '@playwright/test';
|
||||
import { chromium, expect } from '@playwright/test';
|
||||
import { createApp } from '../../apps/server/src/app.ts';
|
||||
import { loadConfig } from '../../apps/server/src/config.ts';
|
||||
import { users } from '../../apps/server/src/db/schema.ts';
|
||||
@@ -28,7 +28,7 @@ const runtime = await createApp(
|
||||
);
|
||||
// A NAS-sized disk verifies comma grouping in the rendered admin card.
|
||||
const realSummary = runtime.service.admin.summary.bind(runtime.service.admin);
|
||||
runtime.service.admin.summary = () => ({ ...realSummary(), diskFreeBytes: 7687e9 });
|
||||
runtime.service.admin.summary = () => ({ ...realSummary(), diskFreeBytes: 7687 * 1024 ** 3 });
|
||||
let browser;
|
||||
try {
|
||||
await runtime.app.listen({ host: '127.0.0.1', port: 0 });
|
||||
@@ -199,11 +199,9 @@ try {
|
||||
await composer.locator('.selected-files').waitFor();
|
||||
await admin.getByRole('button', { name: '네트워크', exact: true }).click();
|
||||
async function banFiles(value) {
|
||||
await admin.getByRole('button', { name: '수정', exact: true }).click();
|
||||
const toggle = admin.getByRole('switch', { name: '파일 전송 금지', exact: true });
|
||||
if ((await toggle.getAttribute('aria-checked')) !== String(value)) await toggle.click();
|
||||
await admin.getByRole('button', { name: '네트워크 설정 저장', exact: true }).click();
|
||||
await admin.getByRole('button', { name: '수정', exact: true }).waitFor();
|
||||
await expect(toggle).toHaveAttribute('aria-checked', String(value));
|
||||
}
|
||||
await banFiles(true);
|
||||
assert.equal(runtime.service.admin.state.uploadsPaused, true);
|
||||
@@ -270,7 +268,7 @@ try {
|
||||
await admin.getByRole('button', { name: '용량 설정', exact: true }).click();
|
||||
const capacityForm = admin.locator('.admin-settings');
|
||||
assert.equal(await capacityForm.locator('input').count(), 0);
|
||||
assert.match(await capacityForm.innerText(), /로그인 보관함 계정당 한도[\s\S]*2\.1GB/);
|
||||
assert.match(await capacityForm.innerText(), /로그인 보관함 계정당 한도[\s\S]*2\.0GB/);
|
||||
await admin.getByRole('button', { name: '수정', exact: true }).click();
|
||||
const rowInputs = await admin.locator('.admin-settings-grid input').evaluateAll((inputs) =>
|
||||
inputs.slice(0, 2).map((input) => ({
|
||||
@@ -305,25 +303,59 @@ try {
|
||||
await admin.getByRole('button', { name: '설정 저장', exact: true }).click();
|
||||
await admin.getByText('관리 설정을 저장했어요.', { exact: true }).waitFor();
|
||||
assert.equal(runtime.service.admin.state.maxConcurrentUploads, 3);
|
||||
assert.equal(runtime.service.admin.state.totalStorageLimitMb, Math.round(16e9 / 1048576));
|
||||
assert.equal(runtime.service.admin.state.totalStorageLimitMb, 16 * 1024);
|
||||
for (const [key, expected] of Object.entries({
|
||||
accountBytes: 3e9,
|
||||
deviceBytes: 4e9,
|
||||
devicesTotalBytes: 12e9,
|
||||
perFileBytes: 1e9,
|
||||
accountBytes: 3 * 1024 ** 3,
|
||||
deviceBytes: 4 * 1024 ** 3,
|
||||
devicesTotalBytes: 12 * 1024 ** 3,
|
||||
perFileBytes: 1024 ** 3,
|
||||
minDiskFreeBytes: 0,
|
||||
}))
|
||||
assert.equal(runtime.service.admin.state[key], expected);
|
||||
assert.equal(await capacityForm.locator('input').count(), 0);
|
||||
await admin.screenshot({ path: 'tests/e2e/admin-capacity-desktop.png', fullPage: true });
|
||||
// Fixed precision is formatting, not an automatic save or a quota conversion.
|
||||
await admin.getByRole('button', { name: '수정', exact: true }).click();
|
||||
const singleFile = admin.getByLabel('파일 한 개 한도 (GB)', { exact: true });
|
||||
assert.equal(await singleFile.inputValue(), '1.000');
|
||||
await singleFile.fill('');
|
||||
assert.equal(await singleFile.getAttribute('placeholder'), '0.000');
|
||||
await singleFile.fill('15');
|
||||
await singleFile.blur();
|
||||
assert.equal(await singleFile.inputValue(), '15.000');
|
||||
await singleFile.fill('15.125');
|
||||
await singleFile.blur();
|
||||
assert.equal(await singleFile.inputValue(), '15.125');
|
||||
await singleFile.fill('1.2345');
|
||||
assert.equal(await singleFile.evaluate((input) => input.checkValidity()), false);
|
||||
const totalLimit = admin.getByLabel('전체 파일 저장 한도 (GB)', { exact: true });
|
||||
await totalLimit.fill('1234567');
|
||||
assert.equal(await totalLimit.evaluate((input) => input.checkValidity()), false);
|
||||
const beforeReset = { ...runtime.service.admin.state };
|
||||
const defaults = await admin.evaluate(
|
||||
async () => (await (await fetch('/api/admin/summary')).json()).capacityDefaults,
|
||||
);
|
||||
await admin.getByRole('button', { name: '기본값으로 돌리기', exact: true }).click();
|
||||
assert.equal(await totalLimit.inputValue(), '20.000');
|
||||
assert.equal(await singleFile.inputValue(), '2.000');
|
||||
assert.equal(await totalLimit.evaluate((input) => input.checkValidity()), true);
|
||||
assert.deepEqual(runtime.service.admin.state, beforeReset, 'reset fills the form without saving');
|
||||
await admin.getByRole('button', { name: '취소', exact: true }).click();
|
||||
assert.deepEqual(runtime.service.admin.state, beforeReset, 'cancel preserves saved settings');
|
||||
await admin.getByRole('button', { name: '수정', exact: true }).click();
|
||||
await admin.getByRole('button', { name: '기본값으로 돌리기', exact: true }).click();
|
||||
await admin.getByRole('button', { name: '설정 저장', exact: true }).click();
|
||||
await admin.getByRole('button', { name: '수정', exact: true }).waitFor();
|
||||
for (const [key, value] of Object.entries(defaults))
|
||||
assert.equal(runtime.service.admin.state[key], value, key);
|
||||
assert.equal(
|
||||
runtime.service.admin.state.uploadsPaused,
|
||||
false,
|
||||
'capacity reset never changes file policy',
|
||||
);
|
||||
await admin.getByRole('button', { name: '네트워크', exact: true }).click();
|
||||
assert.equal(await admin.locator('.network-auto-fields input').count(), 0);
|
||||
assert.equal(await admin.getByRole('switch').count(), 0);
|
||||
await admin.getByRole('button', { name: '수정', exact: true }).click();
|
||||
await admin.getByLabel('로그인 실패 (회)', { exact: true }).fill('50');
|
||||
await admin.getByRole('button', { name: '취소', exact: true }).click();
|
||||
assert.equal(await admin.getByRole('switch').count(), 0);
|
||||
await admin.getByRole('button', { name: '수정', exact: true }).click();
|
||||
assert.equal(await admin.getByRole('switch').count(), 3);
|
||||
async function addRule(page, list, kind, value) {
|
||||
await page.getByRole('button', { name: list + ' 추가', exact: true }).click();
|
||||
const modal = page.getByRole('dialog', { name: list + ' 추가', exact: true });
|
||||
@@ -336,6 +368,28 @@ try {
|
||||
}
|
||||
await addRule(admin, '블랙리스트', 'country', 'cn');
|
||||
await addRule(admin, '블랙리스트', 'ip', '9.9.9.*');
|
||||
assert.deepEqual(runtime.service.admin.state.blockedCountries, ['CN']);
|
||||
assert.deepEqual(runtime.service.admin.state.blockedIps, ['9.9.9.0/24']);
|
||||
assert.equal(await admin.locator('.network-auto-fields input').count(), 0);
|
||||
await admin.getByRole('button', { name: '블랙리스트 추가', exact: true }).click();
|
||||
const blockedModal = admin.getByRole('dialog', { name: '블랙리스트 추가', exact: true });
|
||||
await blockedModal.getByLabel('국가 코드', { exact: true }).fill('KR');
|
||||
await blockedModal.getByRole('button', { name: '추가', exact: true }).click();
|
||||
await admin
|
||||
.locator('.error-toast')
|
||||
.filter({ hasText: '현재 관리자 접속을 차단하는 규칙은 저장할 수 없습니다.' })
|
||||
.waitFor();
|
||||
assert.deepEqual(runtime.service.admin.state.blockedCountries, ['CN']);
|
||||
assert.equal(await blockedModal.isVisible(), true);
|
||||
await blockedModal.getByRole('button', { name: '규칙 추가 닫기', exact: true }).click();
|
||||
await admin.getByRole('button', { name: '오류 닫기', exact: true }).click();
|
||||
await admin.getByRole('button', { name: '자동 감지 기준 수정', exact: true }).click();
|
||||
await admin.getByLabel('로그인 실패 (회)', { exact: true }).fill('50');
|
||||
await admin.getByRole('button', { name: '감지 기준 수정 취소', exact: true }).click();
|
||||
assert.equal(runtime.service.admin.state.autoLoginFailures, 10);
|
||||
assert.equal(await admin.locator('.network-auto-fields input').count(), 0);
|
||||
await admin.getByRole('button', { name: '자동 감지 기준 수정', exact: true }).click();
|
||||
await admin.getByLabel('로그인 실패 (회)', { exact: true }).fill('5');
|
||||
await addRule(admin, '화이트리스트', 'country', 'KR');
|
||||
await addRule(admin, '화이트리스트', 'ip', '8.8.8.*');
|
||||
await addRule(admin, '화이트리스트', 'ip', '4.4.4.4');
|
||||
@@ -344,11 +398,25 @@ try {
|
||||
.locator('section[aria-labelledby="whitelist-title"]')
|
||||
.getByRole('button', { name: '선택 항목 제거', exact: true })
|
||||
.click();
|
||||
assert.equal(await admin.getByLabel('로그인 실패 (회)', { exact: true }).inputValue(), '5');
|
||||
assert.equal(
|
||||
runtime.service.admin.state.autoLoginFailures,
|
||||
10,
|
||||
'list operations never save numeric drafts',
|
||||
);
|
||||
await admin.getByRole('switch', { name: '자동 블랙리스트 사용', exact: true }).click();
|
||||
await expect(
|
||||
admin.getByRole('switch', { name: '자동 블랙리스트 사용', exact: true }),
|
||||
).toHaveAttribute('aria-checked', 'true');
|
||||
assert.equal(
|
||||
runtime.service.admin.state.autoLoginFailures,
|
||||
10,
|
||||
'a toggle saves only its own flag',
|
||||
);
|
||||
await admin.getByLabel('로그인 실패 (회)', { exact: true }).fill('3');
|
||||
await admin.getByRole('button', { name: '네트워크 설정 저장', exact: true }).click();
|
||||
await admin.getByText('네트워크 설정을 저장했어요.', { exact: true }).waitFor();
|
||||
assert.equal(await admin.getByRole('switch').count(), 0);
|
||||
await admin.getByRole('button', { name: '감지 기준 저장', exact: true }).click();
|
||||
await admin.getByText('자동 감지 기준을 저장했어요.', { exact: true }).waitFor();
|
||||
assert.equal(await admin.getByRole('switch').count(), 3);
|
||||
assert.equal(runtime.service.admin.state.autoLoginFailures, 3);
|
||||
assert.deepEqual(runtime.service.admin.state.blockedIps, ['9.9.9.0/24']);
|
||||
assert.deepEqual(runtime.service.admin.state.allowedCountries, ['KR']);
|
||||
@@ -366,6 +434,14 @@ try {
|
||||
}, attackSession.csrfToken);
|
||||
await admin.getByRole('button', { name: '자동 차단 목록 새로고침', exact: true }).click();
|
||||
await admin.getByRole('checkbox', { name: '8.8.8.9 자동 차단 선택', exact: true }).waitFor();
|
||||
const networkSections = await admin.locator('.network-sections > section').evaluateAll((items) =>
|
||||
items.map((item) => {
|
||||
const { top, bottom } = item.getBoundingClientRect();
|
||||
return { top, bottom };
|
||||
}),
|
||||
);
|
||||
for (let index = 1; index < networkSections.length; index++)
|
||||
assert.ok(networkSections[index].top - networkSections[index - 1].bottom >= 20);
|
||||
await admin.screenshot({ path: 'tests/e2e/network-desktop.png', fullPage: true });
|
||||
await admin.getByRole('checkbox', { name: '8.8.8.9 자동 차단 선택', exact: true }).check();
|
||||
await admin.getByRole('button', { name: '선택 차단 해제', exact: true }).click();
|
||||
@@ -459,7 +535,10 @@ try {
|
||||
);
|
||||
assert.equal(await mobile.locator('.admin-settings input').count(), 0);
|
||||
await mobile.getByRole('button', { name: '수정', exact: true }).click();
|
||||
assert.equal(await mobile.getByLabel('파일 한 개 한도 (GB)', { exact: true }).inputValue(), '1');
|
||||
assert.equal(
|
||||
await mobile.getByLabel('파일 한 개 한도 (GB)', { exact: true }).inputValue(),
|
||||
'2.000',
|
||||
);
|
||||
assert.equal(
|
||||
await mobile.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
|
||||
true,
|
||||
@@ -471,8 +550,7 @@ try {
|
||||
await mobile.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
|
||||
true,
|
||||
);
|
||||
assert.equal(await mobile.getByRole('switch').count(), 0);
|
||||
await mobile.getByRole('button', { name: '수정', exact: true }).click();
|
||||
assert.equal(await mobile.getByRole('switch').count(), 3);
|
||||
await mobile.getByRole('button', { name: '블랙리스트 추가', exact: true }).click();
|
||||
const ruleModal = mobile.getByRole('dialog', { name: '블랙리스트 추가', exact: true });
|
||||
assert.equal(
|
||||
@@ -494,7 +572,7 @@ try {
|
||||
assert.equal(await mobile.getByRole('menuitem', { name: '관리자', exact: true }).count(), 0);
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(
|
||||
'PASS: verified hello-only menu and server authorization, admin storage/location/IP overview, file-only ban/live menu updates and clipboard-only transfer, comma-separated GB, aligned capacity inputs, saved quotas and country/IP rules, delete confirmation/audit, mobile layout and font size, logout privilege removal.',
|
||||
'PASS: administrator authorization, live file-only ban, 1024-based GB and three-decimal input, explicit default restore/save/cancel, immediate network rules/toggles independent of numeric drafts, scoped detection editor, self-block rejection, section spacing, audit and user support, desktop/mobile layouts and logout.',
|
||||
);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
|
||||
@@ -112,7 +112,7 @@ try {
|
||||
assert.equal(await card.getByText('기기 이름은 신원이 인증된 표시가 아닙니다.').count(), 0);
|
||||
if (kind === 'file') {
|
||||
await expect(card.locator('.request-file-size')).toHaveText(
|
||||
size === 1700000000 ? '1.7GB' : size === 100000 ? '100KB' : '1.3MB',
|
||||
size === 1700000000 ? '1.6GB' : size === 100000 ? '97.7KB' : '1.3MB',
|
||||
);
|
||||
const spacing = await card.locator('.request-files').evaluate((el) => ({
|
||||
padding: getComputedStyle(el).paddingLeft,
|
||||
@@ -196,7 +196,7 @@ try {
|
||||
assert.equal((await api(receiver, '/device/blocks')).blocks.length, 0);
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(
|
||||
'PASS: content icons/titles, desktop/mobile 2:1 buttons, consistent single file sizes 100KB/1.3MB/1.7GB, no list indent or overflow, retry countdown and draft preservation, block confirmation and private unblock UI.',
|
||||
'PASS: content icons/titles, desktop/mobile 2:1 buttons, consistent single file sizes 97.7KB/1.3MB/1.6GB, no list indent or overflow, retry countdown and draft preservation, block confirmation and private unblock UI.',
|
||||
);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
|
||||
Reference in New Issue
Block a user