test(integrations/client/packages): batch 2-4 unit coverage + zip-slip guard extraction
Batch 2-4 of the test-strategy rollout. Test-only except one minimal, behaviour-preserving extraction in file.utils.ts. All suites green: server 82 suites/836+1todo, editor-ext 86, mcp 270, client (new files) 86. integrations (server): - file.utils.ts: extract pure `isEntryPathSafe(entryName, targetDir)` from extractZipInternal so the zip-slip/path-traversal guard is unit-testable; call site rerouted, behaviour identical (only a warn-message string merged). - file.utils.zip-safety.spec.ts: traversal/strip/__MACOSX/prefix-confusion cases (mutation-resistant: fails if containment loses the path.sep). - import-formatter / import.utils / table-utils / export utils / import.service extractTitleAndRemoveHeading: pure import/export transforms, Notion/XWiki formatting, table colspan widths (idempotent), slug/link rewriting. client: - safeRedirectPath: open-redirect guard, every reject branch independently. - buildChatMarkdown (fence anti-breakout), label-colors, normalize-label, share tree build, page URL builders, notification time-grouping (fake clock). packages: - editor-ext: deriveFootnoteId golden table, parseHtmlEmbedHeight crafted values, orphan footnote extraction. - mcp: deriveFootnoteId parity (drift guard vs editor-ext), applyTextEdits idempotency + cross-block replaceAll, diffDocs/summarizeChange on reorder. Reviewed (APPROVE): extraction behaviour-preserving, assertions mutation-resistant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
// Importing ImportService transitively loads import-formatter.ts, which imports
|
||||
// the ESM-only @sindresorhus/slugify package (not in jest's transform
|
||||
// allowlist). slugify is irrelevant to the method under test, so it is mocked
|
||||
// out to keep the module graph loadable under ts-jest.
|
||||
jest.mock('@sindresorhus/slugify', () => ({
|
||||
__esModule: true,
|
||||
default: (input: string) => String(input),
|
||||
}));
|
||||
|
||||
import { ImportService } from './import.service';
|
||||
|
||||
/**
|
||||
* Unit tests for ImportService.extractTitleAndRemoveHeading — a pure method
|
||||
* (no `this`, no I/O). It pulls a leading level-1 heading out of a ProseMirror
|
||||
* document, returning its text as the title and the remaining content, and
|
||||
* guarantees at least one paragraph remains.
|
||||
*
|
||||
* The method does not touch the injected deps, so the service is constructed
|
||||
* with placeholder dependencies.
|
||||
*/
|
||||
|
||||
function makeService(): ImportService {
|
||||
// The method under test never references `this`/injected deps.
|
||||
return new ImportService({} as any, {} as any, {} as any, {} as any);
|
||||
}
|
||||
|
||||
describe('ImportService.extractTitleAndRemoveHeading', () => {
|
||||
const service = makeService();
|
||||
|
||||
it('extracts a leading H1 as the title and removes the heading from content', () => {
|
||||
const state = {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: { level: 1 },
|
||||
content: [{ type: 'text', text: 'My Title' }],
|
||||
},
|
||||
{ type: 'paragraph', content: [{ type: 'text', text: 'body' }] },
|
||||
],
|
||||
};
|
||||
|
||||
const result = service.extractTitleAndRemoveHeading(state);
|
||||
|
||||
expect(result.title).toBe('My Title');
|
||||
// heading removed, only the paragraph remains
|
||||
expect(result.prosemirrorJson.content).toHaveLength(1);
|
||||
expect(result.prosemirrorJson.content[0].type).toBe('paragraph');
|
||||
expect(result.prosemirrorJson.content[0].content[0].text).toBe('body');
|
||||
// doc type preserved via spread
|
||||
expect(result.prosemirrorJson.type).toBe('doc');
|
||||
});
|
||||
|
||||
it('returns a null title and keeps content when there is no leading H1', () => {
|
||||
const state = {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{ type: 'paragraph', content: [{ type: 'text', text: 'first' }] },
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: { level: 1 },
|
||||
content: [{ type: 'text', text: 'Later Heading' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = service.extractTitleAndRemoveHeading(state);
|
||||
|
||||
expect(result.title).toBeNull();
|
||||
// nothing removed
|
||||
expect(result.prosemirrorJson.content).toHaveLength(2);
|
||||
expect(result.prosemirrorJson.content[0].type).toBe('paragraph');
|
||||
});
|
||||
|
||||
it('does not treat a level-2 heading as a title', () => {
|
||||
const state = {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: { level: 2 },
|
||||
content: [{ type: 'text', text: 'Subheading' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = service.extractTitleAndRemoveHeading(state);
|
||||
|
||||
expect(result.title).toBeNull();
|
||||
expect(result.prosemirrorJson.content).toHaveLength(1);
|
||||
expect(result.prosemirrorJson.content[0].type).toBe('heading');
|
||||
});
|
||||
|
||||
it('injects one empty paragraph when the content becomes empty', () => {
|
||||
// A document that is just a single H1 -> after removal, content is empty
|
||||
// and one empty paragraph is injected.
|
||||
const state = {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: { level: 1 },
|
||||
content: [{ type: 'text', text: 'Only Title' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = service.extractTitleAndRemoveHeading(state);
|
||||
|
||||
expect(result.title).toBe('Only Title');
|
||||
expect(result.prosemirrorJson.content).toEqual([
|
||||
{ type: 'paragraph', content: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('injects an empty paragraph for an already-empty document', () => {
|
||||
const state = { type: 'doc', content: [] };
|
||||
|
||||
const result = service.extractTitleAndRemoveHeading(state);
|
||||
|
||||
expect(result.title).toBeNull();
|
||||
expect(result.prosemirrorJson.content).toEqual([
|
||||
{ type: 'paragraph', content: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('yields a null title when an H1 has no text node', () => {
|
||||
const state = {
|
||||
type: 'doc',
|
||||
content: [{ type: 'heading', attrs: { level: 1 }, content: [] }],
|
||||
};
|
||||
|
||||
const result = service.extractTitleAndRemoveHeading(state);
|
||||
|
||||
expect(result.title).toBeNull();
|
||||
// heading removed, empty paragraph injected
|
||||
expect(result.prosemirrorJson.content).toEqual([
|
||||
{ type: 'paragraph', content: [] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user