Skip to content

Commit 214c177

Browse files
committed
Test restore-token reset CLI dispatch
Exercise the public command through its real process boundary, verify the exact Unix-socket request, and reject invalid arity before daemon contact. Co-authored-by: minpeter <minpeterx@gmail.com> Generated with [OpenAI Codex](https://openai.com/codex/)
1 parent 3345fb1 commit 214c177

1 file changed

Lines changed: 183 additions & 0 deletions

File tree

cmd/doubletake-ctl/main_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"errors"
8+
"net"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"testing"
13+
"time"
14+
)
15+
16+
const cliHelperEnvironment = "DOUBLETAKE_CTL_TEST_HELPER"
17+
18+
type controlFixtureResult struct {
19+
request map[string]json.RawMessage
20+
err error
21+
}
22+
23+
func TestResetRestoreTokenCLI(t *testing.T) {
24+
listener, results := startControlFixture(t)
25+
socketPath := listener.Addr().String()
26+
27+
code, stdout, stderr := runCLIProcess(t,
28+
"-socket", socketPath,
29+
"reset-restore-token", "192.0.2.10",
30+
)
31+
if code != 0 {
32+
t.Fatalf("reset-restore-token exit code = %d, want 0; stdout=%q stderr=%q", code, stdout, stderr)
33+
}
34+
35+
result := awaitControlFixture(t, results)
36+
if result.err != nil {
37+
t.Fatalf("control fixture: %v", result.err)
38+
}
39+
if len(result.request) != 2 {
40+
t.Fatalf("request fields = %v, want exactly cmd and target", result.request)
41+
}
42+
assertJSONStringField(t, result.request, "cmd", "reset-restore-token")
43+
assertJSONStringField(t, result.request, "target", "192.0.2.10")
44+
45+
if err := listener.Close(); err != nil {
46+
t.Fatalf("close control fixture: %v", err)
47+
}
48+
}
49+
50+
func TestResetRestoreTokenCLIRejectsInvalidArityWithoutContactingDaemon(t *testing.T) {
51+
for _, test := range []struct {
52+
name string
53+
args []string
54+
}{
55+
{name: "missing target", args: []string{"reset-restore-token"}},
56+
{name: "extra target", args: []string{"reset-restore-token", "192.0.2.10", "192.0.2.11"}},
57+
} {
58+
t.Run(test.name, func(t *testing.T) {
59+
listener, results := startControlFixture(t)
60+
args := append([]string{"-socket", listener.Addr().String()}, test.args...)
61+
62+
code, stdout, stderr := runCLIProcess(t, args...)
63+
if code == 0 {
64+
t.Fatalf("invalid reset exit code = 0, want nonzero; stdout=%q stderr=%q", stdout, stderr)
65+
}
66+
if err := listener.Close(); err != nil {
67+
t.Fatalf("close control fixture: %v", err)
68+
}
69+
70+
result := awaitControlFixture(t, results)
71+
if result.err == nil {
72+
t.Fatalf("invalid reset contacted daemon with request %v", result.request)
73+
}
74+
if !errors.Is(result.err, net.ErrClosed) {
75+
t.Fatalf("control fixture stopped with %v, want listener close", result.err)
76+
}
77+
})
78+
}
79+
}
80+
81+
func TestDoubletakeCtlHelperProcess(t *testing.T) {
82+
if os.Getenv(cliHelperEnvironment) != "1" {
83+
return
84+
}
85+
separator := -1
86+
for i, arg := range os.Args {
87+
if arg == "--" {
88+
separator = i
89+
break
90+
}
91+
}
92+
if separator < 0 {
93+
os.Exit(2)
94+
}
95+
os.Args = append([]string{"doubletake-ctl"}, os.Args[separator+1:]...)
96+
main()
97+
}
98+
99+
func startControlFixture(t *testing.T) (net.Listener, <-chan controlFixtureResult) {
100+
t.Helper()
101+
listener, err := net.Listen("unix", filepath.Join(t.TempDir(), "doubletake.sock"))
102+
if err != nil {
103+
t.Fatalf("listen on control socket: %v", err)
104+
}
105+
t.Cleanup(func() { _ = listener.Close() })
106+
results := make(chan controlFixtureResult, 1)
107+
go func() {
108+
conn, err := listener.Accept()
109+
if err != nil {
110+
results <- controlFixtureResult{err: err}
111+
return
112+
}
113+
defer conn.Close()
114+
115+
var request map[string]json.RawMessage
116+
if err := json.NewDecoder(conn).Decode(&request); err != nil {
117+
results <- controlFixtureResult{err: err}
118+
return
119+
}
120+
if err := json.NewEncoder(conn).Encode(map[string]any{"ok": true, "state": "connecting"}); err != nil {
121+
results <- controlFixtureResult{err: err}
122+
return
123+
}
124+
results <- controlFixtureResult{request: request}
125+
}()
126+
return listener, results
127+
}
128+
129+
func runCLIProcess(t *testing.T, args ...string) (int, string, string) {
130+
t.Helper()
131+
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
132+
defer cancel()
133+
134+
commandArgs := []string{"-test.run=^TestDoubletakeCtlHelperProcess$", "--"}
135+
commandArgs = append(commandArgs, args...)
136+
cmd := exec.CommandContext(ctx, os.Args[0], commandArgs...)
137+
cmd.Env = append(os.Environ(), cliHelperEnvironment+"=1")
138+
var stdout bytes.Buffer
139+
var stderr bytes.Buffer
140+
cmd.Stdout = &stdout
141+
cmd.Stderr = &stderr
142+
143+
err := cmd.Run()
144+
if ctx.Err() != nil {
145+
t.Fatalf("CLI process did not complete: %v", ctx.Err())
146+
}
147+
if err == nil {
148+
return 0, stdout.String(), stderr.String()
149+
}
150+
var exitErr *exec.ExitError
151+
if !errors.As(err, &exitErr) {
152+
t.Fatalf("run CLI process: %v", err)
153+
}
154+
return exitErr.ExitCode(), stdout.String(), stderr.String()
155+
}
156+
157+
func awaitControlFixture(t *testing.T, results <-chan controlFixtureResult) controlFixtureResult {
158+
t.Helper()
159+
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
160+
defer cancel()
161+
select {
162+
case result := <-results:
163+
return result
164+
case <-ctx.Done():
165+
t.Fatalf("control fixture did not finish: %v", ctx.Err())
166+
return controlFixtureResult{}
167+
}
168+
}
169+
170+
func assertJSONStringField(t *testing.T, request map[string]json.RawMessage, field, want string) {
171+
t.Helper()
172+
raw, ok := request[field]
173+
if !ok {
174+
t.Fatalf("request is missing %q: %v", field, request)
175+
}
176+
var got string
177+
if err := json.Unmarshal(raw, &got); err != nil {
178+
t.Fatalf("decode request field %q: %v", field, err)
179+
}
180+
if got != want {
181+
t.Fatalf("request field %q = %q, want %q", field, got, want)
182+
}
183+
}

0 commit comments

Comments
 (0)