14 — QA Checklist (Leave Module)
- 1. Request creation (P0)
- 2. Overlap validation (P1 — known gap)
- 3. Day-count math (P1)
- 4. Balance (P1)
- 5. Approval (P0)
- 6. List & filters (P1)
- 7. Substitutions (P1)
- 8. Calendar (P2)
- 9. Cross-cutting
- 10. Known gaps to track (regression-lock as current behavior)
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)
| # | Case | Expected (source) |
|---|---|---|
| 1.1 | Happy path | 200, status pending, daysRequested = inclusive day count, userId = token user (leave.service.ts:136-144) |
| 1.2 | endDate < startDate | 400 endDate must be on or after startDate. (:133-134) |
| 1.3 | Unknown leaveTypeId | 404 Leave type not found. (:129) |
| 1.4 | Same-day request (start = end) | 200, daysRequested = 1 (inclusive formula :310-312) |
| 1.5 | leaveTypeId spoof in body | ignored — userId always from token (:138); verify no cross-user creation |
| 1.6 | Missing/blank reason | 200 (optional, create-leave-request.dto.ts:17-20) |
| 1.7 | No auth | 401 (leave.controller.ts:27) |
| 1.8 | daysRequested sent in body | rejected by DTO whitelist (not a DTO field, create-leave-request.dto.ts:4-21) |
2. Overlap validation (P1 — known gap)
| # | Case | Expected (source) |
|---|---|---|
| 2.1 | Same user, overlapping ranges, both pending | Both 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.2 | Same user, overlapping ranges, one approved | Same — allowed; balance math handles both (:99-103) |
| 2.3 | Cross-user overlap | Allowed by design (calendar is advisory) |
3. Day-count math (P1)
| # | Case | Expected (source) |
|---|---|---|
| 3.1 | Mon→Fri | daysRequested = 5 (calendar days) |
| 3.2 | Fri→Mon (weekend inside) | = 4 — weekends count (:310-312, ponytail note: working-day count only if policy changes) |
| 3.3 | Holiday inside range | counts as a day (no holiday calendar) |
| 3.4 | Dec 31 → Jan 2 (year span) | 3 days; balance attribution per startDate year (:101-103) |
| 3.5 | DST boundary | Math.floor((end-start)/86400000)+1 (:311) — verify with DST-shifted timezones (tz stored on user, user.schema.ts:52-53) |
4. Balance (P1)
| # | Case | Expected (source) |
|---|---|---|
| 4.1 | Formula | daysRemaining = daysPerYear + carriedForward − daysUsed (leave.service.ts:121) |
| 4.2 | Carry cap | min(maxCarryForward, max(0, daysPerYear − usedPrevYear)) (:107-112) |
| 4.3 | No carry (CL, ML, PL) | carriedForward = 0 always |
| 4.4 | SL/EL carry cap | SL max 30, EL max 60 (:37-51) |
| 4.5 | Balance reflects approval only | pending/rejected excluded (query filters APPROVED, :94-98) |
| 4.6 | Deduction after approval | refetch balance post-approve shows reduced daysRemaining |
| 4.7 | Defaults seeding | first call seeds 5 defaults idempotently (:299-306); double-fire concurrent calls → single set ($setOnInsert upsert, leave-type.repository.ts:17-31) |
5. Approval (P0)
| # | Case | Expected (source) |
|---|---|---|
| 5.1 | Approve pending | 200 → status=approved, decidedBy, decidedAt, decisionNote set (:195-200) |
| 5.2 | Reject pending | 200 → status=rejected; note optional (leave-decision.dto.ts:14-17) |
| 5.3 | Decide already-decided | 409 Leave request is already <status>. (:175-178) |
| 5.4 | Decide own request | 409 You cannot decide your own leave request. (:179-180) |
| 5.5 | Approve over balance | 409 Insufficient leave balance. (:188-189); reject still allowed |
| 5.6 | Approval race — two admins, same request, near-simultaneous PATCH | Both 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.7 | Balance check race — two approvals crossing a shared balance | Both pass check, both approve → daysUsed exceeds entitlement. Live-computed, no lock — document as known limitation; mitigation (forward-looking) |
| 5.8 | Non-admin decision | Server permits (only JWT guard, leave.controller.ts:27) — client gates admin; server gap noted in 12 §7 |
6. List & filters (P1)
| # | Case | Expected (source) |
|---|---|---|
| 6.1 | Non-admin list | Only own rows (leave.service.ts:166-167) |
| 6.2 | Admin list all | All rows; userId filter effective (:163-167) |
| 6.3 | status filter | enum-validated (leave.controller.ts:40); invalid → 400 |
| 6.4 | Sort | createdAt desc (:168) |
| 6.5 | userId filter as non-admin | ignored (overridden to self, :167) |
7. Substitutions (P1)
| # | Case | Expected (source) |
|---|---|---|
| 7.1 | Assign on approved request | 200, status=assigned (:252-263) |
| 7.2 | Assign on pending/rejected | 409 (:227-230) |
| 7.3 | Requester without Teacher record | 404 (:232-238) |
| 7.4 | Exact-slot duplicate | 409 clash (:241-250) |
| 7.5 | Partial overlap (08:00-10:00 vs 09:00-11:00) | 409 (timeOverlaps, :314-321) |
| 7.6 | Adjacent slots (08:00-09:00 vs 09:00-10:00) | allowed (strict </>, :320) |
| 7.7 | Substitute list | sorted date asc (:275-280) |
| 7.8 | completed/cancelled | Unreachable — no endpoint (known gap; schema substitution.schema.ts:7-11) |
8. Calendar (P2)
| # | Case | Expected (source) |
|---|---|---|
| 8.1 | Range filter | startDate ≤ to && endDate ≥ from (:291-292) |
| 8.2 | Approved only | pending/rejected excluded (:290) |
| 8.3 | No from/to | current month default (:283-287) |
| 8.4 | Tenant isolation | auto-scoped via BaseRepository (tenantId) |
9. Cross-cutting
| # | Case | Expected |
|---|---|---|
| 9.1 | Tenant isolation | User A cannot see/decide tenant B rows (base-scoped queries) |
| 9.2 | Soft-deleted rows | excluded by BaseRepository |
| 9.3 | Offline app | reads cached; writes blocked (10 §8) |
| 9.4 | a11y sweep | 00-shared/09 (chips not color-only, live regions for errors) |
| 9.5 | Analytics (proposed) | leave.* events fire once per action |
10. Known gaps to track (regression-lock as current behavior)
- No same-user overlap rejection (2.1).
- No cancel/edit endpoint (
cancelledunreachable,leave-request.schema.ts:11). - No atomic approve (5.6) / balance-crossing race (5.7).
- No
leave.*permissions (permissions.constants.ts:1-97); no RBAC guard on decide/substitution endpoints (leave.controller.ts:27). - Duplicate
codeE11000 not mapped to 409 (12 §4). - Substitution status transitions + absence-side list missing.