1package remote
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "sync/atomic"
9 "testing"
10 "time"
11
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14
15 "github.com/bnema/gordon/internal/adapters/dto"
16)
17
18func withFastRetry(t *testing.T) {
19 t.Helper()
20 prevAttempts := retryMaxAttempts
21 prevDelay := retryBaseDelay
22 retryMaxAttempts = 3
23 retryBaseDelay = 10 * time.Millisecond
24 t.Cleanup(func() {
25 retryMaxAttempts = prevAttempts
26 retryBaseDelay = prevDelay
27 })
28}
29
30func TestClientRestartRetriesOn5xx(t *testing.T) {
31 withFastRetry(t)
32
33 var attempts int32
34 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
35 require.Equal(t, "/admin/restart/test.example.com", r.URL.Path)
36 require.Equal(t, http.MethodPost, r.Method)
37 current := atomic.AddInt32(&attempts, 1)
38 if current < 3 {
39 w.WriteHeader(http.StatusServiceUnavailable)
40 _, _ = w.Write([]byte(`{"error":"temporary outage"}`))
41 return
42 }
43 w.Header().Set("Content-Type", "application/json")
44 _, _ = w.Write([]byte(`{"status":"restarted","domain":"test.example.com"}`))
45 }))
46 defer srv.Close()
47
48 client := NewClient(srv.URL)
49 result, err := client.Restart(context.Background(), "test.example.com", false)
50 require.NoError(t, err)
51 require.NotNil(t, result)
52 assert.Equal(t, "test.example.com", result.Domain)
53 assert.EqualValues(t, 3, atomic.LoadInt32(&attempts))
54}
55
56func TestClientReloadRetriesOn5xx(t *testing.T) {
57 withFastRetry(t)
58
59 var attempts int32
60 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
61 require.Equal(t, "/admin/reload", r.URL.Path)
62 require.Equal(t, http.MethodPost, r.Method)
63 current := atomic.AddInt32(&attempts, 1)
64 if current < 2 {
65 w.WriteHeader(http.StatusBadGateway)
66 _, _ = w.Write([]byte(`upstream down`))
67 return
68 }
69 w.Header().Set("Content-Type", "application/json")
70 _, _ = w.Write([]byte(`{"status":"reloaded"}`))
71 }))
72 defer srv.Close()
73
74 client := NewClient(srv.URL)
75 err := client.Reload(context.Background())
76 require.NoError(t, err)
77 assert.EqualValues(t, 2, atomic.LoadInt32(&attempts))
78}
79
80func TestClientDeployReturnsErrorAfterRetryExhaustion(t *testing.T) {
81 withFastRetry(t)
82
83 var attempts int32
84 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
85 require.Equal(t, "/admin/deploy/test.example.com", r.URL.Path)
86 require.Equal(t, http.MethodPost, r.Method)
87 atomic.AddInt32(&attempts, 1)
88 w.WriteHeader(http.StatusBadGateway)
89 _, _ = w.Write([]byte(`{"error":"upstream unavailable"}`))
90 }))
91 defer srv.Close()
92
93 client := NewClient(srv.URL)
94 _, err := client.Deploy(context.Background(), "test.example.com")
95 require.Error(t, err)
96 assert.Contains(t, err.Error(), "502 Bad Gateway: upstream unavailable")
97 assert.EqualValues(t, retryMaxAttempts, atomic.LoadInt32(&attempts))
98}
99
100func TestClientWithInsecureTLS_AllowsSelfSignedCertificate(t *testing.T) {
101 srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
102 require.Equal(t, "/admin/status", r.URL.Path)
103 w.Header().Set("Content-Type", "application/json")
104 _, _ = w.Write([]byte(`{"routes":0,"registry_domain":"","registry_port":0,"server_port":0,"auto_route":false,"network_isolation":false,"container_status":{}}`))
105 }))
106 defer srv.Close()
107
108 _, err := NewClient(srv.URL).GetStatus(context.Background())
109 require.Error(t, err)
110
111 _, err = NewClient(srv.URL, WithInsecureTLS(true)).GetStatus(context.Background())
112 require.NoError(t, err)
113}
114
115func TestClientListImages(t *testing.T) {
116 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
117 require.Equal(t, "/admin/images", r.URL.Path)
118 require.Equal(t, http.MethodGet, r.Method)
119 w.Header().Set("Content-Type", "application/json")
120 _, _ = w.Write([]byte(`{"images":[{"repository":"registry.example.com/app","tag":"latest","size":1234,"created":"2026-02-08T12:00:00Z","id":"sha256:abc","dangling":false}]}`))
121 }))
122 defer srv.Close()
123
124 client := NewClient(srv.URL)
125 images, err := client.ListImages(context.Background())
126 require.NoError(t, err)
127 require.Len(t, images, 1)
128 assert.Equal(t, "registry.example.com/app", images[0].Repository)
129 assert.Equal(t, "latest", images[0].Tag)
130}
131
132func TestClientPruneImages(t *testing.T) {
133 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
134 require.Equal(t, "/admin/images/prune", r.URL.Path)
135 require.Equal(t, http.MethodPost, r.Method)
136
137 var req dto.ImagePruneRequest
138 require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
139 require.NotNil(t, req.KeepLast)
140 assert.Equal(t, 4, *req.KeepLast)
141
142 w.Header().Set("Content-Type", "application/json")
143 _, _ = w.Write([]byte(`{"runtime":{"deleted_count":2,"space_reclaimed":2048},"registry":{"tags_removed":3,"blobs_removed":1,"space_reclaimed":4096}}`))
144 }))
145 defer srv.Close()
146
147 client := NewClient(srv.URL)
148 resp, err := client.PruneImages(context.Background(), 4)
149 require.NoError(t, err)
150 require.NotNil(t, resp)
151 assert.Equal(t, 2, resp.Runtime.DeletedCount)
152 assert.Equal(t, 3, resp.Registry.TagsRemoved)
153}