112 lines
5.8 KiB
JavaScript
112 lines
5.8 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
const fs = require('node:fs/promises');
|
|
const os = require('node:os');
|
|
const express = require('express');
|
|
const createResumeRouter = require('../routes/home');
|
|
const { buildSeo, siteOrigin } = require('../utils/seo');
|
|
|
|
async function serve(t, options) {
|
|
const app = express();
|
|
app.set('view engine', 'ejs');
|
|
app.set('views', path.join(__dirname, '../views'));
|
|
app.use(createResumeRouter(options));
|
|
const server = app.listen(0, '127.0.0.1');
|
|
await new Promise((resolve, reject) => { server.once('listening', resolve); server.once('error', reject); });
|
|
t.after(() => new Promise((resolve) => server.close(resolve)));
|
|
return `http://127.0.0.1:${server.address().port}`;
|
|
}
|
|
|
|
for (const example of ['cvExample1', 'cvExample2']) {
|
|
test(`renders ${example}, metadata and crawler endpoints`, async (t) => {
|
|
const resumeFile = path.join(__dirname, '../public/examples', example, 'me.json');
|
|
const data = JSON.parse(await fs.readFile(resumeFile, 'utf8'));
|
|
const base = await serve(t, { resumeFile, siteUrl: 'https://resume.example.com' });
|
|
const response = await fetch(base);
|
|
assert.equal(response.status, 200);
|
|
const html = await response.text();
|
|
assert.match(html, /id="experience"/);
|
|
assert.match(html, /id="skills"/);
|
|
assert.match(html, /id="education"/);
|
|
assert.match(html, /media="print"/);
|
|
const schema = JSON.parse(html.match(/<script type="application\/ld\+json">(.*?)<\/script>/s)[1]);
|
|
assert.equal(schema.mainEntity.name, data.name);
|
|
assert.equal(schema.url, 'https://resume.example.com/');
|
|
const robots = await (await fetch(base + '/robots.txt')).text();
|
|
assert.match(robots, /Sitemap: https:\/\/resume.example.com\/sitemap.xml/);
|
|
assert.match(await (await fetch(base + '/sitemap.xml')).text(), /<loc>https:\/\/resume.example.com\/<\/loc>/);
|
|
const summary = await (await fetch(base + '/llms.txt')).text();
|
|
assert.ok(summary.includes(data.name));
|
|
assert.ok(summary.includes(data.workexperiences[0].desc));
|
|
const preview = await fetch(base + '/?pdf-preview');
|
|
assert.equal(preview.headers.get('x-robots-tag'), 'noindex, nofollow');
|
|
});
|
|
}
|
|
|
|
test('missing canonical does not fabricate a production hostname', async (t) => {
|
|
const resumeFile = path.join(__dirname, '../public/examples/cvExample1/me.json');
|
|
const base = await serve(t, { resumeFile, siteUrl: '' });
|
|
assert.equal((await fetch(base + '/sitemap.xml')).status, 503);
|
|
assert.doesNotMatch(await (await fetch(base)).text(), /rel="canonical"/);
|
|
assert.doesNotMatch(await (await fetch(base + '/robots.txt')).text(), /Sitemap:/);
|
|
});
|
|
|
|
test('structured data safely handles closing script tags without inventing profile identities', async () => {
|
|
const data = JSON.parse(await fs.readFile(path.join(__dirname, '../public/examples/cvExample1/me.json'), 'utf8'));
|
|
data.name = '</script><script>alert(1)</script>';
|
|
const seo = buildSeo(data, 'https://resume.example.com');
|
|
assert.ok(!seo.schema.includes('</script>'));
|
|
assert.equal(JSON.parse(seo.schema).mainEntity.name, data.name);
|
|
assert.equal(JSON.parse(seo.schema).mainEntity.sameAs, undefined);
|
|
assert.throws(() => siteOrigin('javascript:alert(1)'));
|
|
assert.throws(() => siteOrigin('https://resume.example.com/subpath'));
|
|
});
|
|
|
|
test('invalid resume returns a clear client error', async (t) => {
|
|
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'resume-test-'));
|
|
t.after(() => fs.rm(directory, { recursive: true, force: true }));
|
|
const resumeFile = path.join(directory, 'me.json');
|
|
await fs.writeFile(resumeFile, '{}');
|
|
const base = await serve(t, { resumeFile, siteUrl: '' });
|
|
assert.equal((await fetch(base)).status, 400);
|
|
});
|
|
|
|
test('me.json updates all public URLs live, overrides environment and recovers from invalid edits', async (t) => {
|
|
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'resume-config-'));
|
|
t.after(() => fs.rm(directory, { recursive: true, force: true }));
|
|
const resumeFile = path.join(directory, 'me.json');
|
|
const data = JSON.parse(await fs.readFile(path.join(__dirname, '../public/examples/cvExample1/me.json'), 'utf8'));
|
|
await fs.writeFile(resumeFile, JSON.stringify(data));
|
|
const writeConfig = (config) => fs.writeFile(resumeFile, JSON.stringify({ ...data, ...config }));
|
|
const base = await serve(t, { resumeFile, siteUrl: 'https://fallback.example.com' });
|
|
const checkOrigin = async (origin) => {
|
|
const html = await (await fetch(base)).text();
|
|
assert.ok(html.includes(`rel="canonical" href="${origin}/"`));
|
|
const schema = JSON.parse(html.match(/<script type="application\/ld\+json">(.*?)<\/script>/s)[1]);
|
|
assert.equal(schema.url, origin + '/');
|
|
for (const endpoint of ['/robots.txt', '/sitemap.xml', '/llms.txt']) {
|
|
const response = await fetch(base + endpoint);
|
|
assert.equal(response.status, 200);
|
|
assert.ok((await response.text()).includes(origin + '/'), endpoint);
|
|
}
|
|
};
|
|
await checkOrigin('https://fallback.example.com');
|
|
await writeConfig({ SITE_URL: 'https://first.example.com' });
|
|
await checkOrigin('https://first.example.com');
|
|
await writeConfig({ SITE_URL: 'https://second.example.com/' });
|
|
await checkOrigin('https://second.example.com');
|
|
for (const invalid of [123, null, 'javascript:alert(1)']) {
|
|
await writeConfig({ SITE_URL: invalid });
|
|
for (const endpoint of ['/', '/robots.txt', '/sitemap.xml', '/llms.txt']) {
|
|
assert.equal((await fetch(base + endpoint)).status, 503, endpoint);
|
|
}
|
|
}
|
|
await fs.writeFile(resumeFile, '{');
|
|
assert.equal((await fetch(base)).status, 503);
|
|
await writeConfig({});
|
|
await checkOrigin('https://fallback.example.com');
|
|
await writeConfig({ SITE_URL: '' });
|
|
assert.doesNotMatch(await (await fetch(base)).text(), /rel="canonical"/);
|
|
assert.equal((await fetch(base + '/sitemap.xml')).status, 503);
|
|
});
|