1package health
2
3import (
4 "context"
5 "errors"
6 "testing"
7
8 "github.com/bnema/zerowrap"
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/mock"
11
12 "github.com/bnema/gordon/internal/boundaries/in/mocks"
13 "github.com/bnema/gordon/internal/domain"
14)
15
16func testLogger() zerowrap.Logger {
17 return zerowrap.Default()
18}
19
20func TestService_CheckRoute_ContainerNotFound(t *testing.T) {
21 configSvc := mocks.NewMockConfigService(t)
22 containerSvc := mocks.NewMockContainerService(t)
23 prober := mocks.NewMockHTTPProber(t)
24
25 containerSvc.EXPECT().Get(mock.Anything, "app.example.com").Return(nil, false)
26
27 svc := NewService(configSvc, containerSvc, prober, testLogger())
28 route := domain.Route{Domain: "app.example.com", Image: "myapp:latest", HTTPS: true}
29
30 health := svc.CheckRoute(context.Background(), route)
31
32 assert.Equal(t, "app.example.com", health.Domain)
33 assert.Equal(t, "not found", health.ContainerStatus)
34 assert.Equal(t, 0, health.HTTPStatus)
35 assert.False(t, health.Healthy)
36 assert.Equal(t, "container not found", health.Error)
37}
38
39func TestService_CheckRoute_ContainerNotRunning(t *testing.T) {
40 configSvc := mocks.NewMockConfigService(t)
41 containerSvc := mocks.NewMockContainerService(t)
42 prober := mocks.NewMockHTTPProber(t)
43
44 container := &domain.Container{
45 ID: "abc123",
46 Status: "stopped",
47 }
48 containerSvc.EXPECT().Get(mock.Anything, "app.example.com").Return(container, true)
49
50 svc := NewService(configSvc, containerSvc, prober, testLogger())
51 route := domain.Route{Domain: "app.example.com", Image: "myapp:latest", HTTPS: true}
52
53 health := svc.CheckRoute(context.Background(), route)
54
55 assert.Equal(t, "app.example.com", health.Domain)
56 assert.Equal(t, "stopped", health.ContainerStatus)
57 assert.Equal(t, 0, health.HTTPStatus)
58 assert.False(t, health.Healthy)
59 assert.Contains(t, health.Error, "container is stopped")
60}
61
62func TestService_CheckRoute_HTTPProbeSuccess(t *testing.T) {
63 configSvc := mocks.NewMockConfigService(t)
64 containerSvc := mocks.NewMockContainerService(t)
65 prober := mocks.NewMockHTTPProber(t)
66
67 container := &domain.Container{
68 ID: "abc123",
69 Status: "running",
70 }
71 containerSvc.EXPECT().Get(mock.Anything, "app.example.com").Return(container, true)
72 prober.EXPECT().Probe(mock.Anything, "https://app.example.com/").Return(200, int64(45), nil)
73
74 svc := NewService(configSvc, containerSvc, prober, testLogger())
75 route := domain.Route{Domain: "app.example.com", Image: "myapp:latest", HTTPS: true}
76
77 health := svc.CheckRoute(context.Background(), route)
78
79 assert.Equal(t, "app.example.com", health.Domain)
80 assert.Equal(t, "running", health.ContainerStatus)
81 assert.Equal(t, 200, health.HTTPStatus)
82 assert.Equal(t, int64(45), health.ResponseTimeMs)
83 assert.True(t, health.Healthy)
84 assert.Empty(t, health.Error)
85}
86
87func TestService_CheckRoute_HTTPOnlyRouteUsesHTTP(t *testing.T) {
88 configSvc := mocks.NewMockConfigService(t)
89 containerSvc := mocks.NewMockContainerService(t)
90 prober := mocks.NewMockHTTPProber(t)
91
92 container := &domain.Container{
93 ID: "abc123",
94 Status: "running",
95 }
96 containerSvc.EXPECT().Get(mock.Anything, "app.example.com").Return(container, true)
97 prober.EXPECT().Probe(mock.Anything, "http://app.example.com/").Return(200, int64(45), nil)
98
99 svc := NewService(configSvc, containerSvc, prober, testLogger())
100 route := domain.Route{Domain: "app.example.com", Image: "myapp:latest", HTTPS: false}
101
102 health := svc.CheckRoute(context.Background(), route)
103
104 assert.Equal(t, "app.example.com", health.Domain)
105 assert.Equal(t, "running", health.ContainerStatus)
106 assert.Equal(t, 200, health.HTTPStatus)
107 assert.Equal(t, int64(45), health.ResponseTimeMs)
108 assert.True(t, health.Healthy)
109 assert.Empty(t, health.Error)
110}
111
112func TestService_CheckRoute_HTTPProbeFailure(t *testing.T) {
113 configSvc := mocks.NewMockConfigService(t)
114 containerSvc := mocks.NewMockContainerService(t)
115 prober := mocks.NewMockHTTPProber(t)
116
117 container := &domain.Container{
118 ID: "abc123",
119 Status: "running",
120 }
121 containerSvc.EXPECT().Get(mock.Anything, "app.example.com").Return(container, true)
122 prober.EXPECT().Probe(mock.Anything, "https://app.example.com/").Return(0, int64(0), errors.New("connection refused"))
123
124 svc := NewService(configSvc, containerSvc, prober, testLogger())
125 route := domain.Route{Domain: "app.example.com", Image: "myapp:latest", HTTPS: true}
126
127 health := svc.CheckRoute(context.Background(), route)
128
129 assert.Equal(t, "app.example.com", health.Domain)
130 assert.Equal(t, "running", health.ContainerStatus)
131 assert.Equal(t, 0, health.HTTPStatus)
132 assert.False(t, health.Healthy)
133 assert.Equal(t, "connection refused", health.Error)
134}
135
136func TestService_CheckRoute_HTTPStatus5xx(t *testing.T) {
137 configSvc := mocks.NewMockConfigService(t)
138 containerSvc := mocks.NewMockContainerService(t)
139 prober := mocks.NewMockHTTPProber(t)
140
141 container := &domain.Container{
142 ID: "abc123",
143 Status: "running",
144 }
145 containerSvc.EXPECT().Get(mock.Anything, "app.example.com").Return(container, true)
146 prober.EXPECT().Probe(mock.Anything, "https://app.example.com/").Return(502, int64(100), nil)
147
148 svc := NewService(configSvc, containerSvc, prober, testLogger())
149 route := domain.Route{Domain: "app.example.com", Image: "myapp:latest", HTTPS: true}
150
151 health := svc.CheckRoute(context.Background(), route)
152
153 assert.Equal(t, "app.example.com", health.Domain)
154 assert.Equal(t, "running", health.ContainerStatus)
155 assert.Equal(t, 502, health.HTTPStatus)
156 assert.Equal(t, int64(100), health.ResponseTimeMs)
157 assert.False(t, health.Healthy) // 5xx is not healthy
158 assert.Empty(t, health.Error)
159}
160
161func TestService_CheckAllRoutes(t *testing.T) {
162 configSvc := mocks.NewMockConfigService(t)
163 containerSvc := mocks.NewMockContainerService(t)
164 prober := mocks.NewMockHTTPProber(t)
165
166 routes := []domain.Route{
167 {Domain: "app1.example.com", Image: "app1:latest", HTTPS: true},
168 {Domain: "app2.example.com", Image: "app2:latest", HTTPS: true},
169 }
170 configSvc.EXPECT().GetRoutes(mock.Anything).Return(routes)
171
172 container1 := &domain.Container{ID: "abc", Status: "running"}
173 container2 := &domain.Container{ID: "def", Status: "stopped"}
174 containerSvc.EXPECT().Get(mock.Anything, "app1.example.com").Return(container1, true)
175 containerSvc.EXPECT().Get(mock.Anything, "app2.example.com").Return(container2, true)
176
177 // Only running containers get probed
178 prober.EXPECT().Probe(mock.Anything, "https://app1.example.com/").Return(200, int64(30), nil)
179
180 svc := NewService(configSvc, containerSvc, prober, testLogger())
181
182 results := svc.CheckAllRoutes(context.Background())
183
184 assert.Len(t, results, 2)
185
186 assert.NotNil(t, results["app1.example.com"])
187 assert.Equal(t, "running", results["app1.example.com"].ContainerStatus)
188 assert.Equal(t, 200, results["app1.example.com"].HTTPStatus)
189 assert.True(t, results["app1.example.com"].Healthy)
190
191 assert.NotNil(t, results["app2.example.com"])
192 assert.Equal(t, "stopped", results["app2.example.com"].ContainerStatus)
193 assert.Equal(t, 0, results["app2.example.com"].HTTPStatus)
194 assert.False(t, results["app2.example.com"].Healthy)
195}
196
197func TestService_CheckAllRoutes_NoRoutes(t *testing.T) {
198 configSvc := mocks.NewMockConfigService(t)
199 containerSvc := mocks.NewMockContainerService(t)
200 prober := mocks.NewMockHTTPProber(t)
201
202 configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{})
203
204 svc := NewService(configSvc, containerSvc, prober, testLogger())
205
206 results := svc.CheckAllRoutes(context.Background())
207
208 assert.Empty(t, results)
209}
210
211func TestService_CheckRoute_InvalidDomain_Blocked(t *testing.T) {
212 configSvc := mocks.NewMockConfigService(t)
213 containerSvc := mocks.NewMockContainerService(t)
214 prober := mocks.NewMockHTTPProber(t)
215
216 // No container lookup should happen for invalid domains
217 // No probe should happen either
218
219 svc := NewService(configSvc, containerSvc, prober, testLogger())
220
221 tests := []struct {
222 name string
223 domain string
224 }{
225 {"IP address", "192.168.1.1"},
226 {"localhost", "localhost"},
227 {"local TLD", "myapp.local"},
228 {"internal TLD", "service.internal"},
229 {"with port", "example.com:8080"},
230 {"IPv6", "::1"},
231 }
232
233 for _, tt := range tests {
234 t.Run(tt.name, func(t *testing.T) {
235 route := domain.Route{Domain: tt.domain, Image: "myapp:latest"}
236 health := svc.CheckRoute(context.Background(), route)
237
238 assert.Equal(t, tt.domain, health.Domain)
239 assert.False(t, health.Healthy)
240 assert.Equal(t, "invalid route domain for health probe", health.Error)
241 })
242 }
243}