131 lines
3.9 KiB
Go
131 lines
3.9 KiB
Go
package adminstore
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// newTestStore connects to the test database, skipping when it is unreachable
|
|
// or migrations have not been applied.
|
|
func newTestStore(t *testing.T) *Store {
|
|
t.Helper()
|
|
dsn := os.Getenv("OPENGOODS_DATABASE_URL")
|
|
if dsn == "" {
|
|
dsn = "postgres://opengoods:opengoods@localhost:5432/opengoods?sslmode=disable"
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
pool, err := pgxpool.New(ctx, dsn)
|
|
if err != nil {
|
|
t.Skipf("no database: %v", err)
|
|
}
|
|
if err := pool.Ping(ctx); err != nil {
|
|
pool.Close()
|
|
t.Skipf("database not reachable: %v", err)
|
|
}
|
|
var hasTable bool
|
|
if err := pool.QueryRow(ctx, "SELECT to_regclass('public.category') IS NOT NULL").Scan(&hasTable); err != nil || !hasTable {
|
|
pool.Close()
|
|
t.Skip("migrations not applied (category missing)")
|
|
}
|
|
t.Cleanup(pool.Close)
|
|
return New(pool)
|
|
}
|
|
|
|
func ptr(s string) *string { return &s }
|
|
|
|
func TestCategoryLifecycle(t *testing.T) {
|
|
s := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
root, err := s.CreateCategory(ctx, "tester", CategoryInput{
|
|
NameZH: "测试根", Slug: ptr("test_root_" + randomHex(6)),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create root: %v", err)
|
|
}
|
|
t.Cleanup(func() { _, _ = s.pool.Exec(ctx, "DELETE FROM category WHERE path <@ $1::ltree", root.Path) })
|
|
|
|
if root.Level != 0 || root.ParentID != nil {
|
|
t.Fatalf("root level/parent wrong: level=%d parent=%v", root.Level, root.ParentID)
|
|
}
|
|
|
|
child, err := s.CreateCategory(ctx, "tester", CategoryInput{
|
|
NameZH: "测试子", NameEN: ptr("Test Child"), ParentID: &root.ID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create child: %v", err)
|
|
}
|
|
if child.Level != 1 || child.ParentID == nil || *child.ParentID != root.ID {
|
|
t.Fatalf("child hierarchy wrong: %+v", child)
|
|
}
|
|
|
|
// Deleting a node with children must fail.
|
|
if err := s.DeleteCategory(ctx, root.ID, "tester"); !errors.Is(err, ErrCategoryHasChildren) {
|
|
t.Fatalf("expected ErrCategoryHasChildren, got %v", err)
|
|
}
|
|
|
|
// Rename child.
|
|
renamed, err := s.UpdateCategory(ctx, child.ID, "tester", CategoryInput{NameZH: "测试子-改名"})
|
|
if err != nil {
|
|
t.Fatalf("rename: %v", err)
|
|
}
|
|
if renamed.NameZH != "测试子-改名" {
|
|
t.Fatalf("rename not applied: %q", renamed.NameZH)
|
|
}
|
|
|
|
// Move child to a second root, descendants' path/level should follow.
|
|
root2, err := s.CreateCategory(ctx, "tester", CategoryInput{
|
|
NameZH: "测试根2", Slug: ptr("test_root2_" + randomHex(6)),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create root2: %v", err)
|
|
}
|
|
t.Cleanup(func() { _, _ = s.pool.Exec(ctx, "DELETE FROM category WHERE path <@ $1::ltree", root2.Path) })
|
|
|
|
moved, err := s.UpdateCategory(ctx, child.ID, "tester", CategoryInput{NameZH: "测试子-改名", ParentID: &root2.ID})
|
|
if err != nil {
|
|
t.Fatalf("move: %v", err)
|
|
}
|
|
if moved.ParentID == nil || *moved.ParentID != root2.ID {
|
|
t.Fatalf("move parent wrong: %+v", moved)
|
|
}
|
|
if moved.Level != 1 {
|
|
t.Fatalf("moved level wrong: %d", moved.Level)
|
|
}
|
|
|
|
// Moving a node under itself must be rejected.
|
|
if _, err := s.UpdateCategory(ctx, root2.ID, "tester", CategoryInput{NameZH: "测试根2", ParentID: &child.ID}); !errors.Is(err, ErrInvalidParent) {
|
|
t.Fatalf("expected ErrInvalidParent for self-move, got %v", err)
|
|
}
|
|
|
|
// Duplicate path on create must be rejected.
|
|
if _, err := s.CreateCategory(ctx, "tester", CategoryInput{NameZH: "dup", Slug: ptr(root.Path)}); !errors.Is(err, ErrDuplicatePath) {
|
|
t.Fatalf("expected ErrDuplicatePath, got %v", err)
|
|
}
|
|
|
|
// Now the leaf can be deleted.
|
|
if err := s.DeleteCategory(ctx, child.ID, "tester"); err != nil {
|
|
t.Fatalf("delete leaf: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSlugify(t *testing.T) {
|
|
cases := map[string]string{
|
|
"Cooking Oil": "cooking_oil",
|
|
" Hello--Wld": "hello_wld",
|
|
"食品": "",
|
|
"a__b": "a_b",
|
|
}
|
|
for in, want := range cases {
|
|
if got := slugify(in); got != want {
|
|
t.Errorf("slugify(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|