diff --git a/.github/workflows/nightly-e2e.yml b/.github/workflows/nightly-e2e.yml
index 3543706b..431ff4a0 100644
--- a/.github/workflows/nightly-e2e.yml
+++ b/.github/workflows/nightly-e2e.yml
@@ -36,10 +36,10 @@ jobs:
with:
python-version: '3.11'
- - name: Install LibreOffice + zip
+ - name: Install LibreOffice + archive tools
run: |
sudo apt-get update
- sudo apt-get install -y libreoffice zip
+ sudo apt-get install -y libreoffice zip p7zip-full
- name: Setup Python deps for office fixtures
run: |
diff --git a/.github/workflows/pr-e2e-mvp.yml b/.github/workflows/pr-e2e-mvp.yml
index 161b525d..62e111d5 100644
--- a/.github/workflows/pr-e2e-mvp.yml
+++ b/.github/workflows/pr-e2e-mvp.yml
@@ -36,10 +36,10 @@ jobs:
with:
python-version: '3.11'
- - name: Install LibreOffice + zip
+ - name: Install LibreOffice + archive tools
run: |
sudo apt-get update
- sudo apt-get install -y libreoffice zip
+ sudo apt-get install -y libreoffice zip p7zip-full
- name: Setup Python deps for office fixtures
run: |
diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore
index f3b87092..55cbce0a 100644
--- a/tests/e2e/.gitignore
+++ b/tests/e2e/.gitignore
@@ -3,7 +3,7 @@ playwright-report/
test-results/
__pycache__/
-fixtures/zip-tmp/
+fixtures/archive-tmp/
fixtures/sample.docx
fixtures/sample.xlsx
fixtures/sample.pptx
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
index c5336162..2d7bcb29 100644
--- a/tests/e2e/README.md
+++ b/tests/e2e/README.md
@@ -6,7 +6,7 @@ This folder contains a first MVP of end-to-end automated tests.
- Basic preview smoke checks for common file types (txt/md/json/xml/csv/html/png)
- Office Phase-2 smoke checks (docx/xlsx/pptx)
-- Archive smoke check (zip)
+- Archive smoke checks (zip/tar/tgz/7z/rar)
- Basic endpoint reachability
- Security regression checks for blocked internal-network hosts (`10.*`) on:
- `/onlinePreview`
@@ -31,7 +31,7 @@ npx playwright install --with-deps chromium
pip3 install -r requirements.txt
```
-> Prerequisite: ensure `zip` command is available in PATH (used for `sample.zip` fixture generation).
+> Prerequisite: ensure `python3`, `zip`, and `7z` (or `bsdtar` as a fallback) are available in PATH for archive fixtures.
3. Generate fixtures and start fixture server:
diff --git a/tests/e2e/fixtures/sample.7z b/tests/e2e/fixtures/sample.7z
new file mode 100644
index 00000000..527017d1
Binary files /dev/null and b/tests/e2e/fixtures/sample.7z differ
diff --git a/tests/e2e/fixtures/sample.rar b/tests/e2e/fixtures/sample.rar
new file mode 100644
index 00000000..a0e135fc
Binary files /dev/null and b/tests/e2e/fixtures/sample.rar differ
diff --git a/tests/e2e/fixtures/sample.tar b/tests/e2e/fixtures/sample.tar
new file mode 100644
index 00000000..6a4c4f6d
Binary files /dev/null and b/tests/e2e/fixtures/sample.tar differ
diff --git a/tests/e2e/fixtures/sample.tgz b/tests/e2e/fixtures/sample.tgz
new file mode 100644
index 00000000..671d81cf
Binary files /dev/null and b/tests/e2e/fixtures/sample.tgz differ
diff --git a/tests/e2e/scripts/generate-fixtures.mjs b/tests/e2e/scripts/generate-fixtures.mjs
index afd108cb..ca98657a 100644
--- a/tests/e2e/scripts/generate-fixtures.mjs
+++ b/tests/e2e/scripts/generate-fixtures.mjs
@@ -16,20 +16,100 @@ write('sample.xml', 'kkFileViewtrue');
write('sample.csv', 'name,value\nkkFileView,1\ne2e,1\n');
write('sample.html', '
kkFileView fixture
');
-// zip (contains txt) - only generate if missing to avoid noisy local diffs
-const zipPath = path.join(fixturesDir, 'sample.zip');
-if (!fs.existsSync(zipPath)) {
- const zipWork = path.join(fixturesDir, 'zip-tmp');
- fs.mkdirSync(zipWork, { recursive: true });
- fs.writeFileSync(path.join(zipWork, 'inner.txt'), 'kkFileView zip inner file');
+// archive fixtures (contains inner.txt) - generate if missing
+const archiveWork = path.join(fixturesDir, 'archive-tmp');
+fs.mkdirSync(archiveWork, { recursive: true });
+const innerFile = path.join(archiveWork, 'inner.txt');
+fs.writeFileSync(innerFile, 'kkFileView archive inner file');
+
+const ensureArchive = (name, generator) => {
+ const out = path.join(fixturesDir, name);
+ if (fs.existsSync(out)) return;
try {
- execFileSync('zip', ['-X', '-q', '-r', zipPath, 'inner.txt'], { cwd: zipWork });
+ generator(out);
} catch (err) {
- console.error('Failed to create sample.zip fixture. Ensure "zip" is installed and available in PATH.');
- throw err instanceof Error ? err : new Error(String(err));
- } finally {
- fs.rmSync(zipWork, { recursive: true, force: true });
+ try {
+ fs.rmSync(out, { force: true });
+ } catch {
+ // ignore cleanup errors; original error will be rethrown
+ }
+ throw err;
}
+};
+
+const buildDeterministicTar = (out, gzip = false) => {
+ const py = String.raw`import io, tarfile, gzip
+from pathlib import Path
+
+out = Path(r'''${out}''')
+inner_path = Path(r'''${innerFile}''')
+data = inner_path.read_bytes()
+use_gzip = ${gzip ? 'True' : 'False'}
+
+if use_gzip:
+ with out.open('wb') as f:
+ with gzip.GzipFile(filename='', mode='wb', fileobj=f, mtime=0) as gz:
+ with tarfile.open(fileobj=gz, mode='w', format=tarfile.USTAR_FORMAT) as tf:
+ info = tarfile.TarInfo('inner.txt')
+ info.size = len(data)
+ info.mtime = 946684800 # 2000-01-01 00:00:00 UTC
+ info.uid = 0
+ info.gid = 0
+ info.uname = 'root'
+ info.gname = 'root'
+ tf.addfile(info, io.BytesIO(data))
+else:
+ with tarfile.open(out, mode='w', format=tarfile.USTAR_FORMAT) as tf:
+ info = tarfile.TarInfo('inner.txt')
+ info.size = len(data)
+ info.mtime = 946684800 # 2000-01-01 00:00:00 UTC
+ info.uid = 0
+ info.gid = 0
+ info.uname = 'root'
+ info.gname = 'root'
+ tf.addfile(info, io.BytesIO(data))
+`;
+ execFileSync('python3', ['-c', py]);
+};
+
+try {
+ ensureArchive('sample.zip', out => {
+ execFileSync('zip', ['-X', '-q', '-r', out, 'inner.txt'], { cwd: archiveWork });
+ });
+
+ ensureArchive('sample.tar', out => {
+ buildDeterministicTar(out, false);
+ });
+
+ ensureArchive('sample.tgz', out => {
+ buildDeterministicTar(out, true);
+ });
+
+ ensureArchive('sample.7z', out => {
+ try {
+ execFileSync('7z', ['a', '-bd', '-y', '-mtc=off', '-mta=off', '-mtm=off', out, 'inner.txt'], {
+ cwd: archiveWork,
+ });
+ } catch (err) {
+ if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {
+ execFileSync('bsdtar', ['-a', '-cf', out, 'inner.txt'], { cwd: archiveWork });
+ } else {
+ throw err;
+ }
+ }
+ });
+} catch (err) {
+ console.error('Failed to create archive fixtures. Ensure python3, zip, 7z (or bsdtar) are available in PATH.');
+ throw err instanceof Error ? err : new Error(String(err));
+} finally {
+ fs.rmSync(archiveWork, { recursive: true, force: true });
+}
+
+const rarFixture = path.join(fixturesDir, 'sample.rar');
+if (!fs.existsSync(rarFixture)) {
+ throw new Error(
+ 'Missing required fixture tests/e2e/fixtures/sample.rar. Restore it from git (e.g. `git checkout -- tests/e2e/fixtures/sample.rar`) before running e2e.'
+ );
}
// 1x1 png
diff --git a/tests/e2e/specs/preview-smoke.spec.ts b/tests/e2e/specs/preview-smoke.spec.ts
index afa769b4..03df155d 100644
--- a/tests/e2e/specs/preview-smoke.spec.ts
+++ b/tests/e2e/specs/preview-smoke.spec.ts
@@ -25,6 +25,10 @@ test.beforeAll(async () => {
'sample.xlsx',
'sample.pptx',
'sample.zip',
+ 'sample.tar',
+ 'sample.tgz',
+ 'sample.7z',
+ 'sample.rar',
];
try {
@@ -97,13 +101,33 @@ test('12 zip preview', async ({ request }) => {
expect(resp.status()).toBe(200);
});
-test('13 security: block 10.x host in onlinePreview', async ({ request }) => {
+test('13 tar preview', async ({ request }) => {
+ const resp = await openPreview(request, `${fixtureBase}/sample.tar`);
+ expect(resp.status()).toBe(200);
+});
+
+test('14 tgz preview', async ({ request }) => {
+ const resp = await openPreview(request, `${fixtureBase}/sample.tgz`);
+ expect(resp.status()).toBe(200);
+});
+
+test('15 7z preview', async ({ request }) => {
+ const resp = await openPreview(request, `${fixtureBase}/sample.7z`);
+ expect(resp.status()).toBe(200);
+});
+
+test('16 rar preview', async ({ request }) => {
+ const resp = await openPreview(request, `${fixtureBase}/sample.rar`);
+ expect(resp.status()).toBe(200);
+});
+
+test('17 security: block 10.x host in onlinePreview', async ({ request }) => {
const resp = await openPreview(request, `http://10.1.2.3/a.pdf`);
const body = await resp.text();
expect(body).toContain('不受信任');
});
-test('14 security: block 10.x host in getCorsFile', async ({ request }) => {
+test('18 security: block 10.x host in getCorsFile', async ({ request }) => {
const encoded = b64('http://10.1.2.3/a.pdf');
const resp = await request.get(`/getCorsFile?urlPath=${encoded}`);
const body = await resp.text();