Files
sm-journal-plugin/journal_api_categories_test.go
Jakub Zych 740dcd086d feat(api): serve the category content field on GET /_journal/api/v1/categories
- add the nullable TEXT column golem15_journal_categories.content (migration 202610090001)
- map it as models.Category.Content and emit category content on every categories row
- document the categories row shape and the stored-as-is HTML semantics
2026-10-09 16:24:20 +02:00

132 lines
3.9 KiB
Go

package journal_test
import (
"encoding/json"
"net/http"
"reflect"
"sort"
"testing"
"time"
"git.golem15.com/golem15/sm-journal-plugin/models"
)
// flattenCategoryRows walks the categories tree (data plus children) into one
// slice of rows.
func flattenCategoryRows(rows []map[string]any) []map[string]any {
var out []map[string]any
for _, row := range rows {
out = append(out, row)
if kids, ok := row["children"].([]any); ok {
nested := make([]map[string]any, 0, len(kids))
for _, k := range kids {
if m, ok := k.(map[string]any); ok {
nested = append(nested, m)
}
}
out = append(out, flattenCategoryRows(nested)...)
}
}
return out
}
func categoryRowsBySlug(t *testing.T, env *adminEnv, path string) map[string]map[string]any {
t.Helper()
rec := env.expect(t, http.StatusOK, http.MethodGet, path, "", nil)
var body struct {
Data []map[string]any `json:"data"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("categories json: %s err=%v", rec.Body.String(), err)
}
out := map[string]map[string]any{}
for _, row := range flattenCategoryRows(body.Data) {
slug, _ := row["slug"].(string)
out[slug] = row
}
return out
}
func assertNullContent(t *testing.T, rows map[string]map[string]any, slug string) {
t.Helper()
row, ok := rows[slug]
if !ok {
t.Fatalf("category %s not listed", slug)
}
v, present := row["content"]
if !present || v != nil {
t.Fatalf("%s content = %#v (present %v), want JSON null", slug, v, present)
}
}
func TestJournalCategoryContentAPI(t *testing.T) {
env := newPostsEnv(t)
html := "<h2>Acme news</h2>\n<p>Pillar copy</p>"
left, right, depth := 3, 4, 0
news := models.Category{Name: "News", Slug: "news", Content: &html, NestLeft: &left, NestRight: &right, NestDepth: &depth}
if err := env.gdb.Create(&news).Error; err != nil {
t.Fatal(err)
}
quiet := models.Category{Name: "Quiet", Slug: "quiet"}
if err := env.gdb.Create(&quiet).Error; err != nil {
t.Fatal(err)
}
past := time.Now().UTC().Add(-time.Hour)
post := models.Post{Title: "Listed", Slug: "listed-content", Content: "x", Published: true, PublishedAt: &past}
if err := env.gdb.Create(&post).Error; err != nil {
t.Fatal(err)
}
if err := env.gdb.Create(&models.PostCategory{PostID: post.ID, CategoryID: news.ID}).Error; err != nil {
t.Fatal(err)
}
all := categoryRowsBySlug(t, env, "/_journal/api/v1/categories?include_empty=1")
if got, ok := all["news"]["content"].(string); !ok || got != html {
t.Fatalf("news content = %#v, want %q", all["news"]["content"], html)
}
assertNullContent(t, all, "quiet")
assertNullContent(t, all, "uncategorized")
listed := categoryRowsBySlug(t, env, "/_journal/api/v1/categories")
if got, ok := listed["news"]["content"].(string); !ok || got != html {
t.Fatalf("news content without include_empty = %#v, want %q", listed["news"]["content"], html)
}
if _, ok := listed["quiet"]; ok {
t.Fatal("empty category quiet listed without include_empty")
}
rec := env.expect(t, http.StatusOK, http.MethodGet, "/_journal/api/v1/posts", "", nil)
var posts struct {
Data []map[string]any `json:"data"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &posts); err != nil {
t.Fatalf("posts json: %s err=%v", rec.Body.String(), err)
}
var found bool
for _, p := range posts.Data {
if p["slug"] != "listed-content" {
continue
}
found = true
cats, ok := p["categories"].([]any)
if !ok || len(cats) == 0 {
t.Fatalf("post categories = %#v", p["categories"])
}
first, ok := cats[0].(map[string]any)
if !ok {
t.Fatalf("post category entry = %#v", cats[0])
}
keys := make([]string, 0, len(first))
for k := range first {
keys = append(keys, k)
}
sort.Strings(keys)
if want := []string{"id", "name", "slug"}; !reflect.DeepEqual(keys, want) {
t.Fatalf("post category keys = %v, want %v", keys, want)
}
}
if !found {
t.Fatalf("post listed-content missing from /posts: %s", rec.Body.String())
}
}