Skip to content
Merged
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
136 changes: 100 additions & 36 deletions modules/public/tests/public.docs.integration.tests.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
/**
* Integration tests — GET /api/public/docs and GET /api/public/docs/:slug.md.
* Drives the real Express app against the on-disk markdown guides (the two
* shipped sample guides: welcome + quickstart) so the whole content contract
* (tree shape, real grouping, raw markdown, 404) is exercised end-to-end,
* unauthenticated. The expectations assert the REAL wire shape against the REAL
* sample guides — not a self-authored mock.
* Drives the real Express app end-to-end, unauthenticated, against CONTROLLED
* fixture guides — never the repo's real on-disk `modules/home/doc/guides/*.md`
* samples. A downstream consumer can ship its own guides at any slug (including
* `welcome`/`quickstart`, which a consumer legitimately owns/overrides — see
* the slug-collision precedence block below) without ever changing the outcome
* of these tests. Content-agnostic tests (status/shape only, no slug or guide
* count) are the one exception: they run against whatever `config.files.guides`
* already resolves to, real or consumer-provided, because the assertion holds
Comment thread
coderabbitai[bot] marked this conversation as resolved.
* either way.
*/
import request from 'supertest';
import path from 'path';
Expand All @@ -19,42 +23,96 @@ import mongooseService from '../../../lib/services/mongoose.js';
import config from '../../../config/index.js';
import logger from '../../../lib/services/logger.js';

