@@ -3,8 +3,10 @@ package agentcontainers_test
3
3
import (
4
4
"context"
5
5
"encoding/json"
6
+ "math/rand"
6
7
"net/http"
7
8
"net/http/httptest"
9
+ "strings"
8
10
"testing"
9
11
"time"
10
12
@@ -13,11 +15,13 @@ import (
13
15
"github.com/google/uuid"
14
16
"github.com/stretchr/testify/assert"
15
17
"github.com/stretchr/testify/require"
18
+ "go.uber.org/mock/gomock"
16
19
"golang.org/x/xerrors"
17
20
18
21
"cdr.dev/slog"
19
22
"cdr.dev/slog/sloggers/slogtest"
20
23
"github.com/coder/coder/v2/agent/agentcontainers"
24
+ "github.com/coder/coder/v2/agent/agentcontainers/acmock"
21
25
"github.com/coder/coder/v2/agent/agentcontainers/watcher"
22
26
"github.com/coder/coder/v2/codersdk"
23
27
"github.com/coder/coder/v2/testutil"
@@ -146,6 +150,136 @@ func (w *fakeWatcher) sendEventWaitNextCalled(ctx context.Context, event fsnotif
146
150
func TestAPI (t * testing.T ) {
147
151
t .Parallel ()
148
152
153
+ // List tests the API.getContainers method using a mock
154
+ // implementation. It specifically tests caching behavior.
155
+ t .Run ("List" , func (t * testing.T ) {
156
+ t .Parallel ()
157
+
158
+ fakeCt := fakeContainer (t )
159
+ fakeCt2 := fakeContainer (t )
160
+ makeResponse := func (cts ... codersdk.WorkspaceAgentContainer ) codersdk.WorkspaceAgentListContainersResponse {
161
+ return codersdk.WorkspaceAgentListContainersResponse {Containers : cts }
162
+ }
163
+
164
+ // Each test case is called multiple times to ensure idempotency
165
+ for _ , tc := range []struct {
166
+ name string
167
+ // data to be stored in the handler
168
+ cacheData codersdk.WorkspaceAgentListContainersResponse
169
+ // duration of cache
170
+ cacheDur time.Duration
171
+ // relative age of the cached data
172
+ cacheAge time.Duration
173
+ // function to set up expectations for the mock
174
+ setupMock func (mcl * acmock.MockLister , preReq * gomock.Call )
175
+ // expected result
176
+ expected codersdk.WorkspaceAgentListContainersResponse
177
+ // expected error
178
+ expectedErr string
179
+ }{
180
+ {
181
+ name : "no cache" ,
182
+ setupMock : func (mcl * acmock.MockLister , preReq * gomock.Call ) {
183
+ mcl .EXPECT ().List (gomock .Any ()).Return (makeResponse (fakeCt ), nil ).After (preReq ).AnyTimes ()
184
+ },
185
+ expected : makeResponse (fakeCt ),
186
+ },
187
+ {
188
+ name : "no data" ,
189
+ cacheData : makeResponse (),
190
+ cacheAge : 2 * time .Second ,
191
+ cacheDur : time .Second ,
192
+ setupMock : func (mcl * acmock.MockLister , preReq * gomock.Call ) {
193
+ mcl .EXPECT ().List (gomock .Any ()).Return (makeResponse (fakeCt ), nil ).After (preReq ).AnyTimes ()
194
+ },
195
+ expected : makeResponse (fakeCt ),
196
+ },
197
+ {
198
+ name : "cached data" ,
199
+ cacheAge : time .Second ,
200
+ cacheData : makeResponse (fakeCt ),
201
+ cacheDur : 2 * time .Second ,
202
+ expected : makeResponse (fakeCt ),
203
+ },
204
+ {
205
+ name : "lister error" ,
206
+ setupMock : func (mcl * acmock.MockLister , preReq * gomock.Call ) {
207
+ mcl .EXPECT ().List (gomock .Any ()).Return (makeResponse (), assert .AnError ).After (preReq ).AnyTimes ()
208
+ },
209
+ expectedErr : assert .AnError .Error (),
210
+ },
211
+ {
212
+ name : "stale cache" ,
213
+ cacheAge : 2 * time .Second ,
214
+ cacheData : makeResponse (fakeCt ),
215
+ cacheDur : time .Second ,
216
+ setupMock : func (mcl * acmock.MockLister , preReq * gomock.Call ) {
217
+ mcl .EXPECT ().List (gomock .Any ()).Return (makeResponse (fakeCt2 ), nil ).After (preReq ).AnyTimes ()
218
+ },
219
+ expected : makeResponse (fakeCt2 ),
220
+ },
221
+ } {
222
+ tc := tc
223
+ t .Run (tc .name , func (t * testing.T ) {
224
+ t .Parallel ()
225
+ var (
226
+ ctx = testutil .Context (t , testutil .WaitShort )
227
+ clk = quartz .NewMock (t )
228
+ ctrl = gomock .NewController (t )
229
+ mockLister = acmock .NewMockLister (ctrl )
230
+ now = time .Now ().UTC ()
231
+ logger = slogtest .Make (t , nil ).Leveled (slog .LevelDebug )
232
+ r = chi .NewRouter ()
233
+ api = agentcontainers .NewAPI (logger ,
234
+ agentcontainers .WithCacheDuration (tc .cacheDur ),
235
+ agentcontainers .WithClock (clk ),
236
+ agentcontainers .WithLister (mockLister ),
237
+ )
238
+ )
239
+ defer api .Close ()
240
+
241
+ r .Mount ("/" , api .Routes ())
242
+
243
+ preReq := mockLister .EXPECT ().List (gomock .Any ()).Return (tc .cacheData , nil ).Times (1 )
244
+ if tc .setupMock != nil {
245
+ tc .setupMock (mockLister , preReq )
246
+ }
247
+
248
+ if tc .cacheAge != 0 {
249
+ clk .Set (now .Add (- tc .cacheAge )).MustWait (ctx )
250
+ } else {
251
+ clk .Set (now ).MustWait (ctx )
252
+ }
253
+
254
+ // Prime the cache with the initial data.
255
+ req := httptest .NewRequest (http .MethodGet , "/" , nil )
256
+ rec := httptest .NewRecorder ()
257
+ r .ServeHTTP (rec , req )
258
+
259
+ clk .Set (now ).MustWait (ctx )
260
+
261
+ // Repeat the test to ensure idempotency
262
+ for i := 0 ; i < 2 ; i ++ {
263
+ req = httptest .NewRequest (http .MethodGet , "/" , nil )
264
+ rec = httptest .NewRecorder ()
265
+ r .ServeHTTP (rec , req )
266
+
267
+ if tc .expectedErr != "" {
268
+ got := & codersdk.Error {}
269
+ err := json .NewDecoder (rec .Body ).Decode (got )
270
+ require .NoError (t , err , "unmarshal response failed" )
271
+ require .ErrorContains (t , got , tc .expectedErr , "expected error (attempt %d)" , i )
272
+ } else {
273
+ var got codersdk.WorkspaceAgentListContainersResponse
274
+ err := json .NewDecoder (rec .Body ).Decode (& got )
275
+ require .NoError (t , err , "unmarshal response failed" )
276
+ require .Equal (t , tc .expected , got , "expected containers to be equal (attempt %d)" , i )
277
+ }
278
+ }
279
+ })
280
+ }
281
+ })
282
+
149
283
t .Run ("Recreate" , func (t * testing.T ) {
150
284
t .Parallel ()
151
285
@@ -734,3 +868,32 @@ func mustFindDevcontainerByPath(t *testing.T, devcontainers []codersdk.Workspace
734
868
require .Failf (t , "no devcontainer found with workspace folder %q" , path )
735
869
return codersdk.WorkspaceAgentDevcontainer {} // Unreachable, but required for compilation
736
870
}
871
+
872
+ func fakeContainer (t * testing.T , mut ... func (* codersdk.WorkspaceAgentContainer )) codersdk.WorkspaceAgentContainer {
873
+ t .Helper ()
874
+ ct := codersdk.WorkspaceAgentContainer {
875
+ CreatedAt : time .Now ().UTC (),
876
+ ID : uuid .New ().String (),
877
+ FriendlyName : testutil .GetRandomName (t ),
878
+ Image : testutil .GetRandomName (t ) + ":" + strings .Split (uuid .New ().String (), "-" )[0 ],
879
+ Labels : map [string ]string {
880
+ testutil .GetRandomName (t ): testutil .GetRandomName (t ),
881
+ },
882
+ Running : true ,
883
+ Ports : []codersdk.WorkspaceAgentContainerPort {
884
+ {
885
+ Network : "tcp" ,
886
+ Port : testutil .RandomPortNoListen (t ),
887
+ HostPort : testutil .RandomPortNoListen (t ),
888
+ //nolint:gosec // this is a test
889
+ HostIP : []string {"127.0.0.1" , "[::1]" , "localhost" , "0.0.0.0" , "[::]" , testutil .GetRandomName (t )}[rand .Intn (6 )],
890
+ },
891
+ },
892
+ Status : testutil .MustRandString (t , 10 ),
893
+ Volumes : map [string ]string {testutil .GetRandomName (t ): testutil .GetRandomName (t )},
894
+ }
895
+ for _ , m := range mut {
896
+ m (& ct )
897
+ }
898
+ return ct
899
+ }
0 commit comments