refactor(html-embed): extract the admin-gate strip into one tested helper (#90)
The 4-step html-embed gate (feature-enabled AND role-allowed -> stripHtmlEmbedNodes)
was replicated across call-sites, pinned only by brittle source-regex tests. Add
stripHtmlEmbedIfNotAllowed(json, {featureEnabled, role, onStrip}) and migrate the
5 plain strip-all sites (collab handler, page create+duplicate, both import paths,
transclusion) to it, each keeping its own feature/role resolve + log via onStrip.
Left the 2 sites with different semantics: persistence.extension (#29 preserve-
admin) and share.service (feature-only kill-switch, no role gate). Real unit tests
replace the regex pins; behavior identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
htmlEmbedAllowed,
|
||||
isHtmlEmbedFeatureEnabled,
|
||||
stripDisallowedHtmlEmbedNodes,
|
||||
stripHtmlEmbedIfNotAllowed,
|
||||
stripHtmlEmbedNodes,
|
||||
} from './html-embed.util';
|
||||
import { htmlToJson, jsonToHtml } from '../../../collaboration/collaboration.util';
|
||||
@@ -413,6 +414,119 @@ describe('htmlEmbedAllowed (toggle AND admin)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// The shared write-path strip ritual extracted from the 5 plain call-sites
|
||||
// (collab handler, page create/duplicate, import, file-import-task,
|
||||
// transclusion-unsync). Tested here once instead of being re-verified in each
|
||||
// call-site's spec.
|
||||
describe('stripHtmlEmbedIfNotAllowed (shared write-path gate)', () => {
|
||||
const docWithEmbed = () => ({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{ type: 'paragraph', content: [{ type: 'text', text: 'keep' }] },
|
||||
{ type: 'htmlEmbed', attrs: { source: '<script>x()</script>' } },
|
||||
],
|
||||
});
|
||||
const docWithoutEmbed = () => ({
|
||||
type: 'doc',
|
||||
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'keep' }] }],
|
||||
});
|
||||
|
||||
it('keeps the doc unchanged when feature is ON and role is admin (allowed)', () => {
|
||||
const json = docWithEmbed();
|
||||
const onStrip = jest.fn();
|
||||
const result = stripHtmlEmbedIfNotAllowed(json, {
|
||||
featureEnabled: true,
|
||||
role: 'admin',
|
||||
onStrip,
|
||||
});
|
||||
// Allowed => same reference returned, embed preserved, no side-effect.
|
||||
expect(result).toBe(json);
|
||||
expect(hasHtmlEmbedNode(result)).toBe(true);
|
||||
expect(onStrip).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps the doc unchanged for an owner when feature is ON (allowed)', () => {
|
||||
const json = docWithEmbed();
|
||||
const onStrip = jest.fn();
|
||||
const result = stripHtmlEmbedIfNotAllowed(json, {
|
||||
featureEnabled: true,
|
||||
role: 'owner',
|
||||
onStrip,
|
||||
});
|
||||
expect(result).toBe(json);
|
||||
expect(hasHtmlEmbedNode(result)).toBe(true);
|
||||
expect(onStrip).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('strips the embed when the feature is OFF (even for an admin)', () => {
|
||||
const json = docWithEmbed();
|
||||
const onStrip = jest.fn();
|
||||
const result = stripHtmlEmbedIfNotAllowed(json, {
|
||||
featureEnabled: false,
|
||||
role: 'admin',
|
||||
onStrip,
|
||||
});
|
||||
expect(hasHtmlEmbedNode(result)).toBe(false);
|
||||
expect(onStrip).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('strips the embed for a non-admin when the feature is ON', () => {
|
||||
const json = docWithEmbed();
|
||||
const onStrip = jest.fn();
|
||||
const result = stripHtmlEmbedIfNotAllowed(json, {
|
||||
featureEnabled: true,
|
||||
role: 'member',
|
||||
onStrip,
|
||||
});
|
||||
expect(hasHtmlEmbedNode(result)).toBe(false);
|
||||
expect(onStrip).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('strips the embed for a null/undefined role when the feature is ON', () => {
|
||||
for (const role of [null, undefined]) {
|
||||
const onStrip = jest.fn();
|
||||
const result = stripHtmlEmbedIfNotAllowed(docWithEmbed(), {
|
||||
featureEnabled: true,
|
||||
role,
|
||||
onStrip,
|
||||
});
|
||||
expect(hasHtmlEmbedNode(result)).toBe(false);
|
||||
expect(onStrip).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns input unchanged and does NOT call onStrip when no embed is present', () => {
|
||||
const json = docWithoutEmbed();
|
||||
const onStrip = jest.fn();
|
||||
// Not allowed (feature OFF), but there is nothing to strip.
|
||||
const result = stripHtmlEmbedIfNotAllowed(json, {
|
||||
featureEnabled: false,
|
||||
role: 'member',
|
||||
onStrip,
|
||||
});
|
||||
expect(result).toBe(json);
|
||||
expect(onStrip).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onStrip exactly once per strip', () => {
|
||||
const onStrip = jest.fn();
|
||||
stripHtmlEmbedIfNotAllowed(docWithEmbed(), {
|
||||
featureEnabled: false,
|
||||
role: 'member',
|
||||
onStrip,
|
||||
});
|
||||
expect(onStrip).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('works without an onStrip callback (optional)', () => {
|
||||
const result = stripHtmlEmbedIfNotAllowed(docWithEmbed(), {
|
||||
featureEnabled: false,
|
||||
role: 'member',
|
||||
});
|
||||
expect(hasHtmlEmbedNode(result)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// NOTE: a previous revision of this file re-implemented the write-path admin
|
||||
// gate as a local `applyAdminGate` stand-in and asserted against THAT. A
|
||||
// deleted/misplaced real guard would have kept those green. The stand-in is
|
||||
|
||||
@@ -197,6 +197,30 @@ export function htmlEmbedAllowed(
|
||||
return featureEnabled === true && canAuthorHtmlEmbed(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip htmlEmbed nodes unless the (feature-enabled AND role-allowed) gate
|
||||
* passes. Returns the possibly-stripped doc. The caller resolves featureEnabled
|
||||
* (from workspace settings) and role (actor) itself — those legitimately differ
|
||||
* per call-site (e.g. share path uses role=null) — this helper owns only the
|
||||
* has-check + AND + strip + optional onStrip callback.
|
||||
*
|
||||
* Centralizes the 4-step write-path ritual (resolve role -> resolve
|
||||
* featureEnabled -> htmlEmbedAllowed AND -> stripHtmlEmbedNodes) so the plain
|
||||
* strip-all call-sites share one tested decision. Sites with CUSTOM strip logic
|
||||
* (e.g. the collab persist path's preserve-admin variant) keep their own code.
|
||||
*/
|
||||
export function stripHtmlEmbedIfNotAllowed<T>(
|
||||
json: T,
|
||||
opts: { featureEnabled: boolean; role: string | null | undefined; onStrip?: () => void },
|
||||
): T {
|
||||
if (htmlEmbedAllowed(opts.featureEnabled, opts.role)) return json;
|
||||
if (hasHtmlEmbedNode(json)) {
|
||||
opts.onStrip?.();
|
||||
return stripHtmlEmbedNodes(json);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the workspace-level htmlEmbed feature toggle from a workspace's settings
|
||||
* jsonb. ABSENT/non-true => OFF (the default). Kept here so every server write
|
||||
|
||||
Reference in New Issue
Block a user