287 lines
6.6 KiB
Go
287 lines
6.6 KiB
Go
package nodes
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"verstak/internal/core/storage"
|
||
)
|
||
|
||
func openTestDB(t *testing.T) *storage.DB {
|
||
t.Helper()
|
||
dir := t.TempDir()
|
||
db, err := storage.Open(filepath.Join(dir, "test.db"))
|
||
if err != nil {
|
||
t.Fatalf("open db: %v", err)
|
||
}
|
||
t.Cleanup(func() { db.Close() })
|
||
return db
|
||
}
|
||
|
||
func nodePtr(s string) *string { return &s }
|
||
|
||
func TestSlugify(t *testing.T) {
|
||
cases := []struct{ in, want string }{
|
||
{"ООО Ромашка", "ооо-ромашка"},
|
||
{"My Project!", "my-project"},
|
||
{"---", "untitled"},
|
||
{"", "untitled"},
|
||
{"A B", "a-b"},
|
||
{"hello_world", "hello-world"},
|
||
}
|
||
for _, c := range cases {
|
||
got := Slugify(c.in)
|
||
if got != c.want {
|
||
t.Errorf("Slugify(%q) = %q, want %q", c.in, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCreateAndGet(t *testing.T) {
|
||
db := openTestDB(t)
|
||
repo := NewRepository(db)
|
||
|
||
n, err := repo.Create(nil, TypeCase, "Test Case", 0, "", "")
|
||
if err != nil {
|
||
t.Fatalf("Create: %v", err)
|
||
}
|
||
if n.ID == "" {
|
||
t.Fatal("empty ID")
|
||
}
|
||
if n.Slug != "test-case" {
|
||
t.Errorf("slug = %q, want test-case", n.Slug)
|
||
}
|
||
|
||
got, err := repo.Get(n.ID)
|
||
if err != nil {
|
||
t.Fatalf("Get: %v", err)
|
||
}
|
||
if got.Title != "Test Case" {
|
||
t.Errorf("title = %q", got.Title)
|
||
}
|
||
|
||
// GetActive on a live node.
|
||
if _, err := repo.GetActive(n.ID); err != nil {
|
||
t.Errorf("GetActive: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCreateChild(t *testing.T) {
|
||
db := openTestDB(t)
|
||
repo := NewRepository(db)
|
||
|
||
parent, err := repo.Create(nil, TypeFolder, "Folder", 0, "", "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
child, err := repo.Create(nodePtr(parent.ID), TypeCase, "Child", 0, "", "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if child.ParentID == nil || *child.ParentID != parent.ID {
|
||
t.Errorf("parent_id = %v, want %s", child.ParentID, parent.ID)
|
||
}
|
||
}
|
||
|
||
func TestListChildren(t *testing.T) {
|
||
db := openTestDB(t)
|
||
repo := NewRepository(db)
|
||
|
||
parent, _ := repo.Create(nil, TypeFolder, "Folder", 0, "", "")
|
||
repo.Create(nodePtr(parent.ID), TypeCase, "A", 0, "", "")
|
||
repo.Create(nodePtr(parent.ID), TypeCase, "B", 0, "", "")
|
||
|
||
children, err := repo.ListChildren(parent.ID, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(children) != 2 {
|
||
t.Errorf("children = %d, want 2", len(children))
|
||
}
|
||
// Ordered by title.
|
||
if children[0].Title != "A" || children[1].Title != "B" {
|
||
t.Errorf("order: %s, %s", children[0].Title, children[1].Title)
|
||
}
|
||
}
|
||
|
||
func TestListRoots(t *testing.T) {
|
||
db := openTestDB(t)
|
||
repo := NewRepository(db)
|
||
|
||
repo.Create(nil, TypeCase, "One", 0, "", "")
|
||
repo.Create(nil, TypeCase, "Two", 0, "", "")
|
||
|
||
roots, err := repo.ListRoots(false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(roots) != 2 {
|
||
t.Errorf("roots = %d, want 2", len(roots))
|
||
}
|
||
}
|
||
|
||
func TestUpdateTitle(t *testing.T) {
|
||
db := openTestDB(t)
|
||
repo := NewRepository(db)
|
||
|
||
n, _ := repo.Create(nil, TypeCase, "Old", 0, "", "")
|
||
if err := repo.UpdateTitle(n.ID, "New Title"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
got, _ := repo.Get(n.ID)
|
||
if got.Title != "New Title" {
|
||
t.Errorf("title = %q", got.Title)
|
||
}
|
||
if got.Revision != 2 {
|
||
t.Errorf("revision = %d, want 2", got.Revision)
|
||
}
|
||
}
|
||
|
||
func TestMove(t *testing.T) {
|
||
db := openTestDB(t)
|
||
repo := NewRepository(db)
|
||
|
||
a, _ := repo.Create(nil, TypeFolder, "A", 0, "", "")
|
||
b, _ := repo.Create(nil, TypeFolder, "B", 0, "", "")
|
||
child, _ := repo.Create(nodePtr(a.ID), TypeCase, "Child", 0, "", "")
|
||
|
||
// Move child from A to B.
|
||
if err := repo.Move(child.ID, nodePtr(b.ID), 0); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
got, _ := repo.Get(child.ID)
|
||
if got.ParentID == nil || *got.ParentID != b.ID {
|
||
t.Errorf("parent = %v, want %s", got.ParentID, b.ID)
|
||
}
|
||
|
||
// Move to root.
|
||
if err := repo.Move(child.ID, nil, 0); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got2, _ := repo.Get(child.ID)
|
||
if got2.ParentID != nil {
|
||
t.Errorf("parent = %v, want nil", got2.ParentID)
|
||
}
|
||
}
|
||
|
||
func TestSoftDelete(t *testing.T) {
|
||
db := openTestDB(t)
|
||
repo := NewRepository(db)
|
||
|
||
n, _ := repo.Create(nil, TypeCase, "To Delete", 0, "", "")
|
||
if err := repo.SoftDelete(n.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
if _, err := repo.GetActive(n.ID); err != ErrNotFound {
|
||
t.Errorf("GetActive returned %v, want ErrNotFound", err)
|
||
}
|
||
|
||
// Should still be fetchable via Get.
|
||
got, err := repo.Get(n.ID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !got.IsDeleted() {
|
||
t.Error("node should be deleted")
|
||
}
|
||
|
||
// ListChildren without includeDeleted must skip it.
|
||
parent, _ := repo.Create(nil, TypeFolder, "P", 0, "", "")
|
||
child, _ := repo.Create(nodePtr(parent.ID), TypeCase, "Kid", 0, "", "")
|
||
repo.SoftDelete(child.ID)
|
||
|
||
kids, _ := repo.ListChildren(parent.ID, false)
|
||
if len(kids) != 0 {
|
||
t.Errorf("expected 0 children, got %d", len(kids))
|
||
}
|
||
|
||
kidsAll, _ := repo.ListChildren(parent.ID, true)
|
||
if len(kidsAll) != 1 {
|
||
t.Errorf("expected 1 child with deleted, got %d", len(kidsAll))
|
||
}
|
||
}
|
||
|
||
func TestMetaKV(t *testing.T) {
|
||
db := openTestDB(t)
|
||
repo := NewRepository(db)
|
||
|
||
n, _ := repo.Create(nil, TypeCase, "M", 0, "", "")
|
||
if err := repo.MetaSet(n.ID, "status", "active"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
v, ok, err := repo.MetaGet(n.ID, "status")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !ok || v != "active" {
|
||
t.Errorf("meta got %q, ok=%v", v, ok)
|
||
}
|
||
|
||
// Overwrite.
|
||
repo.MetaSet(n.ID, "status", "archived")
|
||
v, _, _ = repo.MetaGet(n.ID, "status")
|
||
if v != "archived" {
|
||
t.Errorf("meta = %q, want archived", v)
|
||
}
|
||
|
||
list, err := repo.MetaList(n.ID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(list) != 1 {
|
||
t.Errorf("meta count = %d", len(list))
|
||
}
|
||
}
|
||
|
||
func TestNotFound(t *testing.T) {
|
||
db := openTestDB(t)
|
||
repo := NewRepository(db)
|
||
|
||
if _, err := repo.Get("nonexistent"); err != ErrNotFound {
|
||
t.Errorf("Get returned %v, want ErrNotFound", err)
|
||
}
|
||
if err := repo.UpdateTitle("nonexistent", "x"); err != ErrNotFound {
|
||
t.Errorf("UpdateTitle returned %v, want ErrNotFound", err)
|
||
}
|
||
if err := repo.SoftDelete("nonexistent"); err != ErrNotFound {
|
||
t.Errorf("SoftDelete returned %v, want ErrNotFound", err)
|
||
}
|
||
if err := repo.Move("nonexistent", nil, 0); err != ErrNotFound {
|
||
t.Errorf("Move returned %v, want ErrNotFound", err)
|
||
}
|
||
}
|
||
|
||
func TestInitEndToEnd(t *testing.T) {
|
||
// Integration-like test: open a temp vault through storage, create and
|
||
// read a node — proves the migration + repository stack works.
|
||
dir := t.TempDir()
|
||
dbPath := filepath.Join(dir, "index.db")
|
||
db, err := storage.Open(dbPath)
|
||
if err != nil {
|
||
t.Fatalf("open db: %v", err)
|
||
}
|
||
defer db.Close()
|
||
|
||
repo := NewRepository(db)
|
||
n, err := repo.Create(nil, TypeCase, "Integration Case", 0, "", "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
got, err := repo.Get(n.ID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got.Title != "Integration Case" {
|
||
t.Errorf("title = %q", got.Title)
|
||
}
|
||
}
|
||
|
||
// Silence "os" import; keep unused-reference guard from breaking.
|
||
var _ = os.Args
|