Coverage for server / models / class_message.py: 96%

28 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-10-04 09:33 +0000

1from datetime import datetime, timezone 

2from typing import Optional 

3from beanie import Document 

4from fastapi import HTTPException, status 

5from pydantic import BaseModel, Field, field_validator 

6 

7from server.utilities.html_sanitizer import to_plain_text 

8 

9 

10class ClassMessage(Document): 

11 class_id: str 

12 author_id: str 

13 author_name: str 

14 content: str 

15 created_at: Optional[datetime] = None 

16 

17 @field_validator("created_at", mode="before") 

18 @classmethod 

19 def set_created_at(cls, v): 

20 return v or datetime.now(timezone.utc) 

21 

22 class Settings: 

23 name = "class_messages" 

24 indexes = [ 

25 [("class_id", 1), ("created_at", -1)], 

26 ] 

27 

28 

29class CreateClassMessage(BaseModel): 

30 content: str = Field(..., min_length=1, max_length=5000) 

31 

32 @field_validator("content") 

33 @classmethod 

34 def _strip_markup(cls, value): 

35 """Store announcement text as plain text — never as markup. 

36 

37 Added by Allan Ninal — 2026-09-28 (EI-T485). A class announcement created 

38 with `<script>alert('XSS')</script>hello` was stored and returned verbatim; 

39 verified live on QA 2026-09-27. Mirrors the class title/description rule and 

40 the existing `theme_name` validator: strip markup first, then refuse a value 

41 that was nothing but markup. 

42 """ 

43 cleaned = (to_plain_text(value) or "").strip() 

44 if not cleaned: 

45 raise HTTPException( 

46 status_code=status.HTTP_400_BAD_REQUEST, 

47 detail="content cannot be blank", 

48 ) 

49 return cleaned