Coverage for server / validators / class_code_validator.py: 100%
7 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
1"""One bound for the `class_code` path parameter, shared by every route that takes it.
3WHY THIS MODULE EXISTS
4----------------------
5`class_code` is the join-a-class credential: `generate_unique_code()` mints it as a
6**6-character** alphanumeric string, and a student types it to enrol. EI-TC-984 already
7settled its shape — 4-12 alphanumeric characters — but the bound was applied to exactly
8ONE route (`GET /v1/teacher/class/{class_code}/fetch`). The other thirteen handlers
9declared a bare `class_code: str`, so an unbounded string of any length or content went
10straight into `db["class_collection"].find_one({"class_code": ...})`.
12Zephyr EI-T358 records the gap: "an excessively long value is rejected on the length
13limit". It was not, anywhere but that one route.
15WHAT THIS DELIBERATELY DOES NOT CHANGE
16--------------------------------------
17An **unknown** class code still answers 200 with an empty payload, NOT 404 — the same
18answer a student gets for a class they are simply not enrolled in. That uniformity is
19load-bearing: splitting it into 404-vs-200 would turn the endpoint into a class-code
20enumeration oracle, and the class code is the credential for joining a class. Only the
21malformed/over-length case changes here, and it is refused before any lookup runs.
23Developer: Allan Ninal
24Date: 2026-09-23
25"""
27from typing import Annotated, Optional
29from fastapi import Path, Query
31# EI-TC-984's constants, kept in one place rather than retyped at fourteen call sites.
32# `generate_unique_code()` mints 6 characters from uppercase + digits; the 4-12 window
33# leaves room for the shorter and mixed-case codes that predate it.
34CLASS_CODE_MIN_LENGTH = 4
35CLASS_CODE_MAX_LENGTH = 12
36CLASS_CODE_PATTERN = r"^[A-Za-z0-9]+$"
38ClassCodePath = Annotated[
39 str,
40 Path(
41 min_length=CLASS_CODE_MIN_LENGTH,
42 max_length=CLASS_CODE_MAX_LENGTH,
43 pattern=CLASS_CODE_PATTERN,
44 description=(
45 f"Alphanumeric class code, {CLASS_CODE_MIN_LENGTH}-"
46 f"{CLASS_CODE_MAX_LENGTH} chars (EI-TC-984)."
47 ),
48 ),
49]
51# The quota endpoint takes the code as an OPTIONAL query parameter (`class_id` is the
52# alternative), so it needs its own variant: same bound, but None still allowed through.
53ClassCodeQuery = Annotated[
54 Optional[str],
55 Query(
56 min_length=CLASS_CODE_MIN_LENGTH,
57 max_length=CLASS_CODE_MAX_LENGTH,
58 pattern=CLASS_CODE_PATTERN,
59 description="Class code to get per-class assignment quota for.",
60 ),
61]