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:
claude code agent 227
2026-06-21 03:49:52 +03:00
parent c486750b2a
commit a2ded7ecfb
7 changed files with 193 additions and 63 deletions

View File

@@ -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

View File

@@ -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