1package cli
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "net/http"
8 "os"
9 "path/filepath"
10 "testing"
11 "time"
12
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15
16 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
17 "github.com/bnema/gordon/internal/adapters/localadmin"
18)
19
20func isolateControlPlaneResolver(t *testing.T) {
21 t.Helper()
22 t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "xdg"))
23 t.Setenv("GORDON_REMOTE", "")
24 t.Setenv("GORDON_TOKEN", "")
25 originalRemoteFlag, originalTokenFlag, originalInsecureTLSFlag := remoteFlag, tokenFlag, insecureTLSFlag
26 originalFactory := newLocalControlPlaneClient
27 t.Cleanup(func() {
28 remoteFlag = originalRemoteFlag
29 tokenFlag = originalTokenFlag
30 insecureTLSFlag = originalInsecureTLSFlag
31 newLocalControlPlaneClient = originalFactory
32 })
33 remoteFlag = ""
34 tokenFlag = ""
35 insecureTLSFlag = false
36}
37
38func localControlPlaneTestClient(t *testing.T, handler http.Handler) *remote.Client {
39 t.Helper()
40 dir := t.TempDir()
41 listener, _, err := localadmin.Listen(dir)
42 require.NoError(t, err)
43 server := &http.Server{Handler: handler, ReadHeaderTimeout: 5 * time.Second}
44 go func() { _ = server.Serve(listener) }()
45 t.Cleanup(func() { _ = server.Close() })
46 return remote.NewLocalClientForSocket(localadmin.SocketPath(dir))
47}
48
49func TestResolveControlPlane_LocalUsesSocketBackedRemotePlane(t *testing.T) {
50 isolateControlPlaneResolver(t)
51 requests := make(chan string, 1)
52 client := localControlPlaneTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
53 requests <- r.URL.Path
54 w.Header().Set("Content-Type", "application/json")
55 _ = json.NewEncoder(w).Encode(remote.Status{RegistryDomain: "socket.local"})
56 }))
57 factoryCalls := 0
58 newLocalControlPlaneClient = func() (*remote.Client, error) {
59 factoryCalls++
60 return client, nil
61 }
62
63 // A deliberately unusable config path proves local resolution does not open a kernel/store.
64 handle, err := resolveControlPlane(filepath.Join(t.TempDir(), "missing", "gordon.toml"))
65 require.NoError(t, err)
66 require.NotNil(t, handle)
67 assert.False(t, handle.isRemote)
68 assert.IsType(t, &remote.Client{}, handle.plane)
69 assert.Equal(t, 1, factoryCalls)
70
71 status, err := handle.plane.GetStatus(context.Background())
72 require.NoError(t, err)
73 assert.Equal(t, "socket.local", status.RegistryDomain)
74 select {
75 case path := <-requests:
76 assert.Equal(t, "/admin/status", path)
77 case <-time.After(2 * time.Second):
78 t.Fatal("socket server received no request")
79 }
80}
81
82func TestResolveControlPlane_LocalFailsClosedWhenDaemonUnavailable(t *testing.T) {
83 isolateControlPlaneResolver(t)
84 newLocalControlPlaneClient = func() (*remote.Client, error) {
85 return nil, remote.ErrDaemonUnavailable
86 }
87
88 handle, err := resolveControlPlane(filepath.Join(t.TempDir(), "would-create-store.toml"))
89 require.Error(t, err)
90 assert.ErrorIs(t, err, remote.ErrDaemonUnavailable)
91 assert.Nil(t, handle)
92}
93
94func TestResolveControlPlane_ExplicitRemoteDoesNotUseLocalFactory(t *testing.T) {
95 isolateControlPlaneResolver(t)
96 remoteFlag = "https://example.invalid"
97 tokenFlag = "token"
98 newLocalControlPlaneClient = func() (*remote.Client, error) {
99 t.Fatal("local factory invoked for explicit remote")
100 return nil, errors.New("unreachable")
101 }
102
103 handle, err := resolveControlPlane("")
104 require.NoError(t, err)
105 assert.True(t, handle.isRemote)
106 assert.IsType(t, &remote.Client{}, handle.plane)
107}
108
109func TestResolveControlPlane_ExplicitUnknownRemoteReturnsError(t *testing.T) {
110 isolateControlPlaneResolver(t)
111 t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "xdg"))
112 remoteFlag = "does-not-exist"
113
114 handle, err := resolveControlPlane("")
115 require.Error(t, err)
116 assert.Nil(t, handle)
117 assert.Contains(t, err.Error(), "does-not-exist")
118}
119
120func TestResolveControlPlane_LocalIgnoresConfigAndStoreState(t *testing.T) {
121 isolateControlPlaneResolver(t)
122 client := localControlPlaneTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
123 w.Header().Set("Content-Type", "application/json")
124 _ = json.NewEncoder(w).Encode(remote.Status{})
125 }))
126 newLocalControlPlaneClient = func() (*remote.Client, error) { return client, nil }
127
128 lockedStore := filepath.Join(t.TempDir(), "state.db")
129 require.NoError(t, os.WriteFile(lockedStore, []byte("active daemon store"), 0o600))
130 handle, err := resolveControlPlane(lockedStore)
131 require.NoError(t, err)
132 _, err = handle.plane.GetStatus(context.Background())
133 require.NoError(t, err)
134}