CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
The previous cleanup emptied proto/public/partners/ entirely, deleting real committed partner logos during the pre-deploy run. Now the test uploads to unique run-specific filenames and removes only those.
190 lines
7.4 KiB
JavaScript
190 lines
7.4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Integration test for CMS logo upload (MITHOME-65):
|
|
* 1. GET /branding serves the logo page (session-auth)
|
|
* 2. POST /logo?target=icon with a valid PNG replaces the file, backs up the
|
|
* old one into .content-backups and audits logo_updated
|
|
* 3. non-PNG bytes → 415; >1 MiB → 413; bad target → 400; no CSRF → 403;
|
|
* unauthenticated → 401
|
|
* Original logo files are restored at the end.
|
|
*/
|
|
const assert = require('assert/strict');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { spawn } = require('child_process');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
const PORT = 4132;
|
|
const BASE = `http://127.0.0.1:${PORT}`;
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cms-logo-'));
|
|
const auditFile = path.join(tmp, 'audit.jsonl');
|
|
|
|
// Minimal valid 1x1 transparent PNG
|
|
const TINY_PNG = Buffer.from(
|
|
'89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d4944415478da636460f8ff9f0001040100c9fe92ef0000000049454e44ae426082',
|
|
'hex'
|
|
);
|
|
|
|
const child = spawn('node', ['content-editor.js'], {
|
|
cwd: ROOT,
|
|
env: {
|
|
...process.env,
|
|
CONTENT_EDITOR_PORT: String(PORT),
|
|
CONTENT_EDITOR_AUDIT_FILE: auditFile,
|
|
CMS_USER: 'logo-test-user',
|
|
CMS_PASS: 'logo-test-pass',
|
|
CMS_DEPLOY_ENV: 'staging',
|
|
},
|
|
stdio: 'ignore',
|
|
});
|
|
|
|
async function waitForServer(timeoutMs = 10000) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
await fetch(`${BASE}/version`);
|
|
return;
|
|
} catch {
|
|
await new Promise(r => setTimeout(r, 200));
|
|
}
|
|
}
|
|
throw new Error('server did not start');
|
|
}
|
|
|
|
async function main() {
|
|
await waitForServer();
|
|
const login = await fetch(`${BASE}/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ user: 'logo-test-user', pass: 'logo-test-pass' }),
|
|
});
|
|
const cookie = (login.headers.get('set-cookie') || '').split(';')[0];
|
|
const page = await (await fetch(`${BASE}/`, { headers: { Cookie: cookie } })).text();
|
|
const csrf = page.match(/CSRF_TOKEN = "([a-f0-9]+)"/)[1];
|
|
|
|
const iconPath = path.join(ROOT, 'proto', 'public', 'mozdit_logo.png');
|
|
const headerPath = path.join(ROOT, 'proto', 'public', 'mozdit_logo_text.png');
|
|
const originalIcon = fs.readFileSync(iconPath);
|
|
const originalHeader = fs.readFileSync(headerPath);
|
|
const backupDir = path.join(ROOT, '.content-backups');
|
|
|
|
try {
|
|
// 1. branding page
|
|
const branding = await fetch(`${BASE}/branding`, { headers: { Cookie: cookie } });
|
|
assert.equal(branding.status, 200);
|
|
assert.match(await branding.text(), /Logó kezelése/);
|
|
|
|
// 2. valid upload replaces the file and creates a backup
|
|
const before = fs.readdirSync(backupDir).filter(n => n.startsWith('mozdit_logo.png.'));
|
|
const up = await fetch(`${BASE}/logo?target=icon`, {
|
|
method: 'POST',
|
|
headers: { Cookie: cookie, 'Content-Type': 'image/png', 'X-CSRF-Token': csrf },
|
|
body: TINY_PNG,
|
|
});
|
|
assert.equal(up.status, 200);
|
|
const upBody = await up.json();
|
|
assert.equal(upBody.ok, true);
|
|
assert.match(upBody.backup, /^mozdit_logo\.png\./);
|
|
assert.deepEqual(fs.readFileSync(iconPath), TINY_PNG, 'icon file replaced');
|
|
const after = fs.readdirSync(backupDir).filter(n => n.startsWith('mozdit_logo.png.'));
|
|
assert.equal(after.length, before.length + 1, 'old logo backed up');
|
|
// audit entry
|
|
const audit = fs.readFileSync(auditFile, 'utf8').trim().split('\n').map(l => JSON.parse(l));
|
|
assert.ok(audit.some(e => e.event === 'logo_updated' && e.result === 'ok'));
|
|
|
|
// variant preview route serves the header logo
|
|
const headerPreview = await fetch(`${BASE}/logo.png?variant=header`);
|
|
assert.equal(headerPreview.status, 200);
|
|
assert.deepEqual(Buffer.from(await headerPreview.arrayBuffer()), originalHeader);
|
|
|
|
// 3a. non-PNG → 415
|
|
const bad = await fetch(`${BASE}/logo?target=icon`, {
|
|
method: 'POST',
|
|
headers: { Cookie: cookie, 'Content-Type': 'image/png', 'X-CSRF-Token': csrf },
|
|
body: Buffer.from('definitely not a png'),
|
|
});
|
|
assert.equal(bad.status, 415);
|
|
|
|
// 3b. oversized → 413
|
|
const big = Buffer.alloc(1024 * 1024 + 1);
|
|
big.set(TINY_PNG.subarray(0, 8));
|
|
const tooBig = await fetch(`${BASE}/logo?target=icon`, {
|
|
method: 'POST',
|
|
headers: { Cookie: cookie, 'Content-Type': 'image/png', 'X-CSRF-Token': csrf },
|
|
body: big,
|
|
});
|
|
assert.equal(tooBig.status, 413);
|
|
|
|
// 3c. bad target → 400
|
|
const badTarget = await fetch(`${BASE}/logo?target=../../etc`,
|
|
{ method: 'POST', headers: { Cookie: cookie, 'Content-Type': 'image/png', 'X-CSRF-Token': csrf }, body: TINY_PNG });
|
|
assert.equal(badTarget.status, 400);
|
|
|
|
// 3d. authenticated but no CSRF → 403
|
|
const noCsrf = await fetch(`${BASE}/logo?target=icon`,
|
|
{ method: 'POST', headers: { Cookie: cookie, 'Content-Type': 'image/png' }, body: TINY_PNG });
|
|
assert.equal(noCsrf.status, 403);
|
|
|
|
// 3e. unauthenticated (valid CSRF token but no session) → 401
|
|
const anon = await fetch(`${BASE}/logo?target=icon`,
|
|
{ method: 'POST', headers: { 'Content-Type': 'image/png', 'X-CSRF-Token': csrf }, body: TINY_PNG });
|
|
assert.equal(anon.status, 401);
|
|
|
|
// 4. partner logo upload (MITHOME-83)
|
|
const partnerDir = path.join(ROOT, 'proto', 'public', 'partners');
|
|
// Unique names so the test never collides with real partner logos.
|
|
const runId = process.pid + '-' + Date.now();
|
|
const created = [];
|
|
try {
|
|
const up = await fetch(`${BASE}/partner-logo?name=acme-${runId}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'image/png', 'X-CSRF-Token': csrf, 'Cookie': cookie },
|
|
body: TINY_PNG,
|
|
});
|
|
assert.equal(up.status, 200);
|
|
const body = await up.json();
|
|
assert.equal(body.ok, true);
|
|
assert.equal(body.path, `/partners/acme-${runId}.png`);
|
|
created.push(path.join(partnerDir, `acme-${runId}.png`));
|
|
assert.ok(fs.existsSync(created[0]), 'partner logo file created');
|
|
|
|
// non-PNG → 415
|
|
const bad = await fetch(`${BASE}/partner-logo?name=x-${runId}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'image/png', 'X-CSRF-Token': csrf, 'Cookie': cookie },
|
|
body: Buffer.from('not a png'),
|
|
});
|
|
assert.equal(bad.status, 415);
|
|
|
|
// traversal name is sanitized (no path escape)
|
|
const trav = await fetch(`${BASE}/partner-logo?name=../evil-${runId}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'image/png', 'X-CSRF-Token': csrf, 'Cookie': cookie },
|
|
body: TINY_PNG,
|
|
});
|
|
assert.equal(trav.status, 200);
|
|
const tBody = await trav.json();
|
|
assert.ok(tBody.path.startsWith('/partners/'), 'traversal name is sanitized to a safe slug');
|
|
assert.ok(!tBody.path.includes('..'), 'no traversal in the returned path');
|
|
created.push(path.join(ROOT, 'proto', 'public', tBody.path));
|
|
} finally {
|
|
for (const f of created) { try { fs.unlinkSync(f); } catch { /* noop */ } }
|
|
try { fs.rmdirSync(partnerDir); } catch { /* only removes when empty */ }
|
|
}
|
|
|
|
console.log('Content Editor logo upload test: OK');
|
|
} finally {
|
|
fs.writeFileSync(iconPath, originalIcon);
|
|
fs.writeFileSync(headerPath, originalHeader);
|
|
}
|
|
}
|
|
|
|
main()
|
|
.catch(err => { console.error('❌', err.message); process.exitCode = 1; })
|
|
.finally(() => {
|
|
child.kill('SIGTERM');
|
|
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
});
|