Skip to main content

API Test Cases — SAPOT Backend (FastAPI)

Generated: 2026-06-20
Framework: Pytest + FastAPI TestClient
Format: ID | Endpoint | Scenario | Request | Expected Response | Priority | Severity | Automate


Auth Setup

All tests requiring authentication use a valid_user_token fixture that:

  1. Creates a test user via POST /auth/
  2. Returns the access_token from the response
  3. Includes the token as Authorization: Bearer <token>

Admin tests seed the DB directly (do not rely on the unauthenticated /testing/* endpoints — those are the bug under test).


1. Root & Health

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-001GET /Server runningNo auth200 {"state": "running"}P0CriticalPytest
API-002GET /auth/Auth health checkNo auth200P1HighPytest

2. Registration & Login

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-010POST /auth/Valid registration, terms accepted{username, firstName, lastName, password, terms_accepted: true}200 UserPublic + access + refresh tokensP0CriticalPytest
API-011POST /auth/terms_accepted is false{..., terms_accepted: false}400P0CriticalPytest
API-012POST /auth/Duplicate usernameSame username as existing409P0CriticalPytest
API-013POST /auth/Duplicate emailSame email as existing409P0CriticalPytest
API-014POST /auth/Password exactly 8 chars{password: "12345678"}200P1HighPytest
API-015POST /auth/Password 7 chars{password: "1234567"}422P1HighPytest
API-016POST /auth/tokenValid credentialsusername=x&password=y (OAuth2 form)200 {access_token, refresh_token, token_type}P0CriticalPytest
API-017POST /auth/tokenWrong passwordusername=x&password=wrong401P0CriticalPytest
API-018POST /auth/tokenNon-existent usernameusername=unknown&password=x401 (generic — not "user not found")P0CriticalPytest
API-019POST /auth/tokenBanned userValid creds, banned account403P0CriticalPytest
API-020POST /auth/tokenRate limit (5/min)6 requests in 1 min429 on 6thP0CriticalPytest
API-021POST /auth/tokenAccount locked after repeated failuresMultiple wrong passwords429 with lockedUntilP0CriticalPytest
API-022POST /auth/logoutValid tokenBearer token200; JTI blacklistedP0CriticalPytest
API-023POST /auth/logoutAlready blacklisted tokenReuse token after logout401P0CriticalPytest
API-024POST /auth/refreshValid refresh token{refresh_token: valid}200 new Token; old refresh JTI blacklistedP0CriticalPytest
API-025POST /auth/refreshInvalid refresh token{refresh_token: "bad"}401P0CriticalPytest
API-026POST /auth/refreshRevoked refresh token (already used)Used token401P0CriticalPytest
API-027POST /auth/refreshRate limit (10/min)11 requests/min429P1HighPytest
API-028GET /auth/existsUsername exists?identifier=existing_user200 {exists: true}P1HighPytest
API-029GET /auth/existsUsername does not exist?identifier=ghost200 {exists: false}P1HighPytest
API-030GET /auth/termsGet T&CNo auth200 {content: string}P2MediumPytest
API-031POST /auth/reauthenticateCorrect current passwordBearer + {current_password}200 {reauth_token}P0CriticalPytest
API-032POST /auth/reauthenticateWrong passwordBearer + wrong password401P0CriticalPytest
API-033POST /auth/change-passwordValid old + new passwordBearer + {current_password, new_password}200P0CriticalPytest
API-034POST /auth/change-passwordWrong current passwordBearer + wrong old password401P0CriticalPytest
API-035POST /auth/change-passwordNew password too short{new_password: "short"}422P1HighPytest
API-036POST /auth/change-passwordRate limit (3/min)4 requests in 1 min429P1HighPytest

3. Email Verification

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-040POST /auth/verify/verify-codeValid code{code: valid_code}200; email_verified=True set on userP0CriticalPytest
API-041POST /auth/verify/verify-codeExpired codeOld code400P0CriticalPytest
API-042POST /auth/verify/verify-codeInvalid code{code: "000000"}400P0CriticalPytest
API-043POST /auth/verify/resend-verification-codeResend without new emailBearer200; new code sentP1HighPytest
API-044POST /auth/verify/resend-verification-codeChange email without reauth headerBearer + ?email=new@x.com (no X-Reauth-Token)401P0CriticalPytest
API-045POST /auth/verify/resend-verification-codeChange email with valid reauthBearer + ?email=new@x.com + valid reauth token200P0CriticalPytest

4. Forgot Password

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-050POST /auth/forgot-password/emailKnown email?email=known@x.com200 generic successP0CriticalPytest
API-051POST /auth/forgot-password/emailUnknown email (enumeration protection)?email=ghost@x.com200 generic success (NOT 404)P0CriticalPytest
API-052POST /auth/forgot-password/email-codeValid OTP?email=x&code=valid200 {link, recovery_token}P0CriticalPytest
API-053POST /auth/forgot-password/email-codeInvalid OTPWrong code400P0CriticalPytest
API-054POST /auth/forgot-password/email-codeExpired OTP (>10 min)Old code400P0CriticalPytest
API-055POST /auth/forgot-password/email-codeRate limit (10/min)11 requests/min429P1HighPytest
API-056POST /auth/forgot-password/phoneKnown phone{phone_number: "+63912..."}200 generic successP0CriticalPytest
API-057POST /auth/forgot-password/phone-codeValid OTP{phone_number, code}200 {link, recovery_token}P0CriticalPytest
API-058POST /auth/forgot-password/phone-code3 wrong OTPs triggers lock3x wrong code429 after 3rdP0CriticalPytest
API-059GET /auth/forgot-password/reset-passwordValid token?token=valid200 {user_id}P0CriticalPytest
API-060GET /auth/forgot-password/reset-passwordExpired tokenOld token400P0CriticalPytest
API-061POST /auth/forgot-password/reset-passwordValid reset?token=valid + {new_password}200P0CriticalPytest
API-062POST /auth/forgot-password/reset-passwordShort new password{new_password: "short"}400P1HighPytest
API-063POST /auth/forgot-password/recovery-with-recovery-keyValid key file?user_identifier=x + valid file200 {recovery-link, recovery_token}P0CriticalPytest
API-064POST /auth/forgot-password/recovery-with-recovery-keyFile too short (<20 chars)Short file content400P0CriticalPytest
API-065POST /auth/forgot-password/recovery-with-recovery-keyWrong file type (binary)PDF file400P1HighPytest
API-066GET /auth/forgot-password/security-questionUser has questions set?identifier=username200 {question: string}P0CriticalPytest
API-067GET /auth/forgot-password/security-questionUser has no questions?identifier=user-no-q404P0CriticalPytest
API-068POST /auth/forgot-password/security-question/answerCorrect answer{question, answer: correct}200 {correct: true, reset_link, recovery_token}P0CriticalPytest
API-069POST /auth/forgot-password/security-question/answerWrong answer{question, answer: wrong}200 {correct: false} (not 401)P0CriticalPytest
API-070POST /auth/forgot-password/generate-new-recovery-keyValid password, not in cooldownBearer + X-Current-Password header200 file downloadP0CriticalPytest
API-071POST /auth/forgot-password/generate-new-recovery-keyWithin cooldown (30-day)Key generated < 30 days ago429P0CriticalPytest
API-072POST /auth/forgot-password/generate-new-recovery-keyWrong passwordWrong X-Current-Password401P0CriticalPytest
API-073POST /auth/forgot-password/security-questionsSet valid questionsBearer + {questions: [{q, a}]}200P0CriticalPytest
API-074POST /auth/forgot-password/security-questionsEmpty questions array{questions: []}422P0CriticalPytest
API-075POST /auth/forgot-password/security-questionsWithin 90-day cooldownChanged < 90 days ago429P1HighPytest

5. User Utilities

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-080GET /user-utils/current-user-infoGet own profileBearer200 UserInfo with roleP0CriticalPytest
API-081GET /user-utils/current-user-infoUnauthenticatedNo token401P0CriticalPytest
API-082POST /user-utils/search-userSearch by substring?identifier_string=user200 {res: [...]} case-insensitiveP1HighPytest
API-083GET /user-utils/search-user/{id}Valid UUIDPath user_id=valid-uuid200 user infoP1HighPytest
API-084GET /user-utils/search-user/{id}Invalid UUIDuser_id=not-a-uuid404P1HighPytest
API-085GET /user-utils/is-adminNon-adminBearer (user role)200 falseP1HighPytest
API-086GET /user-utils/is-rescuerRescuerBearer (rescuer)200 trueP1HighPytest
API-087GET /user-utils/get-announcementsRegular user — user-onlyBearer (user)200 user audience onlyP1HighPytest
API-088GET /user-utils/get-announcementsRescuer — sees moreBearer (rescuer)200 rescuer + user audienceP1HighPytest
API-089GET /user-utils/get-announcementsExpired excludedSome expired announcements200 excludes expiredP0CriticalPytest

6. ECDH Keys

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-100POST /keys/registerValid 32-byte base64 keyBearer + {ecdh_public_key: valid_b64}200 SignedCredential (Ed25519-signed)P0CriticalPytest
API-101POST /keys/registerInvalid base64{ecdh_public_key: "!!! bad"}422P0CriticalPytest
API-102POST /keys/registerWrong key length (31 bytes)31-byte b64422P0CriticalPytest
API-103POST /keys/registerReplace existing keyRegister twice200; new credentialP0CriticalPytest
API-104GET /keys/server-public-keyGet server Ed25519 keyNo auth200 {ed25519PublicKey}P0CriticalPytest
API-105GET /keys/{peer_id}Peer has registered keyBearer + valid UUID200 SignedCredentialP0CriticalPytest
API-106GET /keys/{peer_id}Peer has no keyBearer + UUID with no key404P0CriticalPytest
API-107GET /keys/{peer_id}/typeGuest peerBearer + guest UUID200 {is_guest: true}P1HighPytest
API-108POST /keys/contacts/{peer_id}Store encrypted contact keyBearer + {encrypted_public_key: blob}200P1HighPytest
API-109GET /keys/contactsGet all backed-up keysBearer200 list of {peer_id, encrypted_public_key}P1HighPytest

7. Wrapped Key & Recovery

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-110POST /users/wrapped-keyStore wrapped keyBearer + {wrapped_blob: string}200P0CriticalPytest
API-111GET /users/wrapped-keyGet existing keyBearer (key stored)200 {wrapped_blob, created_at}P0CriticalPytest
API-112GET /users/wrapped-keyNo key storedBearer (no prior POST)404P0CriticalPytest
API-113PUT /users/wrapped-keyUpdate existing keyBearer + new blob200P0CriticalPytest
API-114PUT /users/wrapped-keyUpdate non-existentBearer (no prior POST)404P0CriticalPytest
API-115POST /users/recovery-setupBulk upsert recovery blobsBearer + {blobs: [{method, wrapped_blob}]}200P0CriticalPytest
API-116GET /users/recovery-keyGet blob with valid recovery token?recovery_token=valid&method=password200 {wrapped_blob, metadata, user_id}P0CriticalPytest
API-117GET /users/recovery-keyInvalid recovery token?recovery_token=bad404P0CriticalPytest

8. Profile Picture

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-120POST /profile-picture/meUpload JPEGBearer + multipart JPEG200 {photo_id, url}P1HighPytest
API-121POST /profile-picture/meUpload PNGBearer + multipart PNG200P1HighPytest
API-122POST /profile-picture/meUpload PDF (unsupported)Bearer + PDF400P1HighPytest
API-123GET /profile-picture/meGet own photoBearer200 {url}P1HighPytest
API-124GET /profile-picture/meNo photo uploadedBearer, no upload200 {url: /static/default.jpg}P1HighPytest
API-125GET /profile-picture/{user_id}Get any user photoNo auth + valid user_id200 {url}P1HighPytest
API-126GET /profile-picture/{user_id}No photo → defaultNo auth200 {url: /static/default.jpg}P2MediumPytest

9. Update Profile

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-130POST /update/profile/Update first nameBearer + {firstName: "New"}200 {status: "ok"}P1HighPytest
API-131POST /update/profile/Email field silently skippedBearer + {email: "new@x.com"}200 but email NOT changedP0CriticalPytest
API-132POST /update/profile/Phone field silently skippedBearer + {phone_number: "+63912..."}200 but phone NOT changedP0CriticalPytest
API-133POST /update/profile/Conflict on usernameBearer + taken username409P1HighPytest

10. Sync (WatermelonDB Protocol)

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-140GET /sync/pullFull sync (last_pulled_at=0)Bearer + ?last_pulled_at=0200 {changes, timestamp} all owned dataP0CriticalPytest
API-141GET /sync/pullIncremental syncBearer + ?last_pulled_at=<recent_ts>200 only data changed since timestampP0CriticalPytest
API-142GET /sync/pullOnly returns own dataTwo users; bearer = user A200 excludes user B's private conversationsP0CriticalPytest
API-143GET /sync/pullRespects limit param?limit=5200 at most 5 items per collectionP1HighPytest
API-144GET /sync/pullUnauthenticatedNo token401P0CriticalPytest
API-145POST /sync/pushValid push payloadBearer + valid PushSyncRequest200 {status: "ok"}P0CriticalPytest
API-146POST /sync/pushConflict: old updated_atPush with stale updated_at vs server409P0CriticalPytest
API-147POST /sync/pushUnknown sender_id auto-creates guestMessage with unknown sender UUID200; guest user createdP1HighPytest
API-148POST /sync/pushReceipt for missing message skippedPush receipt with no parent200 (not error)P1HighPytest
API-149POST /sync/pushEmpty payload{created: {}, updated: {}, deleted: {}}200P1HighPytest

11. GPS

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-150WS /gps/ws/{user_id}Auth matches user_id?token=own_token + own user_idConnected; location saved on messageP0CriticalPytest
API-151WS /gps/ws/{user_id}Auth mismatch (spoofing)?token=user_a_token + user_b_idClose 1008P0CriticalPytest
API-152WS /gps/ws/{user_id}Stream valid coordinates{"lat": 14.5, "lng": 121.0}Saved to DB; broadcast to monitorsP0CriticalPytest
API-153WS /gps/ws/{user_id}Stream invalid coords{"lat": "bad", "lng": null}Handled gracefully (not crash)P1HighPytest
API-154GET /gps/latestRescuer gets latestBearer (rescuer)200 list of {user_id, lat, lng, timestamp}P0CriticalPytest
API-155GET /gps/latestRegular user blockedBearer (non-rescuer)403P0CriticalPytest
API-156GET /gps/history/{user_id}Valid historyBearer (rescuer)200 list of locationsP1HighPytest
API-157GET /gps/history/{user_id}No historyBearer (rescuer) + user with no GPS404P1HighPytest
API-158WS /gps/ws/monitor/rescuers/{id}No auth required (BUG)No tokenCurrently connects — MUST be 1008/401P0CriticalPytest

12. WebSocket Signaling

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-160WS /ws/Connect with valid access token?token=validConnected; {type: "status-update", status: "online"} broadcastP0CriticalPytest
API-161WS /ws/Connect with expired token?token=expiredClose 1008P0CriticalPytest
API-162WS /ws/Connect without tokenNo ?tokenClose 1008P0CriticalPytest
API-163WS /ws/Ping → pong{type: "ping"}{type: "pong"}P0CriticalPytest
API-164WS /ws/Get active users{type: "get-active-users"}List of connected user UUIDsP1HighPytest
API-165WS /ws/Chat to online peer{type: "chat", data: {to: online_peer}}Peer receives; no server-ackP0CriticalPytest
API-166WS /ws/Chat to offline peer{type: "chat", data: {to: offline_peer}}Message queued; sender gets server-ackP0CriticalPytest
API-167WS /ws/ACK deletes queued message{type: "ack", data: {messageId: x}}Queue entry deletedP0CriticalPytest
API-168WS /ws/Public-chat message{type: "public-chat", data: {...}}Saved to DB; broadcast to allP0CriticalPytest
API-169WS /ws/WebRTC offer relay{type: "offer", data: {to: peer_id}}Relayed to peerP0CriticalPytest
API-170WS /ws/WebRTC answer relay{type: "answer", data: {to: peer_id}}Relayed to peerP0CriticalPytest
API-171WS /ws/ICE candidate relay{type: "ICE", data: {to: peer_id}}Relayed to peerP0CriticalPytest
API-172WS /ws/Disconnect broadcasts offlineClient disconnects{type: "status-update", status: "offline"} broadcastP0CriticalPytest
API-173WS /ws/Queued messages drained on connectMessages in queueAll delivered immediately on connectP0CriticalPytest
API-174WS /ws/Stale ACK-type entries deleted on drainACK in queueDeleted without deliveryP1HighPytest
API-175WS /ws/Seen-type delivered then deletedSeen in queueDelivered then removedP1HighPytest

13. Admin

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-180POST /admin/loginAdmin loginAdmin credentials200 with tokensP0CriticalPytest
API-181POST /admin/loginNon-adminRegular user creds401P0CriticalPytest
API-182GET /admin/get-active-usersAdmin sees countsAdmin token200 {active_users, total_users, inactive_users}P1HighPytest
API-183POST /admin/create/user/rescuerPromote to rescuerAdmin + ?user_id=uuid200P0CriticalPytest
API-184POST /admin/create/user/rescuerAlready rescuerAdmin + rescuer UUID403P0CriticalPytest
API-185POST /admin/remove/user/rescuerDemote rescuerAdmin + rescuer UUID200P0CriticalPytest
API-186POST /admin/ban/userBan user 7 daysAdmin + ?user_id=x&duration_in_days=7200P0CriticalPytest
API-187POST /admin/ban/userNegative duration?duration_in_days=-1422P2MediumPytest
API-188POST /admin/unban/userUnban userAdmin + ?user_id=x200P0CriticalPytest
API-189POST /admin/delete/userHard deleteAdmin + ?user_id=x200P0CriticalPytest
API-190POST /admin/post-announcementCreate announcementAdmin + query params (title, content, priority, target_audience, expires_at)200 {announcement}P1HighPytest
API-191PATCH /admin/announcements/{id}Partial updateAdmin + update params200P1HighPytest
API-192DELETE /admin/announcements/{id}DeleteAdmin token200P1HighPytest
API-193GET /adminRegular user blockedBearer (user)401P0CriticalPytest
API-194GET /adminRescuer blocked from adminBearer (rescuer)401P0CriticalPytest

14. GSM / SMS

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-200POST /gsm/requestRequest phone verification OTPBearer + unverified phone200 {detail}P1HighPytest
API-201POST /gsm/verifyValid OTPBearer + {code: valid}200; phone marked verified on userP0CriticalPytest
API-202POST /gsm/verifyInvalid OTP{code: wrong}400P0CriticalPytest
API-203POST /gsm/verifyExpired OTPOld code400P0CriticalPytest
API-204GET /gsm/phone-is-verifiedCheck statusBearer200 {is_verified: bool}P1HighPytest
API-205POST /gsm/contact-unknown-userValid PH numberBearer + ?target_phone_number=+63912...200 {status, user_id, is_sapot_user}P1HighPytest
API-206POST /gsm/contact-unknown-userInvalid format?target_phone_number=0917... (not +63)422P1HighPytest
API-207POST /gsm/migrate-phone-userMigrate ghostBearer (user with phone, ghost exists)200 {migrated: true}P0CriticalPytest
API-208POST /gsm/migrate-phone-userNo phone on accountBearer (no phone)400P1HighPytest
API-209POST /gsm/sms/sendSend to banned userBearer + banned user_id403P0CriticalPytest

15. Security — Critical Issues

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-220POST /testing/test-make-adminUnauthenticated privilege escalation (CRITICAL BUG)No token + ?username=any_userCurrently 200 — MUST be 404 or removedP0CriticalPytest
API-221POST /testing/test-make-rescuerUnauthenticated privilege escalation (CRITICAL BUG)No token + ?username=any_userCurrently 200 — MUST be 404 or removedP0CriticalPytest
API-222GET /auth/existsNo rate limit enables enumeration100 requests without throttleShould 429 after thresholdP0CriticalPytest
API-223WS /gps/ws/monitor/rescuers/{id}No auth on GPS monitor (CRITICAL BUG)No tokenShould 1008 (currently open)P0CriticalPytest
API-224POST /portal/api/v1/guests/{id}/disconnectNameError crash (BUG)Valid session_idCurrently 500 (db vs session variable)P0CriticalPytest
API-225AnyCORS allow_origins=["*"] + credentialsOrigin: https://evil.comCredentials not returned by browserP0CriticalManual
API-226POST /auth/tokenHardcoded JWT secret in productionJWT decode with known fallback secretShould fail (env var must be set)P0CriticalManual

16. Public Chat

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-230GET /public-chatPaginated historyBearer + ?limit=10200 {messages: [...], oldest_created_at}P1HighPytest
API-231GET /public-chatCursor paginationBearer + ?before=<epoch_ms>200 messages older than cursorP1HighPytest
API-232GET /public-chatUnauthenticatedNo token401P0CriticalPytest

17. Authorization Boundaries

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-240GET /gps/latestUser (non-rescuer) tries rescuer endpointBearer (user)403P0CriticalPytest
API-241GET /admin/get-active-usersRegular user tries adminBearer (user)401P0CriticalPytest
API-242GET /admin/get-active-usersRescuer tries adminBearer (rescuer)401P0CriticalPytest
API-243GET /sync/pullUser only gets own dataTwo users; request as user A200 excludes user B's private dataP0CriticalPytest
API-244POST /gsm/inboundWithout GSM secret headerNo X-GSM-Secret403P0CriticalPytest
API-245GET /gsm/users/by-phone/{phone}Without GSM secret headerNo header403P0CriticalPytest
API-246GET /admin/router/health/latestNon-adminBearer (user)401P1HighPytest

18. Captive Portal

IDEndpointScenarioRequestExpected ResponsePrioritySeverityAutomate
API-250POST /portal/api/v1/guestsCreate guest sessionGuestLoginRequest200 GuestSessionReadP1HighPytest
API-251POST /portal/api/v1/guestsIdempotent on duplicate session_idSame session_id twice200; existing session returnedP1HighPytest
API-252PATCH /portal/api/v1/guests/{id}/disconnectValid sessionValid session_id200 (currently 500 NameError bug)P0CriticalPytest
API-253PATCH /portal/api/v1/guests/{id}/disconnectUnknown sessionInvalid session_id404P1HighPytest
API-254GET /portal/api/v1/guests/statsGet aggregate countsNo auth200 {total, active, disconnected}P2MediumPytest
API-255GET /portal/api/v1/guestsPaginated list?status=active&limit=10200 paginatedP2MediumPytest