- Add TestJournalEndToEnd through party/surf/cabana on real Postgres - Extract the admin harness and activate user+translate+journal in Requires order - Anonymous list hides drafts; Bearer writes; JOURNAL-005 404 has no data key Co-authored-by: Cursor <cursoragent@cursor.com>
231 lines
7.6 KiB
Go
231 lines
7.6 KiB
Go
package journal_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/sm-journal-plugin/controllers"
|
|
"git.golem15.com/golem15/sm-journal-plugin/models"
|
|
)
|
|
|
|
func TestJournalEndToEnd(t *testing.T) {
|
|
env := newIntegrationEnv(t)
|
|
|
|
if len(env.plugins) != 3 {
|
|
ids := make([]string, len(env.plugins))
|
|
for i, p := range env.plugins {
|
|
ids[i] = p.ID()
|
|
}
|
|
t.Fatalf("activated plugins = %v, want user+translate+journal", ids)
|
|
}
|
|
if env.plugins[0].ID() != "golem15.user" || env.plugins[1].ID() != "golem15.translate" || env.plugins[2].ID() != "golem15.journal" {
|
|
ids := make([]string, len(env.plugins))
|
|
for i, p := range env.plugins {
|
|
ids[i] = p.ID()
|
|
}
|
|
t.Fatalf("Requires order = %v", ids)
|
|
}
|
|
if !env.app.HasPlugin("golem15.user") || !env.app.HasPlugin("golem15.translate") || !env.app.HasPlugin("golem15.journal") {
|
|
t.Fatal("activated plugin set is incomplete")
|
|
}
|
|
|
|
if !env.gdb.Migrator().HasColumn("golem15_journal_posts", "redactor_id") {
|
|
t.Fatal("redactor_id column missing")
|
|
}
|
|
for _, name := range (models.Post{}).Fillable() {
|
|
if name == "redactor_id" || name == "user_id" {
|
|
t.Fatalf("Fillable includes forbidden %q", name)
|
|
}
|
|
}
|
|
|
|
ownerTok := env.admin(t, controllers.PermissionAccessPosts, controllers.PermissionAccessPublish)
|
|
ownerID := env.lastUserID(t)
|
|
otherTok := env.admin(t, controllers.PermissionAccessPosts)
|
|
privTok := env.admin(t, controllers.PermissionAccessPosts, controllers.PermissionAccessOtherPosts)
|
|
|
|
past := time.Now().UTC().Add(-time.Hour).Format(time.RFC3339)
|
|
created := env.expect(t, http.StatusCreated, http.MethodPost, adminAPI(adminPostsPath), ownerTok, map[string]any{
|
|
"title": map[string]any{"en": "Hello", "pl": "Witaj"},
|
|
"slug": map[string]any{"en": "hello-e2e", "pl": "witaj-e2e"},
|
|
"content": map[string]any{"en": "Hello **world**", "pl": "Witaj **świecie**"},
|
|
"excerpt": map[string]any{"en": "Excerpt", "pl": "Wyjątek"},
|
|
"published": true,
|
|
"published_at": past,
|
|
})
|
|
var published struct {
|
|
ID uint `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(adminDecode(t, created).Data, &published); err != nil || published.ID == 0 {
|
|
t.Fatalf("published create data %s: %v", created.Body.String(), err)
|
|
}
|
|
|
|
var host models.Post
|
|
if err := env.gdb.First(&host, published.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if host.Title != "Hello" || host.Slug != "hello-e2e" {
|
|
t.Fatalf("host columns = title %q slug %q", host.Title, host.Slug)
|
|
}
|
|
if host.UserID == nil || *host.UserID != ownerID {
|
|
t.Fatalf("host user_id = %v, want %d", host.UserID, ownerID)
|
|
}
|
|
|
|
var attrs []struct {
|
|
Locale string `gorm:"column:locale"`
|
|
AttributeData *string `gorm:"column:attribute_data"`
|
|
}
|
|
if err := env.gdb.Table("golem15_translate_attributes").
|
|
Where("model_id = ? AND model_type = ?", fmt.Sprint(host.ID), host.MorphName()).
|
|
Find(&attrs).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(attrs) != 1 || attrs[0].Locale != "pl" || attrs[0].AttributeData == nil {
|
|
t.Fatalf("translate attributes = %+v", attrs)
|
|
}
|
|
var data map[string]any
|
|
if err := json.Unmarshal([]byte(*attrs[0].AttributeData), &data); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if data["title"] != "Witaj" {
|
|
t.Fatalf("polish title = %#v", data["title"])
|
|
}
|
|
|
|
rid := uint(42)
|
|
host.RedactorID = &rid
|
|
if err := env.gdb.Save(&host).Error; err != nil {
|
|
t.Fatalf("explicit redactor_id assign: %v", err)
|
|
}
|
|
var stored models.Post
|
|
if err := env.gdb.First(&stored, host.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored.RedactorID == nil || *stored.RedactorID != 42 {
|
|
t.Fatalf("redactor_id after explicit assign = %v", stored.RedactorID)
|
|
}
|
|
|
|
draftRec := env.expect(t, http.StatusCreated, http.MethodPost, adminAPI(adminPostsPath), ownerTok, map[string]any{
|
|
"title": map[string]any{"en": "Draft", "pl": "Szkic"},
|
|
"slug": map[string]any{"en": "draft-e2e", "pl": "szkic-e2e"},
|
|
"content": map[string]any{"en": "Draft body", "pl": "Treść szkicu"},
|
|
})
|
|
var draft struct {
|
|
ID uint `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(adminDecode(t, draftRec).Data, &draft); err != nil || draft.ID == 0 {
|
|
t.Fatalf("draft create data %s: %v", draftRec.Body.String(), err)
|
|
}
|
|
|
|
list := env.call(t, http.MethodGet, "/_journal/api/v1/posts", "", nil)
|
|
if list.Code != http.StatusOK {
|
|
t.Fatalf("anonymous list = %d %s", list.Code, list.Body.String())
|
|
}
|
|
var listBody struct {
|
|
Data []struct {
|
|
Slug string `json:"slug"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(list.Body.Bytes(), &listBody); err != nil {
|
|
t.Fatalf("list json: %s err=%v", list.Body.String(), err)
|
|
}
|
|
if len(listBody.Data) != 1 || listBody.Data[0].Slug != "hello-e2e" {
|
|
t.Fatalf("anonymous list = %#v, want only hello-e2e", listBody.Data)
|
|
}
|
|
|
|
unauth := env.call(t, http.MethodPost, "/_journal/api/v1/posts", "", map[string]any{
|
|
"title": "Nope",
|
|
"content": "Body",
|
|
})
|
|
if unauth.Code != http.StatusUnauthorized {
|
|
t.Fatalf("POST without Bearer = %d, want 401: %s", unauth.Code, unauth.Body.String())
|
|
}
|
|
msg, unauthBody := phpErrorString(t, unauth.Body.Bytes())
|
|
if msg != "Authentication required" {
|
|
t.Fatalf("401 error = %#v", unauthBody["error"])
|
|
}
|
|
if _, has := unauthBody["data"]; has {
|
|
t.Fatalf("401 must not include data: %s", unauth.Body.String())
|
|
}
|
|
|
|
apiCreated := env.call(t, http.MethodPost, "/_journal/api/v1/posts", ownerTok, map[string]any{
|
|
"title": "API created",
|
|
"content": "Bearer write",
|
|
"slug": "api-created-e2e",
|
|
})
|
|
if apiCreated.Code != http.StatusCreated {
|
|
t.Fatalf("Bearer POST = %d, want 201: %s", apiCreated.Code, apiCreated.Body.String())
|
|
}
|
|
|
|
anonDraft := env.call(t, http.MethodGet, "/_journal/api/v1/posts/draft-e2e", "", nil)
|
|
if anonDraft.Code != http.StatusNotFound {
|
|
t.Fatalf("anonymous draft show = %d, want 404: %s", anonDraft.Code, anonDraft.Body.String())
|
|
}
|
|
_, anonBody := phpErrorString(t, anonDraft.Body.Bytes())
|
|
if _, has := anonBody["data"]; has {
|
|
t.Fatalf("JOURNAL-005: 404 must not include data: %s", anonDraft.Body.String())
|
|
}
|
|
|
|
stranger := env.call(t, http.MethodGet, "/_journal/api/v1/posts/draft-e2e", otherTok, nil)
|
|
if stranger.Code != http.StatusNotFound {
|
|
t.Fatalf("stranger draft show = %d, want 404: %s", stranger.Code, stranger.Body.String())
|
|
}
|
|
|
|
ownerShow := env.call(t, http.MethodGet, "/_journal/api/v1/posts/draft-e2e", ownerTok, nil)
|
|
if ownerShow.Code != http.StatusOK {
|
|
t.Fatalf("owner draft show = %d, want 200: %s", ownerShow.Code, ownerShow.Body.String())
|
|
}
|
|
|
|
privShow := env.call(t, http.MethodGet, "/_journal/api/v1/posts/draft-e2e", privTok, nil)
|
|
if privShow.Code != http.StatusOK {
|
|
t.Fatalf("access_other_posts draft show = %d, want 200: %s", privShow.Code, privShow.Body.String())
|
|
}
|
|
|
|
assertFixtureAbsentFromProduction(t)
|
|
}
|
|
|
|
func TestJournalEndToEndFixtureAbsentFromProduction(t *testing.T) {
|
|
assertFixtureAbsentFromProduction(t)
|
|
}
|
|
|
|
func assertFixtureAbsentFromProduction(t *testing.T) {
|
|
t.Helper()
|
|
root, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
if info.Name() == ".git" {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if strings.HasSuffix(path, "_test.go") || !strings.HasSuffix(path, ".go") {
|
|
return nil
|
|
}
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
text := string(body)
|
|
if strings.Contains(text, "acme.fixture") || strings.Contains(text, `Acme\Fixture\Models\Post`) {
|
|
t.Errorf("production source %s names the test fixture", path)
|
|
}
|
|
if strings.Contains(text, `"git.golem15.com/golem15/sm-user-plugin"`) {
|
|
t.Errorf("production source %s imports sm-user-plugin", path)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|