44"bytes"
55"context"
66"database/sql"
7+ "encoding/json"
78"fmt"
89"io"
910"net/http"
@@ -17,6 +18,7 @@ import (
1718"github.com/google/uuid"
1819"github.com/stretchr/testify/require"
1920"go.uber.org/mock/gomock"
21+ "golang.org/x/xerrors"
2022
2123"cdr.dev/slog"
2224"cdr.dev/slog/sloggers/slogtest"
@@ -35,6 +37,17 @@ import (
3537"github.com/coder/websocket"
3638)
3739
40+ // newSDKError creates a codersdk.Error for testing by simulating an HTTP response.
41+ func newSDKError (statusCode int ,resp codersdk.Response )error {
42+ body ,_ := json .Marshal (resp )
43+ httpResp := & http.Response {
44+ StatusCode :statusCode ,
45+ Body :io .NopCloser (bytes .NewReader (body )),
46+ Request :& http.Request {URL :& url.URL {}},
47+ }
48+ return codersdk .ReadBodyAsError (httpResp )
49+ }
50+
3851type fakeAgentProvider struct {
3952agentConn func (ctx context.Context ,agentID uuid.UUID ) (_ workspacesdk.AgentConn ,release func (),_ error )
4053}
@@ -319,3 +332,145 @@ func TestWatchAgentContainers(t *testing.T) {
319332}
320333})
321334}
335+
336+ func TestWorkspaceAgentDeleteDevcontainer (t * testing.T ) {
337+ t .Parallel ()
338+
339+ tests := []struct {
340+ name string
341+ agentConnected bool // Controls FirstConnectedAt/LastConnectedAt validity
342+ agentConnError error // Error returned by fakeAgentProvider.AgentConn (nil = success)
343+ deleteError error // Error returned by DeleteDevcontainer mock (nil = success)
344+ expectedStatusCode int
345+ }{
346+ {
347+ name :"OK" ,
348+ agentConnected :true ,
349+ agentConnError :nil ,
350+ deleteError :nil ,
351+ expectedStatusCode :http .StatusNoContent ,
352+ },
353+ {
354+ name :"AgentNotConnected" ,
355+ agentConnected :false ,
356+ expectedStatusCode :http .StatusBadRequest ,
357+ },
358+ {
359+ name :"DevcontainerNotFound" ,
360+ agentConnected :true ,
361+ deleteError :newSDKError (http .StatusNotFound , codersdk.Response {
362+ Message :"Devcontainer not found." ,
363+ }),
364+ expectedStatusCode :http .StatusNotFound ,
365+ },
366+ {
367+ name :"AgentConnectionFailure" ,
368+ agentConnected :true ,
369+ agentConnError :xerrors .New ("connection failed" ),
370+ expectedStatusCode :http .StatusInternalServerError ,
371+ },
372+ {
373+ name :"InternalError" ,
374+ agentConnected :true ,
375+ deleteError :xerrors .New ("internal error" ),
376+ expectedStatusCode :http .StatusInternalServerError ,
377+ },
378+ }
379+
380+ for _ ,tc := range tests {
381+ t .Run (tc .name ,func (t * testing.T ) {
382+ t .Parallel ()
383+
384+ var (
385+ ctx = testutil .Context (t ,testutil .WaitShort )
386+ logger = slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true }).Leveled (slog .LevelDebug ).Named ("coderd" )
387+
388+ mCtrl = gomock .NewController (t )
389+ mDB = dbmock .NewMockStore (mCtrl )
390+ mCoordinator = tailnettest .NewMockCoordinator (mCtrl )
391+
392+ agentID = uuid .New ()
393+ resourceID = uuid .New ()
394+ jobID = uuid .New ()
395+ buildID = uuid .New ()
396+ workspaceID = uuid .New ()
397+ devcontainerID = uuid .NewString ()
398+
399+ r = chi .NewMux ()
400+
401+ api = API {
402+ ctx :ctx ,
403+ Options :& Options {
404+ AgentInactiveDisconnectTimeout :testutil .WaitShort ,
405+ Database :mDB ,
406+ Logger :logger ,
407+ DeploymentValues :& codersdk.DeploymentValues {},
408+ TailnetCoordinator :tailnettest .NewFakeCoordinator (),
409+ },
410+ }
411+ )
412+
413+ var tailnetCoordinator tailnet.Coordinator = mCoordinator
414+ api .TailnetCoordinator .Store (& tailnetCoordinator )
415+
416+ // Setup agent provider based on test case.
417+ if tc .agentConnected && tc .agentConnError == nil {
418+ mAgentConn := agentconnmock .NewMockAgentConn (mCtrl )
419+ mAgentConn .EXPECT ().DeleteDevcontainer (gomock .Any (),devcontainerID ).Return (tc .deleteError )
420+ api .agentProvider = fakeAgentProvider {
421+ agentConn :func (_ context.Context ,_ uuid.UUID ) (_ workspacesdk.AgentConn ,release func (),_ error ) {
422+ return mAgentConn ,func () {},nil
423+ },
424+ }
425+ }else if tc .agentConnError != nil {
426+ api .agentProvider = fakeAgentProvider {
427+ agentConn :func (_ context.Context ,_ uuid.UUID ) (_ workspacesdk.AgentConn ,release func (),_ error ) {
428+ return nil ,nil ,tc .agentConnError
429+ },
430+ }
431+ }
432+
433+ // Setup database mocks for ExtractWorkspaceAgentParam middleware.
434+ mDB .EXPECT ().GetWorkspaceAgentByID (gomock .Any (),agentID ).Return (database.WorkspaceAgent {
435+ ID :agentID ,
436+ ResourceID :resourceID ,
437+ LifecycleState :database .WorkspaceAgentLifecycleStateReady ,
438+ FirstConnectedAt : sql.NullTime {Valid :tc .agentConnected ,Time :dbtime .Now ()},
439+ LastConnectedAt : sql.NullTime {Valid :tc .agentConnected ,Time :dbtime .Now ()},
440+ },nil )
441+ mDB .EXPECT ().GetWorkspaceResourceByID (gomock .Any (),resourceID ).Return (database.WorkspaceResource {
442+ ID :resourceID ,
443+ JobID :jobID ,
444+ },nil )
445+ mDB .EXPECT ().GetProvisionerJobByID (gomock .Any (),jobID ).Return (database.ProvisionerJob {
446+ ID :jobID ,
447+ Type :database .ProvisionerJobTypeWorkspaceBuild ,
448+ },nil )
449+ mDB .EXPECT ().GetWorkspaceBuildByJobID (gomock .Any (),jobID ).Return (database.WorkspaceBuild {
450+ WorkspaceID :workspaceID ,
451+ ID :buildID ,
452+ },nil )
453+
454+ // Allow db2sdk.WorkspaceAgent to complete.
455+ mCoordinator .EXPECT ().Node (gomock .Any ()).Return (nil )
456+
457+ // Mount the HTTP handler and create the test server.
458+ r .With (httpmw .ExtractWorkspaceAgentParam (mDB )).
459+ Delete ("/workspaceagents/{workspaceagent}/containers/devcontainers/{devcontainer}" ,api .workspaceAgentDeleteDevcontainer )
460+
461+ srv := httptest .NewServer (r )
462+ defer srv .Close ()
463+
464+ // Send the DELETE request using the test server's client.
465+ req ,err := http .NewRequestWithContext (ctx ,http .MethodDelete ,
466+ fmt .Sprintf ("%s/workspaceagents/%s/containers/devcontainers/%s" ,srv .URL ,agentID ,devcontainerID ),nil )
467+ require .NoError (t ,err )
468+
469+ resp ,err := srv .Client ().Do (req )
470+ require .NoError (t ,err )
471+ defer resp .Body .Close ()
472+
473+ require .Equal (t ,tc .expectedStatusCode ,resp .StatusCode )
474+ })
475+ }
476+ }