Named JOURNAL-001/002 fillable, FormatHTML unsafe tags, rollback/remigrate, RSS/categories/tags/media, and TestSearchGateOff with zero Typesense HTTP. Co-authored-by: Cursor <cursoragent@cursor.com>
175 lines
5.4 KiB
Go
175 lines
5.4 KiB
Go
package journal_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/sm-journal-plugin/console"
|
|
"git.golem15.com/golem15/sm-journal-plugin/controllers"
|
|
"git.golem15.com/golem15/sm-journal-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/bouncer"
|
|
)
|
|
|
|
func TestJournalAPIShowNeighbors(t *testing.T) {
|
|
env := newPostsEnv(t)
|
|
past := time.Now().UTC().Add(-2 * time.Hour)
|
|
mid := past.Add(time.Hour)
|
|
for i, slug := range []string{"alpha", "beta", "gamma"} {
|
|
p := models.Post{
|
|
Title: "Post " + slug,
|
|
Slug: slug,
|
|
Content: "body " + slug,
|
|
Published: true,
|
|
PublishedAt: ptrTime(past.Add(time.Duration(i) * time.Hour)),
|
|
}
|
|
if err := env.gdb.Create(&p).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
_ = mid
|
|
rec := env.call(t, http.MethodGet, "/_journal/api/v1/posts/beta", "", nil)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("show beta = %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := body["data"].(map[string]any)
|
|
if data == nil {
|
|
t.Fatalf("missing data: %s", rec.Body.String())
|
|
}
|
|
for _, key := range []string{"previous_post", "next_post", "related_posts"} {
|
|
if _, ok := data[key]; !ok {
|
|
t.Fatalf("show missing %s: %s", key, rec.Body.String())
|
|
}
|
|
}
|
|
title, _ := data["title"].(string)
|
|
if strings.Contains(title, console.UnpublishedTitlePrefix) {
|
|
t.Fatalf("unpublished lock prefix leaked onto JSON title: %q", title)
|
|
}
|
|
}
|
|
|
|
func TestJournalPerPageClamp(t *testing.T) {
|
|
env := newPostsEnv(t)
|
|
past := time.Now().UTC().Add(-time.Hour)
|
|
for i := 0; i < 12; i++ {
|
|
p := models.Post{
|
|
Title: "Row " + strconv.Itoa(i),
|
|
Slug: "row-" + strconv.Itoa(i),
|
|
Content: "x",
|
|
Published: true,
|
|
PublishedAt: &past,
|
|
}
|
|
if err := env.gdb.Create(&p).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
nine := env.call(t, http.MethodGet, "/_journal/api/v1/posts?per_page=9", "", nil)
|
|
if nine.Code != http.StatusOK {
|
|
t.Fatalf("per_page=9 = %d: %s", nine.Code, nine.Body.String())
|
|
}
|
|
var nineBody struct {
|
|
Data []any `json:"data"`
|
|
Meta struct {
|
|
PerPage int `json:"per_page"`
|
|
} `json:"meta"`
|
|
}
|
|
if err := json.Unmarshal(nine.Body.Bytes(), &nineBody); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if nineBody.Meta.PerPage != 9 {
|
|
t.Fatalf("per_page=9 meta = %d", nineBody.Meta.PerPage)
|
|
}
|
|
if len(nineBody.Data) != 9 {
|
|
t.Fatalf("per_page=9 data len = %d", len(nineBody.Data))
|
|
}
|
|
capped := env.call(t, http.MethodGet, "/_journal/api/v1/posts?per_page=30", "", nil)
|
|
var capBody struct {
|
|
Meta struct {
|
|
PerPage int `json:"per_page"`
|
|
} `json:"meta"`
|
|
}
|
|
if err := json.Unmarshal(capped.Body.Bytes(), &capBody); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if capBody.Meta.PerPage != 30 {
|
|
t.Fatalf("per_page=30 meta = %d, want 30", capBody.Meta.PerPage)
|
|
}
|
|
}
|
|
|
|
func TestJournalEditorListDrafts(t *testing.T) {
|
|
env := newPostsEnv(t)
|
|
tok := env.admin(t, controllers.PermissionAccessPosts)
|
|
if err := env.gdb.Create(&models.Post{Title: "Hidden draft", Slug: "hidden-draft", Content: "x", Published: false}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
anon := env.call(t, http.MethodGet, "/_journal/api/v1/posts", "", nil)
|
|
if strings.Contains(anon.Body.String(), "hidden-draft") {
|
|
t.Fatalf("anonymous list leaked draft: %s", anon.Body.String())
|
|
}
|
|
editor := env.call(t, http.MethodGet, "/_journal/api/v1/posts", tok, nil)
|
|
if editor.Code != http.StatusOK {
|
|
t.Fatalf("editor list = %d: %s", editor.Code, editor.Body.String())
|
|
}
|
|
if !strings.Contains(editor.Body.String(), "hidden-draft") {
|
|
t.Fatalf("editor Bearer list omitted drafts: %s", editor.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestJournalAPIMassAssignRedactor(t *testing.T) {
|
|
env := newPostsEnv(t)
|
|
tok := env.admin(t, controllers.PermissionAccessPosts)
|
|
created := env.call(t, http.MethodPost, "/_journal/api/v1/posts", tok, map[string]any{
|
|
"title": "Assign trap",
|
|
"content": "body",
|
|
"redactor_id": 99,
|
|
"user_id": 1,
|
|
})
|
|
if created.Code != http.StatusCreated {
|
|
t.Fatalf("create = %d: %s", created.Code, created.Body.String())
|
|
}
|
|
var stored models.Post
|
|
if err := env.gdb.Where("slug = ?", "assign-trap").Or("title = ?", "Assign trap").First(&stored).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored.RedactorID != nil {
|
|
t.Fatalf("redactor_id persisted from body: %#v", stored.RedactorID)
|
|
}
|
|
authorID := env.lastUserID(t)
|
|
if stored.UserID == nil || *stored.UserID != authorID {
|
|
t.Fatalf("user_id = %#v, want stamped principal %d", stored.UserID, authorID)
|
|
}
|
|
}
|
|
|
|
func TestPostsAdminForbidden(t *testing.T) {
|
|
env := newPostsEnv(t)
|
|
noPerm := env.admin(t)
|
|
rec := env.call(t, http.MethodGet, adminAPI(adminPostsPath), noPerm, nil)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("admin list without access_posts = %d, want 403: %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestJournalWriteFrontendAudience(t *testing.T) {
|
|
env := newPostsEnv(t)
|
|
front, _, err := bouncer.MintAudience(adminJWTSecret, "1", "https://app.example.test/login", time.Hour, bouncer.AudienceUser)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
denied := env.call(t, http.MethodPost, "/_journal/api/v1/posts", front, map[string]any{
|
|
"title": "Frontend",
|
|
"content": "Should not write",
|
|
})
|
|
if denied.Code != http.StatusUnauthorized {
|
|
t.Fatalf("frontend-audience POST = %d, want 401: %s", denied.Code, denied.Body.String())
|
|
}
|
|
if got, _ := phpErrorString(t, denied.Body.Bytes()); got != "Authentication required" {
|
|
t.Fatalf("frontend-audience error = %q", got)
|
|
}
|
|
}
|