Align capacity units and scope administrator network edits

This commit is contained in:
2026-10-15 14:20:39 +09:00
parent 9cda9cbe84
commit 60a14e9c6c
14 changed files with 458 additions and 259 deletions

View File

@@ -153,11 +153,20 @@ try {
assert.equal(restored.account.id, verifiedAccount.userId);
assert.equal(restored.account.verified, true);
assert.equal(restored.account.administrator, true);
const settings = JSON.parse(
const { settings, capacityDefaults } = JSON.parse(
node(
`const r=await fetch('http://127.0.0.1:${port}/api/admin/summary',{headers:{Cookie:${JSON.stringify(verifiedAccount.cookie)}}});if(!r.ok)throw Error(await r.text());console.log(JSON.stringify((await r.json()).settings));`,
`const r=await fetch('http://127.0.0.1:${port}/api/admin/summary',{headers:{Cookie:${JSON.stringify(verifiedAccount.cookie)}}});if(!r.ok)throw Error(await r.text());const {settings,capacityDefaults}=await r.json();console.log(JSON.stringify({settings,capacityDefaults}));`,
),
);
assert.deepEqual(capacityDefaults, {
maxConcurrentUploads: 4,
totalStorageLimitMb: 20480,
accountBytes: 2 * 1024 ** 3,
deviceBytes: 2 * 1024 ** 3,
devicesTotalBytes: 20 * 1024 ** 3,
perFileBytes: 2 * 1024 ** 3,
minDiskFreeBytes: 5 * 1024 ** 3,
});
assert.equal(settings.maxConcurrentUploads, 3);
for (const [key, value] of Object.entries({
accountBytes: 3e9,

View File

@@ -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();

View File

@@ -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();