feat: add deployment foundation and cross-device handoff
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"server-deploy/internal/installplan"
|
||||
"server-deploy/internal/preflight"
|
||||
)
|
||||
|
||||
func TestEnvironmentPlanCLI(t *testing.T) {
|
||||
r := preflight.Collect()
|
||||
suite, arch := "resolute", "amd64"
|
||||
if r.Runtime.OS == "linux" {
|
||||
suite = r.Distribution.Codename
|
||||
arch = r.Runtime.Architecture
|
||||
}
|
||||
if suite != "resolute" && suite != "noble" && suite != "jammy" {
|
||||
t.Skip("local Linux distribution outside initial lock policy")
|
||||
}
|
||||
l := installplan.Lock{ProtocolVersion: 1, Repository: "https://download.docker.com/linux/ubuntu", Suite: suite, Architecture: arch, ReleaseDigest: "sha256:" + strings.Repeat("a", 64), Packages: []installplan.Package{}}
|
||||
for _, name := range []string{"docker-ce", "docker-ce-cli", "containerd.io", "docker-buildx-plugin", "docker-compose-plugin"} {
|
||||
l.Packages = append(l.Packages, installplan.Package{Name: name, Version: "1.2.3-1", Filename: "dists/" + suite + "/pool/stable/" + arch + "/" + name + "_1.2.3-1_" + arch + ".deb", Digest: "sha256:" + strings.Repeat("b", 64), Size: 123})
|
||||
}
|
||||
raw, _ := json.Marshal(struct {
|
||||
Lock installplan.Lock `json:"lock"`
|
||||
}{l})
|
||||
code, out, diagnostics := run([]string{"plan-environment"}, string(raw))
|
||||
var result struct {
|
||||
Mode string `json:"mode"`
|
||||
Draft installplan.Draft `json:"draft"`
|
||||
}
|
||||
if code != 0 || json.Unmarshal([]byte(out), &result) != nil || result.Mode != "local-environment-draft" || result.Draft.Executable || result.Draft.RepositoryAuthenticated || len(result.Draft.RequestedPackages) != 5 || len(result.Draft.Blockers) == 0 {
|
||||
t.Fatalf("bad draft %s %s", out, diagnostics)
|
||||
}
|
||||
for _, bad := range []string{`{}`, strings.Replace(string(raw), `"1.2.3-1"`, `"secret;reboot"`, 1)} {
|
||||
code, out, diagnostics := run([]string{"plan-environment"}, bad)
|
||||
if code == 0 || out != "" || strings.Contains(diagnostics, "secret") {
|
||||
t.Fatal("invalid request accepted or leaked")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"io"
|
||||
"server-deploy/internal/wire"
|
||||
)
|
||||
|
||||
func decodeStrict(in io.Reader, target any) error { return wire.Decode(in, target, 1<<20) }
|
||||
@@ -0,0 +1,117 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func packageFixture(t *testing.T) (string, string) {
|
||||
return packageFixturePayload(t, []byte("services: {}\n"))
|
||||
}
|
||||
|
||||
func packageFixturePayload(t *testing.T, payload []byte) (string, string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
digest := func(data []byte) string { sum := sha256.Sum256(data); return "sha256:" + hex.EncodeToString(sum[:]) }
|
||||
manifest := map[string]any{
|
||||
"protocolVersion": 1, "appId": "example", "version": "1.0.0", "runtime": "compose", "entrypoint": "compose.yaml",
|
||||
"platforms": []string{"linux/amd64"},
|
||||
"components": []map[string]string{{"name": "server", "image": "example/server@sha256:" + strings.Repeat("a", 64)}},
|
||||
"files": []map[string]string{{"path": "compose.yaml", "digest": digest(payload)}},
|
||||
}
|
||||
raw, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "manifest.json"), raw, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "compose.yaml"), payload, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dir, digest(raw)
|
||||
}
|
||||
|
||||
func TestCheckPackagePolicyCLI(t *testing.T) {
|
||||
payload := `{"services":{"server":{"image":"example/server@sha256:` + strings.Repeat("a", 64) + `","user":"1000:1000","read_only":true,"cap_drop":["ALL"],"security_opt":["no-new-privileges:true"],"restart":"no","networks":["backend"],"volumes":[]}},"networks":{"backend":{"internal":true}},"volumes":{}}`
|
||||
for _, tc := range []struct {
|
||||
payload string
|
||||
accepted bool
|
||||
}{
|
||||
{payload, true},
|
||||
{strings.Replace(payload, `"read_only":true`, `"read_only":false`, 1), false},
|
||||
{`services: {}`, false},
|
||||
} {
|
||||
dir, digest := packageFixturePayload(t, []byte(tc.payload))
|
||||
input, _ := json.Marshal(map[string]string{"directory": dir, "expectedDigest": digest})
|
||||
code, out, diagnostics := run([]string{"check-package"}, string(input))
|
||||
if tc.accepted {
|
||||
var result struct {
|
||||
PolicyPassed bool `json:"policyPassed"`
|
||||
Executable bool `json:"executable"`
|
||||
Profile string `json:"profile"`
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
if code != 0 || json.Unmarshal([]byte(out), &result) != nil || !result.PolicyPassed || result.Executable || result.Profile == "" || result.Digest != digest {
|
||||
t.Fatalf("bad check result: %s %s", out, diagnostics)
|
||||
}
|
||||
if strings.Contains(out, "1000:1000") {
|
||||
t.Fatal("payload leaked")
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "compose.yaml"), []byte(tc.payload+" "), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if code, _, _ := run([]string{"check-package"}, string(input)); code == 0 {
|
||||
t.Fatal("policy bypassed integrity check")
|
||||
}
|
||||
} else if code == 0 || out != "" {
|
||||
t.Fatal("unsafe package accepted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPackageCLI(t *testing.T) {
|
||||
dir, digest := packageFixture(t)
|
||||
input, _ := json.Marshal(map[string]string{"directory": dir, "expectedDigest": digest})
|
||||
code, out, diagnostics := run([]string{"verify-package"}, string(input))
|
||||
if code != 0 || diagnostics != "" {
|
||||
t.Fatalf("package verification failed: %s", diagnostics)
|
||||
}
|
||||
var response struct {
|
||||
Verified bool `json:"verified"`
|
||||
Executable bool `json:"executable"`
|
||||
PublisherAuthenticated bool `json:"publisherAuthenticated"`
|
||||
FileCount int `json:"fileCount"`
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(out), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !response.Verified || response.Executable || response.PublisherAuthenticated || response.FileCount != 1 || response.Digest != digest {
|
||||
t.Fatalf("misleading verification: %s", out)
|
||||
}
|
||||
if strings.Contains(out, "services: {}") {
|
||||
t.Fatal("file contents leaked")
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "compose.yaml"), []byte("secret tampering"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
code, out, diagnostics = run([]string{"verify-package"}, string(input))
|
||||
if code == 0 || out != "" || strings.Contains(diagnostics, "secret tampering") {
|
||||
t.Fatal("tampering accepted or leaked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPackageInvalidRequests(t *testing.T) {
|
||||
for _, input := range []string{`{}`, `{"directory":".","expectedDigest":"latest"}`, `{"directory":"secret","expectedDigest":"bad","password":"do-not-echo"}`} {
|
||||
code, out, diagnostics := run([]string{"verify-package"}, input)
|
||||
if code == 0 || out != "" || strings.Contains(diagnostics, "do-not-echo") {
|
||||
t.Fatal("invalid package request accepted or leaked")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPreflightDoesNotAuthorizeInstallation(t *testing.T) {
|
||||
code, out, diagnostics := run([]string{"preflight"}, "")
|
||||
var response struct {
|
||||
ProtocolVersion int `json:"protocolVersion"`
|
||||
Mode string `json:"mode"`
|
||||
Proposal struct {
|
||||
Executable bool `json:"executable"`
|
||||
Blockers []string `json:"blockers"`
|
||||
} `json:"proposal"`
|
||||
}
|
||||
if code != 0 || json.Unmarshal([]byte(out), &response) != nil || response.ProtocolVersion != 1 || response.Mode != "local-environment-proposal" || response.Proposal.Executable || len(response.Proposal.Blockers) == 0 {
|
||||
t.Fatalf("unsafe report: %s %s", out, diagnostics)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRepositoryRejectsUntrustedRequests(t *testing.T) {
|
||||
for _, input := range []string{`{}`, `{"directory":"secret-relative","suite":"resolute","architecture":"amd64","versions":{}}`,
|
||||
`{"directory":"secret-relative","suite":"resolute","architecture":"amd64","versions":{},"repositoryAuthenticated":true}`} {
|
||||
var out, diagnostics bytes.Buffer
|
||||
if Run([]string{"verify-repository"}, strings.NewReader(input), &out, &diagnostics, time.Now) == 0 {
|
||||
t.Fatal("accepted invalid request")
|
||||
}
|
||||
if out.Len() != 0 || strings.Contains(diagnostics.String(), "secret-relative") {
|
||||
t.Fatal("invalid response leaked input")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactCommandRejectsCallerTrust(t *testing.T) {
|
||||
for _, field := range []string{`"packageBytesVerified":true`, `"repositoryAuthenticated":true`, `"lock":{}`} {
|
||||
var out, diagnostics bytes.Buffer
|
||||
input := `{"directory":"secret-relative","artifactDirectory":"secret-artifacts","suite":"resolute","architecture":"amd64","versions":{},` + field + `}`
|
||||
if Run([]string{"verify-artifacts"}, strings.NewReader(input), &out, &diagnostics, time.Now) == 0 {
|
||||
t.Fatal("caller trust accepted")
|
||||
}
|
||||
if out.Len() != 0 || strings.Contains(diagnostics.String(), "secret") {
|
||||
t.Fatal("input leaked")
|
||||
}
|
||||
if diagnostics.String() != "invalid artifact verification request\n" {
|
||||
t.Fatal("request was not rejected at the protocol boundary", diagnostics.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Package cli exposes read-only protocol endpoints, not a shell wrapper.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"server-deploy/internal/appbundle"
|
||||
"server-deploy/internal/aptrepo"
|
||||
"server-deploy/internal/composepolicy"
|
||||
"server-deploy/internal/inspect"
|
||||
"server-deploy/internal/installplan"
|
||||
"server-deploy/internal/planner"
|
||||
"server-deploy/internal/preflight"
|
||||
)
|
||||
|
||||
func Run(args []string, in io.Reader, out, diagnostics io.Writer, now func() time.Time) int {
|
||||
fail := func(message string) int { fmt.Fprintln(diagnostics, message); return 1 }
|
||||
if len(args) != 1 {
|
||||
return fail("usage: deployctl version | inspect | preflight | plan-environment | verify-repository | verify-artifacts | plan | verify-plan | verify-package | check-package")
|
||||
}
|
||||
var response any
|
||||
switch args[0] {
|
||||
case "verify-artifacts":
|
||||
var request struct {
|
||||
Directory string `json:"directory"`
|
||||
ArtifactDirectory string `json:"artifactDirectory"`
|
||||
Suite string `json:"suite"`
|
||||
Architecture string `json:"architecture"`
|
||||
Versions map[string]string `json:"versions"`
|
||||
}
|
||||
if decodeStrict(in, &request) != nil {
|
||||
return fail("invalid artifact verification request")
|
||||
}
|
||||
verified, err := aptrepo.VerifyArtifacts(request.Directory, request.ArtifactDirectory, request.Suite, request.Architecture, request.Versions, now())
|
||||
if err != nil {
|
||||
return fail("artifact verification failed")
|
||||
}
|
||||
response = verified
|
||||
case "verify-repository":
|
||||
var request struct {
|
||||
Directory string `json:"directory"`
|
||||
Suite string `json:"suite"`
|
||||
Architecture string `json:"architecture"`
|
||||
Versions map[string]string `json:"versions"`
|
||||
}
|
||||
if decodeStrict(in, &request) != nil {
|
||||
return fail("invalid repository verification request")
|
||||
}
|
||||
verified, err := aptrepo.Verify(request.Directory, request.Suite, request.Architecture, request.Versions, now())
|
||||
if err != nil {
|
||||
return fail("repository verification failed")
|
||||
}
|
||||
response = verified
|
||||
case "plan-environment":
|
||||
var request struct {
|
||||
Lock installplan.Lock `json:"lock"`
|
||||
}
|
||||
if decodeStrict(in, &request) != nil {
|
||||
return fail("invalid environment lock request")
|
||||
}
|
||||
report := preflight.Collect()
|
||||
draft, err := installplan.Build(report, request.Lock)
|
||||
if err != nil {
|
||||
return fail("environment lock rejected")
|
||||
}
|
||||
response = struct {
|
||||
ProtocolVersion int `json:"protocolVersion"`
|
||||
Mode string `json:"mode"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
Report preflight.Report `json:"report"`
|
||||
Draft installplan.Draft `json:"draft"`
|
||||
}{1, "local-environment-draft", now().UTC().Truncate(time.Second), report, draft}
|
||||
case "preflight":
|
||||
report := preflight.Collect()
|
||||
response = struct {
|
||||
ProtocolVersion int `json:"protocolVersion"`
|
||||
Mode string `json:"mode"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
Report preflight.Report `json:"report"`
|
||||
Proposal preflight.Proposal `json:"proposal"`
|
||||
}{1, "local-environment-proposal", now().UTC().Truncate(time.Second), report, preflight.Plan(report)}
|
||||
case "verify-package", "check-package":
|
||||
var request struct {
|
||||
Directory string `json:"directory"`
|
||||
ExpectedDigest string `json:"expectedDigest"`
|
||||
}
|
||||
if err := decodeStrict(in, &request); err != nil {
|
||||
return fail("invalid package verification request")
|
||||
}
|
||||
verified, err := appbundle.Verify(request.Directory, request.ExpectedDigest)
|
||||
if err != nil {
|
||||
return fail("package verification failed: invalid manifest, inventory or digest")
|
||||
}
|
||||
if args[0] == "check-package" {
|
||||
if composepolicy.Check(verified.Manifest, verified.Files[verified.Manifest.Entrypoint]) != nil {
|
||||
return fail("package rejected by restricted Compose policy")
|
||||
}
|
||||
response = struct {
|
||||
ProtocolVersion int `json:"protocolVersion"`
|
||||
PolicyPassed bool `json:"policyPassed"`
|
||||
Profile string `json:"profile"`
|
||||
Digest string `json:"digest"`
|
||||
Executable bool `json:"executable"`
|
||||
PublisherAuthenticated bool `json:"publisherAuthenticated"`
|
||||
}{1, true, composepolicy.Profile, verified.Digest, false, false}
|
||||
break
|
||||
}
|
||||
response = struct {
|
||||
ProtocolVersion int `json:"protocolVersion"`
|
||||
Verified bool `json:"verified"`
|
||||
Executable bool `json:"executable"`
|
||||
PublisherAuthenticated bool `json:"publisherAuthenticated"`
|
||||
Digest string `json:"digest"`
|
||||
FileCount int `json:"fileCount"`
|
||||
Manifest appbundle.Manifest `json:"manifest"`
|
||||
}{1, true, false, false, verified.Digest, len(verified.Files), verified.Manifest}
|
||||
case "inspect":
|
||||
response = inspect.Collect()
|
||||
case "version":
|
||||
response = struct {
|
||||
Version string `json:"version"`
|
||||
ProtocolVersion int `json:"protocolVersion"`
|
||||
WritesEnabled bool `json:"writesEnabled"`
|
||||
}{"0.1.0-dev", planner.ProtocolVersion, false}
|
||||
case "plan":
|
||||
var intent planner.Intent
|
||||
if err := decodeStrict(in, &intent); err != nil {
|
||||
return fail("invalid request: expected strict protocol JSON (maximum 1 MiB)")
|
||||
}
|
||||
plan, err := planner.Build(intent, now())
|
||||
if err != nil {
|
||||
return fail("invalid deployment intent")
|
||||
}
|
||||
response = struct {
|
||||
ProtocolVersion int `json:"protocolVersion"`
|
||||
Mode string `json:"mode"`
|
||||
Executable bool `json:"executable"`
|
||||
Plan planner.Plan `json:"plan"`
|
||||
}{planner.ProtocolVersion, "offline-preview", false, plan}
|
||||
case "verify-plan":
|
||||
var request struct {
|
||||
Plan planner.Plan `json:"plan"`
|
||||
Current planner.Intent `json:"current"`
|
||||
}
|
||||
if err := decodeStrict(in, &request); err != nil {
|
||||
return fail("invalid verification request")
|
||||
}
|
||||
if err := request.Plan.Verify(request.Current, now()); err != nil {
|
||||
return fail("plan rejected: expired, changed or invalid")
|
||||
}
|
||||
response = struct {
|
||||
ProtocolVersion int `json:"protocolVersion"`
|
||||
Mode string `json:"mode"`
|
||||
Valid bool `json:"valid"`
|
||||
Executable bool `json:"executable"`
|
||||
}{planner.ProtocolVersion, "offline-preview", true, false}
|
||||
default:
|
||||
return fail("unsupported command; deployment writes are not enabled")
|
||||
}
|
||||
if err := json.NewEncoder(out).Encode(response); err != nil {
|
||||
return fail("cannot write response")
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -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