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 00000000..964e9309 Binary files /dev/null and b/tests/e2e/fixtures/sample.png differ 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('不受信任'); +});