Make admin quotas editable and simplify uploader storage

This commit is contained in:
2026-10-15 12:51:42 +09:00
parent 4d527f74e6
commit 328fad2e36
26 changed files with 1101 additions and 205 deletions

View File

@@ -113,19 +113,22 @@ try {
files: [{ name: 'sample.mp4', size: 4, type: 'video/mp4' }],
});
await call(receiver, b.csrfToken, '/transfer-requests/' + transfer.requestId + '/accept', {});
await sender.evaluate(
async ({ csrf, id }) => {
const body = new FormData();
body.append('file', new Blob(['test'], { type: 'video/mp4' }), 'sample.mp4');
const response = await fetch('/api/uploads?requestId=' + id + '&fileIndex=0', {
method: 'POST',
headers: { 'X-CSRF-Token': csrf, 'Idempotency-Key': crypto.randomUUID() },
body,
});
if (!response.ok) throw Error(await response.text());
},
{ csrf: a.csrfToken, id: transfer.requestId },
);
async function uploadFile(page, csrf, id, name, type) {
await page.evaluate(
async ({ csrf, id, name, type }) => {
const body = new FormData();
body.append('file', new Blob(['test'], { type }), name);
const response = await fetch('/api/uploads?requestId=' + id + '&fileIndex=0', {
method: 'POST',
headers: { 'X-CSRF-Token': csrf, 'Idempotency-Key': crypto.randomUUID() },
body,
});
if (!response.ok) throw Error(await response.text());
},
{ csrf, id, name, type },
);
}
await uploadFile(sender, a.csrfToken, transfer.requestId, 'sample.mp4', 'video/mp4');
async function login(page, email) {
await page.locator('.login-button').click();
await page.locator('.account-auth').waitFor();
@@ -150,7 +153,8 @@ try {
await admin.locator('.admin-file').filter({ hasText: 'sample.mp4' }).waitFor();
assert.match(await admin.locator('.admin-file').innerText(), /중국\(CN\) · Beijing/);
assert.match(await admin.locator('.admin-file').innerText(), /1\.1\.1\.1/);
assert.match(await admin.locator('.admin-file-info').first().innerText(), /^\[.+\]\s+guest-/);
assert.match(await admin.locator('.admin-file-owner').innerText(), /^[a-f0-9-]{36}\s+비로그인$/);
assert.doesNotMatch(await admin.locator('.admin-file-owner').innerText(), /guest-|user-/);
assert.match(await admin.locator('.admin-file').innerText(), /접속 지역 · 중국\(CN\) · Beijing/);
assert.doesNotMatch(await admin.locator('.admin-file').innerText(), /최근 접속/);
assert.match(
@@ -163,42 +167,67 @@ try {
await admin.getByRole('button', { name: '업로드 재개', exact: true }).click();
await admin.getByText('신규 업로드 허용 중', { exact: true }).waitFor();
await admin.getByRole('button', { name: '용량 설정', exact: true }).click();
assert.match(
await admin.locator('.admin-limit-details').innerText(),
/로그인 보관함[\s\S]*계정당 2\.1GB/,
);
assert.match(
await admin.locator('.admin-limit-details').innerText(),
/기기 전송 업로드[\s\S]*기기 세션당 2\.1GB/,
);
const capacityForm = admin.locator('.admin-settings');
assert.equal(await capacityForm.locator('input').count(), 0);
assert.match(await capacityForm.innerText(), /로그인 보관함 계정당 한도[\s\S]*2\.1GB/);
await admin.getByRole('button', { name: '수정', exact: true }).click();
await admin.getByRole('button', { name: '설정 저장', exact: true }).click();
await admin.getByText('관리 설정을 저장했어요.', { exact: true }).waitFor();
assert.equal(
runtime.service.admin.state.totalStorageLimitMb,
20480,
'rounded GB presentation must not change the stored quota',
'unmodified rounded GB keeps exact stored quota',
);
await admin.getByLabel('동시에 처리할 업로드 수').fill('3');
await admin.getByLabel('전체 파일 저장 한도 (GB)').fill('16');
assert.equal(runtime.service.admin.state.accountBytes, 2147483648);
await admin.getByRole('button', { name: '수정', exact: true }).click();
await admin.getByLabel('동시에 처리할 업로드 수', { exact: true }).fill('8');
await admin.getByRole('button', { name: '취소', exact: true }).click();
assert.equal(runtime.service.admin.state.maxConcurrentUploads, 4);
assert.equal(await capacityForm.locator('input').count(), 0);
await admin.getByRole('button', { name: '수정', exact: true }).click();
await admin.getByLabel('동시에 처리할 업로드 수', { exact: true }).fill('3');
await admin.getByLabel('전체 파일 저장 한도 (GB)', { exact: true }).fill('16');
await admin.getByLabel('로그인 보관함 계정당 한도 (GB)', { exact: true }).fill('3');
await admin.getByLabel('기기 세션당 업로드 한도 (GB)', { exact: true }).fill('4');
await admin.getByLabel('기기 전송 업로드 전체 한도 (GB)', { exact: true }).fill('12');
await admin.getByLabel('파일 한 개 한도 (GB)', { exact: true }).fill('1');
await admin.getByLabel('NAS 최소 여유 공간 (GB)', { exact: true }).fill('0');
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));
for (const [key, expected] of Object.entries({
accountBytes: 3e9,
deviceBytes: 4e9,
devicesTotalBytes: 12e9,
perFileBytes: 1e9,
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 });
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();
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 });
await modal.getByLabel('등록 대상').selectOption(kind);
await modal
.getByLabel(kind === 'country' ? '국가 코드' : 'IP 주소 또는 CIDR', { exact: true })
.getByLabel(kind === 'country' ? '국가 코드' : 'IP 주소 또는 IP 대역', { exact: true })
.fill(value);
await modal.getByRole('button', { name: '추가', exact: true }).click();
await modal.waitFor({ state: 'hidden' });
}
await addRule(admin, '블랙리스트', 'country', 'cn');
await addRule(admin, '블랙리스트', 'ip', '9.9.9.0/24');
await addRule(admin, '블랙리스트', 'ip', '9.9.9.*');
await addRule(admin, '화이트리스트', 'country', 'KR');
await addRule(admin, '화이트리스트', 'ip', '8.8.8.0/24');
await addRule(admin, '화이트리스트', 'ip', '8.8.8.*');
await addRule(admin, '화이트리스트', 'ip', '4.4.4.4');
await admin.getByRole('checkbox', { name: '4.4.4.4 허용 규칙 선택', exact: true }).check();
await admin
@@ -209,6 +238,9 @@ try {
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);
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']);
assert.deepEqual(runtime.service.admin.state.allowedIps, ['8.8.8.0/24']);
assert.equal(runtime.service.admin.state.autoBlockEnabled, true);
@@ -241,6 +273,25 @@ try {
await admin.getByRole('button', { name: '원본 삭제 확인', exact: true }).click();
await admin.getByText('저장된 파일이 없습니다.', { exact: true }).waitFor();
assert.equal(runtime.service.admin.summary().storedBytes, 0);
const normalSession = await session(normal);
const owned = await call(normal, normalSession.csrfToken, '/vault', {
kind: 'file',
files: [{ name: 'account-owner.txt', size: 4, type: 'text/plain' }],
});
await uploadFile(
normal,
normalSession.csrfToken,
owned.requestId,
'account-owner.txt',
'text/plain',
);
await admin.getByRole('button', { name: '새로고침', exact: true }).click();
const ownedCard = admin.locator('.admin-file').filter({ hasText: 'account-owner.txt' });
await ownedCard.waitFor();
const ownerLabel = await ownedCard.locator('.admin-file-owner').innerText();
assert.match(ownerLabel, /^[a-f0-9-]{36}\s+other@hop\.test$/);
assert.doesNotMatch(ownerLabel, /user-|guest-|\[/);
await admin.screenshot({ path: 'tests/e2e/admin-file-account.png', fullPage: true });
await admin.getByRole('button', { name: '관리 기록', exact: true }).click();
await admin.getByText('원본 파일 삭제', { exact: true }).waitFor();
const changedRow = admin
@@ -296,11 +347,22 @@ try {
await mobile.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
true,
);
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.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
true,
);
await mobile.screenshot({ path: 'tests/e2e/admin-capacity-mobile.png', fullPage: true });
await mobile.getByRole('button', { name: '취소', exact: true }).click();
await mobile.getByRole('button', { name: '네트워크', exact: true }).click();
assert.equal(
await mobile.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
true,
);
assert.equal(await mobile.getByRole('switch').count(), 0);
await mobile.getByRole('button', { name: '수정', exact: true }).click();
await mobile.getByRole('button', { name: '블랙리스트 추가', exact: true }).click();
const ruleModal = mobile.getByRole('dialog', { name: '블랙리스트 추가', exact: true });
assert.equal(