Skip to content

Commit 6682725

Browse files
committed
fix import cycle due to acmock usage in internal test
1 parent 2e133c2 commit 6682725

File tree

3 files changed

+172
-163
lines changed

3 files changed

+172
-163
lines changed

agent/agentcontainers/api.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ func WithClock(clock quartz.Clock) Option {
6969
}
7070
}
7171

72+
// WithCacheDuration sets the cache duration for the API.
73+
// This is used to control how often the API refreshes the list of
74+
// containers. The default is 10 seconds.
75+
func WithCacheDuration(d time.Duration) Option {
76+
return func(api *API) {
77+
api.cacheDuration = d
78+
}
79+
}
80+
7281
// WithExecer sets the agentexec.Execer implementation to use.
7382
func WithExecer(execer agentexec.Execer) Option {
7483
return func(api *API) {

agent/agentcontainers/api_internal_test.go

Lines changed: 0 additions & 163 deletions
This file was deleted.

agent/agentcontainers/api_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package agentcontainers_test
33
import (
44
"context"
55
"encoding/json"
6+
"math/rand"
67
"net/http"
78
"net/http/httptest"
9+
"strings"
810
"testing"
911
"time"
1012

@@ -13,11 +15,13 @@ import (
1315
"github.com/google/uuid"
1416
"github.com/stretchr/testify/assert"
1517
"github.com/stretchr/testify/require"
18+
"go.uber.org/mock/gomock"
1619
"golang.org/x/xerrors"
1720

1821
"cdr.dev/slog"
1922
"cdr.dev/slog/sloggers/slogtest"
2023
"github.com/coder/coder/v2/agent/agentcontainers"
24+
"github.com/coder/coder/v2/agent/agentcontainers/acmock"
2125
"github.com/coder/coder/v2/agent/agentcontainers/watcher"
2226
"github.com/coder/coder/v2/codersdk"
2327
"github.com/coder/coder/v2/testutil"
@@ -146,6 +150,136 @@ func (w *fakeWatcher) sendEventWaitNextCalled(ctx context.Context, event fsnotif
146150
func TestAPI(t *testing.T) {
147151
t.Parallel()
148152

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+
149283
t.Run("Recreate", func(t *testing.T) {
150284
t.Parallel()
151285

@@ -734,3 +868,32 @@ func mustFindDevcontainerByPath(t *testing.T, devcontainers []codersdk.Workspace
734868
require.Failf(t, "no devcontainer found with workspace folder %q", path)
735869
return codersdk.WorkspaceAgentDevcontainer{} // Unreachable, but required for compilation
736870
}
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

Comments
 (0)