Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

14 — QA Checklist (Leave Module)

Test checklist anchored to the exact server semantics. Base QA baseline: 00-shared/10. Priority: P0 = blocks release, P1 = high, P2 = low. Known gaps are listed as explicit "known behavior" rows, not failures.


1. Request creation (P0)

#CaseExpected (source)
1.1Happy path200, status pending, daysRequested = inclusive day count, userId = token user (leave.service.ts:136-144)
1.2endDate < startDate400 endDate must be on or after startDate. (:133-134)
1.3Unknown leaveTypeId404 Leave type not found. (:129)
1.4Same-day request (start = end)200, daysRequested = 1 (inclusive formula :310-312)
1.5leaveTypeId spoof in bodyignored — userId always from token (:138); verify no cross-user creation
1.6Missing/blank reason200 (optional, create-leave-request.dto.ts:17-20)
1.7No auth401 (leave.controller.ts:27)
1.8daysRequested sent in bodyrejected by DTO whitelist (not a DTO field, create-leave-request.dto.ts:4-21)

2. Overlap validation (P1 — known gap)

#CaseExpected (source)
2.1Same user, overlapping ranges, both pendingBoth accepted — no overlap check in createRequest (leave.service.ts:126-156). Documented gap; client warns (06 §2.4). Test to lock current behavior + track gap
2.2Same user, overlapping ranges, one approvedSame — allowed; balance math handles both (:99-103)
2.3Cross-user overlapAllowed by design (calendar is advisory)

3. Day-count math (P1)

#CaseExpected (source)
3.1Mon→FridaysRequested = 5 (calendar days)
3.2Fri→Mon (weekend inside)= 4 — weekends count (:310-312, ponytail note: working-day count only if policy changes)
3.3Holiday inside rangecounts as a day (no holiday calendar)
3.4Dec 31 → Jan 2 (year span)3 days; balance attribution per startDate year (:101-103)
3.5DST boundaryMath.floor((end-start)/86400000)+1 (:311) — verify with DST-shifted timezones (tz stored on user, user.schema.ts:52-53)

4. Balance (P1)

#CaseExpected (source)
4.1FormuladaysRemaining = daysPerYear + carriedForward − daysUsed (leave.service.ts:121)
4.2Carry capmin(maxCarryForward, max(0, daysPerYear − usedPrevYear)) (:107-112)
4.3No carry (CL, ML, PL)carriedForward = 0 always
4.4SL/EL carry capSL max 30, EL max 60 (:37-51)
4.5Balance reflects approval onlypending/rejected excluded (query filters APPROVED, :94-98)
4.6Deduction after approvalrefetch balance post-approve shows reduced daysRemaining
4.7Defaults seedingfirst call seeds 5 defaults idempotently (:299-306); double-fire concurrent calls → single set ($setOnInsert upsert, leave-type.repository.ts:17-31)

5. Approval (P0)

#CaseExpected (source)
5.1Approve pending200 → status=approved, decidedBy, decidedAt, decisionNote set (:195-200)
5.2Reject pending200 → status=rejected; note optional (leave-decision.dto.ts:14-17)
5.3Decide already-decided409 Leave request is already <status>. (:175-178)
5.4Decide own request409 You cannot decide your own leave request. (:179-180)
5.5Approve over balance409 Insufficient leave balance. (:188-189); reject still allowed
5.6Approval race — two admins, same request, near-simultaneous PATCHBoth read pending; second write wins; at least one caller gets 409 (no atomic conditional update — verify behavior: second updateById overwrites; test + raise as gap (planned) optimistic-lock awareness, base.schema.ts version)
5.7Balance check race — two approvals crossing a shared balanceBoth pass check, both approve → daysUsed exceeds entitlement. Live-computed, no lock — document as known limitation; mitigation (forward-looking)
5.8Non-admin decisionServer permits (only JWT guard, leave.controller.ts:27) — client gates admin; server gap noted in 12 §7

6. List & filters (P1)

#CaseExpected (source)
6.1Non-admin listOnly own rows (leave.service.ts:166-167)
6.2Admin list allAll rows; userId filter effective (:163-167)
6.3status filterenum-validated (leave.controller.ts:40); invalid → 400
6.4SortcreatedAt desc (:168)
6.5userId filter as non-adminignored (overridden to self, :167)

7. Substitutions (P1)

#CaseExpected (source)
7.1Assign on approved request200, status=assigned (:252-263)
7.2Assign on pending/rejected409 (:227-230)
7.3Requester without Teacher record404 (:232-238)
7.4Exact-slot duplicate409 clash (:241-250)
7.5Partial overlap (08:00-10:00 vs 09:00-11:00)409 (timeOverlaps, :314-321)
7.6Adjacent slots (08:00-09:00 vs 09:00-10:00)allowed (strict </>, :320)
7.7Substitute listsorted date asc (:275-280)
7.8completed/cancelledUnreachable — no endpoint (known gap; schema substitution.schema.ts:7-11)

8. Calendar (P2)

#CaseExpected (source)
8.1Range filterstartDate ≤ to && endDate ≥ from (:291-292)
8.2Approved onlypending/rejected excluded (:290)
8.3No from/tocurrent month default (:283-287)
8.4Tenant isolationauto-scoped via BaseRepository (tenantId)

9. Cross-cutting

#CaseExpected
9.1Tenant isolationUser A cannot see/decide tenant B rows (base-scoped queries)
9.2Soft-deleted rowsexcluded by BaseRepository
9.3Offline appreads cached; writes blocked (10 §8)
9.4a11y sweep00-shared/09 (chips not color-only, live regions for errors)
9.5Analytics (proposed)leave.* events fire once per action

10. Known gaps to track (regression-lock as current behavior)

  1. No same-user overlap rejection (2.1).
  2. No cancel/edit endpoint (cancelled unreachable, leave-request.schema.ts:11).
  3. No atomic approve (5.6) / balance-crossing race (5.7).
  4. No leave.* permissions (permissions.constants.ts:1-97); no RBAC guard on decide/substitution endpoints (leave.controller.ts:27).
  5. Duplicate code E11000 not mapped to 409 (12 §4).
  6. Substitution status transitions + absence-side list missing.