feat(314): add RTBlazorfied rich text editor + HtmlSanitizer backend#315
Conversation
…ession - Merge decision inbox into .squad/decisions.md (decisions 30-31) - Decision 30: Dark Mode Base Text Colours Must Contrast Against Dark Backgrounds (Legolas) - Decision 31: CSS Visual Regressions and Test/Visual Finding Arbitration (Aragorn) - Update Legolas history: UI regression review findings (Sprint 16 fan-out) - Update Gimli history: Blazor UI regression testing (Issue #292 coverage + visual findings) - Update Aragorn history: Issue triage (#292) and PR #295 arbitration workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…und 1) - Closed duplicate issue #294, merged PR #295 with all 19 CI checks green - Sprint-stamped issues #293 and #296 with title normalization - Removed go:needs-research from #293 (sufficient context), kept for #296 (Auth investigation needed) - Decision #32: PR self-authored approval pattern and Sprint triage workflow Co-authored-by: Boromir <lead-organizer@squad.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace old non-existent project ID (PVT_kwHOA5k0b84BVFTy) with new one (PVT_kwHOA5k0b84BXZpa) - Update STATUS_FIELD_ID to match new project (PVTSSF_lAHOA5k0b84BXZpazhSmuGY) - Map old status options to available field options (Backlog/InSprint: f75ad846, InReview: 47fc9ee4, Done: 98236657) - Use GH_PROJECT_TOKEN secret for GraphQL mutations (fallback to GITHUB_TOKEN) - Fixes issue where project board sync was failing silently due to missing project Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace token fallback with explicit GH_PROJECT_TOKEN requirement - Ensures workflows have required 'project' scope for user-owned project boards - Fixes 'Resource not accessible by integration' errors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Kept HEAD version with correct option IDs from board fix: - IN_SPRINT_OPTION_ID: f75ad846 - IN_REVIEW_OPTION_ID: 47fc9ee4 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t test - Edit.razor: null-value branch now navigates to /blog instead of leaving _model null (which caused infinite 'Loading...' state) - EditAclTests: added EditRedirectsToBlogWhenPostNotFound covering the Result.Ok(null) path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ox, seed schema) Resolves merge conflict in MongoDbResourceBuilderExtensions.cs — indentation-only divergence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add HtmlSanitizer 9.0.892 package to Directory.Packages.props and Web.csproj - Register IHtmlSanitizer as singleton in Program.cs - Sanitize Content field in CreateBlogPostHandler before persisting - Sanitize Content field in EditBlogPostHandler before persisting - Log audit warning (LoggerMessage pattern) when unsafe HTML is stripped - Return meaningful validation error if sanitized content is empty/whitespace - Update existing handler tests to supply IHtmlSanitizer and NullLogger Closes #314 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…r behavior tests - Upgrade HtmlSanitizer 9.0.892 → 9.1.923-beta (uses AngleSharp 1.4.0, compatible with bunit 2.7.2) - Pin AngleSharp 1.4.0 and AngleSharp.Css 1.0.0-beta.216 as transitive resolution - Bump SharpCompress transitive pin to 1.0.0 - dotnet format: Razor and bunit formatting fixes - Add HtmlSanitizerBehaviorTests (7 new tests covering sanitizer code paths) - Fix MD013/MD032/MD012 markdown lint issues in aragorn/history.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🏗️ PR Added to Squad Triage QueueThis PR has been labeled with Next steps:
|
There was a problem hiding this comment.
Pull request overview
This PR adds server-side HTML sanitization for blog post rich-text content during create/edit operations, and wires in a rich text editor UI component plus targeted tests to validate the behavior.
Changes:
- Sanitize blog post
ContentinCreateBlogPostHandlerandEditBlogPostHandler, failing requests when content becomes empty after sanitization. - Add/central-pin
HtmlSanitizerandRTBlazorfiedpackages and registerIHtmlSanitizerin DI. - Update and expand unit + bUnit tests to cover sanitization behavior and rich text editor rendering/interaction.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Web.Tests/Handlers/HtmlSanitizerBehaviorTests.cs | New tests validating handler behavior when sanitization changes/empties content + basic real-sanitizer integration assertions. |
| tests/Web.Tests/Handlers/EditBlogPostHandlerTests.cs | Updates unit tests to inject/mimic sanitizer + logger for the updated handler constructor. |
| tests/Web.Tests/Handlers/CreateBlogPostHandlerTests.cs | Updates unit tests to inject/mimic sanitizer + logger for the updated handler constructor. |
| tests/Web.Tests.Bunit/Features/RichTextEditorTests.cs | New bUnit tests asserting the rich text editor component renders on Create/Edit pages. |
| tests/Web.Tests.Bunit/Features/EditAclTests.cs | Sets JSInterop to loose mode for tests impacted by new JS-based editor. |
| tests/Web.Tests.Bunit/Components/RazorSmokeTests.cs | Updates smoke tests to account for the editor component + loosens JSInterop mode; adjusts create submit tests to be async. |
| src/Web/Web.csproj | Adds package references for HtmlSanitizer + RTBlazorfied to the web project. |
| src/Web/Program.cs | Registers IHtmlSanitizer in DI as a singleton. |
| src/Web/Features/BlogPosts/Edit/EditBlogPostHandler.cs | Sanitizes edited content before update; logs when sanitization changes content; guards against empty-after-sanitization. |
| src/Web/Features/BlogPosts/Edit/Edit.razor | Replaces textarea with RTBlazorfied rich text editor component. |
| src/Web/Features/BlogPosts/Create/CreateBlogPostHandler.cs | Sanitizes created content before persistence; logs when sanitization changes content; guards against empty-after-sanitization. |
| src/Web/Features/BlogPosts/Create/Create.razor | Replaces textarea with RTBlazorfied rich text editor component. |
| src/Web/Features/_Imports.razor | Adds @using RichTextBlazorfied for feature components. |
| src/Web/Components/App.razor | Includes RTBlazorfied static JS asset. |
| src/Web/Components/_Imports.razor | Adds @using RichTextBlazorfied for shared components. |
| src/AppHost/MongoDbResourceBuilderExtensions.cs | Minor formatting fix in seed document initializer. |
| scripts/inspect-dll/inspect.csx | Adds a small script to list exported types from a DLL. |
| Directory.Packages.props | Central version pins for HtmlSanitizer + AngleSharp(+Css) + RTBlazorfied; updates SharpCompress pin. |
| .squad/agents/gimli/history.md | Adds historical test run notes (docs/logging). |
| .squad/agents/aragorn/history.md | Adds/rewraps historical review notes (docs/logging). |
| [LoggerMessage(Level = LogLevel.Warning, Message = "HTML sanitized on CreateBlogPost — unsafe markup was removed. Title: {Title}")] | ||
| private static partial void LogHtmlSanitized(ILogger logger, string title); | ||
|
|
||
| public async Task<Result<Guid>> Handle(CreateBlogPostCommand request, CancellationToken cancellationToken) | ||
| { | ||
| try | ||
| { | ||
| var post = BlogPost.Create(request.Title, request.Content, request.Author); | ||
| var sanitizedContent = sanitizer.Sanitize(request.Content); | ||
| if (sanitizedContent != request.Content) | ||
| { | ||
| LogHtmlSanitized(logger, request.Title); | ||
| } |
| @@ -23,7 +28,7 @@ | |||
| <PackageVersion Include="MongoDB.Driver" Version="3.8.0" /> | |||
| <PackageVersion Include="MongoDB.EntityFrameworkCore" Version="10.0.1" /> | |||
| <!-- Transitive pin: MongoDB.Driver 3.6.x currently resolves vulnerable SharpCompress 0.30.1 --> | |||
| [LoggerMessage(Level = LogLevel.Warning, Message = "HTML sanitized on EditBlogPost — unsafe markup was removed. PostId: {PostId}")] | ||
| private static partial void LogHtmlSanitized(ILogger logger, Guid postId); | ||
|
|
||
| public async Task<Result> Handle(EditBlogPostCommand request, CancellationToken cancellationToken) | ||
| { | ||
| try | ||
| { | ||
| var sanitizedContent = sanitizer.Sanitize(request.Content); | ||
| if (sanitizedContent != request.Content) | ||
| { | ||
| LogHtmlSanitized(logger, request.Id); | ||
| } | ||
|
|
||
| if (string.IsNullOrWhiteSpace(sanitizedContent)) | ||
| { | ||
| return Result.Fail("Content is empty after sanitization. Please provide valid content."); | ||
| } |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #315 +/- ##
==========================================
+ Coverage 83.84% 84.01% +0.17%
==========================================
Files 47 47
Lines 1300 1314 +14
Branches 157 161 +4
==========================================
+ Hits 1090 1104 +14
Misses 148 148
Partials 62 62
🚀 New features to boost your workflow:
|
|
) Merged decision inbox entries: - Decision 32: OnParametersSetAsync state reset pattern (Aragorn) - Decision 33: Cached state field reset (Aragorn) - Decision 34: UserIdClaimsHelper recommendation (Aragorn) - Decision 35: Rich Text Editor architectural plan (Aragorn) - Decision 36: bUnit 2.x re-render API (Gimli) - Decision 37: Display state reset pattern (Legolas) - Decision 38: RTBlazorfied editor selection (Legolas) - Decision 39: Loading-state reset + bUnit API (Sam) Also updated Directory.Packages.props: - MongoDB.Driver: 3.8.0 → 3.8.1 - Fixed stale SharpCompress transitive dependency comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes #314
Working as Legolas (Frontend Developer) + Sam (Backend Developer)
Summary
This PR completes Issue #314 end-to-end:
<InputTextArea>with RTBlazorfied v2.0.20 rich text editor on Create and Edit blog post pagesIHtmlSanitizerin the Create and Edit handlersChanges
Frontend — RTBlazorfied Rich Text Editor (Legolas)
Directory.Packages.props: AddedRTBlazorfiedv2.0.20src/Web/Web.csproj: Added<PackageReference Include="RTBlazorfied" />src/Web/Components/App.razor: Added RTBlazorfied JS script tagsrc/Web/Features/_Imports.razor+Components/_Imports.razor: Added@using RichTextBlazorfiedsrc/Web/Features/BlogPosts/Create/Create.razor: Replaced<InputTextArea>→<RTBlazorfied @bind-Value="_model.Content" Height="400px" />src/Web/Features/BlogPosts/Edit/Edit.razor: Same replacementtests/Web.Tests.Bunit/Features/RichTextEditorTests.cs: New — 2 smoke teststests/Web.Tests.Bunit/Components/RazorSmokeTests.cs: Updated for shadow DOM +ValueChanged+JSRuntimeMode.Loosetests/Web.Tests.Bunit/Features/EditAclTests.cs: AddedJSRuntimeMode.LooseBackend — HtmlSanitizer (Sam)
CreateBlogPostHandlerandEditBlogPostHandler: sanitizeContentwithIHtmlSanitizerHtmlSanitizer9.1.923-beta + pinned AngleSharp 1.4.0IHtmlSanitizerregistered as singleton inProgram.csHtmlSanitizerBehaviorTests(7 tests)Test results
All tests pass ✅: 94 bUnit + 165 Web.Tests + 42 Domain.Tests
Technical notes (RTBlazorfied)
RichTextBlazorfied.RTBlazorfied(namespace ambiguity with assembly root namespace)cut.MarkupJSRuntimeMode.Looserequired in any test class rendering Create/Edit pages