@@ -1840,6 +1840,68 @@ func TestPostWorkspaceBuild(t *testing.T) {
18401840require .NoError (t ,err )
18411841require .Equal (t ,codersdk .BuildReasonDashboard ,build .Reason )
18421842})
1843+ t .Run ("DeletedWorkspace" ,func (t * testing.T ) {
1844+ t .Parallel ()
1845+
1846+ // Given: a workspace that has already been deleted
1847+ var (
1848+ ctx = testutil .Context (t ,testutil .WaitShort )
1849+ logger = slogtest .Make (t ,& slogtest.Options {}).Leveled (slog .LevelError )
1850+ adminClient ,db = coderdtest .NewWithDatabase (t ,& coderdtest.Options {
1851+ Logger :& logger ,
1852+ })
1853+ admin = coderdtest .CreateFirstUser (t ,adminClient )
1854+ workspaceOwnerClient ,member1 = coderdtest .CreateAnotherUser (t ,adminClient ,admin .OrganizationID )
1855+ otherMemberClient ,_ = coderdtest .CreateAnotherUser (t ,adminClient ,admin .OrganizationID )
1856+ ws = dbfake .WorkspaceBuild (t ,db , database.WorkspaceTable {OwnerID :member1 .ID ,OrganizationID :admin .OrganizationID }).
1857+ Seed (database.WorkspaceBuild {Transition :database .WorkspaceTransitionDelete }).
1858+ Do ()
1859+ )
1860+
1861+ // This needs to be done separately as provisionerd handles marking the workspace as deleted
1862+ // and we're skipping provisionerd here for speed.
1863+ require .NoError (t ,db .UpdateWorkspaceDeletedByID (dbauthz .AsProvisionerd (ctx ), database.UpdateWorkspaceDeletedByIDParams {
1864+ ID :ws .Workspace .ID ,
1865+ Deleted :true ,
1866+ }))
1867+
1868+ // Assert test invariant: Workspace should be deleted
1869+ dbWs ,err := db .GetWorkspaceByID (dbauthz .AsProvisionerd (ctx ),ws .Workspace .ID )
1870+ require .NoError (t ,err )
1871+ require .True (t ,dbWs .Deleted ,"workspace should be deleted" )
1872+
1873+ for _ ,tc := range []struct {
1874+ user * codersdk.Client
1875+ tr codersdk.WorkspaceTransition
1876+ expectStatus int
1877+ }{
1878+ // You should not be allowed to mess with a workspace you don't own, regardless of its deleted state.
1879+ {otherMemberClient ,codersdk .WorkspaceTransitionStart ,http .StatusNotFound },
1880+ {otherMemberClient ,codersdk .WorkspaceTransitionStop ,http .StatusNotFound },
1881+ {otherMemberClient ,codersdk .WorkspaceTransitionDelete ,http .StatusNotFound },
1882+ // Starting or stopping a workspace is not allowed when it is deleted.
1883+ {workspaceOwnerClient ,codersdk .WorkspaceTransitionStart ,http .StatusConflict },
1884+ {workspaceOwnerClient ,codersdk .WorkspaceTransitionStop ,http .StatusConflict },
1885+ // We allow a delete just in case a retry is required. In most cases, this will be a no-op.
1886+ // Note: this is the last test case because it will change the state of the workspace.
1887+ {workspaceOwnerClient ,codersdk .WorkspaceTransitionDelete ,http .StatusOK },
1888+ } {
1889+ // When: we create a workspace build with the given transition
1890+ _ ,err = tc .user .CreateWorkspaceBuild (ctx ,ws .Workspace .ID , codersdk.CreateWorkspaceBuildRequest {
1891+ Transition :tc .tr ,
1892+ })
1893+
1894+ // Then: we allow ONLY a delete build for a deleted workspace.
1895+ if tc .expectStatus < http .StatusBadRequest {
1896+ require .NoError (t ,err ,"creating a %s build for a deleted workspace should not error" ,tc .tr )
1897+ }else {
1898+ var apiError * codersdk.Error
1899+ require .Error (t ,err ,"creating a %s build for a deleted workspace should return an error" ,tc .tr )
1900+ require .ErrorAs (t ,err ,& apiError )
1901+ require .Equal (t ,tc .expectStatus ,apiError .StatusCode ())
1902+ }
1903+ }
1904+ })
18431905}
18441906
18451907func TestWorkspaceBuildTimings (t * testing.T ) {