From a0d78c57e36a50a793ce71d4a91a059a8be9a2b0 Mon Sep 17 00:00:00 2001
From: kl <632104866@QQ.com>
Date: Wed, 4 Mar 2026 10:46:41 +0800
Subject: [PATCH] test(e2e): add MVP end-to-end automation suite and CI
workflow (#713)
* test(e2e): add mvp playwright suite and PR workflow
* ci(e2e): use JDK 21 for kkFileView build
---
.github/workflows/pr-e2e-mvp.yml | 93 ++++++++++++++++++++++++
tests/e2e/.gitignore | 3 +
tests/e2e/README.md | 49 +++++++++++++
tests/e2e/fixtures/sample.csv | 3 +
tests/e2e/fixtures/sample.html | 1 +
tests/e2e/fixtures/sample.json | 4 +
tests/e2e/fixtures/sample.md | 3 +
tests/e2e/fixtures/sample.pdf | 19 +++++
tests/e2e/fixtures/sample.png | Bin 0 -> 68 bytes
tests/e2e/fixtures/sample.txt | 1 +
tests/e2e/fixtures/sample.xml | 1 +
tests/e2e/package-lock.json | 78 ++++++++++++++++++++
tests/e2e/package.json | 14 ++++
tests/e2e/playwright.config.ts | 11 +++
tests/e2e/scripts/generate-fixtures.mjs | 31 ++++++++
tests/e2e/specs/preview-smoke.spec.ts | 65 +++++++++++++++++
16 files changed, 376 insertions(+)
create mode 100644 .github/workflows/pr-e2e-mvp.yml
create mode 100644 tests/e2e/.gitignore
create mode 100644 tests/e2e/README.md
create mode 100644 tests/e2e/fixtures/sample.csv
create mode 100644 tests/e2e/fixtures/sample.html
create mode 100644 tests/e2e/fixtures/sample.json
create mode 100644 tests/e2e/fixtures/sample.md
create mode 100644 tests/e2e/fixtures/sample.pdf
create mode 100644 tests/e2e/fixtures/sample.png
create mode 100644 tests/e2e/fixtures/sample.txt
create mode 100644 tests/e2e/fixtures/sample.xml
create mode 100644 tests/e2e/package-lock.json
create mode 100644 tests/e2e/package.json
create mode 100644 tests/e2e/playwright.config.ts
create mode 100644 tests/e2e/scripts/generate-fixtures.mjs
create mode 100644 tests/e2e/specs/preview-smoke.spec.ts
diff --git a/.github/workflows/pr-e2e-mvp.yml b/.github/workflows/pr-e2e-mvp.yml
new file mode 100644
index 00000000..8e6c88bb
--- /dev/null
+++ b/.github/workflows/pr-e2e-mvp.yml
@@ -0,0 +1,93 @@
+name: PR E2E MVP
+
+on:
+ pull_request:
+ branches: [master]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ e2e-mvp:
+ runs-on: ubuntu-latest
+ timeout-minutes: 40
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup JDK 21
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '21'
+ cache: maven
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+ cache-dependency-path: tests/e2e/package-lock.json
+
+ - name: Install LibreOffice
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libreoffice
+
+ - name: Build kkFileView
+ run: mvn -q -pl server -DskipTests package
+
+ - name: Install E2E deps
+ working-directory: tests/e2e
+ run: |
+ npm install
+ npx playwright install --with-deps chromium
+
+ - name: Generate fixtures
+ run: node tests/e2e/scripts/generate-fixtures.mjs
+
+ - name: Start fixture server
+ run: |
+ cd tests/e2e/fixtures
+ python3 -m http.server 18080 > /tmp/fixture-server.log 2>&1 &
+
+ - name: Start kkFileView
+ run: |
+ JAR_PATH=$(ls server/target/kkFileView-*.jar | head -n 1)
+ nohup env KK_TRUST_HOST='*' KK_NOT_TRUST_HOST='10.*,172.16.*,192.168.*' java -jar "$JAR_PATH" > /tmp/kkfileview.log 2>&1 &
+
+ - name: Wait for services
+ run: |
+ for i in {1..60}; do
+ curl -fsS http://127.0.0.1:18080/sample.txt >/dev/null && break
+ sleep 1
+ done
+ for i in {1..120}; do
+ curl -fsS http://127.0.0.1:8012/ >/dev/null && break
+ sleep 1
+ done
+
+ - name: Run E2E
+ working-directory: tests/e2e
+ env:
+ KK_BASE_URL: http://127.0.0.1:8012
+ FIXTURE_BASE_URL: http://127.0.0.1:18080
+ run: npm test
+
+ - name: Upload Playwright report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: playwright-report
+ path: tests/e2e/playwright-report
+
+ - name: Upload service logs
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-service-logs
+ path: |
+ /tmp/kkfileview.log
+ /tmp/fixture-server.log
diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore
new file mode 100644
index 00000000..945fcd0d
--- /dev/null
+++ b/tests/e2e/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+playwright-report/
+test-results/
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
new file mode 100644
index 00000000..d3861d6e
--- /dev/null
+++ b/tests/e2e/README.md
@@ -0,0 +1,49 @@
+# kkFileView E2E MVP
+
+This folder contains a first MVP of end-to-end automated tests.
+
+## What is covered
+
+- Basic preview smoke checks for common file types (txt/md/json/xml/csv/html/png)
+- Basic endpoint reachability
+- Security regression checks for blocked internal-network hosts (`10.*`) on:
+ - `/onlinePreview`
+ - `/getCorsFile`
+
+## Local run
+
+1. Build server jar:
+
+```bash
+mvn -q -pl server -DskipTests package
+```
+
+2. Install deps + browser:
+
+```bash
+cd tests/e2e
+npm install
+npx playwright install --with-deps chromium
+```
+
+3. Generate fixtures and start fixture server:
+
+```bash
+cd /path/to/kkFileView
+node tests/e2e/scripts/generate-fixtures.mjs
+cd tests/e2e/fixtures && python3 -m http.server 18080
+```
+
+4. Start kkFileView in another terminal:
+
+```bash
+JAR_PATH=$(ls server/target/kkFileView-*.jar | head -n 1)
+KK_TRUST_HOST='*' KK_NOT_TRUST_HOST='10.*,172.16.*,192.168.*' java -jar "$JAR_PATH"
+```
+
+5. Run tests:
+
+```bash
+cd tests/e2e
+KK_BASE_URL=http://127.0.0.1:8012 FIXTURE_BASE_URL=http://127.0.0.1:18080 npm test
+```
diff --git a/tests/e2e/fixtures/sample.csv b/tests/e2e/fixtures/sample.csv
new file mode 100644
index 00000000..aecb69c8
--- /dev/null
+++ b/tests/e2e/fixtures/sample.csv
@@ -0,0 +1,3 @@
+name,value
+kkFileView,1
+e2e,1
diff --git a/tests/e2e/fixtures/sample.html b/tests/e2e/fixtures/sample.html
new file mode 100644
index 00000000..de6029a4
--- /dev/null
+++ b/tests/e2e/fixtures/sample.html
@@ -0,0 +1 @@
+
kkFileView fixture
\ No newline at end of file
diff --git a/tests/e2e/fixtures/sample.json b/tests/e2e/fixtures/sample.json
new file mode 100644
index 00000000..c5aa57b3
--- /dev/null
+++ b/tests/e2e/fixtures/sample.json
@@ -0,0 +1,4 @@
+{
+ "app": "kkFileView",
+ "e2e": true
+}
\ No newline at end of file
diff --git a/tests/e2e/fixtures/sample.md b/tests/e2e/fixtures/sample.md
new file mode 100644
index 00000000..c49c38fb
--- /dev/null
+++ b/tests/e2e/fixtures/sample.md
@@ -0,0 +1,3 @@
+# kkFileView
+
+This is a markdown fixture.
\ No newline at end of file
diff --git a/tests/e2e/fixtures/sample.pdf b/tests/e2e/fixtures/sample.pdf
new file mode 100644
index 00000000..8374527e
--- /dev/null
+++ b/tests/e2e/fixtures/sample.pdf
@@ -0,0 +1,19 @@
+%PDF-1.1
+1 0 obj<< /Type /Catalog /Pages 2 0 R >>endobj
+2 0 obj<< /Type /Pages /Kids [3 0 R] /Count 1 >>endobj
+3 0 obj<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R >>endobj
+4 0 obj<< /Length 44 >>stream
+BT /F1 12 Tf 72 120 Td (kkFileView e2e pdf) Tj ET
+endstream
+endobj
+xref
+0 5
+0000000000 65535 f
+0000000010 00000 n
+0000000060 00000 n
+0000000117 00000 n
+0000000212 00000 n
+trailer<< /Root 1 0 R /Size 5 >>
+startxref
+306
+%%EOF
diff --git a/tests/e2e/fixtures/sample.png b/tests/e2e/fixtures/sample.png
new file mode 100644
index 0000000000000000000000000000000000000000..964e9309649f5c9d3d480852029db2dff1191eea
GIT binary patch
literal 68
zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$fBwreFf%hTyt{ck
Q8Ys-*>FVdQ&MBb@0E{ROssI20
literal 0
HcmV?d00001
diff --git a/tests/e2e/fixtures/sample.txt b/tests/e2e/fixtures/sample.txt
new file mode 100644
index 00000000..9df09327
--- /dev/null
+++ b/tests/e2e/fixtures/sample.txt
@@ -0,0 +1 @@
+kkFileView e2e sample text
\ No newline at end of file
diff --git a/tests/e2e/fixtures/sample.xml b/tests/e2e/fixtures/sample.xml
new file mode 100644
index 00000000..6daf1839
--- /dev/null
+++ b/tests/e2e/fixtures/sample.xml
@@ -0,0 +1 @@
+kkFileViewtrue
\ No newline at end of file
diff --git a/tests/e2e/package-lock.json b/tests/e2e/package-lock.json
new file mode 100644
index 00000000..1bcb46f4
--- /dev/null
+++ b/tests/e2e/package-lock.json
@@ -0,0 +1,78 @@
+{
+ "name": "kkfileview-e2e",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "kkfileview-e2e",
+ "version": "0.1.0",
+ "devDependencies": {
+ "@playwright/test": "^1.55.0"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
+ "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
+ "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
+ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ }
+ }
+}
diff --git a/tests/e2e/package.json b/tests/e2e/package.json
new file mode 100644
index 00000000..49cb525c
--- /dev/null
+++ b/tests/e2e/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "kkfileview-e2e",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "test": "playwright test",
+ "test:headed": "playwright test --headed",
+ "gen:fixtures": "node ./scripts/generate-fixtures.mjs"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.55.0"
+ }
+}
diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts
new file mode 100644
index 00000000..8504c5a8
--- /dev/null
+++ b/tests/e2e/playwright.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './specs',
+ timeout: 30_000,
+ expect: { timeout: 10_000 },
+ reporter: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]],
+ use: {
+ baseURL: process.env.KK_BASE_URL || 'http://127.0.0.1:8012',
+ },
+});
diff --git a/tests/e2e/scripts/generate-fixtures.mjs b/tests/e2e/scripts/generate-fixtures.mjs
new file mode 100644
index 00000000..e43b5d8c
--- /dev/null
+++ b/tests/e2e/scripts/generate-fixtures.mjs
@@ -0,0 +1,31 @@
+import fs from 'node:fs';
+import path from 'node:path';
+
+const fixturesDir = path.resolve(process.cwd(), 'tests/e2e/fixtures');
+fs.mkdirSync(fixturesDir, { recursive: true });
+
+const write = (name, content) => fs.writeFileSync(path.join(fixturesDir, name), content);
+
+write('sample.txt', 'kkFileView e2e sample text');
+write('sample.md', '# kkFileView\n\nThis is a markdown fixture.');
+write('sample.json', JSON.stringify({ app: 'kkFileView', e2e: true }, null, 2));
+write('sample.xml', 'kkFileViewtrue');
+write('sample.csv', 'name,value\nkkFileView,1\ne2e,1\n');
+write('sample.html', 'kkFileView fixture
');
+
+// 1x1 png
+write(
+ 'sample.png',
+ Buffer.from(
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7Zx1sAAAAASUVORK5CYII=',
+ 'base64'
+ )
+);
+
+// tiny valid pdf
+write(
+ 'sample.pdf',
+ `%PDF-1.1\n1 0 obj<< /Type /Catalog /Pages 2 0 R >>endobj\n2 0 obj<< /Type /Pages /Kids [3 0 R] /Count 1 >>endobj\n3 0 obj<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R >>endobj\n4 0 obj<< /Length 44 >>stream\nBT /F1 12 Tf 72 120 Td (kkFileView e2e pdf) Tj ET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000010 00000 n \n0000000060 00000 n \n0000000117 00000 n \n0000000212 00000 n \ntrailer<< /Root 1 0 R /Size 5 >>\nstartxref\n306\n%%EOF\n`
+);
+
+console.log('fixtures generated in', fixturesDir);
diff --git a/tests/e2e/specs/preview-smoke.spec.ts b/tests/e2e/specs/preview-smoke.spec.ts
new file mode 100644
index 00000000..df26d16d
--- /dev/null
+++ b/tests/e2e/specs/preview-smoke.spec.ts
@@ -0,0 +1,65 @@
+import { test, expect } from '@playwright/test';
+
+const fixtureBase = process.env.FIXTURE_BASE_URL || 'http://127.0.0.1:18080';
+
+function b64(v: string): string {
+ return Buffer.from(v).toString('base64');
+}
+
+async function openPreview(request: any, fileUrl: string) {
+ const encoded = b64(fileUrl);
+ return request.get(`/onlinePreview?url=${encoded}`);
+}
+
+test('01 home/index reachable', async ({ request }) => {
+ const resp = await request.get('/');
+ expect(resp.status()).toBeLessThan(500);
+});
+
+test('02 txt preview', async ({ request }) => {
+ const resp = await openPreview(request, `${fixtureBase}/sample.txt`);
+ expect(resp.status()).toBe(200);
+});
+
+test('03 markdown preview', async ({ request }) => {
+ const resp = await openPreview(request, `${fixtureBase}/sample.md`);
+ expect(resp.status()).toBe(200);
+});
+
+test('04 json preview', async ({ request }) => {
+ const resp = await openPreview(request, `${fixtureBase}/sample.json`);
+ expect(resp.status()).toBe(200);
+});
+
+test('05 xml preview', async ({ request }) => {
+ const resp = await openPreview(request, `${fixtureBase}/sample.xml`);
+ expect(resp.status()).toBe(200);
+});
+
+test('06 csv preview', async ({ request }) => {
+ const resp = await openPreview(request, `${fixtureBase}/sample.csv`);
+ expect(resp.status()).toBe(200);
+});
+
+test('07 html preview', async ({ request }) => {
+ const resp = await openPreview(request, `${fixtureBase}/sample.html`);
+ expect(resp.status()).toBe(200);
+});
+
+test('08 png preview', async ({ request }) => {
+ const resp = await openPreview(request, `${fixtureBase}/sample.png`);
+ expect(resp.status()).toBe(200);
+});
+
+test('09 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('10 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();
+ expect(body).toContain('不受信任');
+});