Skip to content
Merged
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
12 changes: 9 additions & 3 deletions api/api_models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@

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
from .technical_task import TechnicalTaskSubmissionResponse
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(...)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -208,7 +214,7 @@ 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)

Expand Down
19 changes: 17 additions & 2 deletions api/routes/stacks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Optional
from fastapi import APIRouter, Depends, Response, status
from sqlalchemy.orm import Session

Expand All @@ -16,8 +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,
current_user=Depends(user_accepted)):
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)


Expand Down
6 changes: 5 additions & 1 deletion services/skill_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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!"}

Expand Down
38 changes: 38 additions & 0 deletions test/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions test/test_stacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Loading