This commit is contained in:
Martino Ferrari
2026-06-24 01:39:15 +02:00
parent 11120bedca
commit c0f7e662be
76 changed files with 4368 additions and 210 deletions
+374
View File
@@ -0,0 +1,374 @@
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(`<interface id="vers" name="V1" version="1"><widget/></interface>`), "")
if err != nil {
t.Fatalf("Create: %v", err)
}
if err := s.Update(id, []byte(`<interface id="vers" name="V2" version="1"><widget/></interface>`), "second"); err != nil {
t.Fatalf("Update→v2: %v", err)
}
if err := s.Update(id, []byte(`<interface id="vers" name="V3" version="1"><widget/></interface>`), ""); 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(`<interface name="My Cool Panel!" version="1"/>`), "")
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(`<interface name="" version="1"/>`), "")
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(`<interface name="Stamp Me"><widget/></interface>`), "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)
}
}