240 lines
8.0 KiB
Go
240 lines
8.0 KiB
Go
// SPDX-License-Identifier: MPL-2.0
|
|
package cmssqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"gamertan.com/web/cms"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func fixture(t *testing.T) (*sql.DB, *Reader) {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite", "file:"+filepath.Join(t.TempDir(), "cms.sqlite")+"?_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_txlock=immediate")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
mutate(t, db, nil, func(tx *sql.Tx) error { return CreateSchema(context.Background(), tx) })
|
|
r, e := New(db, "merchant")
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
for _, scope := range []string{"merchant", "other"} {
|
|
mutate(t, db, nil, func(tx *sql.Tx) error {
|
|
return PutTaxonomy(context.Background(), tx, scope, cms.Taxonomy{ID: "topics", Slug: "topics", Name: "Topics", Active: true, Revision: 1}, 0)
|
|
})
|
|
}
|
|
mutate(t, db, nil, func(tx *sql.Tx) error {
|
|
return PutTerm(context.Background(), tx, "merchant", cms.Term{ID: "go", TaxonomyID: "topics", Slug: "go", Name: "Go", Active: true, Revision: 1}, 0)
|
|
})
|
|
return db, r
|
|
}
|
|
func mutate(t *testing.T, db *sql.DB, want error, fn func(*sql.Tx) error) {
|
|
t.Helper()
|
|
tx, e := db.BeginTx(context.Background(), nil)
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
defer tx.Rollback()
|
|
err := fn(tx)
|
|
if !errors.Is(err, want) {
|
|
t.Fatalf("mutation error %v, want %v", err, want)
|
|
}
|
|
if err == nil {
|
|
if e = tx.Commit(); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
}
|
|
}
|
|
func save(t *testing.T, db *sql.DB, ref cms.Reference, rev int64, a cms.Associations, publish bool) {
|
|
t.Helper()
|
|
mutate(t, db, nil, func(tx *sql.Tx) error {
|
|
if err := PutRevision(context.Background(), tx, "merchant", ref, rev, a); err != nil {
|
|
return err
|
|
}
|
|
if publish {
|
|
return SetPublished(context.Background(), tx, "merchant", ref, rev)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
func TestPublicationRelationshipsAndHistory(t *testing.T) {
|
|
db, r := fixture(t)
|
|
ctx := context.Background()
|
|
project := cms.Reference{Kind: "project", ID: "hime"}
|
|
news := cms.Reference{Kind: "news", ID: "launch"}
|
|
product := cms.Reference{Kind: "product", ID: "support"}
|
|
save(t, db, project, 1, cms.Associations{Terms: []string{"go"}}, true)
|
|
save(t, db, news, 1, cms.Associations{Terms: []string{"go"}, Links: []cms.Reference{project}}, false)
|
|
p, e := r.Related(ctx, project, nil, 10)
|
|
if e != nil || len(p.Items) != 0 {
|
|
t.Fatalf("draft leak: %+v %v", p, e)
|
|
}
|
|
mutate(t, db, nil, func(tx *sql.Tx) error { return SetPublished(ctx, tx, "merchant", news, 1) })
|
|
for _, ref := range []cms.Reference{project, news} {
|
|
p, e = r.Related(ctx, ref, nil, 10)
|
|
if e != nil || len(p.Items) != 1 {
|
|
t.Fatalf("reverse missing: %+v %v", p, e)
|
|
}
|
|
}
|
|
save(t, db, product, 1, cms.Associations{}, true)
|
|
save(t, db, news, 2, cms.Associations{Links: []cms.Reference{product}}, false)
|
|
p, e = r.Related(ctx, project, nil, 10)
|
|
if e != nil || len(p.Items) != 1 {
|
|
t.Fatal("draft replaced published graph", e)
|
|
}
|
|
mutate(t, db, nil, func(tx *sql.Tx) error { return SetPublished(ctx, tx, "merchant", news, 2) })
|
|
p, e = r.Related(ctx, project, nil, 10)
|
|
if e != nil || len(p.Items) != 0 {
|
|
t.Fatal("stale published edge", e)
|
|
}
|
|
p, e = r.Related(ctx, product, nil, 10)
|
|
if e != nil || len(p.Items) != 1 {
|
|
t.Fatal("missing new edge", e)
|
|
}
|
|
old, e := r.Revision(ctx, news, 1)
|
|
if e != nil || old.Links[0] != project {
|
|
t.Fatal("history changed", e)
|
|
}
|
|
mutate(t, db, nil, func(tx *sql.Tx) error { return SetPublished(ctx, tx, "merchant", product, 0) })
|
|
p, e = r.Related(ctx, news, nil, 10)
|
|
if e != nil || len(p.Items) != 0 {
|
|
t.Fatal("unpublished target leak", e)
|
|
}
|
|
p, e = r.Related(ctx, product, nil, 10)
|
|
if e != nil || len(p.Items) != 0 {
|
|
t.Fatal("unpublished source discovery", e)
|
|
}
|
|
// Restoring an old association is a new revision, not an overwritten row.
|
|
save(t, db, news, 3, old, true)
|
|
p, e = r.Related(ctx, project, nil, 10)
|
|
if e != nil || len(p.Items) != 1 || p.Items[0].Revision != 3 {
|
|
t.Fatal("restore", e)
|
|
}
|
|
mutate(t, db, cms.ErrConflict, func(tx *sql.Tx) error { return PutRevision(ctx, tx, "merchant", news, 1, cms.Associations{}) })
|
|
mutate(t, db, cms.ErrNotFound, func(tx *sql.Tx) error { return SetPublished(ctx, tx, "merchant", news, 999) })
|
|
}
|
|
|
|
func TestPublishWithoutTaxonomyOrRelationships(t *testing.T) {
|
|
db, reader := fixture(t)
|
|
ctx := context.Background()
|
|
ref := cms.Reference{Kind: "project", ID: "unclassified"}
|
|
save(t, db, ref, 1, cms.Associations{}, true)
|
|
value, err := reader.Revision(ctx, ref, 1)
|
|
if err != nil || len(value.Terms) != 0 || len(value.Links) != 0 {
|
|
t.Fatalf("empty associations: %+v %v", value, err)
|
|
}
|
|
page, err := reader.Related(ctx, ref, nil, 10)
|
|
if err != nil || len(page.Items) != 0 {
|
|
t.Fatalf("unexpected related content: %+v %v", page, err)
|
|
}
|
|
save(t, db, ref, 2, cms.Associations{Terms: []string{"go"}}, true)
|
|
save(t, db, ref, 3, cms.Associations{}, true)
|
|
old, err := reader.Revision(ctx, ref, 2)
|
|
if err != nil || len(old.Terms) != 1 || old.Terms[0] != "go" {
|
|
t.Fatal("clearing optional classification rewrote history", err)
|
|
}
|
|
page, err = reader.Members(ctx, "go", nil, 10)
|
|
if err != nil || len(page.Items) != 0 {
|
|
t.Fatalf("cleared classification still published: %+v %v", page, err)
|
|
}
|
|
}
|
|
func TestTermRenameRetirementAndScope(t *testing.T) {
|
|
db, r := fixture(t)
|
|
ctx := context.Background()
|
|
ref := cms.Reference{Kind: "writing", ID: "essay"}
|
|
save(t, db, ref, 1, cms.Associations{Terms: []string{"go"}}, true)
|
|
term, e := r.Term(ctx, "go")
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
term.Slug = "golang"
|
|
term.Name = "Go language"
|
|
term.Revision = 2
|
|
mutate(t, db, nil, func(tx *sql.Tx) error { return PutTerm(ctx, tx, "merchant", term, 1) })
|
|
alias, e := r.TermBySlug(ctx, "topics", "go")
|
|
if e != nil || alias.ID != term.ID || alias.Slug != "golang" {
|
|
t.Fatal("alias", e)
|
|
}
|
|
stolen := cms.Term{ID: "stolen", TaxonomyID: "topics", Slug: "go", Name: "Other", Revision: 1, Active: true}
|
|
mutate(t, db, cms.ErrConflict, func(tx *sql.Tx) error { return PutTerm(ctx, tx, "merchant", stolen, 0) })
|
|
duplicate := term
|
|
duplicate.Revision = 1
|
|
mutate(t, db, cms.ErrConflict, func(tx *sql.Tx) error { return PutTerm(ctx, tx, "merchant", duplicate, 0) })
|
|
other, _ := New(db, "other")
|
|
if _, e = other.Term(ctx, "go"); !errors.Is(e, cms.ErrNotFound) {
|
|
t.Fatal("cross scope term", e)
|
|
}
|
|
mutate(t, db, cms.ErrNotFound, func(tx *sql.Tx) error {
|
|
return PutRevision(ctx, tx, "other", ref, 1, cms.Associations{Terms: []string{"go"}})
|
|
})
|
|
p, e := other.Members(ctx, "go", nil, 10)
|
|
if e != nil || len(p.Items) != 0 {
|
|
t.Fatal("scope leak", e)
|
|
}
|
|
p, e = r.Members(ctx, "go", nil, 10)
|
|
if e != nil || len(p.Items) != 1 {
|
|
t.Fatal("membership lost on rename", e)
|
|
}
|
|
term.Active = false
|
|
term.Revision = 3
|
|
mutate(t, db, nil, func(tx *sql.Tx) error { return PutTerm(ctx, tx, "merchant", term, 2) })
|
|
p, e = r.Members(ctx, "go", nil, 10)
|
|
if e != nil || len(p.Items) != 0 {
|
|
t.Fatal("retired term discovery", e)
|
|
}
|
|
old, e := r.Revision(ctx, ref, 1)
|
|
if e != nil || len(old.Terms) != 1 {
|
|
t.Fatal("retirement rewrote history", e)
|
|
}
|
|
save(t, db, ref, 2, old, true)
|
|
}
|
|
func TestPaginationFiltersDraftsBeforeLimitAndRollback(t *testing.T) {
|
|
db, r := fixture(t)
|
|
ctx := context.Background()
|
|
for i := 0; i < 35; i++ {
|
|
save(t, db, cms.Reference{Kind: "news", ID: fmt.Sprintf("news-%02d", i)}, 1, cms.Associations{Terms: []string{"go"}}, i >= 30)
|
|
}
|
|
var after *cms.Reference
|
|
var ids []string
|
|
for {
|
|
p, e := r.Members(ctx, "go", after, 2)
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
for _, v := range p.Items {
|
|
ids = append(ids, v.ID)
|
|
}
|
|
if p.Next == nil {
|
|
break
|
|
}
|
|
after = p.Next
|
|
}
|
|
if len(ids) != 5 || ids[0] != "news-30" || ids[4] != "news-34" {
|
|
t.Fatal(ids)
|
|
}
|
|
tx, e := db.BeginTx(ctx, nil)
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
ref := cms.Reference{Kind: "project", ID: "rollback"}
|
|
if e = PutRevision(ctx, tx, "merchant", ref, 1, cms.Associations{}); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
if e = SetPublished(ctx, tx, "merchant", ref, 1); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
if e = tx.Rollback(); e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
if _, e = r.PublishedRevision(ctx, ref); !errors.Is(e, cms.ErrNotFound) {
|
|
t.Fatal("partial transaction", e)
|
|
}
|
|
}
|