| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "crypto/ed25519" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "testing" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/moves" |
| 14 | "github.com/carlosframework/platform/internal/release" |
| 15 | ) |
| 16 | |
| 17 | func TestPlan14FloorRiseWithoutPlatformUpdate(t *testing.T) { |
| 18 | for _, prev := range []string{"none", "v2.0.0", "v1.0.0"} { |
| 19 | t.Run(prev, func(t *testing.T) { |
| 20 | id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"} |
| 21 | f := rootMovesFloors{private: &rollbackMemory{files: map[string][]byte{}}, public: &rollbackMemory{files: map[string][]byte{}}, identity: id} |
| 22 | a := moves.Floor{Min: 1, MinRelease: "v1.0.0"} |
| 23 | b := moves.Floor{Min: 2, MinRelease: "v2.0.0"} |
| 24 | if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &a, Source: "history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { |
| 25 | t.Fatal(err) |
| 26 | } |
| 27 | store := release.NewDirStore(t.TempDir()) |
| 28 | raw, _ := json.Marshal(b) |
| 29 | if _, err := store.Put(t.Context(), moves.FloorKey, raw, ""); err != nil { |
| 30 | t.Fatal(err) |
| 31 | } |
| 32 | mirror := t.TempDir() |
| 33 | if _, err := syncLocalFloor(t.Context(), store, mirror); err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | if p, err := f.projection(); err != nil || p.Floor.MinRelease != a.MinRelease { |
| 37 | t.Fatal("mirror acknowledged B", p, err) |
| 38 | } |
| 39 | reads := 0 |
| 40 | read := func(context.Context) (*moves.Floor, error) { reads++; return &b, nil } |
| 41 | binary := func(path string) (rootFloorBinary, error) { |
| 42 | if path == "prev" && prev == "none" { |
| 43 | return rootFloorBinary{}, os.ErrNotExist |
| 44 | } |
| 45 | v := "v2.0.0" |
| 46 | if path == "prev" { |
| 47 | v = prev |
| 48 | } |
| 49 | return rootFloorBinary{Version: v, Protocol: 2, Digest: floorTestDigest}, nil |
| 50 | } |
| 51 | judge := rootFloorJudge{floors: f, remote: read, legacy: func() ([]*moves.Floor, error) { return rootFloorLegacy(mirror) }, binary: binary, current: "current", prev: "prev", idle: func(context.Context) error { return nil }} |
| 52 | if err := judge.refresh(t.Context()); err != nil { |
| 53 | t.Fatal(err) |
| 54 | } |
| 55 | if reads != 1 { |
| 56 | t.Fatal("no-prev path skipped authoritative GET") |
| 57 | } |
| 58 | p, err := f.projection() |
| 59 | if err != nil || p.Floor == nil || *p.Floor != b { |
| 60 | t.Fatal("root refresh", p, err) |
| 61 | } |
| 62 | ad, err := f.advertisement() |
| 63 | if err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | if ad.PrevRelease != prev { |
| 67 | t.Fatal("prev identity", ad) |
| 68 | } |
| 69 | if (ad.PrevAllowedFor == b.MinRelease) != (prev == "v2.0.0") { |
| 70 | t.Fatal("verdict", ad) |
| 71 | } |
| 72 | if err := os.RemoveAll(mirror); err != nil { |
| 73 | t.Fatal(err) |
| 74 | } |
| 75 | if again, err := f.advertisement(); err != nil || again != ad { |
| 76 | t.Fatal("mirror changed advertisement", again, err) |
| 77 | } |
| 78 | // A failed persistence cannot publish the new generation's acknowledgement. |
| 79 | n := 0 |
| 80 | broken := f |
| 81 | broken.private = &floorFaultStore{Store: f.private, n: &n, fail: 1} |
| 82 | judge.floors = broken |
| 83 | if judge.refresh(t.Context()) == nil { |
| 84 | t.Fatal("failed floor sync accepted") |
| 85 | } |
| 86 | if _, err := f.advertisement(); err == nil { |
| 87 | t.Fatal("failed sync left acceptance") |
| 88 | } |
| 89 | }) |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | func TestPlan14RootFloorRecoveryOffline(t *testing.T) { |
| 94 | for _, entry := range []string{"recover", "rollback"} { |
| 95 | t.Run(entry, func(t *testing.T) { |
| 96 | f, _ := applyHappy(t) |
| 97 | j := f.brokenBox("v2.0.0", "v1.0.0", "v1.0.0") |
| 98 | journal := filepath.Join(systemRootDir(f.a.optDir), "journal.json") |
| 99 | before, _ := os.ReadFile(systemBinaryPath(f.a.optDir)) |
| 100 | original, _ := os.ReadFile(journal) |
| 101 | unavailable := errors.New("provider unavailable") |
| 102 | reads := 0 |
| 103 | f.a.rootMoves = func() (rootFloorIdentity, bool, error) { |
| 104 | return rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"}, true, nil |
| 105 | } |
| 106 | f.a.openStore = func(context.Context) (release.Store, ed25519.PublicKey, error) { reads++; return nil, nil, unavailable } |
| 107 | var err error |
| 108 | if entry == "recover" { |
| 109 | _, err = f.a.recoverIfNeeded(t.Context()) |
| 110 | } else { |
| 111 | _, err = f.a.restoreFiles(t.Context(), &j) |
| 112 | } |
| 113 | if err == nil || reads != 0 { |
| 114 | t.Fatalf("recovery used network or accepted missing signed identity: reads=%d err=%v", reads, err) |
| 115 | } |
| 116 | after, _ := os.ReadFile(systemBinaryPath(f.a.optDir)) |
| 117 | retained, _ := os.ReadFile(journal) |
| 118 | if !bytes.Equal(before, after) || !bytes.Equal(original, retained) { |
| 119 | t.Fatal("failed refresh changed executable or journal") |
| 120 | } |
| 121 | }) |
| 122 | } |
| 123 | } |
| 124 | |