Add resumable uploads, proxy discovery, PWA install and 2 GiB personal vaults
This commit is contained in:
106
tests/e2e/chunks.mjs
Normal file
106
tests/e2e/chunks.mjs
Normal file
@@ -0,0 +1,106 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { chromium } from '@playwright/test';
|
||||
import { mkdtemp, open, rm, stat, readFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { createApp } from '../../apps/server/src/app.ts';
|
||||
import { loadConfig } from '../../apps/server/src/config.ts';
|
||||
const folder = await mkdtemp(resolve(tmpdir(), 'hop-chunks-'));
|
||||
const runtime = await createApp({
|
||||
...loadConfig({
|
||||
APP_URL: 'http://127.0.0.1',
|
||||
DATABASE_PATH: resolve(folder, 'db.sqlite'),
|
||||
UPLOAD_DIR: resolve(folder, 'uploads'),
|
||||
MIN_FREE_DISK_MB: '0',
|
||||
SESSION_SECRET: 's'.repeat(40),
|
||||
DISCOVERY_SECRET: 'd'.repeat(40),
|
||||
}),
|
||||
webRoot: resolve('apps/web/dist'),
|
||||
});
|
||||
let browser;
|
||||
try {
|
||||
await runtime.app.listen({ host: '127.0.0.1', port: 0 });
|
||||
const url = `http://127.0.0.1:${runtime.app.server.address().port}`;
|
||||
runtime.setDevOrigin(url);
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
...(process.env.HOP_BROWSER_EXECUTABLE
|
||||
? { executablePath: process.env.HOP_BROWSER_EXECUTABLE }
|
||||
: {}),
|
||||
});
|
||||
const a = await browser.newContext(),
|
||||
b = await browser.newContext();
|
||||
const sender = await a.newPage(),
|
||||
receiver = await b.newPage(),
|
||||
errors = [];
|
||||
for (const page of [sender, receiver])
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
await Promise.all([sender.goto(url), receiver.goto(url)]);
|
||||
await sender.locator('.peer-card').first().waitFor();
|
||||
await receiver.locator('.peer-card').first().waitFor();
|
||||
const size = 101 * 1024 * 1024 + 23,
|
||||
source = resolve(folder, 'over-100MiB.bin');
|
||||
const file = await open(source, 'w');
|
||||
await file.truncate(size);
|
||||
await file.write(Buffer.from('actual large transfer'), 0, 21, size - 21);
|
||||
await file.close();
|
||||
let lostReply = false;
|
||||
let failedAttempts = 0,
|
||||
allowRetry = false;
|
||||
const sizes = [],
|
||||
offsets = [];
|
||||
await sender.route('**/api/uploads/resumable/*', async (route) => {
|
||||
if (route.request().method() !== 'PUT') return route.continue();
|
||||
const bytes = route.request().postDataBuffer();
|
||||
if (lostReply && !allowRetry && failedAttempts < 3) {
|
||||
failedAttempts++;
|
||||
return route.abort('failed');
|
||||
}
|
||||
sizes.push(bytes.length);
|
||||
offsets.push(Number(route.request().headers()['upload-offset']));
|
||||
// Emulate a proxy with a per-request cap, and lose an acknowledged chunk response.
|
||||
if (bytes.length > 9 * 1024 * 1024) return route.fulfill({ status: 413, body: '{}' });
|
||||
if (!lostReply) {
|
||||
await route.fetch();
|
||||
lostReply = true;
|
||||
return route.abort('failed');
|
||||
}
|
||||
return route.continue();
|
||||
});
|
||||
await sender.locator('.peer-card').first().click();
|
||||
await sender.locator('input[type=file]').setInputFiles(source);
|
||||
await sender.getByRole('button', { name: '전송하기', exact: true }).click();
|
||||
const downloaded = receiver.waitForEvent('download', { timeout: 120000 });
|
||||
void downloaded.catch(() => {});
|
||||
await receiver.getByRole('button', { name: '수락', exact: true }).click();
|
||||
const retry = sender.getByRole('button', { name: '다시 전송', exact: true });
|
||||
await retry.waitFor({ timeout: 30000 });
|
||||
assert.equal(failedAttempts, 3);
|
||||
allowRetry = true;
|
||||
await retry.click();
|
||||
const download = await downloaded,
|
||||
path = await download.path();
|
||||
assert.equal((await stat(path)).size, size);
|
||||
const handle = await open(path, 'r'),
|
||||
tail = Buffer.alloc(21);
|
||||
await handle.read(tail, 0, 21, size - 21);
|
||||
await handle.close();
|
||||
assert.equal(tail.toString(), 'actual large transfer');
|
||||
assert.equal(sizes.length, 13);
|
||||
assert(sizes.every((value) => value <= 8 * 1024 * 1024));
|
||||
assert.equal(offsets[0], 0);
|
||||
assert.equal(offsets[1], 8 * 1024 * 1024, '응답이 유실돼도 이미 받은 조각을 다시 전송하지 않음');
|
||||
assert.equal(
|
||||
sizes.reduce((sum, value) => sum + value, 0),
|
||||
size,
|
||||
);
|
||||
await sender.locator('.send-dialog').waitFor({ state: 'hidden' });
|
||||
assert.equal(errors.length, 0, errors.join('\n'));
|
||||
console.log(
|
||||
'PASS: 두 실제 브라우저 자동 발견, 101MiB 파일의 8MiB 분할, 요청 크기 제한, 응답 유실 후 이어 보내기, 수락 후 자동 다운로드, 원본 크기·내용 검증',
|
||||
);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
await runtime.app.close();
|
||||
await rm(folder, { recursive: true, force: true });
|
||||
}
|
||||
Reference in New Issue
Block a user