Yimaru LMS Backend — Test Plan
1. System Overview
| Component |
Tech |
| Language |
Go 1.x |
| Framework |
Fiber v2 |
| Database |
PostgreSQL (pgx/v5, sqlc) |
| Auth |
JWT (access + refresh tokens) |
| Video |
Vimeo API, CloudConvert |
| Payments |
ArifPay |
| Notifications |
WebSocket, SMS (AfroSMS), Resend, Push |
| Logging |
zap (MongoDB), slog |
Roles: SUPER_ADMIN, ADMIN, STUDENT, INSTRUCTOR, SUPPORT
Team Roles: super_admin, admin, content_manager, support_agent, instructor, finance, hr, analyst
2. Test Strategy
Test Pyramid
┌─────────┐
│ E2E / │ ← API integration tests (Postman/httptest)
│ API │
┌┴─────────┴┐
│ Service │ ← Business logic unit tests (mocked stores)
┌┴────────────┴┐
│ Repository │ ← DB integration tests (test DB + sqlc)
┌┴──────────────┴┐
│ Domain/Utils │ ← Pure unit tests (no deps)
└────────────────┘
Priority Levels
- P0 — Critical: Auth, Payments, Subscriptions — money + access
- P1 — High: Course CRUD, Video upload pipeline, Ratings
- P2 — Medium: Notifications, Questions, Issue Reporting, Team Mgmt
- P3 — Low: Activity Logs, Analytics, Settings, Static files
3. Module Test Plans
3.1 Authentication & Authorization (P0)
Unit Tests
| ID |
Test Case |
Input |
Expected |
| AUTH-01 |
Register user with valid data |
valid phone, email, password |
201, user created with status=PENDING |
| AUTH-02 |
Register with existing email |
duplicate email |
400/409, ErrEmailAlreadyRegistered |
| AUTH-03 |
Register with existing phone |
duplicate phone |
400/409, ErrPhoneAlreadyRegistered |
| AUTH-04 |
Login with correct credentials |
valid email+password |
200, access_token + refresh_token |
| AUTH-05 |
Login with wrong password |
valid email, bad password |
401 |
| AUTH-06 |
Login unverified user |
PENDING user |
401, ErrUserNotVerified |
| AUTH-07 |
Refresh token (valid) |
valid refresh token |
200, new access_token |
| AUTH-08 |
Refresh token (expired/revoked) |
expired/revoked token |
401 |
| AUTH-09 |
Google OAuth login (Android) |
valid Google ID token |
200, tokens returned |
| AUTH-10 |
Logout |
valid token |
200, refresh token revoked |
| AUTH-11 |
OTP send |
valid phone/email |
200, OTP generated |
| AUTH-12 |
OTP verify (correct) |
correct OTP |
200, user status → ACTIVE |
| AUTH-13 |
OTP verify (wrong/expired) |
wrong OTP |
400 |
| AUTH-14 |
OTP resend |
valid user |
200, new OTP |
| AUTH-15 |
Password reset flow |
sendResetCode → verify → resetPassword |
200 at each step |
Middleware Tests
| ID |
Test Case |
Expected |
| MW-01 |
Request without Authorization header |
401 |
| MW-02 |
Request with malformed JWT |
401 |
| MW-03 |
Request with expired JWT |
401 |
| MW-04 |
Valid token → user_id/role in Locals |
Next handler receives correct locals |
| MW-05 |
SuperAdminOnly — ADMIN role |
403 |
| MW-06 |
SuperAdminOnly — SUPER_ADMIN role |
Next called |
| MW-07 |
OnlyAdminAndAbove — STUDENT role |
403 |
| MW-08 |
OnlyAdminAndAbove — ADMIN role |
Next called |
3.2 User Management (P1)
| ID |
Test Case |
Input |
Expected |
| USR-01 |
Get user profile (own) |
auth token |
200, profile data |
| USR-02 |
Update user profile |
valid fields |
200, updated |
| USR-03 |
Upload profile picture (jpg) |
≤5MB jpg |
200, /static/profile_pictures/<uuid>.webp (if CloudConvert on) |
| USR-04 |
Upload profile picture (>5MB) |
6MB file |
400, file too large |
| USR-05 |
Upload profile picture (invalid type) |
.pdf file |
400, invalid file type |
| USR-06 |
Profile picture CloudConvert fallback |
CloudConvert disabled |
200, saved as original format |
| USR-07 |
Check profile completed |
user_id |
200, boolean |
| USR-08 |
Update knowledge level |
valid level |
200 |
| USR-09 |
Get all users (admin) |
admin token |
200, paginated list |
| USR-10 |
Delete user |
admin token, user_id |
200, user deleted |
| USR-11 |
Search user by name/phone |
search term |
200, results |
| USR-12 |
Check phone/email exist |
phone+email |
200, exists flags |
3.3 Admin Management (P1)
| ID |
Test Case |
Input |
Expected |
| ADM-01 |
Create admin (SUPER_ADMIN) |
valid admin data |
201 |
| ADM-02 |
Create admin (ADMIN role) |
valid data |
403, SuperAdminOnly |
| ADM-03 |
Get all admins |
SUPER_ADMIN token |
200, list |
| ADM-04 |
Update admin |
SUPER_ADMIN token |
200 |
3.4 Course Categories (P1)
| ID |
Test Case |
Input |
Expected |
| CAT-01 |
Create category |
{ "name": "Math" } |
201, category returned |
| CAT-02 |
Create category (empty name) |
{ "name": "" } |
400 |
| CAT-03 |
Get all categories (paginated) |
limit=10, offset=0 |
200, list + total_count |
| CAT-04 |
Get category by ID |
valid ID |
200 |
| CAT-05 |
Get category by ID (not found) |
ID=999999 |
404 |
| CAT-06 |
Update category |
new name |
200 |
| CAT-07 |
Delete category |
valid ID |
200 |
| CAT-08 |
Delete category (cascade) |
category with courses |
200, courses also deleted |
3.5 Courses (P1)
| ID |
Test Case |
Input |
Expected |
| CRS-01 |
Create course |
category_id + title |
201, sends notifications to students |
| CRS-02 |
Create course (invalid category) |
nonexistent category_id |
500 (FK violation) |
| CRS-03 |
Get course by ID |
valid ID |
200, with thumbnail |
| CRS-04 |
Get courses by category (paginated) |
category_id, limit, offset |
200 |
| CRS-05 |
Update course |
title, description, thumbnail |
200, activity log recorded |
| CRS-06 |
Upload course thumbnail (jpg→webp) |
valid image |
200, stored as webp if CloudConvert enabled |
| CRS-07 |
Upload course thumbnail (>10MB) |
large file |
400 |
| CRS-08 |
Delete course |
valid ID |
200, cascades to sub-courses |
3.6 Sub-Courses (P1)
| ID |
Test Case |
Input |
Expected |
| SUB-01 |
Create sub-course |
course_id, title, level=BEGINNER |
201, notifications sent |
| SUB-02 |
Create sub-course (invalid level) |
level=EXPERT |
500 (CHECK constraint) |
| SUB-03 |
List sub-courses by course |
course_id |
200, list + count |
| SUB-04 |
List active sub-courses |
— |
200, only is_active=true |
| SUB-05 |
Update sub-course |
title, display_order |
200 |
| SUB-06 |
Upload sub-course thumbnail |
valid image |
200 |
| SUB-07 |
Deactivate sub-course |
valid ID |
200, is_active=false |
| SUB-08 |
Delete sub-course |
valid ID |
200, cascades to videos |
3.7 Sub-Course Videos (P1)
| ID |
Test Case |
Input |
Expected |
| VID-01 |
Create video (direct URL) |
video_url, title |
201, provider=DIRECT |
| VID-02 |
Upload video file (with Vimeo) |
file + Vimeo configured |
201, provider=VIMEO, status=DRAFT |
| VID-03 |
Upload video (CloudConvert + Vimeo) |
file + both configured |
201, video compressed then uploaded |
| VID-04 |
Upload video (>500MB) |
large file |
400 |
| VID-05 |
Create video via Vimeo pull URL |
source_url |
201 |
| VID-06 |
Import from existing Vimeo ID |
vimeo_video_id |
201, thumbnail from Vimeo |
| VID-07 |
Publish video |
video_id |
200, is_published=true |
| VID-08 |
Get published videos by sub-course |
sub_course_id |
200, only published |
| VID-09 |
Update video metadata |
title, description, status |
200 |
| VID-10 |
Delete video |
video_id |
200 |
| VID-11 |
CloudConvert disabled fallback |
upload without CC |
201, no compression step |
3.8 Learning Tree (P2)
| ID |
Test Case |
Expected |
| TREE-01 |
Full learning tree (populated) |
200, nested courses → sub-courses |
| TREE-02 |
Full learning tree (empty) |
200, empty array |
3.9 Questions System (P2)
| ID |
Test Case |
Input |
Expected |
| Q-01 |
Create MCQ question |
question_text, type=MCQ, options |
201 |
| Q-02 |
Create TRUE_FALSE question |
type=TRUE_FALSE |
201 |
| Q-03 |
Create SHORT_ANSWER question |
type=SHORT_ANSWER, acceptable answers |
201 |
| Q-04 |
List questions (filtered) |
type=MCQ, difficulty=EASY |
200, filtered list |
| Q-05 |
Search questions |
search query |
200, matching results |
| Q-06 |
Update question with options |
new options |
200, old options replaced |
| Q-07 |
Delete question |
question_id |
200, cascades options/answers |
3.10 Question Sets (P2)
| ID |
Test Case |
Input |
Expected |
| QS-01 |
Create question set (PRACTICE) |
type=PRACTICE, owner |
201 |
| QS-02 |
Create question set (INITIAL_ASSESSMENT) |
type=INITIAL_ASSESSMENT |
201 |
| QS-03 |
Add question to set |
set_id, question_id |
201 |
| QS-04 |
Add duplicate question to set |
same set_id+question_id |
409/error |
| QS-05 |
Reorder question in set |
display_order |
200 |
| QS-06 |
Remove question from set |
set_id, question_id |
200 |
| QS-07 |
Get questions in set |
set_id |
200, ordered list |
| QS-08 |
Add user persona |
set_id, user_id |
200 |
| QS-09 |
Remove user persona |
set_id, user_id |
200 |
3.11 Subscription Plans (P0)
| ID |
Test Case |
Input |
Expected |
| PLAN-01 |
Create plan (admin) |
name, price, duration |
201 |
| PLAN-02 |
List plans (public, no auth) |
— |
200 |
| PLAN-03 |
Get plan by ID (public) |
plan_id |
200 |
| PLAN-04 |
Update plan |
new price |
200 |
| PLAN-05 |
Delete plan |
plan_id |
200 |
| PLAN-06 |
Duration calculation (MONTH) |
start + 3 MONTH |
+3 months |
| PLAN-07 |
Duration calculation (YEAR) |
start + 1 YEAR |
+1 year |
| PLAN-08 |
Duration calculation (DAY) |
start + 30 DAY |
+30 days |
3.12 User Subscriptions (P0)
| ID |
Test Case |
Input |
Expected |
| SUBS-01 |
Admin creates subscription (no payment) |
user_id, plan_id |
201, status=ACTIVE |
| SUBS-02 |
User subscribes with payment |
plan_id |
201, status=PENDING, payment initiated |
| SUBS-03 |
Get my subscription |
auth token |
200, current active sub |
| SUBS-04 |
Get subscription history |
auth token |
200, all subs |
| SUBS-05 |
Check subscription status |
auth token |
200, active/expired |
| SUBS-06 |
Cancel subscription |
subscription_id |
200, status=CANCELLED |
| SUBS-07 |
Set auto-renew on |
subscription_id, true |
200 |
| SUBS-08 |
Set auto-renew off |
subscription_id, false |
200 |
| SUBS-09 |
Expired subscription status check |
expired sub |
status=EXPIRED |
3.13 Payments — ArifPay (P0)
| ID |
Test Case |
Input |
Expected |
| PAY-01 |
Initiate payment |
plan_id, phone, email |
201, payment URL returned |
| PAY-02 |
Verify payment (success) |
valid session_id |
200, status=SUCCESS, subscription activated |
| PAY-03 |
Verify payment (failed) |
failed session |
200, status=FAILED |
| PAY-04 |
Get my payments |
auth token |
200, payment list |
| PAY-05 |
Cancel payment |
payment_id |
200, status=CANCELLED |
| PAY-06 |
Webhook handler (valid) |
valid ArifPay webhook |
200, payment updated |
| PAY-07 |
Webhook handler (invalid signature) |
tampered data |
400/401 |
| PAY-08 |
Get payment methods (public) |
— |
200, methods list |
| PAY-09 |
Direct payment (OTP flow) |
phone, method |
200, OTP sent |
| PAY-10 |
Verify direct payment OTP |
correct OTP |
200, payment confirmed |
3.14 Ratings (P1)
| ID |
Test Case |
Input |
Expected |
| RAT-01 |
Rate app (5 stars) |
target_type=app, target_id=0, stars=5 |
201 |
| RAT-02 |
Rate course (3 stars + review) |
target_type=course, target_id=1, stars=3, review="good" |
201 |
| RAT-03 |
Rate sub-course |
target_type=sub_course, target_id=1, stars=4 |
201 |
| RAT-04 |
Update existing rating (upsert) |
same user+target, new stars |
200, stars updated |
| RAT-05 |
Invalid stars (0) |
stars=0 |
400 |
| RAT-06 |
Invalid stars (6) |
stars=6 |
400 |
| RAT-07 |
Invalid target_type |
target_type=video |
400 |
| RAT-08 |
Get my rating for target |
target_type=course, target_id=1 |
200, my rating |
| RAT-09 |
Get my rating (not rated yet) |
— |
404 |
| RAT-10 |
Get ratings by target (paginated) |
target_type=course, limit=20 |
200, list |
| RAT-11 |
Get rating summary |
target_type=course, target_id=1 |
200, avg + count |
| RAT-12 |
Get rating summary (no ratings) |
unrated target |
200, avg=0, count=0 |
| RAT-13 |
Get all my ratings |
auth token |
200, list |
| RAT-14 |
Delete own rating |
rating_id |
200 |
| RAT-15 |
Delete other user's rating |
other's rating_id |
fails (no rows) |
3.15 Notifications (P2)
| ID |
Test Case |
Input |
Expected |
| NOT-01 |
WebSocket connection |
valid auth |
101 upgrade |
| NOT-02 |
Get user notifications |
auth token |
200, list |
| NOT-03 |
Mark as read |
notification_id |
200 |
| NOT-04 |
Mark all as read |
— |
200 |
| NOT-05 |
Mark as unread |
notification_id |
200 |
| NOT-06 |
Count unread |
— |
200, count |
| NOT-07 |
Delete user notifications |
— |
200 |
| NOT-08 |
Auto-notify on course creation |
create course |
students receive in-app notification |
| NOT-09 |
Auto-notify on sub-course creation |
create sub-course |
students notified |
| NOT-10 |
Send SMS (AfroSMS) |
phone + message |
200 |
| NOT-11 |
Register device token (push) |
device token |
200 |
| NOT-12 |
Unregister device token |
device token |
200 |
3.16 Issue Reporting (P2)
| ID |
Test Case |
Input |
Expected |
| ISS-01 |
Create issue |
subject, description, type=bug |
201 |
| ISS-02 |
Get my issues |
auth token |
200 |
| ISS-03 |
Get all issues (admin) |
admin token |
200 |
| ISS-04 |
Update issue status (admin) |
status=resolved |
200, notifications sent |
| ISS-05 |
Delete issue (admin) |
issue_id |
200 |
| ISS-06 |
Get user issues (admin) |
user_id |
200 |
3.17 Team Management (P2)
| ID |
Test Case |
Input |
Expected |
| TM-01 |
Team member login |
email + password |
200, tokens |
| TM-02 |
Create team member (admin) |
valid data, role=instructor |
201 |
| TM-03 |
Create team member (duplicate email) |
existing email |
409 |
| TM-04 |
Get all team members (admin) |
admin token |
200, list |
| TM-05 |
Get team member stats (admin) |
admin token |
200 |
| TM-06 |
Update team member status |
status=suspended |
200 |
| TM-07 |
Delete team member (SUPER_ADMIN only) |
super_admin token |
200 |
| TM-08 |
Delete team member (ADMIN) |
admin token |
403 |
| TM-09 |
Change team member password |
old+new password |
200 |
| TM-10 |
Get my team profile |
team auth token |
200 |
3.18 Vimeo Integration (P2)
| ID |
Test Case |
Input |
Expected |
| VIM-01 |
Get video info |
vimeo_video_id |
200, video details |
| VIM-02 |
Get embed code |
vimeo_video_id |
200, iframe HTML |
| VIM-03 |
Get transcode status |
vimeo_video_id |
200, status |
| VIM-04 |
Delete Vimeo video |
vimeo_video_id |
200 |
| VIM-05 |
Pull upload |
source URL |
201 |
| VIM-06 |
TUS upload |
file size + title |
201, upload link |
| VIM-07 |
OEmbed |
Vimeo URL |
200, embed data |
| VIM-08 |
Vimeo service disabled |
no config |
graceful handling |
3.19 CloudConvert (P2)
| ID |
Test Case |
Input |
Expected |
| CC-01 |
Video compression job |
video file |
compressed mp4 returned |
| CC-02 |
Image optimization job |
jpg file |
webp returned |
| CC-03 |
Job timeout |
slow job |
error after 30min (video) / 5min (image) |
| CC-04 |
Job failure |
invalid file |
error with task message |
| CC-05 |
Service disabled |
no config |
nil service, callers skip |
3.20 Activity Logs & Analytics (P3)
| ID |
Test Case |
Expected |
| LOG-01 |
Get activity logs (admin, filtered) |
200, filtered by action/resource/date |
| LOG-02 |
Get activity log by ID |
200 |
| LOG-03 |
Activity log recorded on course CRUD |
log entry with actor, IP, UA |
| LOG-04 |
Analytics dashboard (admin) |
200, dashboard data |
| LOG-05 |
MongoDB logs endpoint |
200, log entries |
3.21 Settings (P3)
| ID |
Test Case |
Expected |
| SET-01 |
Get settings (SUPER_ADMIN) |
200, settings list |
| SET-02 |
Get settings (ADMIN) |
403 |
| SET-03 |
Update settings |
200 |
| SET-04 |
Get setting by key |
200 |
3.22 Assessment (P2)
| ID |
Test Case |
Expected |
| ASM-01 |
Create assessment question |
201 |
| ASM-02 |
List assessment questions |
200 |
| ASM-03 |
Get assessment question by ID |
200 |
4. Cross-Cutting Concerns
4.1 Input Validation
| ID |
Test |
Expected |
| VAL-01 |
Empty required fields |
400 with field-level errors |
| VAL-02 |
Invalid email format |
400 |
| VAL-03 |
Negative IDs in path params |
400 |
| VAL-04 |
Non-numeric path params (e.g., /courses/abc) |
400 |
| VAL-05 |
Malformed JSON body |
400 |
| VAL-06 |
Oversized request body (>500MB) |
413 |
4.2 Cascading Deletes
| ID |
Test |
Expected |
| CAS-01 |
Delete category → courses deleted |
verified via get |
| CAS-02 |
Delete course → sub-courses deleted |
verified via get |
| CAS-03 |
Delete sub-course → videos deleted |
verified via get |
| CAS-04 |
Delete user → ratings deleted |
verified via get |
4.3 Concurrency / Race Conditions
| ID |
Test |
Expected |
| RACE-01 |
Two users rating same target simultaneously |
both succeed, no duplicates |
| RACE-02 |
Same user submitting duplicate rating |
upsert, only one record |
| RACE-03 |
Concurrent subscription creation for same user |
one succeeds or both idempotent |
4.4 File Upload Security
| ID |
Test |
Expected |
| SEC-01 |
Upload with spoofed content-type header (exe as jpg) |
400 (content sniffing rejects) |
| SEC-02 |
Path traversal in filename |
UUID-renamed, no traversal |
| SEC-03 |
Null bytes in filename |
handled safely |
5. Integration Test Scenarios (E2E Flows)
Flow 1: Student Registration → Subscription → Learning
1. POST /user/register → PENDING
2. POST /user/verify-otp → ACTIVE
3. POST /auth/customer-login → tokens
4. GET /subscription-plans → choose plan
5. POST /subscriptions/checkout → payment URL
6. POST /payments/webhook → payment SUCCESS
7. GET /subscriptions/status → ACTIVE
8. GET /course-management/learning-tree → courses
9. GET /sub-courses/:id/videos/published → watch
10. POST /ratings → rate course
Flow 2: Admin Content Management
1. POST /auth/admin-login → tokens
2. POST /course-management/categories → create category
3. POST /course-management/courses → create course
4. POST /courses/:id/thumbnail → upload thumbnail
5. POST /course-management/sub-courses → create sub-course
6. POST /sub-courses/:id/thumbnail → upload thumbnail
7. POST /course-management/videos/upload → upload video (CloudConvert → Vimeo)
8. PUT /videos/:id/publish → publish
9. GET /activity-logs → verify all actions logged
Flow 3: Issue Resolution
1. Student: POST /issues → bug report
2. Admin: GET /issues → see all
3. Admin: PATCH /issues/:id/status → resolved
4. Student: GET /notifications → sees status update
Flow 4: Team Onboarding
1. SUPER_ADMIN: POST /team/members → create instructor
2. Instructor: POST /team/login → tokens
3. Instructor: GET /team/me → profile
4. SUPER_ADMIN: PATCH /team/members/:id/status → active
6. Performance & Load Testing
| Test |
Target |
Threshold |
| Login throughput |
POST /auth/customer-login |
<200ms p95, 100 RPS |
| Course listing |
GET /categories/:id/courses |
<100ms p95 |
| Video listing |
GET /sub-courses/:id/videos |
<100ms p95 |
| Rating summary |
GET /ratings/summary |
<50ms p95 |
| Notification list |
GET /notifications |
<100ms p95 |
| File upload (5MB image) |
POST /user/:id/profile-picture |
<5s (without CC) |
| Concurrent ratings |
50 users rating simultaneously |
no errors, correct counts |
7. Test Environment Requirements
- Test database: Isolated PostgreSQL instance, migrated with all 17 migrations
- External services: Vimeo, CloudConvert, ArifPay, AfroSMS — stub/mock in tests
- Test data seeding: Create fixtures for users (each role), categories, courses, sub-courses, plans
- Cleanup: Each test suite truncates its tables or runs in a transaction with rollback
8. Recommended Test Tooling
| Purpose |
Tool |
| Go tests |
testing + testify/assert |
| HTTP testing |
net/http/httptest or Fiber's app.Test() |
| DB integration |
testcontainers-go (Postgres) |
| Mocking |
testify/mock or gomock |
| API collection |
Postman / Bruno (manual & CI) |
| Load testing |
k6 or vegeta |
| CI runner |
GitHub Actions / Gitea Actions |