Files
box-manifest-android/server/internal/tree/store_test.go
T

319 lines
9.5 KiB
Go

package tree
import (
"context"
"database/sql"
"errors"
"strings"
"testing"
_ "modernc.org/sqlite"
)
func TestInitializeCreatesProtectedTree(t *testing.T) {
store := newTestStore(t)
root, err := store.Tree(context.Background())
if err != nil {
t.Fatal(err)
}
if root.ID != RootID || root.ParentID != nil || root.Name != "Root" {
t.Fatalf("unexpected root: %#v", root.Node)
}
if len(root.Children) != 1 || root.Children[0].ID != UnsortedID {
t.Fatalf("expected Unsorted beneath Root, got %#v", root.Children)
}
if root.LookupCode == "" || root.Children[0].LookupCode == "" || root.LookupCode == root.Children[0].LookupCode {
t.Fatalf("expected stable unique lookup codes: %#v", root)
}
}
func TestInitializeBackfillsLookupCodesInExistingDatabase(t *testing.T) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
if _, err := db.Exec(`CREATE TABLE nodes (
id TEXT PRIMARY KEY,
parent_id TEXT REFERENCES nodes(id),
name TEXT NOT NULL,
quantity INTEGER,
system INTEGER NOT NULL DEFAULT 0
)`); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`INSERT INTO nodes (id, name, system) VALUES ('root', 'Root', 1)`); err != nil {
t.Fatal(err)
}
store := NewStore(db)
if err := store.Initialize(context.Background()); err != nil {
t.Fatal(err)
}
root, err := store.Get(context.Background(), RootID)
if err != nil || root.LookupCode == "" {
t.Fatalf("existing node was not backfilled: %#v, %v", root, err)
}
}
func TestLookupCodeFindsNodeAndSurvivesMoveAndRename(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
node := createTestNode(t, store, RootID, "Box")
code := node.LookupCode
newName := "Renamed box"
if _, err := store.Update(ctx, node.ID, &newName, QuantityUpdate{}); err != nil {
t.Fatal(err)
}
if _, err := store.Move(ctx, node.ID, UnsortedID, WithSubtree); err != nil {
t.Fatal(err)
}
found, err := store.GetByLookupCode(ctx, strings.ToLower(code))
if err != nil {
t.Fatal(err)
}
if found.ID != node.ID || found.LookupCode != code || found.Name != newName {
t.Fatalf("lookup identity changed: %#v", found)
}
}
func TestMoveWithSubtree(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
a := createTestNode(t, store, RootID, "A")
b := createTestNode(t, store, RootID, "B")
child := createTestNode(t, store, a.ID, "child")
moved, err := store.Move(ctx, a.ID, b.ID, WithSubtree)
if err != nil {
t.Fatal(err)
}
assertParent(t, moved, b.ID)
assertParent(t, mustGet(t, store, child.ID), a.ID)
}
func TestMovePromotesChildren(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
a := createTestNode(t, store, RootID, "A")
b := createTestNode(t, store, RootID, "B")
child := createTestNode(t, store, a.ID, "child")
if _, err := store.Move(ctx, a.ID, b.ID, PromoteChildren); err != nil {
t.Fatal(err)
}
assertParent(t, mustGet(t, store, child.ID), RootID)
}
func TestMoveSendsChildrenToUnsorted(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
a := createTestNode(t, store, RootID, "A")
b := createTestNode(t, store, RootID, "B")
child := createTestNode(t, store, a.ID, "child")
if _, err := store.Move(ctx, a.ID, b.ID, ChildrenToUnsorted); err != nil {
t.Fatal(err)
}
assertParent(t, mustGet(t, store, child.ID), UnsortedID)
}
func TestMoveRejectsCycle(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
a := createTestNode(t, store, RootID, "A")
child := createTestNode(t, store, a.ID, "child")
if _, err := store.Move(ctx, a.ID, child.ID, WithSubtree); !errors.Is(err, ErrConflict) {
t.Fatalf("expected conflict, got %v", err)
}
assertParent(t, mustGet(t, store, a.ID), RootID)
assertParent(t, mustGet(t, store, child.ID), a.ID)
}
func TestDeleteOnlyAllowsEmptyNodes(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
parent := createTestNode(t, store, RootID, "parent")
child := createTestNode(t, store, parent.ID, "child")
if err := store.Delete(ctx, parent.ID); !errors.Is(err, ErrConflict) {
t.Fatalf("expected conflict deleting non-empty node, got %v", err)
}
if err := store.Delete(ctx, child.ID); err != nil {
t.Fatal(err)
}
if err := store.Delete(ctx, parent.ID); err != nil {
t.Fatal(err)
}
if _, err := store.Get(ctx, parent.ID); !errors.Is(err, ErrNotFound) {
t.Fatalf("expected deleted node to be missing, got %v", err)
}
}
func TestProtectedNodesCannotBeMovedDeletedOrRenamed(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
newName := "Elsewhere"
for _, id := range []string{RootID, UnsortedID} {
if err := store.Delete(ctx, id); !errors.Is(err, ErrConflict) {
t.Errorf("delete %s: expected conflict, got %v", id, err)
}
if _, err := store.Move(ctx, id, RootID, WithSubtree); !errors.Is(err, ErrConflict) {
t.Errorf("move %s: expected conflict, got %v", id, err)
}
if _, err := store.Update(ctx, id, &newName, QuantityUpdate{}); !errors.Is(err, ErrConflict) {
t.Errorf("rename %s: expected conflict, got %v", id, err)
}
}
}
func TestCreateDefaultsToUnsortedAndAcceptsDuplicateNames(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
quantity := int64(0)
first, err := store.Create(ctx, nil, "Pens", &quantity)
if err != nil {
t.Fatal(err)
}
second, err := store.Create(ctx, nil, "Pens", nil)
if err != nil {
t.Fatal(err)
}
assertParent(t, first, UnsortedID)
assertParent(t, second, UnsortedID)
if first.Quantity == nil || *first.Quantity != 0 || second.Quantity != nil {
t.Fatalf("quantity did not preserve zero versus missing: %#v %#v", first, second)
}
}
func TestCreateManyIsOrderedAndAtomic(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
parent := createTestNode(t, store, RootID, "parent")
created, err := store.CreateMany(ctx, parent.ID, []string{" Hammer ", "Hammer", "Screws"})
if err != nil {
t.Fatal(err)
}
if len(created) != 3 || created[0].Name != "Hammer" || created[1].Name != "Hammer" || created[2].Name != "Screws" {
t.Fatalf("unexpected created nodes: %#v", created)
}
if _, err := store.CreateMany(ctx, parent.ID, []string{"Valid", " "}); !errors.Is(err, ErrInvalid) {
t.Fatalf("expected invalid bulk request, got %v", err)
}
tree, err := store.Tree(ctx)
if err != nil {
t.Fatal(err)
}
parentAfter := tree.findForTest(parent.ID)
if parentAfter == nil || len(parentAfter.Children) != 3 {
t.Fatalf("invalid request inserted a partial batch: %#v", parentAfter)
}
}
func TestMoveManyMovesSiblingSubtreesAtomically(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
source := createTestNode(t, store, RootID, "source")
target := createTestNode(t, store, RootID, "target")
first := createTestNode(t, store, source.ID, "first")
second := createTestNode(t, store, source.ID, "second")
child := createTestNode(t, store, first.ID, "child")
moved, err := store.MoveMany(ctx, []string{first.ID, second.ID}, target.ID, WithSubtree)
if err != nil {
t.Fatal(err)
}
if len(moved) != 2 {
t.Fatalf("expected two moved nodes, got %d", len(moved))
}
assertParent(t, mustGet(t, store, first.ID), target.ID)
assertParent(t, mustGet(t, store, second.ID), target.ID)
assertParent(t, mustGet(t, store, child.ID), first.ID)
}
func TestMoveManyRejectsNodesFromDifferentParentsWithoutChanges(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
firstParent := createTestNode(t, store, RootID, "first parent")
secondParent := createTestNode(t, store, RootID, "second parent")
target := createTestNode(t, store, RootID, "target")
first := createTestNode(t, store, firstParent.ID, "first")
second := createTestNode(t, store, secondParent.ID, "second")
if _, err := store.MoveMany(ctx, []string{first.ID, second.ID}, target.ID, WithSubtree); !errors.Is(err, ErrConflict) {
t.Fatalf("expected conflict, got %v", err)
}
assertParent(t, mustGet(t, store, first.ID), firstParent.ID)
assertParent(t, mustGet(t, store, second.ID), secondParent.ID)
}
func TestDeleteManyIsAtomicWhenAnyNodeIsNotEmpty(t *testing.T) {
ctx := context.Background()
store := newTestStore(t)
parent := createTestNode(t, store, RootID, "parent")
empty := createTestNode(t, store, parent.ID, "empty")
nonEmpty := createTestNode(t, store, parent.ID, "non-empty")
createTestNode(t, store, nonEmpty.ID, "child")
if err := store.DeleteMany(ctx, []string{empty.ID, nonEmpty.ID}); !errors.Is(err, ErrConflict) {
t.Fatalf("expected conflict, got %v", err)
}
if _, err := store.Get(ctx, empty.ID); err != nil {
t.Fatalf("empty sibling was partially deleted: %v", err)
}
}
func newTestStore(t *testing.T) *Store {
t.Helper()
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
store := NewStore(db)
if err := store.Initialize(context.Background()); err != nil {
t.Fatal(err)
}
return store
}
func createTestNode(t *testing.T, store *Store, parentID, name string) Node {
t.Helper()
node, err := store.Create(context.Background(), &parentID, name, nil)
if err != nil {
t.Fatal(err)
}
return node
}
func mustGet(t *testing.T, store *Store, id string) Node {
t.Helper()
node, err := store.Get(context.Background(), id)
if err != nil {
t.Fatal(err)
}
return node
}
func assertParent(t *testing.T, node Node, parentID string) {
t.Helper()
if node.ParentID == nil || *node.ParentID != parentID {
t.Fatalf("node %s: expected parent %s, got %v", node.ID, parentID, node.ParentID)
}
}
func (node *TreeNode) findForTest(id string) *TreeNode {
if node.ID == id {
return node
}
for _, child := range node.Children {
if found := child.findForTest(id); found != nil {
return found
}
}
return nil
}