π§ͺ QA Test Cases
TC-AUTH-01) are for bug tracking.
Coverage Summary
| Section | Tests | What's Covered |
|---|---|---|
TC-AUTH | 7 | Teacher signup, email verify, login/logout, child PIN login, lockout, device reuse, inactivity timeout |
TC-ROSTER | 6 | Class creation, single/bulk student add, 33-cap enforcement, teacher invite, parent linking |
TC-READ | 6 | Book open, vocab tap, miles/tokens, RSVP age gate, theme persistence, offline sync |
TC-TEL | 4 | Event dedup, vocab-taps pipeline, slow page detection, event ordering |
TC-BOT | 6 | Placement β memory, nightly reports, inactive alert, quiz feedback, free-tier skip, adaptive leveling |
TC-TEACH | 5 | Dashboard, learner profile, PIN reset, login cards, DPA wizard |
TC-BILL | 7 | Trial access, Stripe failure, grace expiry, entitlement resolution order, cache TTL, enterprise grant, childβteacher chain |
TC-ADMIN | 6 | Impersonation start/end, entitlement grant/revoke, GDPR deletion, audit log immutability, audio not stored |
TC-PARENT | 1 | Read-only access + write block |
TC-FLUENCY | 2 | Recording/scoring, multilingual thresholds |
TC-CONTENT | 2 | Recommendations, pre-warming cache |
TC-SEC | 5 | Tenant isolation, no secrets in responses, internal key auth, rate limiting, no cross-DB |
TC-PWA | 2 | Service worker registration, IndexedDB sync |
TC-NOTIF | 2 | Notification triggers, email log |
1. Authentication & Identity
Reference: Page 03 β Identity & Auth
TC-AUTH-01 β Teacher registration: happy path
AC-01A fresh browser with no session.
Visit account.readingtester.com/register, fill name + valid email + password β₯8 chars, submit.
- HTTP 201 in browser Network tab
- "Check your email" confirmation screen shown
usersrow:state=pending_verification,email_verified=falsesubscriptionsrow:state=trialing,trial_ends_at= ~14 days from nowaudit_logrow:action=register- Email received in inbox (VERIFY_EMAIL template)
TC-AUTH-02 β Teacher email verification
AC-01TC-AUTH-01 completed, verification email received.
Click the verification link in the email.
users.email_verified=true,state=active- Browser redirected to
/onboarding uc_sessioncookie set (httpOnly, Secure)audit_logrow:action=email_verified- WELCOME_TEACHER email sent
TC-AUTH-03 β Teacher login and logout
Verified teacher account.
Submit correct email + password at account.readingtester.com/login.
- Redirect to Teacher Portal (
teacher.readingtester.com) uc_sessioncookie presentaudit_logrow:action=login
Click logout.
uc_sessioncookie cleared- Redirect to
/login - Attempting
/dashboardβ redirect to/login(no access without session)
TC-AUTH-04 β Child login: happy path
AC-03 AC-13Teacher created student account (username: emma001, PIN: 1234), placement_test_completed=false.
Visit app.readingtester.com, enter username + PIN.
device_sessionsrow created (learner_idmapped,policy_applied=shared)audit_logrow:action=child_login- Redirected to
/placement-test(not library)
Login again after placement test done.
Redirect directly to /library β placement test NOT shown again.
TC-AUTH-05 β Child login: PIN lockout
AC-03Valid child account with PIN 1234.
Enter wrong PIN 5 times in a row.
- After 5th failure: HTTP 423
students.locked=trueteacher_notificationsrow inserted (type=pin_lockout)- "Account locked β ask your teacher" shown to child
- Teacher Portal shows notification badge
TC-AUTH-06 β Device reuse / shared device session
Child A (emma001) logged in on a tablet, device_session_id=AAA.
Child B (james002) logs in on the same tablet.
- Previous
device_sessionsrow for AAA:expired_at=NOW(),end_reason=device_reuse - New
device_sessionsrow created for james002 - Child A's session is invalid β navigating redirects to login
TC-AUTH-07 β Session inactivity timeout
Child logged in, shared_device_mode=true, child_inactivity_timeout_min=30.
No activity for 31 minutes.
device_sessions.expired_at=NOW(),end_reason=timeout- Child sees "Session ended β log in again"
- If β₯3 pages AND β₯50 words read: progress saved, miles credited
- If below threshold: session data discarded β no partial miles credited
2. Roster & Class Management
Reference: Page 05 β Roster & Classroom
TC-ROSTER-01 β Create a class
AC-02POST /api/v1/classes with {class_name: "Year3Blue", year_level: 3}
- HTTP 201
classesrow:teacher_id=session.user_id,state=active- Class visible in Teacher Portal immediately
audit_logrow:action=create_class
class_name β HTTP 422, no DB row, error shown in form.TC-ROSTER-02 β Add a single student
POST /api/v1/classes/:class_id/students with {name: "Tommy Anderson", year_level: 3}
- HTTP 201
studentsrow: auto-generated username + 4-digit PIN,state=created,placement_test_completed=falseclass_membershipsrow inserted- Response body contains
usernameandpin(shown once β teacher must record) audit_logrow:action=create_student
TC-ROSTER-03 β Bulk import students (CSV)
POST /api/students/import with CSV file (columns: name, year_level).
- HTTP 200 with array of
{name, username, pin}for each row - All
studentsrows created, allclass_membershipsinserted - Login cards renderable (name + username + PIN + QR code)
CLASS_FULL for row 34; rows 1β33 succeed.TC-ROSTER-04 β Class hard cap enforcement (33 students)
Class already has 33 active students.
Teacher attempts to add 1 more student.
- HTTP 422
{error: "CLASS_FULL"} - No new
class_membershipsrow - Teacher Portal shows "Class is full (33/33)"
Teacher archives one student (students.archived_at=NOW()).
Cap slot freed immediately β next add succeeds.
TC-ROSTER-05 β Teacher invites a colleague
school_admin POSTs /api/v1/schools/:school_id/invites with colleague email + role teacher.
invitesrow:token_hashstored,expires_at=NOW()+7d- INVITE_EMAIL sent
usersrow:role=teacher,school_idset,state=activesubscriptionsrow:state=trialing
TC-ROSTER-06 β Parent linking via link code
Parent has a Pickatale account, teacher has generated a 6-char link code (e.g. PIP423) for the student from the Teacher Portal.
Parent logs in to parents.readingtester.com, clicks "Link Your Child's Account", enters child display name + link code.
- Client validates code against
account.readingtester.com/api/public/link-code/PIP423 - Returns
{valid: true, uuid: "..."}β UUID resolved parent_childrenrow inserted:parentId, childName, learnerServiceId=uuid- Child's reading progress visible to parent (read-only)
{valid: false} β "Invalid code. Please check with your child's teacher." No DB row inserted.3. Reader App & Reading Session
Reference: Page 06 β Reading & Telemetry
TC-READ-01 β Open a book and read (online)
AC-04Authenticated child (placement complete) taps a book on app.readingtester.com/library.
reading_sessionsrow created:state=open,book_id,learner_id,started_at- Telemetry event
book_openedfired (POST/api/v1/events) - Book content displayed (FK-leveled if applicable)
- Telemetry event
page_turnedfired per page:{device_session_id, learner_id, page_number, time_on_page_ms}
session_endedevent firedreading_sessions.state=completed,words_read,pages_readupdated- Miles incremented:
floor(words_read / 100)
TC-READ-02 β Vocabulary tap
AC-04Child taps word "photosynthesis" in Finger-Follow mode.
- Telemetry event
word_tappedfired:{word, page_number, device_session_id, learner_id} - TTS pronounces the word
- Definition popup shown (if enabled)
- Word stored in Learner Bot
vocabularyGapsafter session ends
TC-READ-03 β Miles and token accumulation
Child has miles=9, reads a book with 150 words.
floor(150/100)=1new mile credited βstudents.miles=10- 10 miles threshold crossed β
students.tokensincremented by 1 audit_logrow:action=token_earned
TC-READ-04 β RSVP Speedread (age gate)
Child account with age=7.
Speedread button hidden or HTTP 403 β mode not accessible.
Child with age=9, fk_level β₯ 3.0.
- RSVP interface shown, starts at 60 WPM
- Max session: 10 minutes enforced
- RSVP time does NOT count toward book completion or miles
TC-READ-05 β Theme persistence
Child opens Aa button β selects "Sepia".
- Background changes to
#F5E6C8 localStorage["reader_bg_theme"]="sepia"written
Sepia theme restored from localStorage β no white flash on load.
TC-READ-06 β Offline reading and sync
AC-12Child goes offline (DevTools β Network β Offline) mid-session, turns pages, taps words.
- All events queued in IndexedDB (
telemetry-queuetag) - App continues β no error shown to child
- Miles NOT yet persisted to server
- Service Worker replays all queued events
- Server deduplicates by
event_idβ re-sending same events twice produces no duplicate rows reading_sessions.state=completedupdated- Miles credited to server DB
reading_sessions marked abandoned by 24h cron. No crash, no data corruption in other sessions.4. Telemetry Pipeline
TC-TEL-01 β Event deduplication
Same event (identical event_id UUID) POSTed to /api/v1/events twice.
- First call: HTTP 200, row inserted
- Second call: HTTP 200 (idempotent), no duplicate row
- Only 1 row in
eventstable for thatevent_id
TC-TEL-02 β Vocab-taps pipeline (Telemetry β Bot)
session_ended fires β Telemetry calls POST /api/v1/telemetry/vocab-taps.
- Learner Bot
vocabularyGaps:word=magnificent, tap_count=2(for 2 taps) - Re-tap in next session:
tap_countincremented via SQL (tap_count + N, not overwritten) - Response to Telemetry: HTTP 200 within 5 seconds
TC-TEL-03 β Session summary (slow pages detection)
Session: 5 pages, time_on_page_ms=[30000, 45000, 120000, 40000, 35000] (avg=54s, page 3 = 120s = 2.2Γ avg).
slow_pages=[3]- Bot stores
reading_patternmemory (confidence=0.60if pages read < 80% of total) - If
slow_pages.length > 3: Bot storesengagement_signal={signal:"struggling"}
TC-TEL-04 β Event ordering guarantee
Three events from same device_session_id arrive out of server order (network jitter).
- Events ordered by
client_ts, NOTcreated_at session_endedprocessing waits until all prior events for samedevice_session_idare processed
5. Learner Bot & Adaptive Engine
TC-BOT-01 β Placement test β bot memory
AC-13Child completes placement test β POST /api/placement-result {learner_id, fk_level: 3.5}
placement_resultsrow insertedstudents.placement_test_completed=true- Bot stores
reading_levelmemory withconfidence=1.0 - Child redirected to
/library
fk_level saved.TC-BOT-02 β Nightly report generation
AC-06Learner has active subscription, read β₯1 book in past 7 days.
Nightly cron fires at 04:00 UTC.
nightly_reportsrow:report_type=teacher_report, includes FK progression + reasoningnightly_reportsrow:report_type=parent_digest, warm tone- Both portals display the new reports
TC-BOT-03 β Nightly bot: inactive learner alert
AC-06Learner has not read any book in 4+ days.
teacher_notificationsrow:type=inactive- Teacher Portal shows alert badge
TC-BOT-04 β Quiz result β bot feedback
Child completes comprehension quiz, score 55%.
POST /api/v1/bot/:learner_id/quiz-result {scorePct: 55, levelRecommendation: "hold", ...}
- Bot stores
assessment_resultmemory:confidence=0.55 - Bot triggers re-run for this learner (
setImmediate) - If this is 2nd quiz with avg < 65%:
teacher_notifications type=low_score
TC-BOT-05 β Bot: free-tier learner skipped
AC-06Child's teacher subscription has expired (state=expired).
Nightly cron runs.
checkEntitlementβtier=free- Bot SKIPS this learner β no
nightly_reportsrow generated - Teacher Portal shows "Upgrade to view reports"
TC-BOT-06 β Adaptive content leveling
POST /api/v1/level-page {bookId, pageNumber, text, targetFkLevel: 4.0}
- HTTP 200:
{leveledText, actualFkScore, cached: false} - Cache key stored:
book_id + page_number + target_level_rounded + source_hash - Identical second call:
cached: true, no new GPT call - Short text (<30 words): returned unchanged, no leveling attempted
6. Teacher Portal
TC-TEACH-01 β Dashboard: class overview
Visit teacher.readingtester.com/class/:id (class has 5 students).
- All 5 students listed with: name, reading level, books read this week, miles, latest bot report
- Attention list flags:
placement_test_completed=falseORbooks_read=0OR quiz avg < 65% (2+ quizzes)
TC-TEACH-02 β Learner profile view
Teacher clicks a student's name.
- GET
/api/v1/bot/:learner_id/statuscalled withX-Internal-Key - Displays:
reading_level,books_read,total_miles,readingLevelHistory,vocabGaps,latestReport
TC-TEACH-03 β PIN reset (teacher flow)
Student account locked after 5 failed PIN attempts.
Teacher clicks "Reset PIN".
- New 4-digit PIN generated, shown once to teacher
students.locked=false,pin_hash=bcrypt(new_pin)audit_logrow:action=pin_reset
TC-TEACH-04 β Print login cards
Teacher clicks "Print Login Cards" after bulk import.
- Printable cards shown: student name + username + PIN + QR code (
app.readingtester.com?user=username) + school name + Pip logo
TC-TEACH-05 β DPA setup wizard
New teacher account, first login.
School details form submitted (School Name, Country, DPA checkbox ticked).
school_dpa_agreementsrow:school_id, user_id, accepted_at=NOW(), ip_address- Teacher redirected to dashboard
7. Billing & Entitlement
Reference: Page 04 β Billing & Entitlement
TC-BILL-01 β Trial period access
subscriptions.state=trialing, trial_ends_at=NOW()+14d
checkEntitlement β tier=full. All features accessible, no paywall.
checkEntitlement β tier=free. Full-tier features locked, upgrade prompt shown.
TC-BILL-02 β Stripe payment failure β grace period
AC-08Stripe webhook invoice.payment_failed received (valid Stripe-Signature).
subscriptions.state=past_due,grace_ends_at=NOW()+7d- RENEWAL_FAILED email sent (
email_logINSERT) - Teacher Portal: yellow banner "Payment failed. Update billing within 7 days."
- All features remain accessible (grace active)
audit_logrow:action=stripe_webhook- HTTP 200 returned to Stripe
Stripe-Signature β HTTP 400, audit_log action=stripe_webhook_invalid, NO state change.TC-BILL-03 β Grace period expiry
AC-08subscriptions.state=past_due, grace_ends_at=NOW()-1h.
Hourly expiry cron runs.
subscriptions.state=expired- All learners' full-tier features locked
- Learner Bot skips all learners on next nightly run
- Teacher Portal: red banner "Subscription expired"
- SCHOOL_SUBSCRIPTION_EXPIRED email sent
audit_logrow:action=subscription_expired
TC-BILL-04 β Entitlement resolution order
Teacher has expired subscriptions but platform_admin granted entitlement_grants row with expires_at=NULL.
entitlement_grants checked first β found β returns tier=full. subscriptions table NOT consulted.
Falls back to subscriptions β state=expired β tier=free. Features locked.
TC-BILL-05 β Entitlement cache (60s TTL)
Admin changes subscriptions.state=expired + broadcasts cache invalidation.
- Within 60 seconds: all service instances clear cache for this
user_id - Next
checkEntitlementcall fetches fresh from Account Center βtier=free
tier=free (fail-safe). audit_log action=entitlement_check_failed.TC-BILL-06 β Enterprise entitlement (external grant)
School has no Stripe subscription; platform_admin inserted entitlement_grants row.
checkEntitlement finds grant β tier=full. No Stripe interaction. No upgrade prompt.
TC-BILL-07 β Child entitlement resolves from teacher
Child is in a class owned by teacher with active subscription.
Child opens book beyond free tier limit.
Entitlement chain: child β class_memberships β teacher β subscriptions.state=active β tier=full. Book accessible.
{error: "entitlement_required"}. Child sees "Your school's subscription has expired. Ask your teacher." (no billing details shown to child).8. Admin & Compliance
Reference: Page 08 β Admin & Compliance
TC-ADMIN-01 β Impersonation: start
AC-09platform_admin POSTs /api/admin/users/:teacher_id/impersonate with reason.
- Target must NOT be a
platform_adminβ if admin: HTTP 403 audit_log:action=impersonation_started,actor_id,target_id- New session: expires in 1h,
impersonator_idstored - Orange "Impersonating [Name]" banner visible
- All actions during session logged with
impersonator_id
TC-ADMIN-02 β Impersonation: end
AC-09Click "End Impersonation".
- Impersonated session invalidated
audit_log:action=impersonation_ended- Admin's own session restored, orange banner removed
TC-ADMIN-03 β Grant entitlement (admin)
AC-10POST /api/admin/entitlement/grant {school_id: 5, tier: "full", expires_at: null}
entitlement_grantsrow insertedaudit_log:action=entitlement_grant_created- Cache invalidation broadcast to all services
- Within 60 seconds: school resolves
tier=full
TC-ADMIN-04 β GDPR deletion
AC-11POST /api/gdpr/delete {learner_id: "uuid"}
students.state=archivedβ child cannot log ingdpr_requestsrow:type=deletion, state=pending
- Telemetry events deleted
- Bot memories/reports deleted
- LRS statements deleted
users.name="Deleted User",email="{uuid}@deleted.invalid"audit_logPII stripped (rows retained)gdpr_requests.state=complete
gdpr_requests.error_log populated. State NOT set to complete. Admin dashboard flags for manual resolution.TC-ADMIN-05 β Audit log immutability
Attempt: DELETE FROM audit_log WHERE ...
DB trigger blocks DELETE β error raised. Rows remain intact.
TC-ADMIN-06 β Voice audio not stored
Fluency assessment in progress, child speaks into microphone.
- Audio processed in-memory only β no file written to disk
- No audio data in DB rows, queues, logs, or retry buffers
audit_logrow contains only text transcript + scores β never raw audio bytes
9. Parent Portal
TC-PARENT-01 β Parent read-only access
Parent linked to child via 6-char link code.
- Child's progress visible: books read, miles, latest parent digest
- FK level NOT shown (parents see engagement metrics only)
10. Fluency Assessment
TC-FLUENCY-01 β Recording and scoring
Child reads a passage aloud, recording submitted.
- WCPM calculated
- FK grade score returned
- CEFR level assigned (Pre-A1 β B2+) and stored in
assessment_scores - Multilingual learner (
multilingual_flag=true): relaxed thresholds (40% accuracy vs 50%, WCPM β₯5 vs β₯10) - No audio stored after processing
TC-FLUENCY-02 β Score storage and bot update
assessment_scoresrow: FK grade + CEFR level- POST to Learner Bot
/api/v1/bot/:learner_id/quiz-result - Bot updates
curriculum_statememory
11. Content & Recommendations
TC-CONTENT-01 β "Picked for You" recommendation
Child (fk_level=3.5) loads library page.
- Bot queried for recommendation record
- If Bot has recommendation: returned with
permanentId, title, author, coverUrl, reason - If no Bot recommendation: fallback to
/api/catalog?fk_min=3.0&fk_max=4.0β first result used - LibraryPage renders "Picked for You" hero card
TC-CONTENT-02 β Book pre-warming cache
Pre-warming pipeline has run for a book.
Child with fk_level=3.5 opens any pre-warmed page.
cached: true returned from Adaptive Engine β no GPT call, sub-100ms response.
12. Security & Tenant Isolation
TC-SEC-01 β Tenant isolation
Teacher A calls GET /api/v1/students.
- Response contains ONLY Teacher A's students
- Teacher B's students NOT returned (not even partially)
- Teacher A requesting Teacher B's
class_iddirectly β HTTP 403
TC-SEC-02 β No secrets in API responses
Any authenticated API call inspected.
- No
pin_hashfield in any response - No
password_hashfield in any response - No JWT secret or internal key visible
- No server file paths in error messages
TC-SEC-03 β X-Internal-Key required for internal routes
Call internal endpoint without X-Internal-Key header.
HTTP 401 β rejected.
TC-SEC-04 β Rate limiting
100 POST requests to /api/auth/login within 1 minute from same IP.
After threshold: HTTP 429 {error: "Too Many Requests", retryAfter: 60}. Legitimate logins resume after retry window.
TC-SEC-05 β No cross-service DB access
Inspect each service's DB connection string β service connects ONLY to its own schema. All cross-service data fetched via HTTP API with X-Internal-Key. No service has credentials to another service's DB.
13. Offline & PWA Behaviour
TC-PWA-01 β Service Worker registered
Visit app.readingtester.com in a modern browser β DevTools β Application β Service Workers.
sw.jsregistered and active (status: "running", not "waiting" or "redundant")GET /sw.jsβ HTTP 200
TC-PWA-02 β Background sync queue visible
Child goes offline, reads pages β DevTools β Application β IndexedDB β telemetry-queue.
Queued event rows visible with event_id, event_type, client_ts. Count matches events fired.
- IndexedDB
telemetry-queueempty (all replayed) - Server
eventstable has all rows (verify bydevice_session_id)
14. Notifications
TC-NOTIF-01 β Teacher notification triggers
- Student PIN locked (5 failed attempts)
- Quiz average < 65% (2+ quizzes)
- Student inactive 4+ days
teacher_notificationsrow inserted with correcttype- Notification badge visible in Teacher Portal header
- Clears when teacher marks as read
TC-NOTIF-02 β Email log audit
Any platform email dispatched (welcome, invite, RENEWAL_FAILED, etc.).
email_log row inserted: recipient, template, sent_at β row present even if delivery fails (log the attempt, not just success).
β‘ Post-Deploy Smoke Checklist
Run after every deploy β ~10 minutes, covers the most critical paths.
| # | Test | Expected |
|---|---|---|
| 1 | Teacher login | Redirect to portal, session cookie set |
| 2 | Create class | HTTP 201, appears in list |
| 3 | Add student | Username + PIN returned, student in class |
| 4 | Child login (correct PIN) | Redirect to placement test or library |
| 5 | Child login (wrong PIN Γ5) | Account locked, teacher notified |
| 6 | Open book | reading_sessions row created, book renders |
| 7 | Turn page | page_turned event in Telemetry DB |
| 8 | checkEntitlement (active teacher) | Returns tier=full, no 403 |
| 9 | checkEntitlement (expired) | Returns tier=free, gated feature returns 403 |
| 10 | Stripe webhook (invalid sig) | HTTP 400, no state change |
| 11 | Admin impersonate β orange banner | Banner visible, action logged |
| 12 | sw.js served at /sw.js | HTTP 200 |
Test Environment
# Reader App β app.readingtester.com
# Students (PIN: 1234 for all)
emma001 / james002 / olivia003 / liam004 / sophia005 / sofia001
# Teacher Portal β teacher.readingtester.com
sig@pickatale.com (setup wizard already complete)
demo@pickatale.com
# DB access for verification
mysql -h 172.17.0.1 -P 3316 -u reader_app -preader_app_pw reader_app
mysql -h 172.17.0.1 -P 3316 -u teacherportal -pteacher_pw_2026 teacher_portal
# Internal key (for testing X-Internal-Key endpoints)
# Check INTERNAL_KEY in each service's .env
QA Test Cases v1.0 β 2026-04-19 Β· 61 tests across 14 systems Β· Ref: Wiki v1.9, AC-01βAC-13