From 17202d9c56e67358144fd70c1f123e82b6cfb9e6 Mon Sep 17 00:00:00 2001 From: jbkyei1 Date: Thu, 9 Apr 2026 18:45:36 +0000 Subject: [PATCH 1/2] feat: Add stack_id validation to UserSignUp and ProfileUpdate models --- api/api_models/user.py | 14 ++++++++++++++ api/routes/stacks.py | 3 +-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/api/api_models/user.py b/api/api_models/user.py index a697a91..d00ccd8 100644 --- a/api/api_models/user.py +++ b/api/api_models/user.py @@ -98,6 +98,13 @@ def set_role_id(cls, role_id): return role_id return role_id or check_role.id + @field_validator("stack_id", mode="before", check_fields=True) + def validate_stack_id(cls, stack_id): + # Reject invalid stack_id values like -1, 0, or other non-positive integers + if stack_id is not None and stack_id <= 0: + return None + return stack_id + # --------------------------------------------------------------------------- # Hierarchy / tree schemas @@ -212,6 +219,13 @@ class ProfileUpdate(BaseModel): model_config = ConfigDict(from_attributes=True) + @field_validator("stack_id", mode="before", check_fields=True) + def validate_stack_id(cls, stack_id): + # Reject invalid stack_id values like -1, 0, or other non-positive integers + if stack_id is not None and stack_id <= 0: + return None + return stack_id + class ProfileResponse(ProfileUpdate): id: int = Field(...) diff --git a/api/routes/stacks.py b/api/routes/stacks.py index 2f51c23..4d35c47 100644 --- a/api/routes/stacks.py +++ b/api/routes/stacks.py @@ -16,8 +16,7 @@ def _service(db: Session) -> StackService: @stack_router.get("/", response_model=list[stack_schemas.Stacks]) -async def list_stacks(db: Session = Depends(get_db), page: int = 1, limit: int = 100, - current_user=Depends(user_accepted)): +async def list_stacks(db: Session = Depends(get_db), page: int = 1, limit: int = 100): return _service(db).list_stacks(page=page, limit=limit) From d15853255d868774661ed23f2525678b0dc99da8 Mon Sep 17 00:00:00 2001 From: jbkyei1 Date: Thu, 9 Apr 2026 19:31:30 +0000 Subject: [PATCH 2/2] feat: Implement stack_id validation in UserSignUp and ProfileUpdate models; add tests for invalid stack_id values --- api/api_models/user.py | 26 +++++++++----------------- api/routes/stacks.py | 18 +++++++++++++++++- services/skill_service.py | 6 +++++- test/test_auth.py | 38 ++++++++++++++++++++++++++++++++++++++ test/test_stacks.py | 19 +++++++++++++++++++ 5 files changed, 88 insertions(+), 19 deletions(-) diff --git a/api/api_models/user.py b/api/api_models/user.py index d00ccd8..63b96d2 100644 --- a/api/api_models/user.py +++ b/api/api_models/user.py @@ -2,8 +2,9 @@ from fastapi import Form, UploadFile from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator +from pydantic.types import conint from datetime import datetime -from typing import List, Optional, Union +from typing import Annotated, List, Optional, Union from api.api_models.tags import TagBase from utils.utils import RoleChoices from .stacks import Stacks @@ -11,6 +12,11 @@ import re +# Shared type for stack_id validation - must be positive integer or None +# This prevents invalid values like -1, 0, or negative integers from being accepted +PositiveStackId = Annotated[Optional[int], Field(None, gt=0)] + + class Role(BaseModel): id: int = Field(...) name: str = Field(...) @@ -61,7 +67,7 @@ class UserSignUp(BaseModel): password: str = Field(..., min_length=8) password_confirmation: str = Field(..., min_length=8) role_id: Optional[int] = Field(None) - stack_id: Optional[int] = Field(None) + stack_id: PositiveStackId = None # Must be positive integer or None; rejects -1, 0, etc. bio: Optional[str] = Field(None) phone_number: str = Field(...) years_of_experience: Optional[int] = Field(None) @@ -98,13 +104,6 @@ def set_role_id(cls, role_id): return role_id return role_id or check_role.id - @field_validator("stack_id", mode="before", check_fields=True) - def validate_stack_id(cls, stack_id): - # Reject invalid stack_id values like -1, 0, or other non-positive integers - if stack_id is not None and stack_id <= 0: - return None - return stack_id - # --------------------------------------------------------------------------- # Hierarchy / tree schemas @@ -215,17 +214,10 @@ class ProfileUpdate(BaseModel): linkedin_profile: Optional[str] = None portfolio_url: Optional[str] = None profile_pic_url: Optional[str] = None - stack_id: Optional[int] = Field(None) + stack_id: PositiveStackId = None # Must be positive integer or None; rejects -1, 0, etc. model_config = ConfigDict(from_attributes=True) - @field_validator("stack_id", mode="before", check_fields=True) - def validate_stack_id(cls, stack_id): - # Reject invalid stack_id values like -1, 0, or other non-positive integers - if stack_id is not None and stack_id <= 0: - return None - return stack_id - class ProfileResponse(ProfileUpdate): id: int = Field(...) diff --git a/api/routes/stacks.py b/api/routes/stacks.py index 4d35c47..52566d1 100644 --- a/api/routes/stacks.py +++ b/api/routes/stacks.py @@ -1,3 +1,4 @@ +from typing import Optional from fastapi import APIRouter, Depends, Response, status from sqlalchemy.orm import Session @@ -16,7 +17,22 @@ def _service(db: Session) -> StackService: @stack_router.get("/", response_model=list[stack_schemas.Stacks]) -async def list_stacks(db: Session = Depends(get_db), page: int = 1, limit: int = 100): +async def list_stacks( + db: Session = Depends(get_db), + page: int = 1, + limit: int = 100 +): + """ + List all stacks. + + This endpoint is intentionally public (no authentication required) because: + - The signup page needs to fetch available stacks for users to select during registration + - Users signing up are not yet authenticated, creating a chicken-and-egg problem + - Stack data is non-sensitive organizational metadata (e.g., "Frontend", "Backend", "Mobile") + - No user-specific or confidential information is exposed + + Security consideration: Only basic stack info (id, name) is returned, no sensitive data. + """ return _service(db).list_stacks(page=page, limit=limit) diff --git a/services/skill_service.py b/services/skill_service.py index a69ee42..570bcef 100644 --- a/services/skill_service.py +++ b/services/skill_service.py @@ -73,12 +73,16 @@ def populate_skills(self) -> dict: try: last_skill = None for skill_name in skills_data: - last_skill = self.skill_repo.upsert(skill_name, get_skills_image(skill_name)) + # get_skills_image returns None if CairoSVG is unavailable or image fetch fails + # This is acceptable - skills can exist without images + image_url = get_skills_image(skill_name) + last_skill = self.skill_repo.upsert(skill_name, image_url) self.skill_repo.db.commit() if last_skill: self.skill_repo.db.refresh(last_skill) except Exception as e: self.skill_repo.db.rollback() + logger.error(f"Error populating skills: {e}", exc_info=True) raise HTTPException(status_code=500, detail=f"Error populating skills: {e}") return {"message": "Skills table populated successfully!"} diff --git a/test/test_auth.py b/test/test_auth.py index 9e10a67..78acfb1 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -46,6 +46,44 @@ def test_user_signup_valid(client): assert res.status_code == 201 +def test_user_signup_with_invalid_stack_id_negative_one(client): + """Test that stack_id=-1 is rejected with 422 validation error""" + payload = { + **user_signup_payload, + "stack_id": -1, + "username": "testuser1", + "email": "test1@slightlytechie.com" + } + res = client.post("/api/v1/users/register", json=payload) + assert res.status_code == 422 + assert "stack_id" in res.json()["detail"][0]["loc"] + + +def test_user_signup_with_invalid_stack_id_zero(client): + """Test that stack_id=0 is rejected with 422 validation error""" + payload = { + **user_signup_payload, + "stack_id": 0, + "username": "testuser2", + "email": "test2@slightlytechie.com" + } + res = client.post("/api/v1/users/register", json=payload) + assert res.status_code == 422 + assert "stack_id" in res.json()["detail"][0]["loc"] + + +def test_user_signup_with_null_stack_id(client): + """Test that stack_id=null is accepted (user has no stack)""" + payload = { + **user_signup_payload, + "stack_id": None, + "username": "testuser3", + "email": "test3@slightlytechie.com" + } + res = client.post("/api/v1/users/register", json=payload) + assert res.status_code == 201 + + def test_user_signup_invalid(): res = client.post("/api/v1/users/register", json=user_signup_payload_incomplete) assert res.status_code == 422 diff --git a/test/test_stacks.py b/test/test_stacks.py index a0ff1f5..aa4d319 100644 --- a/test/test_stacks.py +++ b/test/test_stacks.py @@ -36,6 +36,25 @@ def test_list_stacks(session, client, user_cred, stack_factory): assert len(res_data) == 2 +def test_list_stacks_unauthenticated(session, client, stack_factory): + """Test that unauthenticated users can list stacks (for signup page)""" + url = app.url_path_for("list_stacks") + + # list of stack names + stack_names = ["backend", "frontend", "mobile"] + for stack in stack_names: + stack_factory(stack) + + # Make request without authentication headers + res = client.get(url) + res_data = res.json() + + assert res.status_code == status.HTTP_200_OK + assert len(res_data) == 3 + assert all('name' in stack for stack in res_data) + assert all('id' in stack for stack in res_data) + + def test_create_stack(session, client, user_cred): url = app.url_path_for("create_stack")