Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .lighthouserc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"http://localhost:3000/",
"http://localhost:3000/docs",
"http://localhost:3000/docs/tutorials/quick-start",
"http://localhost:3000/petstore/api-playground",
"http://localhost:3000/tfl/api-playground"
]
},
Expand Down
13 changes: 8 additions & 5 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,14 @@ const config: Config = {
[
'@docusaurus/plugin-client-redirects',
{
// Add redirect rules here when pages are moved or removed.
// Example: { from: '/docs/old-path', to: '/docs/new-path' }
redirects: [],
// Uncomment to redirect entire path prefixes:
// createRedirects(existingPath) { ... }
redirects: [
{from: '/docs/getting-started', to: '/docs/tutorials/quick-start'},
{from: '/docs/start-here', to: '/docs/tutorials/quick-start'},
{from: '/docs/api', to: '/docs/reference'},
{from: '/tfl/playground', to: '/tfl/api-playground'},
{from: '/petstore/playground', to: '/petstore/api-playground'},
{from: '/platzi/playground', to: '/platzi/api-playground'},
],
},
],
// Benchmark mode: inject a synthetic fixture docset when BENCHMARK_SCALE is set.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"validate-assistant": "npx tsx scripts/validate-assistant-config.ts",
"validate-assistant-quality": "npx tsx scripts/validate-assistant-quality.ts",
"generate-nginx-auth-config": "npx tsx scripts/generate-nginx-auth-config.ts",
"check-rbac-permission": "npx tsx scripts/check-rbac-permission.ts"
"check-rbac-permission": "npx tsx scripts/check-rbac-permission.ts",
"test": "npx tsx --test scripts/__tests__/*.test.ts"
},
"dependencies": {
"@docusaurus/core": "3.9.2",
Expand Down
118 changes: 118 additions & 0 deletions scripts/__tests__/analytics-event.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {describe, it} from 'node:test';
import assert from 'node:assert/strict';
import {
validateAnalyticsEvent,
createExportBatch,
SCHEMA_VERSION,
type AnalyticsEvent,
} from '../validate-analytics-event.js';

function validEvent(overrides?: Partial<AnalyticsEvent>): AnalyticsEvent {
return {
schemaVersion: SCHEMA_VERSION,
type: 'page.view',
timestamp: '2026-04-03T09:00:00Z',
actor: {type: 'user', sessionId: 'abc-123'},
payload: {docset: 'tfl', version: 'latest', slug: '/tfl/getting-started'},
...overrides,
};
}

describe('validateAnalyticsEvent', () => {
it('passes for a valid page.view event', () => {
const errors = validateAnalyticsEvent(validEvent());
const errs = errors.filter(e => e.level === 'error');
assert.equal(errs.length, 0);
});

it('errors on missing schemaVersion', () => {
const event = validEvent({schemaVersion: ''});
const errors = validateAnalyticsEvent(event);
assert.ok(errors.some(e => e.field === 'schemaVersion'));
});

it('warns on mismatched schemaVersion', () => {
const event = validEvent({schemaVersion: '0.9'});
const errors = validateAnalyticsEvent(event);
assert.ok(errors.some(e => e.level === 'warn' && e.field === 'schemaVersion'));
});

it('errors on invalid event type', () => {
const event = validEvent({type: 'user.logout' as any});
const errors = validateAnalyticsEvent(event);
assert.ok(errors.some(e => e.field === 'type'));
});

it('errors on missing timestamp', () => {
const event = validEvent({timestamp: ''});
const errors = validateAnalyticsEvent(event);
assert.ok(errors.some(e => e.field === 'timestamp'));
});

it('errors on invalid timestamp format', () => {
const event = validEvent({timestamp: 'yesterday'});
const errors = validateAnalyticsEvent(event);
assert.ok(errors.some(e => e.field === 'timestamp'));
});

it('errors on missing actor', () => {
const event = validEvent();
(event as any).actor = undefined;
const errors = validateAnalyticsEvent(event);
assert.ok(errors.some(e => e.field === 'actor'));
});

it('errors on invalid actor type', () => {
const event = validEvent();
event.actor.type = 'admin' as any;
const errors = validateAnalyticsEvent(event);
assert.ok(errors.some(e => e.field === 'actor.type'));
});

it('errors on missing actor sessionId', () => {
const event = validEvent();
event.actor.sessionId = '';
const errors = validateAnalyticsEvent(event);
assert.ok(errors.some(e => e.field === 'actor.sessionId'));
});

it('errors on missing payload', () => {
const event = validEvent();
(event as any).payload = undefined;
const errors = validateAnalyticsEvent(event);
assert.ok(errors.some(e => e.field === 'payload'));
});

it('errors on missing required payload fields for page.view', () => {
const event = validEvent({payload: {docset: 'tfl'} as any});
const errors = validateAnalyticsEvent(event);
assert.ok(errors.some(e => e.field.startsWith('payload.')));
});

it('validates search.query event payload', () => {
const event = validEvent({
type: 'search.query',
payload: {query: 'how to authenticate', resultCount: 5} as any,
});
const errors = validateAnalyticsEvent(event);
const errs = errors.filter(e => e.level === 'error');
assert.equal(errs.length, 0);
});
});

describe('createExportBatch', () => {
it('wraps events with metadata', () => {
const events = [validEvent(), validEvent({type: 'search.query', payload: {query: 'test', resultCount: 0} as any})];
const batch = createExportBatch(events);
assert.equal(batch.schemaVersion, SCHEMA_VERSION);
assert.equal(batch.eventCount, 2);
assert.equal(batch.events.length, 2);
assert.ok(batch.exportedAt);
});

it('handles empty events array', () => {
const batch = createExportBatch([]);
assert.equal(batch.eventCount, 0);
assert.deepEqual(batch.events, []);
});
});
181 changes: 181 additions & 0 deletions scripts/__tests__/docset-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {describe, it} from 'node:test';
import assert from 'node:assert/strict';
import {
isValidCalVer,
resolveLatestVersion,
validateDocsetConfig,
type DocsetConfig,
} from '../docset.config.js';

describe('isValidCalVer', () => {
it('accepts yyyy.mm format', () => {
assert.equal(isValidCalVer('2024.01'), true);
assert.equal(isValidCalVer('2025.12'), true);
assert.equal(isValidCalVer('2030.06'), true);
});

it('accepts yyyy.mm-LTS format', () => {
assert.equal(isValidCalVer('2024.06-LTS'), true);
assert.equal(isValidCalVer('2025.09-LTS'), true);
});

it('rejects invalid formats', () => {
assert.equal(isValidCalVer('v1.0'), false);
assert.equal(isValidCalVer('2024'), false);
assert.equal(isValidCalVer('2024.1'), false);
assert.equal(isValidCalVer('24.01'), false);
assert.equal(isValidCalVer('2024.06-lts'), false);
assert.equal(isValidCalVer('latest'), false);
assert.equal(isValidCalVer(''), false);
});
});

describe('resolveLatestVersion', () => {
it('returns undefined for unversioned docsets', () => {
const config: DocsetConfig = {id: 'test', name: 'Test'};
assert.equal(resolveLatestVersion(config), undefined);
});

it('returns undefined for empty versions array', () => {
const config: DocsetConfig = {id: 'test', name: 'Test', versions: []};
assert.equal(resolveLatestVersion(config), undefined);
});

it('returns explicit latestVersion when set', () => {
const config: DocsetConfig = {
id: 'test',
name: 'Test',
latestVersion: '2024.06',
versions: [
{id: '2025.01', label: 'Jan 2025', state: 'active'},
{id: '2024.06', label: 'Jun 2024', state: 'lts'},
],
};
assert.equal(resolveLatestVersion(config), '2024.06');
});

it('resolves highest active version when no explicit latest', () => {
const config: DocsetConfig = {
id: 'test',
name: 'Test',
versions: [
{id: '2024.01', label: 'Jan 2024', state: 'deprecated'},
{id: '2024.06', label: 'Jun 2024', state: 'active'},
{id: '2025.01', label: 'Jan 2025', state: 'active'},
],
};
assert.equal(resolveLatestVersion(config), '2025.01');
});

it('considers lts versions as candidates', () => {
const config: DocsetConfig = {
id: 'test',
name: 'Test',
versions: [
{id: '2024.06-LTS', label: 'Jun 2024 LTS', state: 'lts'},
{id: '2024.01', label: 'Jan 2024', state: 'deprecated'},
],
};
assert.equal(resolveLatestVersion(config), '2024.06-LTS');
});

it('ignores deprecated and eol versions', () => {
const config: DocsetConfig = {
id: 'test',
name: 'Test',
versions: [
{id: '2025.01', label: 'Jan 2025', state: 'deprecated'},
{id: '2024.06', label: 'Jun 2024', state: 'eol'},
{id: '2024.01', label: 'Jan 2024', state: 'active'},
],
};
assert.equal(resolveLatestVersion(config), '2024.01');
});
});

describe('validateDocsetConfig', () => {
it('passes for a valid unversioned config', () => {
const config: DocsetConfig = {id: 'test', name: 'Test'};
assert.deepEqual(validateDocsetConfig(config), []);
});

it('errors on missing id', () => {
const config = {id: '', name: 'Test'} as DocsetConfig;
const errors = validateDocsetConfig(config);
assert.ok(errors.some(e => e.field === 'id'));
});

it('errors on missing name', () => {
const config = {id: 'test', name: ''} as DocsetConfig;
const errors = validateDocsetConfig(config);
assert.ok(errors.some(e => e.field === 'name'));
});

it('errors on invalid CalVer version id', () => {
const config: DocsetConfig = {
id: 'test',
name: 'Test',
versions: [{id: 'v1.0', label: 'V1', state: 'active'}],
};
const errors = validateDocsetConfig(config);
assert.ok(errors.some(e => e.message.includes('not a valid CalVer')));
});

it('errors on duplicate version ids', () => {
const config: DocsetConfig = {
id: 'test',
name: 'Test',
versions: [
{id: '2024.06', label: 'Jun 2024', state: 'active'},
{id: '2024.06', label: 'Jun 2024 copy', state: 'lts'},
],
};
const errors = validateDocsetConfig(config);
assert.ok(errors.some(e => e.message.includes('duplicate')));
});

it('errors on missing version label', () => {
const config: DocsetConfig = {
id: 'test',
name: 'Test',
versions: [{id: '2024.06', label: '', state: 'active'}],
};
const errors = validateDocsetConfig(config);
assert.ok(errors.some(e => e.field.includes('label')));
});

it('errors on invalid lifecycle state', () => {
const config: DocsetConfig = {
id: 'test',
name: 'Test',
versions: [{id: '2024.06', label: 'Jun', state: 'beta' as any}],
};
const errors = validateDocsetConfig(config);
assert.ok(errors.some(e => e.message.includes('not a valid lifecycle state')));
});

it('errors when latestVersion not in versions list', () => {
const config: DocsetConfig = {
id: 'test',
name: 'Test',
latestVersion: '2024.12',
versions: [{id: '2024.06', label: 'Jun', state: 'active'}],
};
const errors = validateDocsetConfig(config);
assert.ok(errors.some(e => e.field === 'latestVersion'));
});

it('passes for a fully valid versioned config', () => {
const config: DocsetConfig = {
id: 'my-api',
name: 'My API',
latestVersion: '2025.01',
versions: [
{id: '2025.01', label: 'Jan 2025', state: 'active'},
{id: '2024.06-LTS', label: 'Jun 2024 LTS', state: 'lts'},
{id: '2024.01', label: 'Jan 2024', state: 'deprecated'},
],
};
assert.deepEqual(validateDocsetConfig(config), []);
});
});
Loading
Loading