describe('Public docs integration tests:', () => {
describe('Public docs integration tests — shape/status only:', () => {
let app;
let PublicDocsService;
const originalOrgEnabled = config.organizations?.enabled;

beforeAll(async () => {
if (config.organizations) config.organizations.enabled = false;
const init = await bootstrap();
app = init.app;
PublicDocsService = (await import(path.resolve('./modules/public/services/public.docs.service.js'))).default;
});

afterAll(async () => {
if (config.organizations) config.organizations.enabled = originalOrgEnabled;
await mongooseService.disconnect();
});

beforeEach(() => {
if (PublicDocsService) PublicDocsService.clearCache();
});

// No slug, no guide count, no title/persona assertion — passes regardless of
// which guides are on disk, so no fixture setup is needed here.
test('GET /api/public/docs is publicly accessible without authentication', async () => {
const result = await request(app).get('/api/public/docs').expect(200);
expect(result.body.type).toBe('success');
expect(result.body.message).toBe('public docs');
});
});

describe('Public docs integration tests — controlled guide fixtures:', () => {
let app;
let PublicDocsService;
let tmpDir;
const originalOrgEnabled = config.organizations?.enabled;
const originalGuides = Array.isArray(config.files?.guides) ? [...config.files.guides] : [];

// Two guides feed the tree/category assertions below — numeric prefixes
// 00/01, inside the default `guideSections` "Get Started" range [0,9]. A
// third, unrelated guide at a unique slug and an out-of-range prefix (so it
// never lands in "Get Started") feeds the raw-markdown fetch test. All three
// live under a synthetic, non-core module directory — replicating a
// consumer's own `doc/guides/` folder, never the framework's `modules/home`.
const treeGuideOneRel = path.join('modules', 'fixture-docs', 'doc', 'guides', '00-sample-intro.md');
const treeGuideTwoRel = path.join('modules', 'fixture-docs', 'doc', 'guides', '01-sample-next.md');
const rawMarkdownGuideRel = path.join('modules', 'fixture-docs', 'doc', 'guides', '99-raw-markdown-check.md');
const rawMarkdownMarker = 'FIXTURE_RAW_MARKDOWN_MARKER';

beforeAll(async () => {
if (config.organizations) config.organizations.enabled = false;
const init = await bootstrap();
app = init.app;
PublicDocsService = (await import(path.resolve('./modules/public/services/public.docs.service.js'))).default;

tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'public-docs-fixtures-'));
fs.mkdirSync(path.dirname(path.join(tmpDir, treeGuideOneRel)), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, treeGuideOneRel),
'# Sample Intro\n\nFirst fixture guide for the docs integration suite.\n',
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fs.writeFileSync(
path.join(tmpDir, treeGuideTwoRel),
'# Sample Next\n\nSecond fixture guide for the docs integration suite.\n',
);
fs.writeFileSync(
path.join(tmpDir, rawMarkdownGuideRel),
`# Raw Markdown Check\n\nBody contains a fixture-only marker: ${rawMarkdownMarker}.\n`,
);

// Fully replaced, not appended to the real on-disk guides — the #4014
// pattern: this block controls its own input end to end.
config.files.guides = [
path.join(tmpDir, treeGuideOneRel),
path.join(tmpDir, treeGuideTwoRel),
path.join(tmpDir, rawMarkdownGuideRel),
];
});

afterAll(async () => {
if (config.organizations) config.organizations.enabled = originalOrgEnabled;
config.files.guides = originalGuides;
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
await mongooseService.disconnect();
});

test('the tree contains the shipped sample guides grouped into categories', async () => {
beforeEach(() => {
if (PublicDocsService) PublicDocsService.clearCache();
});

test('the tree groups fixture guides into categories via the configured guide sections', async () => {
const result = await request(app).get('/api/public/docs').expect(200);
const { categories } = result.body.data;
expect(Array.isArray(categories)).toBe(true);
expect(categories.length).toBeGreaterThan(0);

const guides = categories.flatMap((c) => c.guides);
// Devkit ships exactly two sample guides out of the box.
expect(guides.length).toBeGreaterThanOrEqual(2);

// Every guide carries the structured contract fields.
for (const guide of guides) {
Expand All @@ -73,46 +131,52 @@ describe('Public docs integration tests:', () => {
expect(typeof cat.order).toBe('number');
}

// The two shipped sample guides are present.
// The two prefix-00/01 fixture guides are present, no duplicate slugs.
const slugs = guides.map((g) => g.slug);
expect(slugs).toContain('welcome');
expect(slugs).toContain('quickstart');
// No duplicate slugs.
expect(slugs).toContain('sample-intro');
expect(slugs).toContain('sample-next');
expect(new Set(slugs).size).toBe(slugs.length);

// The default config groups both samples under the "Get Started" section
// (neutral persona ['all']) — assert the real grouping, not a mock.
const welcome = guides.find((g) => g.slug === 'welcome');
expect(welcome.title).toBe('Welcome');
expect(welcome.persona).toEqual(['all']);
// The default config groups both fixture guides under "Get Started"
// (prefix range [0,9], neutral persona ['all']) — asserts the REAL grouping
// primitive against controlled content, not a self-authored mock of it.
const sampleIntro = guides.find((g) => g.slug === 'sample-intro');
expect(sampleIntro.title).toBe('Sample Intro');
expect(sampleIntro.persona).toEqual(['all']);
const getStarted = categories.find((c) => c.id === 'get-started');
expect(getStarted).toBeDefined();
expect(getStarted.label).toBe('Get Started');
expect(getStarted.guides.map((g) => g.slug)).toEqual(
expect.arrayContaining(['welcome', 'quickstart']),
expect.arrayContaining(['sample-intro', 'sample-next']),
);
});

test('GET /api/public/docs/:slug.md returns raw markdown for a known slug', async () => {
const result = await request(app).get('/api/public/docs/quickstart.md').expect(200);
test('GET /api/public/docs/:slug.md returns raw markdown for a fixture guide at a unique slug', async () => {
const result = await request(app).get('/api/public/docs/raw-markdown-check.md').expect(200);
expect(result.headers['content-type']).toMatch(/text\/markdown/);
expect(typeof result.text).toBe('string');
expect(result.text.length).toBeGreaterThan(0);
// The H1 was stripped by the loader.
expect(result.text).not.toMatch(/^#\s/);
// The real quickstart body uses a generic placeholder API key.
expect(result.text).toContain('<YOUR_API_KEY>');
// Asserts the fixture's OWN content, never a real shipped guide's — a
// consumer overriding this (or any other) slug can never break this.
expect(result.text).toContain(rawMarkdownMarker);
});

test('GET /api/public/docs/:slug.md returns 404 for an unknown slug', async () => {
const result = await request(app).get('/api/public/docs/does-not-exist.md').expect(404);
expect(result.body.type).toBe('error');
expect(result.body.status).toBe(404);
test('the docs tree never leaks raw YAML front-matter into a fixture guide body', async () => {
const result = await request(app).get('/api/public/docs/sample-intro.md').expect(200);
expect(result.text.trimStart().startsWith('---')).toBe(false);
});

test('the docs tree never leaks raw YAML front-matter into a guide body', async () => {
const result = await request(app).get('/api/public/docs/welcome.md').expect(200);
expect(result.text.trimStart().startsWith('---')).toBe(false);
// Scoped to THIS block (config.files.guides fully replaced by the 3 fixture
// guides above) rather than the shape/status block above — a slug this
// suite never configures is a guarantee only the controlled fixture set can
// give; a consumer's own guides could otherwise happen to occupy any fixed
// slug picked outside that guarantee.
test('GET /api/public/docs/:slug.md returns 404 for a slug absent from the fixture set', async () => {
const result = await request(app).get('/api/public/docs/not-a-configured-fixture-slug.md').expect(404);
expect(result.body.type).toBe('error');
expect(result.body.status).toBe(404);
});
});

Expand Down
Loading