diff --git a/backend/app/alembic/versions/60b6c511a485_add_project_id_in_api_key_table.py b/backend/app/alembic/versions/60b6c511a485_add_project_id_in_api_key_table.py new file mode 100644 index 000000000..2c172191d --- /dev/null +++ b/backend/app/alembic/versions/60b6c511a485_add_project_id_in_api_key_table.py @@ -0,0 +1,54 @@ +"""Add project_id in api_key table + +Revision ID: 60b6c511a485 +Revises: 904ed70e7dab +Create Date: 2025-06-10 13:22:59.947971 + +""" +from alembic import op +import sqlalchemy as sa +from sqlmodel import text + + +# revision identifiers, used by Alembic. +revision = "60b6c511a485" +down_revision = "904ed70e7dab" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("apikey", sa.Column("project_id", sa.Integer(), nullable=True)) + op.create_foreign_key( + None, "apikey", "project", ["project_id"], ["id"], ondelete="CASCADE" + ) + + # 2. Bind engine to run raw SQL + conn = op.get_bind() + + # 3. Populate project_id based on default logic (e.g., min project id per org) + conn.execute( + text( + """ + UPDATE apikey + SET project_id = ( + SELECT id FROM project + WHERE project.organization_id = apikey.organization_id + ORDER BY id ASC + LIMIT 1 + ) + """ + ) + ) + + # 4. Alter column to be NOT NULL now that it’s populated + op.alter_column("apikey", "project_id", nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "apikey", type_="foreignkey") + op.drop_column("apikey", "project_id") + # ### end Alembic commands ### diff --git a/backend/app/api/routes/api_keys.py b/backend/app/api/routes/api_keys.py index 267a081b6..8d6905908 100644 --- a/backend/app/api/routes/api_keys.py +++ b/backend/app/api/routes/api_keys.py @@ -5,11 +5,11 @@ from app.crud.api_key import ( create_api_key, get_api_key, - get_api_keys_by_organization, delete_api_key, - get_api_key_by_user_org, + get_api_keys_by_project, + get_api_key_by_project_user, ) -from app.crud.organization import validate_organization +from app.crud.project import validate_project from app.models import APIKeyPublic, User from app.utils import APIResponse @@ -19,7 +19,7 @@ # Create API Key @router.post("/", response_model=APIResponse[APIKeyPublic]) def create_key( - organization_id: int, + project_id: int, user_id: uuid.UUID, session: Session = Depends(get_db), current_user: User = Depends(get_current_active_superuser), @@ -29,18 +29,21 @@ def create_key( """ try: # Validate organization - validate_organization(session, organization_id) + project = validate_project(session, project_id) - existing_api_key = get_api_key_by_user_org(session, organization_id, user_id) + existing_api_key = get_api_key_by_project_user(session, project_id, user_id) if existing_api_key: raise HTTPException( status_code=400, - detail="API Key already exists for this user and organization", + detail="API Key already exists for this user and project.", ) # Create and return API key api_key = create_api_key( - session, organization_id=organization_id, user_id=user_id + session, + organization_id=project.organization_id, + user_id=user_id, + project_id=project_id, ) return APIResponse.success_response(api_key) @@ -51,19 +54,28 @@ def create_key( # List API Keys @router.get("/", response_model=APIResponse[list[APIKeyPublic]]) def list_keys( - organization_id: int, + project_id: int, session: Session = Depends(get_db), current_user: User = Depends(get_current_active_superuser), ): """ - Retrieve all API keys for the user's organization. + Retrieve all API keys for the given project. Superusers get all keys; + regular users get only their own. """ try: - # Validate organization - validate_organization(session, organization_id) + # Validate project + project = validate_project(session=session, project_id=project_id) + + if current_user.is_superuser: + # Superuser: fetch all API keys for the project + api_keys = get_api_keys_by_project(session=session, project_id=project_id) + else: + # Regular user: fetch only their own API key + user_api_key = get_api_key_by_project_user( + session=session, project_id=project_id, user_id=current_user.id + ) + api_keys = [user_api_key] if user_api_key else [] - # Retrieve API keys - api_keys = get_api_keys_by_organization(session, organization_id) return APIResponse.success_response(api_keys) except ValueError as e: diff --git a/backend/app/api/routes/onboarding.py b/backend/app/api/routes/onboarding.py index 0adcfd73b..fa8559d55 100644 --- a/backend/app/api/routes/onboarding.py +++ b/backend/app/api/routes/onboarding.py @@ -10,21 +10,17 @@ create_project, create_user, create_api_key, - get_api_key_by_user_org, + get_api_key_by_project_user, ) from app.models import ( OrganizationCreate, ProjectCreate, UserCreate, - APIKeyPublic, - Organization, Project, User, APIKey, ) -from app.core.security import get_password_hash from app.api.deps import ( - CurrentUser, SessionDep, get_current_active_superuser, ) @@ -90,18 +86,21 @@ def onboard_user(request: OnboardingRequest, session: SessionDep): ) user = create_user(session=session, user_create=user_create) - existing_key = get_api_key_by_user_org( - db=session, organization_id=organization.id, user_id=user.id + existing_key = get_api_key_by_project_user( + session=session, user_id=user.id, project_id=project.id ) if existing_key: raise HTTPException( status_code=400, - detail="API key already exists for this user and organization", + detail="API key already exists for this user and project.", ) api_key_public = create_api_key( - session=session, organization_id=organization.id, user_id=user.id + session=session, + organization_id=organization.id, + user_id=user.id, + project_id=project.id, ) user.is_superuser = False diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py index 748aecc24..a20e8f4aa 100644 --- a/backend/app/crud/__init__.py +++ b/backend/app/crud/__init__.py @@ -24,9 +24,9 @@ from .api_key import ( create_api_key, get_api_key, - get_api_key_by_user_org, get_api_key_by_value, - get_api_keys_by_organization, + get_api_keys_by_project, + get_api_key_by_project_user, delete_api_key, ) diff --git a/backend/app/crud/api_key.py b/backend/app/crud/api_key.py index 10a89885c..5a4f3a047 100644 --- a/backend/app/crud/api_key.py +++ b/backend/app/crud/api_key.py @@ -1,19 +1,13 @@ import uuid import secrets -from datetime import datetime, timezone from sqlmodel import Session, select from app.core.security import ( - verify_password, get_password_hash, encrypt_api_key, decrypt_api_key, ) -import base64 -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from app.core import settings from app.core.util import now - from app.models.api_key import APIKey, APIKeyPublic @@ -25,7 +19,7 @@ def generate_api_key() -> tuple[str, str]: def create_api_key( - session: Session, organization_id: uuid.UUID, user_id: uuid.UUID + session: Session, organization_id: int, user_id: uuid.UUID, project_id: int ) -> APIKeyPublic: """ Generates a new API key for an organization and associates it with a user. @@ -42,6 +36,7 @@ def create_api_key( key=encrypted_key, # Store the encrypted raw key organization_id=organization_id, user_id=user_id, + project_id=project_id, ) session.add(api_key) @@ -75,32 +70,6 @@ def get_api_key(session: Session, api_key_id: int) -> APIKeyPublic | None: return None -def get_api_keys_by_organization( - session: Session, organization_id: uuid.UUID -) -> list[APIKeyPublic]: - """ - Retrieves all active API keys associated with an organization. - Returns the API keys in their original format. - """ - api_keys = session.exec( - select(APIKey).where( - APIKey.organization_id == organization_id, APIKey.is_deleted == False - ) - ).all() - - raw_keys = [] - for api_key in api_keys: - api_key_dict = api_key.model_dump() - - decrypted_key = decrypt_api_key(api_key.key) - - api_key_dict["key"] = decrypted_key - - raw_keys.append(APIKeyPublic.model_validate(api_key_dict)) - - return raw_keys - - def delete_api_key(session: Session, api_key_id: int) -> None: """ Soft deletes (revokes) an API key by marking it as deleted. @@ -137,23 +106,40 @@ def get_api_key_by_value(session: Session, api_key_value: str) -> APIKeyPublic | return None -def get_api_key_by_user_org( - db: Session, organization_id: int, user_id: int -) -> APIKey | None: - """Get an API key by user and organization ID.""" +def get_api_key_by_project_user( + session: Session, project_id: int, user_id: uuid.UUID +) -> APIKeyPublic | None: + """ + Retrieves the single API key associated with a project. + """ statement = select(APIKey).where( - APIKey.organization_id == organization_id, APIKey.user_id == user_id, + APIKey.project_id == project_id, APIKey.is_deleted == False, ) - api_key = db.exec(statement).first() + api_key = session.exec(statement).first() if api_key: api_key_dict = api_key.model_dump() + api_key_dict["key"] = decrypt_api_key(api_key.key) + return APIKeyPublic.model_validate(api_key_dict) - decrypted_key = decrypt_api_key(api_key.key) + return None - api_key_dict["key"] = decrypted_key - return APIKey.model_validate(api_key_dict) - return None +def get_api_keys_by_project(session: Session, project_id: int) -> list[APIKeyPublic]: + """ + Retrieves all API keys associated with a project. + """ + statement = select(APIKey).where( + APIKey.project_id == project_id, APIKey.is_deleted == False + ) + api_keys = session.exec(statement).all() + + result = [] + for key in api_keys: + key_dict = key.model_dump() + key_dict["key"] = decrypt_api_key(key.key) + result.append(APIKeyPublic.model_validate(key_dict)) + + return result diff --git a/backend/app/crud/project.py b/backend/app/crud/project.py index c77f8e567..983af5655 100644 --- a/backend/app/crud/project.py +++ b/backend/app/crud/project.py @@ -2,7 +2,7 @@ from datetime import datetime, timezone from sqlmodel import Session, select -from app.models import Project, ProjectCreate +from app.models import Project, ProjectCreate, Organization from app.core.util import now @@ -24,3 +24,17 @@ def get_project_by_id(*, session: Session, project_id: int) -> Optional[Project] def get_projects_by_organization(*, session: Session, org_id: int) -> List[Project]: statement = select(Project).where(Project.organization_id == org_id) return session.exec(statement).all() + + +def validate_project(session: Session, project_id: int) -> Project: + """ + Ensures that an project exists and is active. + """ + project = get_project_by_id(session=session, project_id=project_id) + if not project: + raise ValueError("Project not found") + + if not project.is_active: + raise ValueError("Project is not active") + + return project diff --git a/backend/app/models/api_key.py b/backend/app/models/api_key.py index 357836dac..188f529b3 100644 --- a/backend/app/models/api_key.py +++ b/backend/app/models/api_key.py @@ -11,6 +11,9 @@ class APIKeyBase(SQLModel): organization_id: int = Field( foreign_key="organization.id", nullable=False, ondelete="CASCADE" ) + project_id: int = Field( + foreign_key="project.id", nullable=False, ondelete="CASCADE" + ) user_id: uuid.UUID = Field( foreign_key="user.id", nullable=False, ondelete="CASCADE" ) @@ -33,4 +36,5 @@ class APIKey(APIKeyBase, table=True): # Relationships organization: "Organization" = Relationship(back_populates="api_keys") + project: "Project" = Relationship(back_populates="api_keys") user: "User" = Relationship(back_populates="api_keys") diff --git a/backend/app/models/project.py b/backend/app/models/project.py index 0d6bddc0b..0d95ab3c4 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -37,6 +37,9 @@ class Project(ProjectBase, table=True): creds: list["Credential"] = Relationship( back_populates="project", sa_relationship_kwargs={"cascade": "all, delete"} ) + api_keys: list["APIKey"] = Relationship( + back_populates="project", sa_relationship_kwargs={"cascade": "all, delete"} + ) organization: Optional["Organization"] = Relationship(back_populates="project") diff --git a/backend/app/seed_data/seed_data.json b/backend/app/seed_data/seed_data.json index 93f344ab5..2be2ca23a 100644 --- a/backend/app/seed_data/seed_data.json +++ b/backend/app/seed_data/seed_data.json @@ -39,6 +39,7 @@ { "organization_name": "Project Tech4dev", "user_email": "superuser@example.com", + "project_name": "Glific", "api_key": "ApiKey No3x47A5qoIGhm0kVKjQ77dhCqEdWRIQZlEPzzzh7i8", "is_deleted": false, "deleted_at": null @@ -46,6 +47,7 @@ { "organization_name": "Project Tech4dev", "user_email": "org_admin@example.com", + "project_name": "Dalgo", "api_key": "ApiKey Px8y47B6roJHin1lWLkR88eiDrFdXSJRZmFQazzai8j9", "is_deleted": false, "deleted_at": null diff --git a/backend/app/seed_data/seed_data.py b/backend/app/seed_data/seed_data.py index 72ce8121a..e42aadbda 100644 --- a/backend/app/seed_data/seed_data.py +++ b/backend/app/seed_data/seed_data.py @@ -35,6 +35,7 @@ class UserData(BaseModel): class APIKeyData(BaseModel): organization_name: str + project_name: str user_email: EmailStr api_key: str is_deleted: bool @@ -135,6 +136,11 @@ def create_api_key(session: Session, api_key_data_raw: dict) -> APIKey: raise ValueError( f"Organization '{api_key_data.organization_name}' not found" ) + project = session.exec( + select(Project).where(Project.name == api_key_data.project_name) + ).first() + if not project: + raise ValueError(f"Project '{api_key_data.project_name}' not found") # Query user ID by email user = session.exec( select(User).where(User.email == api_key_data.user_email) @@ -144,6 +150,7 @@ def create_api_key(session: Session, api_key_data_raw: dict) -> APIKey: encrypted_api_key = encrypt_api_key(api_key_data.api_key) api_key = APIKey( organization_id=organization.id, + project_id=project.id, user_id=user.id, key=encrypted_api_key, is_deleted=api_key_data.is_deleted, diff --git a/backend/app/tests/api/routes/test_api_key.py b/backend/app/tests/api/routes/test_api_key.py index 2e9b2b79e..3c1f49c25 100644 --- a/backend/app/tests/api/routes/test_api_key.py +++ b/backend/app/tests/api/routes/test_api_key.py @@ -3,9 +3,9 @@ from fastapi.testclient import TestClient from sqlmodel import Session from app.main import app -from app.models import APIKey, User, Organization +from app.models import APIKey, User, Organization, Project from app.core.config import settings -from app.crud import api_key as api_key_crud +from app.crud.api_key import create_api_key from app.tests.utils.utils import random_email from app.core.security import get_password_hash @@ -34,13 +34,22 @@ def create_test_organization(db: Session) -> Organization: return org +def create_test_project(db: Session, organization_id: int) -> Project: + project = Project(name="Test Project", organization_id=organization_id) + db.add(project) + db.commit() + db.refresh(project) + return project + + def test_create_api_key(db: Session, superuser_token_headers: dict[str, str]): user = create_test_user(db) org = create_test_organization(db) + project = create_test_project(db, organization_id=org.id) response = client.post( f"{settings.API_V1_STR}/apikeys", - params={"organization_id": org.id, "user_id": user.id}, + params={"project_id": project.id, "user_id": user.id}, headers=superuser_token_headers, ) assert response.status_code == 200 @@ -55,15 +64,16 @@ def test_create_api_key(db: Session, superuser_token_headers: dict[str, str]): def test_create_duplicate_api_key(db: Session, superuser_token_headers: dict[str, str]): user = create_test_user(db) org = create_test_organization(db) + project = create_test_project(db, organization_id=org.id) client.post( f"{settings.API_V1_STR}/apikeys", - params={"organization_id": org.id, "user_id": user.id}, + params={"project_id": project.id, "user_id": user.id}, headers=superuser_token_headers, ) response = client.post( f"{settings.API_V1_STR}/apikeys", - params={"organization_id": org.id, "user_id": user.id}, + params={"project_id": project.id, "user_id": user.id}, headers=superuser_token_headers, ) assert response.status_code == 400 @@ -73,11 +83,14 @@ def test_create_duplicate_api_key(db: Session, superuser_token_headers: dict[str def test_list_api_keys(db: Session, superuser_token_headers: dict[str, str]): user = create_test_user(db) org = create_test_organization(db) - api_key = api_key_crud.create_api_key(db, organization_id=org.id, user_id=user.id) + project = create_test_project(db, organization_id=org.id) + api_key = create_api_key( + db, organization_id=org.id, user_id=user.id, project_id=project.id + ) response = client.get( f"{settings.API_V1_STR}/apikeys", - params={"organization_id": org.id, "user_id": user.id}, + params={"project_id": project.id}, headers=superuser_token_headers, ) assert response.status_code == 200 @@ -85,18 +98,22 @@ def test_list_api_keys(db: Session, superuser_token_headers: dict[str, str]): assert data["success"] is True assert isinstance(data["data"], list) assert len(data["data"]) > 0 - assert data["data"][0]["organization_id"] == org.id - assert data["data"][0]["user_id"] == str(user.id) + + first_key = data["data"][0] + assert first_key["organization_id"] == org.id + assert first_key["user_id"] == str(user.id) def test_get_api_key(db: Session, superuser_token_headers: dict[str, str]): user = create_test_user(db) org = create_test_organization(db) - api_key = api_key_crud.create_api_key(db, organization_id=org.id, user_id=user.id) + project = create_test_project(db, organization_id=org.id) + api_key = create_api_key( + db, organization_id=org.id, user_id=user.id, project_id=project.id + ) response = client.get( f"{settings.API_V1_STR}/apikeys/{api_key.id}", - params={"organization_id": api_key.organization_id, "user_id": user.id}, headers=superuser_token_headers, ) assert response.status_code == 200 @@ -108,12 +125,8 @@ def test_get_api_key(db: Session, superuser_token_headers: dict[str, str]): def test_get_nonexistent_api_key(db: Session, superuser_token_headers: dict[str, str]): - user = create_test_user(db) - org = create_test_organization(db) - response = client.get( f"{settings.API_V1_STR}/apikeys/999999", - params={"organization_id": org.id, "user_id": user.id}, headers=superuser_token_headers, ) assert response.status_code == 404 @@ -123,11 +136,13 @@ def test_get_nonexistent_api_key(db: Session, superuser_token_headers: dict[str, def test_revoke_api_key(db: Session, superuser_token_headers: dict[str, str]): user = create_test_user(db) org = create_test_organization(db) - api_key = api_key_crud.create_api_key(db, organization_id=org.id, user_id=user.id) + project = create_test_project(db, organization_id=org.id) + api_key = create_api_key( + db, organization_id=org.id, user_id=user.id, project_id=project.id + ) response = client.delete( f"{settings.API_V1_STR}/apikeys/{api_key.id}", - params={"organization_id": api_key.organization_id, "user_id": user.id}, headers=superuser_token_headers, ) assert response.status_code == 200 @@ -144,7 +159,6 @@ def test_revoke_nonexistent_api_key( response = client.delete( f"{settings.API_V1_STR}/apikeys/999999", - params={"organization_id": org.id, "user_id": user.id}, headers=superuser_token_headers, ) assert response.status_code == 400 diff --git a/backend/app/tests/api/routes/test_onboarding.py b/backend/app/tests/api/routes/test_onboarding.py index f98b128c8..bf332a583 100644 --- a/backend/app/tests/api/routes/test_onboarding.py +++ b/backend/app/tests/api/routes/test_onboarding.py @@ -79,7 +79,7 @@ def test_create_user_existing_email( assert response.status_code == 400 assert ( response.json()["detail"] - == "400: API key already exists for this user and organization" + == "400: API key already exists for this user and project." ) @@ -88,7 +88,7 @@ def test_is_superuser_flag( ): data = { "organization_name": "TestOrg", - "project_name": "TestProject", + "project_name": "TestProjects", "email": random_email(), "password": "testpassword123", "user_name": "Test User", diff --git a/backend/app/tests/crud/test_api_key.py b/backend/app/tests/crud/test_api_key.py index b6269b700..07d2819c7 100644 --- a/backend/app/tests/crud/test_api_key.py +++ b/backend/app/tests/crud/test_api_key.py @@ -2,7 +2,7 @@ import pytest from sqlmodel import Session, select from app.crud import api_key as api_key_crud -from app.models import APIKey, User, Organization +from app.models import APIKey, User, Organization, Project from app.tests.utils.utils import random_email from app.core.security import get_password_hash, verify_password, decrypt_api_key @@ -27,30 +27,45 @@ def create_test_organization(db: Session) -> Organization: return org +def create_test_project(db: Session, organization_id: int) -> Project: + project = Project( + name=f"Test Project {uuid.uuid4()}", + description="Test project", + organization_id=organization_id, + is_active=True, + ) + db.add(project) + db.commit() + db.refresh(project) + return project + + def test_create_api_key(db: Session) -> None: user = create_test_user(db) org = create_test_organization(db) + project = create_test_project(db, org.id) - api_key = api_key_crud.create_api_key(db, org.id, user.id) + api_key = api_key_crud.create_api_key(db, org.id, user.id, project.id) assert api_key.key.startswith("ApiKey ") assert len(api_key.key) > 32 assert api_key.organization_id == org.id assert api_key.user_id == user.id + assert api_key.project_id == project.id def test_get_api_key(db: Session) -> None: user = create_test_user(db) org = create_test_organization(db) + project = create_test_project(db, org.id) - created_key = api_key_crud.create_api_key(db, org.id, user.id) + created_key = api_key_crud.create_api_key(db, org.id, user.id, project.id) retrieved_key = api_key_crud.get_api_key(db, created_key.id) assert retrieved_key is not None assert retrieved_key.id == created_key.id - # The key should be in its original format assert retrieved_key.key.startswith("ApiKey ") - assert len(retrieved_key.key) > 32 + assert retrieved_key.project_id == project.id def test_get_api_key_not_found(db: Session) -> None: @@ -58,30 +73,12 @@ def test_get_api_key_not_found(db: Session) -> None: assert result is None -def test_get_api_keys_by_organization(db: Session) -> None: - user1 = create_test_user(db) - user2 = create_test_user(db) - org = create_test_organization(db) - - api_key1 = api_key_crud.create_api_key(db, org.id, user1.id) - api_key2 = api_key_crud.create_api_key(db, org.id, user2.id) - - api_keys = api_key_crud.get_api_keys_by_organization(db, org.id) - - assert len(api_keys) == 2 - # Verify that the keys are in their original format - for key in api_keys: - assert key.key.startswith("ApiKey ") - assert len(key.key) > 32 # Raw key should be longer than 32 characters - assert key.organization_id == org.id - assert key.user_id in [user1.id, user2.id] - - def test_delete_api_key(db: Session) -> None: user = create_test_user(db) org = create_test_organization(db) + project = create_test_project(db, org.id) - api_key = api_key_crud.create_api_key(db, org.id, user.id) + api_key = api_key_crud.create_api_key(db, org.id, user.id, project.id) api_key_crud.delete_api_key(db, api_key.id) deleted_key = db.exec(select(APIKey).where(APIKey.id == api_key.id)).first() @@ -94,8 +91,9 @@ def test_delete_api_key(db: Session) -> None: def test_delete_api_key_already_deleted(db: Session) -> None: user = create_test_user(db) org = create_test_organization(db) + project = create_test_project(db, org.id) - api_key = api_key_crud.create_api_key(db, org.id, user.id) + api_key = api_key_crud.create_api_key(db, org.id, user.id, project.id) api_key_crud.delete_api_key(db, api_key.id) with pytest.raises(ValueError, match="API key not found or already deleted"): @@ -105,9 +103,10 @@ def test_delete_api_key_already_deleted(db: Session) -> None: def test_get_api_key_by_value(db: Session) -> None: user = create_test_user(db) org = create_test_organization(db) + project = create_test_project(db, org.id) # Create an API key - api_key = api_key_crud.create_api_key(db, org.id, user.id) + api_key = api_key_crud.create_api_key(db, org.id, user.id, project.id) # Get the raw key that was returned during creation raw_key = api_key.key @@ -124,24 +123,32 @@ def test_get_api_key_by_value(db: Session) -> None: assert len(retrieved_key.key) > 32 -def test_get_api_key_by_user_org(db: Session) -> None: +def test_get_api_key_by_project_user(db: Session) -> None: user = create_test_user(db) org = create_test_organization(db) + project = create_test_project(db, org.id) - api_key = api_key_crud.create_api_key(db, org.id, user.id) - retrieved_key = api_key_crud.get_api_key_by_user_org(db, org.id, user.id) + created_key = api_key_crud.create_api_key(db, org.id, user.id, project.id) + retrieved_key = api_key_crud.get_api_key_by_project_user(db, project.id, user.id) assert retrieved_key is not None - assert retrieved_key.id == api_key.id - assert retrieved_key.organization_id == org.id - assert retrieved_key.user_id == user.id - # The key should be in its original format + assert retrieved_key.id == created_key.id + assert retrieved_key.project_id == project.id assert retrieved_key.key.startswith("ApiKey ") - assert len(retrieved_key.key) > 32 -def test_get_api_key_by_user_org_not_found(db: Session) -> None: +def test_get_api_keys_by_project(db: Session) -> None: + user = create_test_user(db) org = create_test_organization(db) - user_id = uuid.uuid4() - result = api_key_crud.get_api_key_by_user_org(db, org.id, user_id) - assert result is None + project = create_test_project(db, org.id) + + created_key = api_key_crud.create_api_key(db, org.id, user.id, project.id) + retrieved_keys = api_key_crud.get_api_keys_by_project(db, project.id) + + assert retrieved_keys is not None + assert len(retrieved_keys) == 1 + retrieved_key = retrieved_keys[0] + + assert retrieved_key.id == created_key.id + assert retrieved_key.project_id == project.id + assert retrieved_key.key.startswith("ApiKey ") diff --git a/backend/app/tests/crud/test_project.py b/backend/app/tests/crud/test_project.py index 53e19f12d..87d019e39 100644 --- a/backend/app/tests/crud/test_project.py +++ b/backend/app/tests/crud/test_project.py @@ -1,11 +1,12 @@ import pytest -from sqlmodel import SQLModel, Session, create_engine +from sqlmodel import Session from app.models import Project, ProjectCreate, Organization from app.crud.project import ( create_project, get_project_by_id, get_projects_by_organization, + validate_project, ) from app.tests.utils.utils import random_lower_string @@ -84,3 +85,51 @@ def test_get_non_existent_project(db: Session) -> None: """Test retrieving a non-existent project should return None.""" fetched_project = get_project_by_id(session=db, project_id=999) assert fetched_project is None + + +def test_validate_project_success(db: Session) -> None: + """Test that a valid and active project passes validation.""" + org = Organization(name=random_lower_string()) + db.add(org) + db.commit() + db.refresh(org) + + project = create_project( + session=db, + project_create=ProjectCreate( + name=random_lower_string(), + description="Valid project", + is_active=True, + organization_id=org.id, + ), + ) + + validated_project = validate_project(session=db, project_id=project.id) + assert validated_project.id == project.id + + +def test_validate_project_not_found(db: Session) -> None: + """Test that validation fails when project does not exist.""" + with pytest.raises(ValueError, match="Project not found"): + validate_project(session=db, project_id=9999) + + +def test_validate_project_inactive(db: Session) -> None: + """Test that validation fails when project is inactive.""" + org = Organization(name=random_lower_string()) + db.add(org) + db.commit() + db.refresh(org) + + inactive_project = create_project( + session=db, + project_create=ProjectCreate( + name=random_lower_string(), + description="Inactive project", + is_active=False, + organization_id=org.id, + ), + ) + + with pytest.raises(ValueError, match="Project is not active"): + validate_project(session=db, project_id=inactive_project.id)