feat: add deployment foundation and cross-device handoff
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"server-deploy/internal/planner"
|
||||
)
|
||||
|
||||
func fixture() string {
|
||||
return `{"protocolVersion":1,"hostId":"host-one","instanceId":"git-one","appId":"gitea","packageDigest":"sha256:` + strings.Repeat("a", 64) + `","imageDigest":"sha256:` + strings.Repeat("b", 64) + `","domain":"git.example.com","observedStateDigest":"sha256:` + strings.Repeat("c", 64) + `"}`
|
||||
}
|
||||
|
||||
func run(args []string, input string) (int, string, string) {
|
||||
var out, diagnostic bytes.Buffer
|
||||
code := Run(args, strings.NewReader(input), &out, &diagnostic, func() time.Time { return time.Date(2026, 9, 25, 12, 0, 0, 0, time.UTC) })
|
||||
return code, out.String(), diagnostic.String()
|
||||
}
|
||||
|
||||
func TestPlanAndVerify(t *testing.T) {
|
||||
code, output, diagnostic := run([]string{"plan"}, fixture())
|
||||
if code != 0 || diagnostic != "" {
|
||||
t.Fatalf("plan failed: %d %s", code, diagnostic)
|
||||
}
|
||||
var response struct {
|
||||
ProtocolVersion int `json:"protocolVersion"`
|
||||
Mode string `json:"mode"`
|
||||
Executable bool `json:"executable"`
|
||||
Plan planner.Plan `json:"plan"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(output), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.Mode != "offline-preview" || response.Executable || response.ProtocolVersion != 1 || response.Plan.ProjectName != "sd-git-one" {
|
||||
t.Fatalf("misleading plan: %s", output)
|
||||
}
|
||||
request, _ := json.Marshal(struct {
|
||||
Plan planner.Plan `json:"plan"`
|
||||
Current planner.Intent `json:"current"`
|
||||
}{response.Plan, response.Plan.Intent})
|
||||
code, output, diagnostic = run([]string{"verify-plan"}, string(request))
|
||||
if code != 0 || diagnostic != "" || !strings.Contains(output, `"valid":true`) {
|
||||
t.Fatalf("verify failed: %d %s %s", code, output, diagnostic)
|
||||
}
|
||||
response.Plan.Hash = "tampered"
|
||||
request, _ = json.Marshal(struct {
|
||||
Plan planner.Plan `json:"plan"`
|
||||
Current planner.Intent `json:"current"`
|
||||
}{response.Plan, response.Plan.Intent})
|
||||
code, _, _ = run([]string{"verify-plan"}, string(request))
|
||||
if code == 0 {
|
||||
t.Fatal("accepted tampered plan")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsAmbiguousAndOversizedInput(t *testing.T) {
|
||||
cases := []string{
|
||||
``, `{}`, `null`, `[]`,
|
||||
strings.TrimSuffix(fixture(), "}") + `,"password":"do-not-echo"}`,
|
||||
strings.TrimSuffix(fixture(), "}") + `,"hostId":"other"}`,
|
||||
strings.TrimSuffix(fixture(), "}") + `,"HostId":"other"}`,
|
||||
strings.Replace(fixture(), `"hostId"`, `"HostId"`, 1),
|
||||
strings.Replace(fixture(), `"protocolVersion":1`, `"protocolVersion":null`, 1),
|
||||
fixture() + ` {}`, fixture() + ` trailing`,
|
||||
strings.Repeat(" ", 1024*1024) + fixture(),
|
||||
}
|
||||
for _, input := range cases {
|
||||
code, out, diagnostic := run([]string{"plan"}, input)
|
||||
if code == 0 || out != "" || diagnostic == "" {
|
||||
t.Fatalf("invalid input accepted: code=%d", code)
|
||||
}
|
||||
if strings.Contains(diagnostic, "do-not-echo") {
|
||||
t.Fatal("secret leaked")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectsWriteCommandsAndUnexpectedArguments(t *testing.T) {
|
||||
for _, args := range [][]string{nil, {"apply"}, {"upgrade"}, {"restore"}, {"plan", "extra"}, {"version", "extra"}} {
|
||||
code, out, _ := run(args, fixture())
|
||||
if code == 0 || out != "" {
|
||||
t.Fatalf("accepted command %v", args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
code, out, diagnostic := run([]string{"version"}, "")
|
||||
if code != 0 || diagnostic != "" || !json.Valid([]byte(out)) {
|
||||
t.Fatal("version failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectIsReadOnlyAndExplicitlyIncomplete(t *testing.T) {
|
||||
code, out, diagnostics := run([]string{"inspect"}, "")
|
||||
if code != 0 || diagnostics != "" {
|
||||
t.Fatalf("inspect failed: %s", diagnostics)
|
||||
}
|
||||
var report struct {
|
||||
ProtocolVersion int `json:"protocolVersion"`
|
||||
OS string `json:"os"`
|
||||
DeploymentReady bool `json:"deploymentReady"`
|
||||
DockerDaemon string `json:"dockerDaemon"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(out), &report); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if report.ProtocolVersion != 1 || report.OS == "" || report.DeploymentReady || report.DockerDaemon != "not_checked" {
|
||||
t.Fatalf("misleading report: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedVerificationRejectsDuplicateAndNullFields(t *testing.T) {
|
||||
_, output, _ := run([]string{"plan"}, fixture())
|
||||
var response struct {
|
||||
Plan json.RawMessage `json:"plan"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(output), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
valid := `{"plan":` + string(response.Plan) + `,"current":` + fixture() + `}`
|
||||
for _, input := range []string{
|
||||
strings.Replace(valid, `"projectName":"sd-git-one"`, `"projectName":"sd-git-one","projectName":"sd-git-one"`, 1),
|
||||
strings.Replace(valid, `"createdAt":"2026-09-25T12:00:00Z"`, `"createdAt":null`, 1),
|
||||
strings.Replace(valid, `"intent":`, `"Intent":`, 1),
|
||||
} {
|
||||
code, out, _ := run([]string{"verify-plan"}, input)
|
||||
if code == 0 || out != "" {
|
||||
t.Fatal("accepted ambiguous nested request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerificationRequiresCanonicalUTCTimestamps(t *testing.T) {
|
||||
_, output, _ := run([]string{"plan"}, fixture())
|
||||
var response struct {
|
||||
Plan json.RawMessage `json:"plan"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(output), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
valid := `{"plan":` + string(response.Plan) + `,"current":` + fixture() + `}`
|
||||
for _, timestamp := range []string{
|
||||
"2026-09-25T12:00:00,000Z",
|
||||
"2026-09-25T12:00:00.000Z",
|
||||
"2026-09-25T12:00:00+00:00",
|
||||
"2026-09-26T12:00:00+24:00",
|
||||
"2026-09-25T13:00:00+00:60",
|
||||
} {
|
||||
t.Run(timestamp, func(t *testing.T) {
|
||||
input := strings.Replace(valid, "2026-09-25T12:00:00Z", timestamp, 1)
|
||||
code, out, _ := run([]string{"verify-plan"}, input)
|
||||
if code == 0 || out != "" {
|
||||
t.Fatal("accepted noncanonical timestamp")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type brokenReader struct{}
|
||||
|
||||
func (brokenReader) Read([]byte) (int, error) { return 0, errors.New("secret reader error") }
|
||||
|
||||
type brokenWriter struct{}
|
||||
|
||||
func (brokenWriter) Write([]byte) (int, error) { return 0, errors.New("secret writer error") }
|
||||
|
||||
func TestIOErrorsFailWithoutLeakingDetails(t *testing.T) {
|
||||
var out, diagnostics bytes.Buffer
|
||||
if Run([]string{"plan"}, brokenReader{}, &out, &diagnostics, time.Now) == 0 {
|
||||
t.Fatal("ignored read error")
|
||||
}
|
||||
if out.Len() != 0 || strings.Contains(diagnostics.String(), "secret") {
|
||||
t.Fatal("leaked input error")
|
||||
}
|
||||
diagnostics.Reset()
|
||||
if Run([]string{"version"}, strings.NewReader(""), brokenWriter{}, &diagnostics, time.Now) == 0 {
|
||||
t.Fatal("ignored write error")
|
||||
}
|
||||
if strings.Contains(diagnostics.String(), "secret") {
|
||||
t.Fatal("leaked output error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user