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

1"""One bound for the `class_code` path parameter, shared by every route that takes it. 

2 

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": ...})`. 

11 

12Zephyr EI-T358 records the gap: "an excessively long value is rejected on the length 

13limit". It was not, anywhere but that one route. 

14 

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. 

22 

23Developer: Allan Ninal 

24Date: 2026-09-23 

25""" 

26 

27from typing import Annotated, Optional 

28 

29from fastapi import Path, Query 

30 

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]+$" 

37 

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] 

50 

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]