88from sqlalchemy import select
99from sqlalchemy .ext .asyncio import AsyncSession
1010
11+ from dstack ._internal .core .models .fleets import FleetStatus
12+ from dstack ._internal .core .models .runs import RunStatus
1113from dstack ._internal .core .models .users import GlobalRole , ProjectRole
1214from dstack ._internal .server .models import MemberModel , ProjectModel
1315from dstack ._internal .server .services .permissions import DefaultPermissions
1416from dstack ._internal .server .services .projects import add_project_member
1517from dstack ._internal .server .testing .common import (
1618 create_backend ,
19+ create_fleet ,
1720 create_project ,
21+ create_repo ,
22+ create_run ,
1823 create_user ,
24+ create_volume ,
1925 default_permissions_context ,
2026 get_auth_headers ,
2127)
@@ -484,6 +490,19 @@ async def test_deletes_projects(self, test_db, session: AsyncSession, client: As
484490 assert project1 .deleted
485491 assert not project2 .deleted
486492
493+ @pytest .mark .asyncio
494+ @pytest .mark .parametrize ("test_db" , ["sqlite" , "postgres" ], indirect = True )
495+ async def test_returns_400_if_project_does_not_exist (
496+ self , test_db , session : AsyncSession , client : AsyncClient
497+ ):
498+ user = await create_user (session = session , global_role = GlobalRole .ADMIN )
499+ response = await client .post (
500+ "/api/projects/delete" ,
501+ headers = get_auth_headers (user .token ),
502+ json = {"projects_names" : ["random_project" ]},
503+ )
504+ assert response .status_code == 400
505+
487506 @pytest .mark .asyncio
488507 @pytest .mark .parametrize ("test_db" , ["sqlite" , "postgres" ], indirect = True )
489508 async def test_returns_403_if_not_project_admin (
@@ -505,7 +524,7 @@ async def test_returns_403_if_not_project_admin(
505524 json = {"projects_names" : [project1 .name , project2 .name ]},
506525 )
507526 assert response .status_code == 403
508- res = await session .execute (select (ProjectModel ))
527+ res = await session .execute (select (ProjectModel ). where ( ProjectModel . deleted . is_ ( False )) )
509528 assert len (res .all ()) == 2
510529
511530 @pytest .mark .asyncio
@@ -521,8 +540,105 @@ async def test_returns_403_if_not_project_member(
521540 json = {"projects_names" : [project .name ]},
522541 )
523542 assert response .status_code == 403
524- res = await session .execute (select (ProjectModel ))
543+ res = await session .execute (select (ProjectModel ).where (ProjectModel .deleted .is_ (False )))
544+ assert len (res .all ()) == 1
545+
546+ @pytest .mark .asyncio
547+ @pytest .mark .parametrize ("test_db" , ["sqlite" , "postgres" ], indirect = True )
548+ async def test_errors_if_project_has_active_runs (
549+ self , test_db , session : AsyncSession , client : AsyncClient
550+ ):
551+ user = await create_user (session = session , global_role = GlobalRole .ADMIN )
552+ project = await create_project (session = session , name = "project" )
553+ repo = await create_repo (session = session , project_id = project .id )
554+ run = await create_run (
555+ session = session ,
556+ project = project ,
557+ repo = repo ,
558+ user = user ,
559+ status = RunStatus .SUBMITTED ,
560+ )
561+ response = await client .post (
562+ "/api/projects/delete" ,
563+ headers = get_auth_headers (user .token ),
564+ json = {"projects_names" : [project .name ]},
565+ )
566+ assert response .status_code == 400
567+ res = await session .execute (select (ProjectModel ).where (ProjectModel .deleted .is_ (False )))
568+ assert len (res .all ()) == 1
569+ run .status = RunStatus .TERMINATED
570+ await session .commit ()
571+ response = await client .post (
572+ "/api/projects/delete" ,
573+ headers = get_auth_headers (user .token ),
574+ json = {"projects_names" : [project .name ]},
575+ )
576+ assert response .status_code == 200
577+ res = await session .execute (select (ProjectModel ).where (ProjectModel .deleted .is_ (False )))
578+ assert len (res .all ()) == 0
579+
580+ @pytest .mark .asyncio
581+ @pytest .mark .parametrize ("test_db" , ["sqlite" , "postgres" ], indirect = True )
582+ async def test_errors_if_project_has_active_fleets (
583+ self , test_db , session : AsyncSession , client : AsyncClient
584+ ):
585+ user = await create_user (session = session , global_role = GlobalRole .ADMIN )
586+ project = await create_project (session = session , name = "project" )
587+ fleet = await create_fleet (
588+ session = session ,
589+ project = project ,
590+ deleted = False ,
591+ )
592+ response = await client .post (
593+ "/api/projects/delete" ,
594+ headers = get_auth_headers (user .token ),
595+ json = {"projects_names" : [project .name ]},
596+ )
597+ assert response .status_code == 400
598+ res = await session .execute (select (ProjectModel ).where (ProjectModel .deleted .is_ (False )))
525599 assert len (res .all ()) == 1
600+ fleet .status = FleetStatus .TERMINATED
601+ fleet .deleted = True
602+ await session .commit ()
603+ response = await client .post (
604+ "/api/projects/delete" ,
605+ headers = get_auth_headers (user .token ),
606+ json = {"projects_names" : [project .name ]},
607+ )
608+ assert response .status_code == 200
609+ res = await session .execute (select (ProjectModel ).where (ProjectModel .deleted .is_ (False )))
610+ assert len (res .all ()) == 0
611+
612+ @pytest .mark .asyncio
613+ @pytest .mark .parametrize ("test_db" , ["sqlite" , "postgres" ], indirect = True )
614+ async def test_errors_if_project_has_active_volumes (
615+ self , test_db , session : AsyncSession , client : AsyncClient
616+ ):
617+ user = await create_user (session = session , global_role = GlobalRole .ADMIN )
618+ project = await create_project (session = session , name = "project" )
619+ volume = await create_volume (
620+ session = session ,
621+ project = project ,
622+ user = user ,
623+ )
624+ response = await client .post (
625+ "/api/projects/delete" ,
626+ headers = get_auth_headers (user .token ),
627+ json = {"projects_names" : [project .name ]},
628+ )
629+ assert response .status_code == 400
630+ res = await session .execute (select (ProjectModel ).where (ProjectModel .deleted .is_ (False )))
631+ assert len (res .all ()) == 1
632+ volume .deleted = True
633+ await session .commit ()
634+ response = await client .post (
635+ "/api/projects/delete" ,
636+ headers = get_auth_headers (user .token ),
637+ json = {"projects_names" : [project .name ]},
638+ )
639+ assert response .status_code == 200
640+ res = await session .execute (select (ProjectModel ).where (ProjectModel .deleted .is_ (False )))
641+ assert len (res .all ()) == 0
526642
527643
528644class TestGetProject :
0 commit comments