package storage_test import ( "errors" "strings" "testing" "github.com/uopi/uopi/internal/storage" ) // mkVersioned creates an interface and pushes it to version 3 by updating // twice, returning the id. After this the store holds: current (v3) plus // backups id.v1.xml and id.v2.xml. func mkVersioned(t *testing.T) (*storage.Store, string) { t.Helper() s := newStore(t) id, err := s.Create([]byte(``), "") if err != nil { t.Fatalf("Create: %v", err) } if err := s.Update(id, []byte(``), "second"); err != nil { t.Fatalf("Update→v2: %v", err) } if err := s.Update(id, []byte(``), ""); err != nil { t.Fatalf("Update→v3: %v", err) } return s, id } // -------------------------------------------------------------------------- // // Groups // // -------------------------------------------------------------------------- // func TestReadGroupsDefaultsEmptyArray(t *testing.T) { s := newStore(t) data, err := s.ReadGroups() if err != nil { t.Fatalf("ReadGroups: %v", err) } if string(data) != "[]" { t.Errorf("ReadGroups default = %q, want %q", data, "[]") } } func TestWriteThenReadGroups(t *testing.T) { s := newStore(t) want := `[{"id":"a","name":"Area A"}]` if err := s.WriteGroups([]byte(want)); err != nil { t.Fatalf("WriteGroups: %v", err) } got, err := s.ReadGroups() if err != nil { t.Fatalf("ReadGroups: %v", err) } if string(got) != want { t.Errorf("ReadGroups = %q, want %q", got, want) } } // -------------------------------------------------------------------------- // // Versions // // -------------------------------------------------------------------------- // func TestVersionsNewestFirstWithCurrentFlag(t *testing.T) { s, id := mkVersioned(t) vs, err := s.Versions(id) if err != nil { t.Fatalf("Versions: %v", err) } if len(vs) != 3 { t.Fatalf("expected 3 versions, got %d", len(vs)) } if vs[0].Version != 3 || vs[1].Version != 2 || vs[2].Version != 1 { t.Errorf("not newest-first: %d, %d, %d", vs[0].Version, vs[1].Version, vs[2].Version) } if !vs[0].Current { t.Error("v3 should be flagged Current") } if vs[1].Current || vs[2].Current { t.Error("backup revisions must not be flagged Current") } // The "second" tag was stamped on the revision that became backup v2. if vs[1].Tag != "second" { t.Errorf("v2 tag = %q, want %q", vs[1].Tag, "second") } } func TestVersionsNotFound(t *testing.T) { s := newStore(t) if _, err := s.Versions("ghost"); !errors.Is(err, storage.ErrNotFound) { t.Errorf("Versions missing: want ErrNotFound, got %v", err) } } func TestVersionsInvalidID(t *testing.T) { s := newStore(t) if _, err := s.Versions("../x"); !errors.Is(err, storage.ErrNotFound) { t.Errorf("Versions bad ID: want ErrNotFound, got %v", err) } } // -------------------------------------------------------------------------- // // GetVersion // // -------------------------------------------------------------------------- // func TestGetVersionCurrentAndBackup(t *testing.T) { s, id := mkVersioned(t) cur, err := s.GetVersion(id, 3) if err != nil { t.Fatalf("GetVersion(3): %v", err) } if !strings.Contains(string(cur), `name="V3"`) { t.Errorf("GetVersion(3) wrong content: %s", cur) } old, err := s.GetVersion(id, 1) if err != nil { t.Fatalf("GetVersion(1): %v", err) } if !strings.Contains(string(old), `name="V1"`) { t.Errorf("GetVersion(1) wrong content: %s", old) } } func TestGetVersionMissingRevision(t *testing.T) { s, id := mkVersioned(t) if _, err := s.GetVersion(id, 99); !errors.Is(err, storage.ErrNotFound) { t.Errorf("GetVersion(99): want ErrNotFound, got %v", err) } } func TestGetVersionMissingInterface(t *testing.T) { s := newStore(t) if _, err := s.GetVersion("ghost", 1); !errors.Is(err, storage.ErrNotFound) { t.Errorf("GetVersion missing iface: want ErrNotFound, got %v", err) } } // -------------------------------------------------------------------------- // // SetVersionTag // // -------------------------------------------------------------------------- // func TestSetVersionTagOnBackupAndClear(t *testing.T) { s, id := mkVersioned(t) // Tag a backup revision in place (no new revision created). if err := s.SetVersionTag(id, 1, "milestone"); err != nil { t.Fatalf("SetVersionTag(1): %v", err) } vs, _ := s.Versions(id) if len(vs) != 3 { t.Fatalf("tagging must not add a revision: got %d", len(vs)) } var v1 *storage.VersionMeta for i := range vs { if vs[i].Version == 1 { v1 = &vs[i] } } if v1 == nil || v1.Tag != "milestone" { t.Fatalf("v1 tag not set: %+v", v1) } // Clearing the tag. if err := s.SetVersionTag(id, 1, ""); err != nil { t.Fatalf("SetVersionTag clear: %v", err) } vs, _ = s.Versions(id) for _, v := range vs { if v.Version == 1 && v.Tag != "" { t.Errorf("v1 tag should be cleared, got %q", v.Tag) } } } func TestSetVersionTagCurrent(t *testing.T) { s, id := mkVersioned(t) if err := s.SetVersionTag(id, 3, "live"); err != nil { t.Fatalf("SetVersionTag(3): %v", err) } vs, _ := s.Versions(id) if vs[0].Version != 3 || vs[0].Tag != "live" { t.Errorf("current tag not applied: %+v", vs[0]) } } func TestSetVersionTagMissing(t *testing.T) { s, id := mkVersioned(t) if err := s.SetVersionTag(id, 99, "x"); !errors.Is(err, storage.ErrNotFound) { t.Errorf("SetVersionTag missing rev: want ErrNotFound, got %v", err) } if err := s.SetVersionTag("../bad", 1, "x"); !errors.Is(err, storage.ErrNotFound) { t.Errorf("SetVersionTag bad ID: want ErrNotFound, got %v", err) } } // -------------------------------------------------------------------------- // // Promote // // -------------------------------------------------------------------------- // func TestPromoteIsNonDestructive(t *testing.T) { s, id := mkVersioned(t) // Promote v1 — its content becomes the new current revision (v4), while // every prior revision is preserved. if err := s.Promote(id, 1); err != nil { t.Fatalf("Promote(1): %v", err) } vs, err := s.Versions(id) if err != nil { t.Fatalf("Versions after promote: %v", err) } if len(vs) != 4 { t.Fatalf("expected 4 versions after promote, got %d", len(vs)) } if vs[0].Version != 4 || !vs[0].Current { t.Errorf("new current should be v4: %+v", vs[0]) } if vs[0].Name != "V1" { t.Errorf("promoted content should be V1's, got name %q", vs[0].Name) } if vs[0].Tag != "restored from v1" { t.Errorf("promote tag = %q", vs[0].Tag) } } func TestPromoteMissing(t *testing.T) { s, id := mkVersioned(t) if err := s.Promote(id, 99); !errors.Is(err, storage.ErrNotFound) { t.Errorf("Promote missing rev: want ErrNotFound, got %v", err) } } // -------------------------------------------------------------------------- // // Fork // // -------------------------------------------------------------------------- // func TestForkResetsVersionAndClearsTag(t *testing.T) { s, id := mkVersioned(t) if err := s.SetVersionTag(id, 2, "tagged"); err != nil { t.Fatalf("SetVersionTag: %v", err) } newID, err := s.Fork(id, 2) if err != nil { t.Fatalf("Fork(2): %v", err) } if newID == id { t.Fatal("Fork must produce a distinct ID") } data, err := s.Get(newID) if err != nil { t.Fatalf("Get(fork): %v", err) } str := string(data) if !strings.Contains(str, `name="V2"`) { t.Errorf("fork should carry V2 content: %s", str) } if !strings.Contains(str, `version="1"`) { t.Errorf("fork version should reset to 1: %s", str) } if !strings.Contains(str, `id="`+newID+`"`) { t.Errorf("fork id attr should be stamped: %s", str) } if strings.Contains(str, "tagged") { t.Errorf("fork should clear the tag: %s", str) } // The fork is an independent, listable interface at v1. vs, err := s.Versions(newID) if err != nil { t.Fatalf("Versions(fork): %v", err) } if len(vs) != 1 || vs[0].Version != 1 { t.Errorf("fork should have a single v1 revision, got %+v", vs) } } func TestForkMissing(t *testing.T) { s := newStore(t) if _, err := s.Fork("ghost", 1); !errors.Is(err, storage.ErrNotFound) { t.Errorf("Fork missing: want ErrNotFound, got %v", err) } } // -------------------------------------------------------------------------- // // Create: slug derivation + attribute stamping // // -------------------------------------------------------------------------- // func TestCreateSlugifiesNameWhenNoID(t *testing.T) { s := newStore(t) id, err := s.Create([]byte(``), "") if err != nil { t.Fatalf("Create: %v", err) } if id != "my-cool-panel" { t.Errorf("slug id = %q, want %q", id, "my-cool-panel") } } func TestCreateEmptyNameFallsBackToInterface(t *testing.T) { s := newStore(t) id, err := s.Create([]byte(``), "") if err != nil { t.Fatalf("Create: %v", err) } if id != "interface" { t.Errorf("fallback id = %q, want %q", id, "interface") } } func TestCreateStampsMissingVersionAndID(t *testing.T) { s := newStore(t) // XML carries neither id nor version attributes — Create must insert both. id, err := s.Create([]byte(``), "label") if err != nil { t.Fatalf("Create: %v", err) } data, err := s.Get(id) if err != nil { t.Fatalf("Get: %v", err) } str := string(data) if !strings.Contains(str, `version="1"`) { t.Errorf("Create should stamp version=1: %s", str) } if !strings.Contains(str, `id="`+id+`"`) { t.Errorf("Create should stamp id: %s", str) } if !strings.Contains(str, `tag="label"`) { t.Errorf("Create should stamp tag: %s", str) } } // -------------------------------------------------------------------------- // // Update tagging + backup chain // // -------------------------------------------------------------------------- // func TestDeleteMovesVersionedBackups(t *testing.T) { s, id := mkVersioned(t) // current + id.v1.xml + id.v2.xml if err := s.Delete(id); err != nil { t.Fatalf("Delete: %v", err) } if _, err := s.Get(id); !errors.Is(err, storage.ErrNotFound) { t.Errorf("Get after delete: want ErrNotFound, got %v", err) } if _, err := s.Versions(id); !errors.Is(err, storage.ErrNotFound) { t.Errorf("Versions after delete: want ErrNotFound, got %v", err) } } func TestListSkipsVersionedBackups(t *testing.T) { s, _ := mkVersioned(t) // leaves id.v1.xml and id.v2.xml on disk list, err := s.List() if err != nil { t.Fatalf("List: %v", err) } if len(list) != 1 { t.Fatalf("List should skip versioned backups, got %d entries: %+v", len(list), list) } } func TestUpdateStampsIncrementingVersion(t *testing.T) { s, id := mkVersioned(t) data, err := s.Get(id) if err != nil { t.Fatalf("Get: %v", err) } if !strings.Contains(string(data), `version="3"`) { t.Errorf("current should be version 3 after two updates: %s", data) } }