Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/dstack/_internal/server/routers/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ async def list_only_no_fleets(
):
"""
Returns only projects where the user is a member and that have no active fleets,
sorted by ascending `created_at`.
neither owned nor imported, sorted by ascending `created_at`.

Active fleets are those with `deleted == False`. Projects with deleted fleets
(but no active fleets) are included.
Expand Down
12 changes: 10 additions & 2 deletions src/dstack/_internal/server/services/fleets.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ async def list_projects_with_no_active_fleets(
user: UserModel,
) -> List[Project]:
"""
Returns all projects where the user is a member that have no active fleets.
Returns all projects where the user is a member that have no active fleets,
neither owned nor imported.

Active fleets are those with `deleted == False`. Projects with only deleted fleets
(or no fleets) are included. Deleted projects are excluded.
Expand All @@ -178,7 +179,14 @@ async def list_projects_with_no_active_fleets(
.outerjoin(
active_fleet_alias,
and_(
active_fleet_alias.project_id == ProjectModel.id,
or_(
active_fleet_alias.project_id == ProjectModel.id,
exists().where(
ImportModel.project_id == ProjectModel.id,
ImportModel.export_id == ExportedFleetModel.export_id,
ExportedFleetModel.fleet_id == active_fleet_alias.id,
),
),
active_fleet_alias.deleted == False,
),
)
Expand Down
59 changes: 51 additions & 8 deletions src/tests/_internal/server/routers/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from dstack._internal.server.services.permissions import DefaultPermissions
from dstack._internal.server.services.projects import add_project_member
from dstack._internal.server.testing.common import (
create_export,
create_fleet,
create_project,
create_repo,
Expand All @@ -33,14 +34,6 @@ async def test_returns_40x_if_not_authenticated(self, test_db, client: AsyncClie
response = await client.post("/api/projects/list")
assert response.status_code in [401, 403]

@pytest.mark.asyncio
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
async def test_list_only_no_fleets_returns_40x_if_not_authenticated(
self, test_db, client: AsyncClient
):
response = await client.post("/api/projects/list_only_no_fleets")
assert response.status_code in [401, 403]

@pytest.mark.asyncio
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
async def test_returns_empty_list(self, test_db, session: AsyncSession, client: AsyncClient):
Expand Down Expand Up @@ -391,6 +384,14 @@ async def test_returns_total_count(self, test_db, session: AsyncSession, client:


class TestListOnlyNoFleets:
@pytest.mark.asyncio
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
async def test_list_only_no_fleets_returns_40x_if_not_authenticated(
self, test_db, client: AsyncClient
):
response = await client.post("/api/projects/list_only_no_fleets")
assert response.status_code in [401, 403]

@pytest.mark.asyncio
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
async def test_only_no_fleets_returns_projects_without_active_fleets(
Expand Down Expand Up @@ -556,6 +557,48 @@ async def test_only_no_fleets_empty_result(
projects = response.json()
assert len(projects) == 0

@pytest.mark.asyncio
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
async def test_only_no_fleets_not_includes_project_with_imported_fleets(
self, test_db, session: AsyncSession, client: AsyncClient
):
user = await create_user(session=session, global_role=GlobalRole.USER)
exporter_project = await create_project(
session=session, owner=user, name="exporter_project"
)
await add_project_member(
session=session, project=exporter_project, user=user, project_role=ProjectRole.USER
)
fleet = await create_fleet(session=session, project=exporter_project)
importer_project = await create_project(
session=session, owner=user, name="importer_project"
)
await add_project_member(
session=session, project=importer_project, user=user, project_role=ProjectRole.USER
)
await create_export(
session=session,
exporter_project=exporter_project,
importer_projects=[importer_project],
exported_fleets=[fleet],
)
project_no_fleets = await create_project(
session=session, owner=user, name="project_no_fleets"
)
await add_project_member(
session=session, project=project_no_fleets, user=user, project_role=ProjectRole.USER
)

response = await client.post(
"/api/projects/list_only_no_fleets",
headers=get_auth_headers(user.token),
)
assert response.status_code == 200
projects = response.json()

assert len(projects) == 1
assert projects[0]["project_name"] == "project_no_fleets"

@pytest.mark.asyncio
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
async def test_only_no_fleets_respects_user_permissions(
Expand Down
Loading