package journal_test import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "git.golem15.com/golem15/sm-journal-plugin/controllers" "git.golem15.com/golem15/sm-journal-plugin/models" ) func TestPostsFormCompiles(t *testing.T) { env := newPostsEnv(t) token := env.admin(t, controllers.PermissionAccessPosts) rec := env.expect(t, http.StatusOK, http.MethodGet, adminAPI(adminPostsPath+"/schema/form"), token, nil) body := rec.Body.String() if !strings.Contains(body, `"type":"mlblogeditor"`) { t.Fatalf("form schema missing mlblogeditor: %s", body) } if !strings.Contains(body, "mltext") { t.Fatalf("form schema missing mltext: %s", body) } if !strings.Contains(body, "mltextarea") { t.Fatalf("form schema missing mltextarea: %s", body) } if !strings.Contains(body, `"preset":{"field":"title","type":"slug"}`) { t.Fatalf("form schema missing slug preset: %s", body) } } func TestPostsAdminCreateSmoke(t *testing.T) { env := newPostsEnv(t) privileged := env.admin(t, controllers.PermissionAccessPosts, controllers.PermissionAccessPublish, controllers.PermissionAccessOtherPosts, ) rec := env.expect(t, http.StatusCreated, http.MethodPost, adminAPI(adminPostsPath), privileged, map[string]any{ "title": map[string]any{"en": "Hello", "pl": "Witaj"}, "slug": map[string]any{"en": "hello-admin", "pl": "witaj-admin"}, "content": map[string]any{"en": "
Hello world
", "pl": "Witaj świecie
"}, "excerpt": map[string]any{"en": "Excerpt", "pl": "Wyjątek"}, }) if notices := adminNotices(t, rec); notices != nil { t.Fatalf("clean create carries meta.notices: %s", rec.Body.String()) } var created struct { ID uint `json:"id"` } envData := adminDecode(t, rec) if err := json.Unmarshal(envData.Data, &created); err != nil || created.ID == 0 { t.Fatalf("create data %s: %v", rec.Body.String(), err) } var stored models.Post if err := env.gdb.First(&stored, created.ID).Error; err != nil { t.Fatal(err) } if stored.Title != "Hello" { t.Fatalf("host title = %q, want Hello", stored.Title) } if stored.Slug != "hello-admin" { t.Fatalf("host slug = %q, want hello-admin", stored.Slug) } // D-02: the admin path stores the same HTML in both columns. if stored.Content != "Hello world
" || stored.ContentHTML != stored.Content { t.Fatalf("content = %q, content_html = %q, want both the submitted HTML", stored.Content, stored.ContentHTML) } var plCount int64 if err := env.gdb.Table("golem15_translate_attributes"). Where("model_id = ? AND model_type = ? AND locale = ?", fmt.Sprint(stored.ID), stored.MorphName(), "pl"). Count(&plCount).Error; err != nil { t.Fatal(err) } if plCount == 0 { t.Fatal("Polish translate attributes were not written") } t.Run("publish without access_publish", func(t *testing.T) { limited := env.admin(t, controllers.PermissionAccessPosts) denied := env.call(t, http.MethodPost, adminAPI(adminPostsPath), limited, map[string]any{ "title": map[string]any{"en": "Draft", "pl": "Szkic"}, "slug": map[string]any{"en": "draft-admin", "pl": "szkic-admin"}, "content": map[string]any{"en": "Draft body", "pl": "Treść szkicu"}, "published": true, }) if denied.Code != http.StatusForbidden { t.Fatalf("publish without access_publish = %d, want 403: %s", denied.Code, denied.Body.String()) } if adminDecode(t, denied).Error.Code != "forbidden" { t.Fatalf("error code = %s", denied.Body.String()) } var n int64 if err := env.gdb.Model(&models.Post{}).Where("slug = ?", "draft-admin").Count(&n).Error; err != nil { t.Fatal(err) } if n != 0 { t.Fatal("published=true persisted without access_publish") } }) } // 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 stored.ContentHTML != content { t.Fatalf("%s: content_html = %q, want %q", stage, 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") } // TestPostsAdminSaveSanitizesHTML pins CR-01, WR-01 and Phase 16.2 D-02 and // D-10 on the admin write path: unsafe markup in content is stripped, as // Winter's Html::clean strips it, instead of refusing the save; content and // content_html both hold the sanitized HTML for the host locale (en) and the // translation (pl); a "Data:" label is ordinary text; and the response's // meta.notices names what was removed for each locale. func TestPostsAdminSaveSanitizesHTML(t *testing.T) { env := newPostsEnv(t) token := env.admin(t, controllers.PermissionAccessPosts, controllers.PermissionAccessPublish, controllers.PermissionAccessOtherPosts, ) const content = "Data: 12 maja 2024
\n" + "
Data: 12 maja 2024
\n" + "
x
\n" if stored.Content != want || stored.ContentHTML != want { t.Fatalf("host content = %q, content_html = %q, want both %q", stored.Content, stored.ContentHTML, want) } var raw string if err := env.gdb.Table("golem15_translate_attributes"). Select("attribute_data"). Where("model_id = ? AND model_type = ? AND locale = ?", fmt.Sprint(stored.ID), stored.MorphName(), "pl"). Scan(&raw).Error; err != nil { t.Fatal(err) } var pl map[string]any if err := json.Unmarshal([]byte(raw), &pl); err != nil { t.Fatalf("attribute_data %q: %v", raw, err) } if pl["content"] != want || pl["content_html"] != want { t.Fatalf("pl content = %#v, content_html = %#v, want both %q", pl["content"], pl["content_html"], want) } notices := adminNotices(t, rec) for _, locale := range []string{"en", "pl"} { var elements, attributes bool for _, n := range notices { if n.Field != "content" || n.Locale != locale { continue } elements = elements || strings.Contains(n.Message, "2 elements were removed: a, base") attributes = attributes || strings.Contains(n.Message, "1 attribute was removed: onerror") } if !elements || !attributes { t.Fatalf("%s notices = %#v, want removed elements a, base and attribute onerror", locale, notices) } } } // adminNotice is one meta.notices entry of an admin save response. type adminNotice struct { Field string `json:"field"` Locale string `json:"locale"` Message string `json:"message"` } // adminNotices returns a save response's meta.notices, nil when the key is // absent. func adminNotices(t *testing.T, rec *httptest.ResponseRecorder) []adminNotice { t.Helper() var body struct { Meta map[string]json.RawMessage `json:"meta"` } if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("admin body %q: %v", rec.Body.String(), err) } raw, ok := body.Meta["notices"] if !ok { return nil } var notices []adminNotice if err := json.Unmarshal(raw, ¬ices); err != nil { t.Fatalf("meta.notices %s: %v", raw, err) } if notices == nil { notices = []adminNotice{} } return notices }