package main import ( "bytes" "context" "crypto/ed25519" "encoding/json" "errors" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "time" "github.com/carlosframework/platform/internal/historyreadback" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/release" ) func TestPlan14HistoryCommandResume(t *testing.T) { for _, boundary := range []string{"timeout", "save failure"} { t.Run(boundary, func(t *testing.T) { pub, key, _ := ed25519.GenerateKey(nil) _, boxKey, _ := ed25519.GenerateKey(nil) signer := release.RootSigner(key) d := hostactors.Directory{Store: release.NewDirStore(t.TempDir()), PublicKey: pub, Bucket: "bucket"} enrollment := strings.Repeat("a", 32) roster := hostactors.Roster{V: 1, DeploymentBucket: "bucket", Revision: 1, Boxes: map[string]hostactors.Box{"box": {Enrollment: enrollment, Namespace: "platform", BoxID: "box", Label: "box", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-box"}, EvidenceSHA256: strings.Repeat("b", 64), State: "active", CensusPublicKey: boxKey.Public().(ed25519.PublicKey)}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} wire, err := release.SignRootEnvelope(signer, roster) if err != nil { t.Fatal(err) } if _, err = d.Store.Put(t.Context(), hostactors.Key, wire, ""); err != nil { t.Fatal(err) } state := filepath.Join(t.TempDir(), "operation.json") connect := func(bucket string, resume bool) (hostactors.Directory, release.Signer, error) { if bucket != "bucket" { t.Fatal(bucket) } if resume { return d, release.Signer{}, nil } return d, signer, nil } args := []string{"status", "--bucket", "bucket", "--box", "box", "--enrollment", enrollment, "--account", "abc", "--app", "web", "--host", "web.example", "--state", state, "--wait", "1ms"} var out bytes.Buffer save := historySave if boundary == "save failure" { save = func(path string, op historyreadback.Operation) error { if err := historySave(path, op); err != nil { return err } return os.ErrPermission } } if err := historyCommandWithSave(t.Context(), args, connect, &out, save); err == nil { t.Fatal("missing receipt succeeded") } raw, err := os.ReadFile(state) if err != nil { t.Fatal(err) } var op historyreadback.Operation if err = json.Unmarshal(raw, &op); err != nil { t.Fatal(err) } if boundary == "save failure" { scope := historyreadback.Scope{Bucket: "bucket", Box: "box", Enrollment: enrollment, Binding: release.HistoryBinding{Account: "abc", App: "web", Host: "web.example"}} if _, _, err := d.Store.Get(t.Context(), historyreadback.RequestKey(scope)); !errors.Is(err, os.ErrNotExist) { t.Fatal("published after save failure", err) } if err := op.Deliver(t.Context(), d, time.Now()); err != nil { t.Fatal(err) } } var q historyreadback.Request if err = release.VerifyRootEnvelope(pub, op.Request, &q); err != nil { t.Fatal(err) } status := release.HistoryMigrationStatus{Schema: 1, Account: "abc", App: "web", HistoryReconstructionStatus: release.HistoryReconstructionStatus{Identity: strings.Repeat("c", 64), Revision: 1}, Hosts: []release.HistoryHostStatus{{HistoryReconstructionHost: release.HistoryReconstructionHost{Host: "web.example", Source: "canonical"}}}} if err = historyreadback.Respond(t.Context(), d, q.Scope, boxKey, time.Now(), func(context.Context) (release.HistoryMigrationStatus, error) { return status, nil }); err != nil { t.Fatal(err) } out.Reset() if err = historyCommand(t.Context(), []string{"resume", "--bucket", "bucket", "--state", state, "--wait", "1ms"}, connect, &out); err != nil { t.Fatal(err) } var got historyreadback.Receipt if err = json.Unmarshal(out.Bytes(), &got); err != nil { t.Fatal(err) } if got.Status.AppComplete { t.Fatal("invented migration completion") } again, _ := os.ReadFile(state) if !bytes.Equal(raw, again) { t.Fatal("resume changed saved operation") } if err = historyCommand(t.Context(), args, connect, &out); err == nil { t.Fatal("overwrote existing operation") } }) } } func TestPlan14HistoryCommandInputRefusals(t *testing.T) { for _, args := range [][]string{{}, {"wat"}, {"resume"}, {"status", "--state", "x", "--wait", "0s"}, {"resume", "--state", "x", "--input", "other"}, {"status", "--state", "x", "--unknown"}} { connect := func(string, bool) (hostactors.Directory, release.Signer, error) { t.Fatal("invalid arguments connected") return hostactors.Directory{}, release.Signer{}, nil } if err := historyCommand(t.Context(), args, connect, &bytes.Buffer{}); err == nil { t.Fatal("accepted", args) } } } func TestPlan14HistoryCLIDispatch(t *testing.T) { bin := buildCarlos(t) cmd := exec.CommandContext(t.Context(), bin, "proof", "history", "status", "--help") cmd.Env = carlosTestEnv(t) cmd.Dir = t.TempDir() out, err := cmd.CombinedOutput() if err != nil || !bytes.Contains(out, []byte("save the signed operation here before delivery")) { t.Fatalf("dispatch: %v\n%s", err, out) } } func TestPlan14HistorySavedFiles(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "state.json") op := historyreadback.Operation{Request: json.RawMessage(`{"saved":"request"}`)} if err := historySave(path, op); err != nil { t.Fatal(err) } before, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if err := historySave(path, historyreadback.Operation{Request: json.RawMessage(`{"other":"request"}`)}); err == nil { t.Fatal("replaced saved operation") } var got historyreadback.Operation if err := historyReadFile(path, &got, true); err != nil { t.Fatal(err) } after, _ := os.ReadFile(path) if !bytes.Equal(before, after) { t.Fatal("repair changed state") } for _, raw := range []string{`{"request":{}`, `{"request":{},"unknown":true}`, `{"request":{}} {}`, strings.Repeat("x", 3*release.MaxRootEnvelopeBytes+1)} { bad := filepath.Join(t.TempDir(), "bad.json") if err := os.WriteFile(bad, []byte(raw), 0600); err != nil { t.Fatal(err) } if err := historyReadFile(bad, &got, true); err == nil { t.Fatal("accepted incomplete state") } } if runtime.GOOS == "linux" { alias := filepath.Join(dir, "alias") if err := os.Symlink(path, alias); err != nil { t.Fatal(err) } if err := historyReadFile(alias, &got, true); err == nil { t.Fatal("followed state symlink") } } }