package inventory import ( "encoding/json" "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" ) // novox/hq ADR 0325 §3, §11: a module gets one allocation, the same whoever asks and however often, from the range // of its own; no allocation is 0 or a root group's; a number once given is never given again. func TestAModuleIsGivenOneIdentityOnceAndNeverRoot(t *testing.T) { inv := fresh(t) ctx := t.Context() radarr, err := inv.AllocateIdentity(ctx, "radarr") if err != nil { t.Fatal(err) } again, err := inv.AllocateIdentity(ctx, "radarr") if err != nil { t.Fatal(err) } if radarr != again { t.Fatalf("radarr was given %+v, then %+v: one allocation per module", radarr, again) } plex, err := inv.AllocateIdentity(ctx, "plex") if err != nil { t.Fatal(err) } for _, id := range []catalogue.ModuleIdentity{radarr, plex} { if id.UID == 0 || id.GID == 0 || !catalogue.AllocatableID(id.UID) || !catalogue.AllocatableID(id.GID) || id.UID != id.GID { t.Errorf("%s was given %d:%d: within %d–%d, never 0, uid and gid one number", id.Module, id.UID, id.GID, catalogue.IdentityLow, catalogue.IdentityHigh) } } if radarr.UID == plex.UID { t.Fatalf("radarr and plex share %d", plex.UID) } // Read back as composition reads it, from another connection's view: the same numbers (stable across restarts // and machines, since every machine's composition reads this one row). all, err := inv.Identities(ctx) if err != nil { t.Fatal(err) } if all["radarr"].UID != radarr.UID || all["plex"].UID != plex.UID { t.Fatalf("the identities read back are %+v", all) } // A module forgotten keeps its row, and the next module is given a new number: never one given before. if _, err := inv.store.Pool().Exec(ctx, `delete from module_identity where module = 'plex'`); err != nil { t.Fatal(err) } sonarr, err := inv.AllocateIdentity(ctx, "sonarr") if err != nil { t.Fatal(err) } if sonarr.UID == plex.UID || sonarr.UID == radarr.UID { t.Fatalf("sonarr was given %d, a number given before", sonarr.UID) } // The store itself refuses a number outside the range, root's above all: no write by hand makes one root. for _, n := range []int{0, 10, 1000, 49999, 60000} { if _, err := inv.store.Pool().Exec(ctx, `insert into module_identity (module, uid, gid) values ($1, $2, $2)`, "byhand", n); err == nil { t.Errorf("the store took %d as an identity", n) } } } // novox/hq ADR 0325 §8: a folder taken is recorded once per machine, under a warrant, and read back by its path. func TestAFolderIsTakenOnceUnderAWarrant(t *testing.T) { inv := fresh(t) ctx := t.Context() if _, err := inv.AddNode(ctx, "ace"); err != nil { t.Fatal(err) } if err := inv.RecordTaken(ctx, "ace", TakenFolder{Path: "/storage/media/movies/", By: "the operator"}); err == nil { t.Fatal("a folder was recorded taken with no warrant") } first := TakenFolder{Path: "/storage/media/movies/", By: "the operator", Ask: "c01", Warrant: "c01", Said: "started"} if err := inv.RecordTaken(ctx, "ace", first); err != nil { t.Fatal(err) } if err := inv.RecordTaken(ctx, "ace", TakenFolder{Path: "/storage/media/movies", By: "someone else", Ask: "c02", Warrant: "c02"}); err != nil { t.Fatal(err) } kept, err := inv.KeptFolders(ctx, "ace") if err != nil { t.Fatal(err) } got, has := kept["/storage/media/movies"] if !has || got.Ask != "c01" || got.By != "the operator" { t.Fatalf("the taken folder reads %+v (%v): the first take, by its clean path", got, has) } // The folder moved to another filesystem and the operator was asked again: the new take replaces the old. if err := inv.RecordTaken(ctx, "ace", TakenFolder{Path: "/storage/media/movies", By: "the operator", Ask: "c03", Warrant: "c03", Placed: "fs-0000000000ff"}); err != nil { t.Fatal(err) } kept, err = inv.KeptFolders(ctx, "ace") if err != nil { t.Fatal(err) } if got := kept["/storage/media/movies"]; got.Ask != "c03" || got.Placed != "fs-0000000000ff" { t.Fatalf("after a take on another filesystem the folder reads %+v", got) } } // novox/hq issue 575: a module registered without the access movies, whose newest build on its branch declares it — // the build the delivery under way brings — takes a proposal placing movies: judged against that build, not refused. // Without such a build it is refused, as before. func TestAPlacementIsJudgedAgainstTheBuildThatBringsTheAccess(t *testing.T) { inv := fresh(t) ctx := t.Context() if _, err := inv.AddNode(ctx, "ace"); err != nil { t.Fatal(err) } registered := catalogue.Manifest{Module: "media-importer", Version: "1", Accesses: []catalogue.Access{{ID: "downloads", Path: "/services/media/downloads", Mode: catalogue.AccessReadWrite}}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "importer", "image": "x", "volumes": []any{"${access:downloads}:/downloads"}}}} if err := inv.RegisterModule(ctx, registered, Source{Repository: "https://forge.invalid/media.git", Ref: "main", BuiltFrom: "c0ffee33"}); err != nil { t.Fatal(err) } placing := map[string]any{catalogue.AccessesSetting: map[string]any{"movies": "/storage/media/movies"}} err := inv.JudgeSettings(ctx, "ace", "media-importer", placing) if err == nil || !strings.Contains(err.Error(), `places the access "movies", which it does not declare`) { t.Fatalf("a placement no build declares: %v", err) } // main's manifest — the build of #34 — declares movies. ahead := registered ahead.Accesses = append([]catalogue.Access{}, registered.Accesses...) ahead.Accesses = append(ahead.Accesses, catalogue.Access{ID: "movies", Path: "/services/media/movies", Mode: catalogue.AccessReadWrite}) raw, err := json.Marshal(ahead) if err != nil { t.Fatal(err) } b := aBuild("b34", "media-importer", "") b.Ref, b.Manifest = "main", raw if err := inv.RecordBuild(ctx, b); err != nil { t.Fatal(err) } if err := inv.JudgeSettings(ctx, "ace", "media-importer", placing); err != nil { t.Fatalf("the proposal placing movies, which main's build declares, was refused: %v", err) } if err := inv.SetSettings(ctx, "ace", "media-importer", placing); err != nil { t.Fatalf("the layer placing movies was not stored: %v", err) } // Stored ahead, it does not cost the registered module its place: composition passes it by. layers, err := inv.SettingsFor(ctx, "ace", "media-importer") if err != nil { t.Fatal(err) } if err := catalogue.ComposesWith(registered, layers, false); err != nil { t.Fatalf("the registered module no longer composes with the layer stored ahead: %v", err) } // A typo is still a typo: an access neither build declares is refused. typo := map[string]any{catalogue.AccessesSetting: map[string]any{"moveis": "/storage/media/movies"}} if err := inv.JudgeSettings(ctx, "ace", "media-importer", typo); err == nil { t.Fatal("a placement no build declares was taken") } }