| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "testing" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/hostbox" |
| 10 | "github.com/carlosframework/platform/internal/hostclaim" |
| 11 | "github.com/carlosframework/platform/internal/hostfiles" |
| 12 | "github.com/carlosframework/platform/internal/hostguard" |
| 13 | "github.com/carlosframework/platform/internal/hostrelocate" |
| 14 | "github.com/carlosframework/platform/internal/hostwrite" |
| 15 | ) |
| 16 | |
| 17 | // Finding 10, ruling 46: the install policy admits only the install |
| 18 | // actions of this box's relocating op at installed or probing before P, |
| 19 | // at the install stamp, with no other op's hold, and refuses a stale |
| 20 | // capture. |
| 21 | func TestRelocationInstallPolicyIsTyped(t *testing.T) { |
| 22 | hold := "" |
| 23 | held := func(context.Context, string) (string, error) { return hold, nil } |
| 24 | policy := relocationInstallPolicy(held, relocationPolicy("dst")) |
| 25 | c := &hostclaim.Claim{Host: "shop.example", Holder: &hostclaim.Holder{Incarnation: "inc", Stamp: 9}, |
| 26 | Op: hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "op1", Phase: hostclaim.PhaseInstalled, Exec: "dst", Participants: []string{"src", "dst"}, Journal: map[string]string{"install_stamp": "9", "prepared": "P1"}}} |
| 27 | sel := hostbox.Selection{Host: "shop.example", Action: relocationActionInstall, Box: "dst", Op: "op1", Captured: hostguard.Captured{Incarnation: "inc", Stamp: 9}} |
| 28 | for _, action := range []string{relocationActionInstall, relocationActionHeldConfig, relocationActionHeldRelease} { |
| 29 | s := sel |
| 30 | s.Action = action |
| 31 | if err := policy(context.Background(), c, s, hostwrite.Identity{}); err != nil { |
| 32 | t.Fatalf("%s: %v", action, err) |
| 33 | } |
| 34 | } |
| 35 | hold = "op1" |
| 36 | if err := policy(context.Background(), c, sel, hostwrite.Identity{}); err != nil { |
| 37 | t.Fatalf("its own hold: %v", err) |
| 38 | } |
| 39 | for name, tweak := range map[string]func(*hostclaim.Claim, *hostbox.Selection){ |
| 40 | "a probe action": func(_ *hostclaim.Claim, s *hostbox.Selection) { s.Action = "probe" }, |
| 41 | "installing": func(c *hostclaim.Claim, _ *hostbox.Selection) { c.Op.Phase = hostclaim.PhaseInstalling }, |
| 42 | "P opened": func(c *hostclaim.Claim, _ *hostbox.Selection) { c.Op.Journal["prepared_opened"] = "P1" }, |
| 43 | "another executor": func(c *hostclaim.Claim, _ *hostbox.Selection) { c.Op.Exec = "src" }, |
| 44 | "a stale capture": func(_ *hostclaim.Claim, s *hostbox.Selection) { s.Captured.Stamp = 8 }, |
| 45 | "lost": func(c *hostclaim.Claim, _ *hostbox.Selection) { c.Lost = []string{"dst"} }, |
| 46 | "not a participant": func(c *hostclaim.Claim, _ *hostbox.Selection) { |
| 47 | c.Op.Participants = []string{"src"} |
| 48 | }, |
| 49 | "stuck": func(c *hostclaim.Claim, _ *hostbox.Selection) { c.Op.Journal["stuck"] = "x" }, |
| 50 | } { |
| 51 | raw, _ := json.Marshal(c) |
| 52 | var copy hostclaim.Claim |
| 53 | _ = json.Unmarshal(raw, ©) |
| 54 | s := sel |
| 55 | tweak(©, &s) |
| 56 | if err := policy(context.Background(), ©, s, hostwrite.Identity{}); err == nil { |
| 57 | t.Fatalf("%s: admitted", name) |
| 58 | } |
| 59 | } |
| 60 | hold = "another-op" |
| 61 | if err := policy(context.Background(), c, sel, hostwrite.Identity{}); err == nil { |
| 62 | t.Fatal("another op's hold admitted") |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | // Ruling 68, amended in Task 8c (absence is never evidence): a live |
| 67 | // platform item is installed; a missing one is "none" only under a pin of |
| 68 | // a host that never published files (no digest, no retained copy); a |
| 69 | // missing one under a pin of published files, and a tombstone (which no |
| 70 | // publisher writes) under either pin, refuse. |
| 71 | func TestRelocationPlatformItemNeedsEvidenceForNone(t *testing.T) { |
| 72 | published := hostrelocate.PinObject{Files: hostrelocate.FilesPin{Prefix: "loc/files/g-g1/", SHA256: "ab", Retained: "retained/ab"}} |
| 73 | never := hostrelocate.PinObject{Files: hostrelocate.FilesPin{Prefix: "loc/files/g-g1/"}} |
| 74 | live := hostfiles.Manifest{Platform: map[string]hostfiles.Entry{hostfiles.EnvItem: {Object: "o", SHA256: "cd", Size: 1, Seq: 1}}} |
| 75 | missing := hostfiles.Manifest{Platform: map[string]hostfiles.Entry{}} |
| 76 | tomb := hostfiles.Manifest{Platform: map[string]hostfiles.Entry{hostfiles.EnvItem: {Deleted: true, Seq: 2}}} |
| 77 | for _, tc := range []struct { |
| 78 | name string |
| 79 | pin hostrelocate.PinObject |
| 80 | m hostfiles.Manifest |
| 81 | want bool |
| 82 | err error |
| 83 | }{ |
| 84 | {"a live item", published, live, true, nil}, |
| 85 | {"a live item, evidence or not", never, live, true, nil}, |
| 86 | {"missing under published files", published, missing, false, hostrelocate.ErrRefused}, |
| 87 | {"missing under a host that never published", never, missing, false, nil}, |
| 88 | {"a tombstone under published files", published, tomb, false, hostrelocate.ErrRefused}, |
| 89 | {"a tombstone under a host that never published", never, tomb, false, hostrelocate.ErrRefused}, |
| 90 | } { |
| 91 | got, err := relocationPlatformItem(tc.pin, tc.m, hostfiles.EnvItem) |
| 92 | if got != tc.want || !errors.Is(err, tc.err) || (tc.err == nil) != (err == nil) { |
| 93 | t.Errorf("%s: %v %v, want %v %v", tc.name, got, err, tc.want, tc.err) |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | |