diff --git a/classes/format_html_test.go b/classes/format_html_test.go
index d7ec99e..d8beba2 100644
--- a/classes/format_html_test.go
+++ b/classes/format_html_test.go
@@ -31,26 +31,40 @@ func TestFormatHTMLRejectsUnsafeHTML(t *testing.T) {
t.Fatalf("footnote did not render: %s", fn)
}
+ // With unsafe rendering the gate is the only defence, so every case
+ // must be refused with an error, not merely rendered harmless.
cases := []struct {
- name string
- src string
- needle string
+ name string
+ src string
}{
- {"script", "", ""},
+ {"mixed-case script", ""},
+ {"script inside an HTML block", "
"},
+ {"script after markdown", "# Title\n\nText.\n\n"},
+ {"iframe", ``},
+ {"upper-case iframe", ``},
+ {"object", ` `},
+ {"embed", ``},
+ {"event", ` `},
+ {"svg onload", ` `},
+ {"mixed-case event handler", `Click
`},
+ {"event handler in an inline element", "Text with hover ."},
+ {"javascript", "[x](javascript:alert(1))"},
+ {"mixed-case javascript href", `x `},
+ {"vbscript", "[x](vbscript:msgbox(1))"},
+ {"raw vbscript href", `x `},
+ {"data", "[x](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)"},
+ {"raw data URL attribute", ` `},
+ {"markdown image with a data URL", ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
out, err := FormatHTML(tc.src)
- if err == nil && strings.Contains(strings.ToLower(out), tc.needle) {
- t.Fatalf("unsafe HTML survived: %s", out)
+ if err == nil {
+ t.Fatalf("FormatHTML accepted unsafe input %q: %s", tc.src, out)
}
- if err == nil && (tc.name == "script" || tc.name == "iframe" || tc.name == "event") && strings.Contains(strings.ToLower(out), tc.needle) {
- t.Fatalf("FormatHTML accepted %s: %s", tc.name, out)
+ if out != "" {
+ t.Fatalf("FormatHTML returned HTML with its error: %s", out)
}
})
}
@@ -76,6 +90,58 @@ func TestFormatHTMLKeepsRawHTML(t *testing.T) {
if !strings.Contains(mixed, "Tytuł ") || !strings.Contains(mixed, ``) {
t.Fatalf("raw footer was dropped:\n%s", mixed)
}
+
+ // Imported-style bodies: each round-trips unchanged after trimming.
+ bodies := []struct {
+ name string
+ src string
+ }{
+ {"paragraphs", "Pierwszy akapit.
\nDrugi akapit z kursywą .
\nTrzeci.
"},
+ {"class attributes", ``},
+ {"https link", `Zobacz źródło .
`},
+ {"https image", `
`},
+ {"entity in an attribute", `link
`},
+ {"table", ""},
+ {"surrounding whitespace", "\n\n Trimmed.
\n"},
+ }
+ for _, tc := range bodies {
+ t.Run(tc.name, func(t *testing.T) {
+ out, err := FormatHTML(tc.src)
+ if err != nil {
+ t.Fatalf("FormatHTML(%q): %v", tc.src, err)
+ }
+ if strings.TrimSpace(out) != strings.TrimSpace(tc.src) {
+ t.Fatalf("html changed:\n got %q\nwant %q", strings.TrimSpace(out), strings.TrimSpace(tc.src))
+ }
+ if strings.Contains(out, "raw HTML omitted") {
+ t.Fatalf("goldmark placeholder in output: %s", out)
+ }
+ })
+ }
+
+ t.Run("markdown with a trailing raw footer and style", func(t *testing.T) {
+ const footer = ``
+ const style = ""
+ src := "## Nagłówek\n\nTekst z **pogrubieniem** i [linkiem](https://example.test).\n\n" + footer + "\n\n" + style
+ out, err := FormatHTML(src)
+ if err != nil {
+ t.Fatalf("markdown with footer and style: %v", err)
+ }
+ for _, want := range []string{
+ "Nagłówek ",
+ "pogrubieniem ",
+ `linkiem `,
+ footer,
+ style,
+ } {
+ if !strings.Contains(out, want) {
+ t.Fatalf("output lacks %q:\n%s", want, out)
+ }
+ }
+ if strings.Contains(out, "raw HTML omitted") {
+ t.Fatalf("goldmark placeholder in output: %s", out)
+ }
+ })
}
// TestJournalEndToEnd keeps `go test ./... -run TestJournalEndToEnd`
diff --git a/posts_admin_smoke_test.go b/posts_admin_smoke_test.go
index 4f71173..19564ab 100644
--- a/posts_admin_smoke_test.go
+++ b/posts_admin_smoke_test.go
@@ -103,3 +103,51 @@ func TestPostsAdminCreateSmoke(t *testing.T) {
}
})
}
+
+// TestPostsAdminResaveKeepsHTML is the acceptance test of the todo
+// journal-imported-html-admin-resave (D-03): an admin save of a post whose
+// content is HTML, as imported posts are, keeps that HTML in content_html
+// instead of goldmark's raw-HTML placeholder.
+func TestPostsAdminResaveKeepsHTML(t *testing.T) {
+ env := newPostsEnv(t)
+ token := env.admin(t,
+ controllers.PermissionAccessPosts,
+ controllers.PermissionAccessPublish,
+ controllers.PermissionAccessOtherPosts,
+ )
+ const content = "Grzyby rosną w lesie.
\nDruga część z linkiem .
"
+ body := map[string]any{
+ "title": map[string]any{"en": "Imported", "pl": "Zaimportowany"},
+ "slug": map[string]any{"en": "imported-html", "pl": "zaimportowany-html"},
+ "content": map[string]any{"en": content, "pl": content},
+ "excerpt": map[string]any{"en": "Excerpt", "pl": "Wyjątek"},
+ }
+ rec := env.expect(t, http.StatusCreated, http.MethodPost, adminAPI(adminPostsPath), token, body)
+ var created struct {
+ ID uint `json:"id"`
+ }
+ if err := json.Unmarshal(adminDecode(t, rec).Data, &created); err != nil || created.ID == 0 {
+ t.Fatalf("create data %s: %v", rec.Body.String(), err)
+ }
+
+ assertHTML := func(stage string) {
+ t.Helper()
+ var stored models.Post
+ if err := env.gdb.First(&stored, created.ID).Error; err != nil {
+ t.Fatal(err)
+ }
+ if strings.Contains(stored.ContentHTML, "raw HTML omitted") {
+ t.Fatalf("%s: content_html holds the goldmark placeholder: %q", stage, stored.ContentHTML)
+ }
+ if strings.TrimSpace(stored.ContentHTML) != content {
+ t.Fatalf("%s: content_html = %q, want %q", stage, strings.TrimSpace(stored.ContentHTML), content)
+ }
+ if stored.Content != content {
+ t.Fatalf("%s: content = %q, want %q", stage, stored.Content, content)
+ }
+ }
+ assertHTML("create")
+
+ env.expect(t, http.StatusOK, http.MethodPut, adminAPI(fmt.Sprintf("%s/%d", adminPostsPath, created.ID)), token, body)
+ assertHTML("re-save")
+}