Compare commits

..

11 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
4592d0881f fix(e2e): regenerate sample.tgz with deterministic gzip header (mtime=0, no FNAME)
Co-authored-by: klboke <18591662+klboke@users.noreply.github.com>
2026-03-09 03:49:31 +00:00
copilot-swe-agent[bot]
45d38310ac Initial plan 2026-03-09 03:46:03 +00:00
kl
b5c95b261d test(e2e): keep legacy zip temp dir ignored 2026-03-09 11:36:37 +08:00
kl
21be47f560 test(e2e): make tgz fixture gzip header deterministic 2026-03-09 11:29:52 +08:00
kl
8dd9b78c48 test(e2e): add rar smoke coverage and align archive deps 2026-03-09 11:19:27 +08:00
kl
4ab383709c test: address copilot archive fixture review feedback 2026-03-09 11:09:52 +08:00
kl
4690a5353b test(e2e): expand archive coverage to tar/tgz/7z/rar 2026-03-09 10:12:48 +08:00
kl
b10e14899d test(e2e): unify E2E CI command and align preview URL encoding (#719)
* test(e2e): follow-up fixes for post-merge copilot review feedback

* test(e2e): guard E2E_MAX_PREVIEW_MS against sub-second values

* test(e2e): align preview URL encoding and docs
2026-03-04 17:39:41 +08:00
kl
eee3a2ed38 test(e2e): follow-up fixes after post-merge copilot findings (#718)
* test(e2e): follow-up fixes for post-merge copilot review feedback

* test(e2e): guard E2E_MAX_PREVIEW_MS against sub-second values
2026-03-04 15:45:04 +08:00
kl
68d4d23a4b test(e2e): phase-3 add nightly full run and perf smoke checks (#717)
* test(e2e): phase-3 add nightly workflow and perf smoke suite

* test(e2e): address copilot review for phase-3 fixture and readiness flow
2026-03-04 15:06:15 +08:00
kl
bb457924cd test(e2e): phase-2 add Office and zip smoke automation (#714)
* test(e2e): phase-2 add office and zip smoke coverage

* test(e2e): address copilot review for fixture stability and CI python setup

* test(e2e): fix preflight fixture scope and path handling

* test(e2e): harden fixture preflight and remove duplicate generation

* test(e2e): remove redundant zip install and cleanup temp zip dir

* test(e2e): ensure zip dependency and unify python command in docs

* docs(e2e): align README with npm gen scripts and python3 usage
2026-03-04 14:34:32 +08:00
13 changed files with 324 additions and 28 deletions

118
.github/workflows/nightly-e2e.yml vendored Normal file
View File

@@ -0,0 +1,118 @@
name: Nightly E2E Full
on:
schedule:
- cron: '30 18 * * *' # 02:30 Asia/Shanghai
workflow_dispatch:
permissions:
contents: read
jobs:
e2e-nightly:
runs-on: ubuntu-latest
timeout-minutes: 50
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: Setup Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install LibreOffice + archive tools
run: |
sudo apt-get update
sudo apt-get install -y libreoffice zip p7zip-full
- name: Setup Python deps for office fixtures
run: |
python -m pip install --upgrade pip
pip install -r tests/e2e/requirements.txt
- name: Build kkFileView
run: mvn -q -pl server -DskipTests package
- name: Install E2E deps
working-directory: tests/e2e
run: |
npm ci
npx playwright install --with-deps chromium
- 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: |
fixture_ready=false
for i in {1..60}; do
if curl -fsS http://127.0.0.1:18080/sample.txt >/dev/null; then
fixture_ready=true
break
fi
sleep 1
done
if [ "$fixture_ready" != "true" ]; then
echo "Error: fixture server did not become ready within 60 seconds." >&2
exit 1
fi
kkfileview_ready=false
for i in {1..120}; do
if curl -fsS http://127.0.0.1:8012/ >/dev/null; then
kkfileview_ready=true
break
fi
sleep 1
done
if [ "$kkfileview_ready" != "true" ]; then
echo "Error: kkFileView service did not become ready within 120 seconds." >&2
exit 1
fi
- name: Run nightly E2E suites
working-directory: tests/e2e
env:
KK_BASE_URL: http://127.0.0.1:8012
FIXTURE_BASE_URL: http://127.0.0.1:18080
E2E_MAX_PREVIEW_MS: 20000
run: npm run test:ci
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: nightly-playwright-report
path: tests/e2e/playwright-report
- name: Upload service logs
if: always()
uses: actions/upload-artifact@v4
with:
name: nightly-e2e-service-logs
path: |
/tmp/kkfileview.log
/tmp/fixture-server.log

View File

@@ -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: |
@@ -55,11 +55,6 @@ jobs:
npm install
npx playwright install --with-deps chromium
- name: Generate fixtures
run: |
node tests/e2e/scripts/generate-fixtures.mjs
python3 tests/e2e/scripts/generate-office-fixtures.py
- name: Start fixture server
run: |
cd tests/e2e/fixtures

View File

@@ -3,6 +3,7 @@ playwright-report/
test-results/
__pycache__/
fixtures/archive-tmp/
fixtures/zip-tmp/
fixtures/sample.docx
fixtures/sample.xlsx

View File

@@ -6,11 +6,13 @@ 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`
- `/getCorsFile`
- Basic performance smoke checks (configurable threshold): txt/docx/xlsx preview response time
- CI combined run command available via `npm run test:ci`
## Local run
@@ -29,12 +31,13 @@ npx playwright install --with-deps chromium
pip3 install -r requirements.txt
```
> 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:
```bash
cd /path/to/kkFileView
node tests/e2e/scripts/generate-fixtures.mjs
python3 tests/e2e/scripts/generate-office-fixtures.py
npm run gen:all
cd tests/e2e/fixtures && python3 -m http.server 18080
```
@@ -51,3 +54,16 @@ KK_TRUST_HOST='*' KK_NOT_TRUST_HOST='10.*,172.16.*,192.168.*' java -jar "$JAR_PA
cd tests/e2e
KK_BASE_URL=http://127.0.0.1:8012 FIXTURE_BASE_URL=http://127.0.0.1:18080 npm test
```
Optional:
```bash
# smoke only (self-contained: will auto-generate fixtures)
npm run test:smoke
# perf smoke (self-contained; default threshold 15000ms)
E2E_MAX_PREVIEW_MS=15000 npm run test:perf
# CI-style combined run (single fixture generation)
E2E_MAX_PREVIEW_MS=20000 npm run test:ci
```

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -9,7 +9,12 @@
"gen:all": "npm run gen:fixtures && npm run gen:office",
"pretest": "npm run gen:all",
"test": "playwright test",
"test:headed": "playwright test --headed"
"test:headed": "playwright test --headed",
"pretest:smoke": "npm run gen:all",
"test:smoke": "playwright test specs/preview-smoke.spec.ts",
"pretest:perf": "npm run gen:all",
"test:perf": "playwright test specs/perf-smoke.spec.ts",
"test:ci": "npm run gen:all && playwright test specs/preview-smoke.spec.ts specs/perf-smoke.spec.ts"
},
"devDependencies": {
"@playwright/test": "^1.55.0"

View File

@@ -4,8 +4,10 @@ export default defineConfig({
testDir: './specs',
timeout: 30_000,
expect: { timeout: 10_000 },
retries: process.env.CI ? 1 : 0,
reporter: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]],
use: {
baseURL: process.env.KK_BASE_URL || 'http://127.0.0.1:8012',
trace: 'on-first-retry',
},
});

View File

@@ -16,17 +16,87 @@ write('sample.xml', '<root><name>kkFileView</name><e2e>true</e2e></root>');
write('sample.csv', 'name,value\nkkFileView,1\ne2e,1\n');
write('sample.html', '<!doctype html><html><body><h1>kkFileView fixture</h1></body></html>');
// 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 });
} catch {
// fallback: keep going if zip is not available locally
generator(out);
} catch (err) {
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', out, 'inner.txt'], { cwd: archiveWork, stdio: 'ignore' });
} catch {
execFileSync('bsdtar', ['-a', '-cf', out, 'inner.txt'], { cwd: archiveWork });
}
});
} 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 });
}
// 1x1 png

View File

@@ -0,0 +1,49 @@
import { test, expect, request as playwrightRequest } from '@playwright/test';
import type { APIRequestContext } from '@playwright/test';
const fixtureBase = process.env.FIXTURE_BASE_URL || 'http://127.0.0.1:18080';
const DEFAULT_MAX_MS = 15000;
const envMaxMs = Number(process.env.E2E_MAX_PREVIEW_MS);
const maxMs = Number.isFinite(envMaxMs) && envMaxMs >= 1 ? Math.floor(envMaxMs) : DEFAULT_MAX_MS;
function b64(v: string): string {
return Buffer.from(v).toString('base64');
}
async function timedPreview(request: APIRequestContext, fileUrl: string) {
const started = Date.now();
const resp = await request.get(`/onlinePreview?url=${encodeURIComponent(b64(fileUrl))}`);
const elapsed = Date.now() - started;
return { resp, elapsed };
}
test.beforeAll(async () => {
const api = await playwrightRequest.newContext();
const required = ['sample.txt', 'sample.docx', 'sample.xlsx'];
try {
for (const name of required) {
const resp = await api.get(`${fixtureBase}/${name}`);
expect(resp.ok(), `fixture missing or unavailable: ${name}`).toBeTruthy();
}
} finally {
await api.dispose();
}
});
test('perf: txt preview response under threshold', async ({ request }) => {
const { resp, elapsed } = await timedPreview(request, `${fixtureBase}/sample.txt`);
expect(resp.status()).toBe(200);
expect(elapsed).toBeLessThan(maxMs);
});
test('perf: docx preview response under threshold', async ({ request }) => {
const { resp, elapsed } = await timedPreview(request, `${fixtureBase}/sample.docx`);
expect(resp.status()).toBe(200);
expect(elapsed).toBeLessThan(maxMs);
});
test('perf: xlsx preview response under threshold', async ({ request }) => {
const { resp, elapsed } = await timedPreview(request, `${fixtureBase}/sample.xlsx`);
expect(resp.status()).toBe(200);
expect(elapsed).toBeLessThan(maxMs);
});

View File

@@ -7,18 +7,38 @@ function b64(v: string): string {
}
async function openPreview(request: any, fileUrl: string) {
const encoded = b64(fileUrl);
const encoded = encodeURIComponent(b64(fileUrl));
return request.get(`/onlinePreview?url=${encoded}`);
}
test.beforeAll(async () => {
const api = await playwrightRequest.newContext();
const required = ['sample.txt', 'sample.docx', 'sample.xlsx', 'sample.pptx', 'sample.zip'];
for (const name of required) {
const resp = await api.get(`${fixtureBase}/${name}`);
expect(resp.ok(), `fixture missing or unavailable: ${name}`).toBeTruthy();
const required = [
'sample.txt',
'sample.md',
'sample.json',
'sample.xml',
'sample.csv',
'sample.html',
'sample.png',
'sample.docx',
'sample.xlsx',
'sample.pptx',
'sample.zip',
'sample.tar',
'sample.tgz',
'sample.7z',
'sample.rar',
];
try {
for (const name of required) {
const resp = await api.get(`${fixtureBase}/${name}`);
expect(resp.ok(), `fixture missing or unavailable: ${name}`).toBeTruthy();
}
} finally {
await api.dispose();
}
await api.dispose();
});
test('01 home/index reachable', async ({ request }) => {
@@ -81,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();