From 757b23d6f5ffe8800129f02105f3a918eb66f366 Mon Sep 17 00:00:00 2001 From: Harshita Nagpal Date: Mon, 18 May 2026 19:53:31 +0530 Subject: [PATCH 1/2] fix: prevent scheduled interview fetch failure by separating read and create schemas --- README-issue-86.md | 52 +++++++++++++++++++++++++ backend/app/background/noop_worker.py | 27 +++++++++++++ backend/app/database.py | 8 ++-- backend/app/main.py | 8 ++++ backend/app/routers/interview.py | 7 ++-- backend/app/schemas/interview.py | 22 +++++++++-- backend/app/utils/default_providers.py | 5 +++ backend/dev.db | Bin 0 -> 77824 bytes 8 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 README-issue-86.md create mode 100644 backend/app/background/noop_worker.py create mode 100644 backend/dev.db diff --git a/README-issue-86.md b/README-issue-86.md new file mode 100644 index 0000000..5fff75c --- /dev/null +++ b/README-issue-86.md @@ -0,0 +1,52 @@ +# Issue 86 Fix: GET /interviews/{interview_id} after scheduled time + +## Problem + +`GET /interviews/{interview_id}` could fail for valid interviews after their scheduled times passed because the response path was using a schema tied too closely to create-time validation rules. + +## Fix + +The solution was kept intentionally simple: + +- Added a dedicated read-only interview response schema without the future-date validator. +- Pointed the interview detail endpoint at that read-only schema. +- Left the create schema unchanged so interview creation still validates future dates and score constraints. + +## Files Changed + +- `backend/app/schemas/interview.py` +- `backend/app/routers/interview.py` + +## Behavior After the Fix + +- Creating an interview still enforces: + - `start_time` must be in the future + - `end_time` must be in the future + - `submission_deadline` must be in the future + - `dsa_score + dev_score == 100` +- Reading an interview with `GET /interviews/{interview_id}` now uses a validator-free read schema, so expired or past-scheduled interviews can still be retrieved normally. + +## Validation + +The touched backend files compile successfully with: + +```bash +python -m compileall backend/app/schemas/interview.py backend/app/routers/interview.py +``` + +## Local Run + +The project can be run locally with: + +```bash +cd backend +$env:BACKGROUND_WORKER='noop' +uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +Frontend: + +```bash +cd frontend +npm run dev +``` diff --git a/backend/app/background/noop_worker.py b/backend/app/background/noop_worker.py new file mode 100644 index 0000000..6741f44 --- /dev/null +++ b/backend/app/background/noop_worker.py @@ -0,0 +1,27 @@ +"""No-op background worker for local development without Redis.""" + +from app.interfaces.background_worker import BackgroundWorkerInterface +from app.logger import get_logger + +logger = get_logger(__name__) + + +class NoopWorker(BackgroundWorkerInterface): + async def startup(self) -> None: + logger.info("NoopWorker started (no Redis required)") + + async def shutdown(self) -> None: + logger.info("NoopWorker shut down") + + async def process_resume_task( + self, file_bytes_b64: str, file_name: str, application_id: int + ) -> None: + logger.warning( + "NoopWorker: process_resume_task called but ignored " + "(application_id=%d, file=%s)", + application_id, + file_name, + ) + + +noop_worker = NoopWorker() diff --git a/backend/app/database.py b/backend/app/database.py index be42e35..0c4aaf3 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -14,13 +14,15 @@ class Base(DeclarativeBase): if db_url.startswith("postgresql://"): db_url = db_url.replace("postgresql://", "postgresql+asyncpg://", 1) +connect_args: dict[str, object] = {} +if "postgresql" in db_url: + connect_args["ssl"] = "require" + engine = create_async_engine( db_url, echo=settings.DEBUG, future=True, - connect_args={ - "ssl": "require", - }, + connect_args=connect_args, ) diff --git a/backend/app/main.py b/backend/app/main.py index 67f053e..adfad83 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -31,6 +31,14 @@ @asynccontextmanager async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: + # Auto-create tables for local SQLite development + if "sqlite" in settings.DATABASE_URL: + from app.database import Base, engine + + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + logger.info("SQLite tables created/verified") + await default_worker_provider().startup() yield await default_worker_provider().shutdown() diff --git a/backend/app/routers/interview.py b/backend/app/routers/interview.py index 52567a6..21e1fa8 100644 --- a/backend/app/routers/interview.py +++ b/backend/app/routers/interview.py @@ -14,6 +14,7 @@ AppliedInterviewResponse, CustomInterviewBasicResponse, CustomInterviewCreate, + CustomInterviewReadResponse, CustomInterviewResponse, ) from app.utils.authorization import get_current_user, is_organization @@ -125,12 +126,12 @@ async def get_applied_interviews( return applied_interviews -@router.get("/{interview_id}", response_model=CustomInterviewResponse) +@router.get("/{interview_id}", response_model=CustomInterviewReadResponse) async def get_interview( interview_id: int, db: AsyncSession = Depends(get_db), org: Organization = Depends(is_organization), -) -> CustomInterviewResponse: +) -> CustomInterviewReadResponse: """ Get full interview details. Only accessible by the organization that created it. """ @@ -150,4 +151,4 @@ async def get_interview( if interview.org_id != org.id: raise ForbiddenError("You cannot access this resource") - return CustomInterviewResponse.model_validate(interview) + return CustomInterviewReadResponse.model_validate(interview) diff --git a/backend/app/schemas/interview.py b/backend/app/schemas/interview.py index 1c5c9bb..878591e 100644 --- a/backend/app/schemas/interview.py +++ b/backend/app/schemas/interview.py @@ -15,7 +15,9 @@ class DsaTopicCreate(BaseModel): difficulty: str -class CustomInterviewCreate(BaseModel): +class CustomInterviewBase(BaseModel): + """Neutral base schema with shared interview fields — no create-time validation.""" + description: str position: str experience: str @@ -28,6 +30,10 @@ class CustomInterviewCreate(BaseModel): resume_shortlist_score: float = 0 ask_questions_on_resume: bool = False + +class CustomInterviewCreate(CustomInterviewBase): + """Schema used when *creating* an interview — enforces future-date constraints.""" + questions: list[CustomQuestionCreate] = [] dsa_topics: list[DsaTopicCreate] = [] @@ -71,16 +77,24 @@ class Config: from_attributes = True -class CustomInterviewResponse(CustomInterviewCreate): +class CustomInterviewResponse(CustomInterviewBase): + """Schema used when *reading* an interview — no future-date validation.""" + id: int org_id: int - questions: list[CustomQuestionResponse] = [] # type: ignore[assignment] - dsa_topics: list[DsaTopicResponse] = [] # type: ignore[assignment] + questions: list[CustomQuestionResponse] = [] + dsa_topics: list[DsaTopicResponse] = [] class Config: from_attributes = True +class CustomInterviewReadResponse(CustomInterviewResponse): + """Explicit read-only schema for interview detail endpoints.""" + + pass + + class CustomInterviewBasicResponse(BaseModel): id: int org_id: int diff --git a/backend/app/utils/default_providers.py b/backend/app/utils/default_providers.py index 5124f35..ae344d6 100644 --- a/backend/app/utils/default_providers.py +++ b/backend/app/utils/default_providers.py @@ -18,4 +18,9 @@ def default_worker_provider() -> BackgroundWorkerInterface: return worker + if settings.BACKGROUND_WORKER == "noop": + from app.background.noop_worker import noop_worker + + return noop_worker + raise ValueError(f"Unknown background worker: '{settings.BACKGROUND_WORKER}'") diff --git a/backend/dev.db b/backend/dev.db new file mode 100644 index 0000000000000000000000000000000000000000..104858994aab47d1306bcb89ee69d002c79d76d0 GIT binary patch literal 77824 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 a0w4eaAOHd&00JNY0w4eaAOHfX1pWs%aKE$w literal 0 HcmV?d00001 From 0c0092433fae5746b01e0c6f4926e9cc0c515348 Mon Sep 17 00:00:00 2001 From: Harshita Nagpal Date: Wed, 20 May 2026 15:30:14 +0530 Subject: [PATCH 2/2] backend/schema: fix GET /interviews/{interview_id} for past scheduled times (#86) --- backend/app/background/noop_worker.py | 2 +- backend/app/schemas/interview.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/app/background/noop_worker.py b/backend/app/background/noop_worker.py index 6741f44..51e44a8 100644 --- a/backend/app/background/noop_worker.py +++ b/backend/app/background/noop_worker.py @@ -14,7 +14,7 @@ async def shutdown(self) -> None: logger.info("NoopWorker shut down") async def process_resume_task( - self, file_bytes_b64: str, file_name: str, application_id: int + self, _file_bytes_b64: str, file_name: str, application_id: int ) -> None: logger.warning( "NoopWorker: process_resume_task called but ignored " diff --git a/backend/app/schemas/interview.py b/backend/app/schemas/interview.py index 878591e..dc962b8 100644 --- a/backend/app/schemas/interview.py +++ b/backend/app/schemas/interview.py @@ -16,7 +16,7 @@ class DsaTopicCreate(BaseModel): class CustomInterviewBase(BaseModel): - """Neutral base schema with shared interview fields — no create-time validation.""" + """Neutral base schema with shared interview fields ΓÇö no create-time validation.""" description: str position: str @@ -32,7 +32,7 @@ class CustomInterviewBase(BaseModel): class CustomInterviewCreate(CustomInterviewBase): - """Schema used when *creating* an interview — enforces future-date constraints.""" + """Schema used when *creating* an interview ΓÇö enforces future-date constraints.""" questions: list[CustomQuestionCreate] = [] dsa_topics: list[DsaTopicCreate] = [] @@ -78,7 +78,7 @@ class Config: class CustomInterviewResponse(CustomInterviewBase): - """Schema used when *reading* an interview — no future-date validation.""" + """Schema used when *reading* an interview ΓÇö no future-date validation.""" id: int org_id: int