Backend Testing Guide
Comprehensive guide to testing the Tech Tutor backend, including automated tests, test setup, and manual testing.
Automated Testing
Running Tests
# Run all tests
php artisan test
# Run specific test file
php artisan test tests/Feature/AuthFlowTest.php
# Run specific test method
php artisan test tests/Feature/AuthFlowTest.php --filter=test_user_can_register
# Run with coverage report
php artisan test --coverage
# Run watch mode (auto-rerun on changes)
php artisan test --watch
# Run unit tests only
php artisan test tests/Unit
# Run feature tests only
php artisan test tests/FeatureTest Structure
tests/
├── Feature/
│ ├── AuthFlowTest.php # Registration, login, OAuth, password reset
│ ├── CourseFlowTest.php # Course CRUD, enrollment, progress, certificates
│ ├── CommerceFlowTest.php # Payments, reviews, purchase flow
│ ├── QuizFlowTest.php # Quiz creation, attempts, scoring
│ ├── InstructorDashboardFlowTest.php # Instructor analytics
│ ├── AdminPanelFlowTest.php # Admin features, moderation
│ ├── LessonCommentFlowTest.php # Comments, threads, moderation
│ └── UserInviteFlowTest.php # User invitations
├── Unit/
│ └── ExampleTest.php # Unit test examples
├── TestCase.php # Base test class
└── CreatesApplication.php # Application bootstrapTest Coverage Summary
| Feature | Tests | Location |
|---|---|---|
| Authentication | Register, login, OAuth, password reset, email verification | AuthFlowTest |
| Course Management | CRUD, publishing, enrollment, progress | CourseFlowTest |
| Learning Path | Enrollment → Progress → Certificate | CourseFlowTest |
| Payments | Internal payment, Stripe checkout, receipt generation | CommerceFlowTest |
| Quizzes | Creation, attempts, scoring, analytics | QuizFlowTest |
| Instructor Dashboard | Metrics, analytics, certificates | InstructorDashboardFlowTest |
| Admin Panel | User management, moderation, dashboard | AdminPanelFlowTest |
| Comments | Creation, threads, replies, moderation | LessonCommentFlowTest |
| User Invites | Invite creation, acceptance, role assignment | UserInviteFlowTest |
Test Environment Setup
Test configuration in phpunit.xml:
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/> <!-- In-memory SQLite -->
<env name="MAIL_MAILER" value="array"/> <!-- No real emails -->
<env name="QUEUE_CONNECTION" value="sync"/> <!-- Synchronous jobs -->
<env name="CACHE_STORE" value="array"/> <!-- In-memory cache -->
</php>Benefits:
- Fast: No real database I/O
- Isolated: Fresh database for each test
- No Side Effects: No real emails sent
- Deterministic: Same results every run
Test Best Practices
1. Use RefreshDatabase
use Illuminate\Foundation\Testing\RefreshDatabase;
class AuthFlowTest extends TestCase {
use RefreshDatabase; // Fresh DB state for each test
public function test_user_can_register() {
// Test code
}
}2. Create Test Data with Factories
// Create a user
$user = User::factory()->create();
// Create with specific attributes
$admin = User::factory()->admin()->create(['email' => 'admin@test.com']);
// Create multiple
$courses = Course::factory()->count(5)->create();3. Authenticate Test Requests
// Act as authenticated user
$this->actingAs($user, 'sanctum')
->postJson('/api/courses', [...])
->assertStatus(201);
// Or with token
$token = $user->createToken('test')->plainTextToken;
$this->withHeader('Authorization', "Bearer $token")
->postJson('/api/courses', [...]);4. Mock External Services
// Mock CAPTCHA
CAPTCHA::shouldReceive('verify')
->with('test-token', '127.0.0.1')
->andReturn(true);
// Mock Stripe
Stripe::setApiKey('sk_test_...');
Stripe\Charge::shouldReceive('create')
->andReturn((object)['id' => 'ch_test_123']);
// Mock OAuth
Socialite::shouldReceive('driver')
->with('google')
->andReturn(...);5. Assert JSON Responses
$response = $this->getJson('/api/courses/1');
$response
->assertStatus(200)
->assertJsonPath('data.id', 1)
->assertJsonPath('data.title', 'Course Title')
->assertJsonStructure(['data' => ['id', 'title', 'description']]);6. Assert Database State
// Record exists
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
// Record doesn't exist
$this->assertDatabaseMissing('users', ['email' => 'test@example.com']);
// Count records
$this->assertEquals(5, User::count());7. Assert Notifications
use Illuminate\Support\Facades\Notification;
Notification::fake();
// Trigger action
$this->postJson('/api/auth/register', [...]);
// Assert notification sent
Notification::assertSentTo(
$user,
EnrollmentCreatedNotification::class
);
Notification::assertSentTimes(
EmailVerificationCodeNotification::class,
1
);Common Test Patterns
Testing Authorization
public function test_student_cannot_edit_others_review() {
$review = Review::factory()->for($user1)->create();
$this->actingAs($user2, 'sanctum')
->patchJson("/api/courses/{$review->course_id}/reviews/{$review->id}", [...])
->assertStatus(403);
}
public function test_admin_can_edit_any_review() {
$review = Review::factory()->create();
$admin = User::factory()->admin()->create();
$this->actingAs($admin, 'sanctum')
->patchJson("/api/courses/{$review->course_id}/reviews/{$review->id}", [...])
->assertStatus(200);
}Testing Validation
public function test_registration_requires_email() {
$this->postJson('/api/auth/register', [
'name' => 'Test User',
'password' => 'password123',
'password_confirmation' => 'password123',
])
->assertStatus(422)
->assertJsonValidationErrors('email');
}Testing State Transitions
public function test_payment_grants_enrollment_access() {
$user = User::factory()->create();
$course = Course::factory()->create(['price' => 99.99]);
// Before payment - can't enroll
$this->actingAs($user, 'sanctum')
->postJson("/api/courses/{$course->id}/enrollments", [])
->assertStatus(402);
// Create payment
Payment::factory()->paid()->for($user)->for($course)->create();
// After payment - can enroll
$this->actingAs($user, 'sanctum')
->postJson("/api/courses/{$course->id}/enrollments", [])
->assertStatus(201);
}Manual API Testing (cURL)
Setup
BASE_URL="http://127.0.0.1:8000/api"Get Sanctum Token Quickly (Local Dev)
Use this endpoint to mint a token for an existing user in local debug environment.
Endpoint: POST /api/dev/token
Example:
curl -X POST "$BASE_URL/dev/token" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"email": "student@techtutor.test",
"token_name": "frontend-dev-student",
"abilities": ["*"]
}'If you set DEV_TOKEN_KEY in backend .env, include:
-H "X-Dev-Key: your-dev-key"Response contains a token value. Use it as bearer token in requests below.
Auth Security Checks
GET /api/app-config returns the runtime settings used by the frontend auth forms.
If CAPTCHA_ENABLED=true in .env, the auth endpoints require captcha_token. In local development, the demo CAPTCHA helper button in the frontend sends the placeholder token demo-captcha-token, which the backend accepts only in local/testing environments.
If CAPTCHA_ENABLED=false, the auth endpoints accept requests without CAPTCHA and the frontend hides the CAPTCHA UI.
Rate limiting is applied to auth routes server-side, so repeated registration/login attempts may return throttle errors.
Base Variables
For bash/zsh:
BASE_URL="http://127.0.0.1:8000/api"
TOKEN="YOUR_SANCTUM_TOKEN"For PowerShell:
$BASE_URL = "http://127.0.0.1:8000/api"
$TOKEN = "YOUR_SANCTUM_TOKEN"Public Endpoints
Check runtime config:
curl -X GET "$BASE_URL/app-config" \
-H "Accept: application/json"List courses:
curl -X GET "$BASE_URL/courses"Search/filter the catalog:
curl -X GET "$BASE_URL/courses?q=laravel&category=backend&price_type=paid&sort=price_desc"MeiliSearch smoke test with live indexing:
- Keep a queue worker running in another terminal:
php artisan queue:listen --tries=1 --timeout=0- Create or update a published course through the API using an instructor token:
curl -X POST "$BASE_URL/courses" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"title": "MeiliSearch Demo Course",
"slug": "meilisearch-demo-course",
"description": "Used to verify live indexing",
"category": "backend",
"level": "beginner",
"language": "en",
"price": 25,
"is_published": true
}'- Search the catalog for the new course title or category:
curl -X GET "$BASE_URL/courses?q=meili&category=backend&sort=price_desc" \
-H "Accept: application/json"If MeiliSearch is configured correctly, the new course should appear in the response after the queue job is processed.
Get one course:
curl -X GET "$BASE_URL/courses/1"Register a new user with CAPTCHA token when CAPTCHA is enabled:
curl -X POST "$BASE_URL/auth/register" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"name": "New Student",
"email": "new.student@example.com",
"password": "password123",
"password_confirmation": "password123",
"role": "student",
"token_name": "manual-test",
"captcha_token": "demo-captcha-token"
}'Login with email/password and CAPTCHA token when CAPTCHA is enabled:
curl -X POST "$BASE_URL/auth/login" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"email":"student@techtutor.test","password":"password","token_name":"manual-test","captcha_token":"demo-captcha-token"}'If CAPTCHA is disabled, remove captcha_token from the auth examples and requests.
Forgot/reset password:
curl -X POST "$BASE_URL/auth/forgot-password" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"email":"student@techtutor.test"}'Use the token from the reset email:
curl -X POST "$BASE_URL/auth/reset-password" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"email":"student@techtutor.test","token":"RESET_TOKEN","password":"new-password123","password_confirmation":"new-password123"}'Authenticated Endpoints (Sanctum)
Fetch the current authenticated user:
curl -X GET "$BASE_URL/auth/me" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"Resend email verification:
curl -X POST "$BASE_URL/auth/email/resend" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"Logout the current token:
curl -X POST "$BASE_URL/auth/logout" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"Fetch instructor dashboard metrics:
curl -X GET "$BASE_URL/instructor/dashboard" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"Dashboard metrics are calculated live. Revenue currently comes from internal paid payment records.
Fetch admin platform monitoring:
curl -X GET "$BASE_URL/admin/platform-dashboard" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"Admin platform metrics are calculated live. Payment/revenue values currently come from internal payment records and should be refined around provider-backed payment states when Stripe/LiqPay webhooks are added.
Create course:
curl -X POST "$BASE_URL/courses" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"title": "Laravel API Basics",
"slug": "laravel-api-basics",
"description": "Intro backend course",
"subtitle": "Build REST APIs with Laravel",
"category": "backend",
"level": "beginner",
"language": "en",
"thumbnail_path": "/courses/laravel-api-basics.png",
"duration_minutes": 180,
"price": 49.99,
"is_published": true
}'Create module:
curl -X POST "$BASE_URL/courses/1/modules" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"title": "Module 1",
"slug": "module-1",
"position": 1
}'Create lesson:
curl -X POST "$BASE_URL/modules/1/lessons" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"title": "Lesson 1",
"slug": "lesson-1",
"type": "text",
"content": "Hello TechTutor",
"position": 1,
"is_preview": false
}'Enroll in course:
curl -X POST "$BASE_URL/courses/1/enrollments" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"If the backend mailer is configured, a new enrollment sends the student an email confirmation.
Paid courses require purchase before enrollment. Without a paid payment, enrollment returns 402.
Purchase a paid course and receive a receipt:
curl -X POST "$BASE_URL/courses/1/payments" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"provider": "manual_demo",
"amount": 49.99,
"currency": "USD",
"transaction_id": "txn_manual_demo_1001",
"provider_payload": {
"source": "manual_test"
}
}'The purchase response includes both payment and enrollment. The payment includes receipt_number, receipt_issued_at, and access_granted_at.
Fetch one receipt/payment record:
curl -X GET "$BASE_URL/payments/1" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"Create a Stripe Checkout session for a paid course:
curl -X POST "$BASE_URL/courses/1/payments/stripe-checkout" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"success_url": "http://127.0.0.1:5173/payment/success",
"cancel_url": "http://127.0.0.1:5173/payment/cancel"
}'This returns a pending local payment and a Stripe checkout.url. Course access is granted after the verified checkout.session.completed webhook is received.
Run Stripe webhooks locally:
stripe login
stripe listen --forward-to http://127.0.0.1:8000/api/stripe/webhook --events checkout.session.completedCopy the printed whsec_... signing secret into backend .env:
STRIPE_WEBHOOK_SECRET=whsec_...Then clear cached config if needed:
php artisan config:clearAfter a student opens the Stripe Checkout URL and completes a test payment, Stripe sends checkout.session.completed to the local webhook. The backend verifies the signature, marks the pending Stripe payment as paid, issues the receipt, and activates enrollment.
For hosted/staging/production use, create an event destination in Stripe Workbench with endpoint URL:
https://your-domain.example/api/stripe/webhookUse the Workbench endpoint secret as STRIPE_WEBHOOK_SECRET on that deployed environment.
Update lesson progress:
curl -X POST "$BASE_URL/lessons/1/progress" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"progress_percent": 100
}'When the last lesson in a course is completed, the response includes an issued certificate. Until then, certificate is null.
The first certificate issuance also sends the student an email notification.
List certificates:
curl -X GET "$BASE_URL/certificates" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"Manually check certificate eligibility for a course:
curl -X POST "$BASE_URL/courses/1/certificate" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"Create quiz:
curl -X POST "$BASE_URL/courses/1/quizzes" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"title": "Final Quiz",
"description": "Module checkpoint",
"pass_score": 60,
"is_published": true,
"questions": [
{
"type": "single_choice",
"prompt": "Which package protects API routes?",
"options": [
{ "key": "sanctum", "text": "Laravel Sanctum", "is_correct": true },
{ "key": "vite", "text": "Vite" }
]
},
{
"type": "multiple_choice",
"prompt": "Which items are backend responsibilities?",
"points": 2,
"options": [
{ "key": "policies", "text": "Policies", "is_correct": true },
{ "key": "middleware", "text": "Middleware", "is_correct": true },
{ "key": "tailwind", "text": "Tailwind classes" }
]
}
]
}'Submit quiz attempt. Use question IDs from the quiz response as answer keys:
curl -X POST "$BASE_URL/quizzes/1/attempts" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"answers": {
"1": "sanctum",
"2": ["middleware", "policies"]
}
}'The backend calculates score and passed; clients cannot submit their own score.
Submitting a quiz attempt also sends the student an email with the calculated result.
Fetch quiz analytics as the course instructor or admin:
curl -X GET "$BASE_URL/quizzes/1/analytics" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"Quiz analytics are calculated from existing attempts and questions when requested. They are not stored as a separate statistics table.
Create review:
curl -X POST "$BASE_URL/courses/1/reviews" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"rating": 5,
"comment": "Great course"
}'Create payment/purchase:
curl -X POST "$BASE_URL/courses/1/payments" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"provider": "manual_demo",
"amount": 49.99,
"currency": "USD",
"transaction_id": "txn_1001"
}'Publish-request workflow (testing)
Use this to test the instructor→admin publish request flow.
Prereqs:
- Run migrations/seeds so users exist and DB is ready (
php artisan migrate --seed). - The dev token helper is available only in local debug:
app()->isLocal()andconfig('app.debug')must be true. - If
DEV_TOKEN_KEYis set, include headerX-Dev-Key: <key>when requesting a dev token.
- Mint a dev token for the instructor (use seeded instructor email/password):
curl -s -X POST "$BASE_URL/dev/token" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-Dev-Key: $DEV_TOKEN_KEY" \
-d '{"email":"backend@techtutor.test","password":"password","token_name":"dev-instructor"}'- Instructor creates a draft and requests publishing:
curl -X POST "$BASE_URL/courses" \
-H "Authorization: Bearer $INSTRUCTOR_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"title":"Draft Course","price":10.0,"request_publish":true}'- Admin mints a token and accepts the publish request by publishing the course:
curl -s -X POST "$BASE_URL/dev/token" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"email":"admin@techtutor.test","password":"password","token_name":"dev-admin"}'
curl -X PATCH "$BASE_URL/courses/{courseId}" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"is_published":true}'- Admin declines a pending publish request (optional reason):
curl -X PATCH "$BASE_URL/courses/{courseId}" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"decline_publish":true,"publish_request_declined_reason":"Needs more content"}'Notes:
- The dev token endpoint returns
userandrolein the response so you can mint tokens for different roles. - The publish-request records are stored in the
publish_requeststable and can be inspected directly in the DB for test assertions. - Accepting or declining a publish request sends an email notification to the instructor who requested publishing.
- Automated tests use notification fakes, so local SMTP/Gmail credentials are not used by
php artisan test.
Google OAuth Testing
Prerequisites
Environment Setup:
# In .env (backend root)
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_REDIRECT_URI=http://127.0.0.1:8000/auth/google/callbackObtain credentials from Google Cloud Console:
- Create a new OAuth 2.0 credential (Web Application)
- Set authorized redirect URIs:
http://127.0.0.1:8000/auth/google/callback - Copy Client ID and Secret to
.env
Frontend Setup:
The frontend must serve on a specific origin (e.g., http://localhost:5173) and open the OAuth flow in a popup window.
Manual Testing in Browser
Test Flow:
Start the OAuth redirect:
- Visit:
http://127.0.0.1:8000/auth/google/redirect?return_to=http://localhost:5173 - You will be redirected to Google's login page
- Visit:
Authenticate with Google:
- Sign in with a test Google account
- Approve the requested permissions
Receive callback response:
- Backend processes
/auth/google/callback - Backend returns a popup HTML with
window.postMessage()containing auth payload - Popup closes automatically if opener window is available
- Fallback message displayed if popup or opener detection fails
- Backend processes
Expected Payload:
javascript{ type: 'techtutor-google-auth', message: 'Google sign-in completed successfully.', payload: { token: 'SANCTUM_BEARER_TOKEN', token_type: 'Bearer', user: { id: 1, email: 'user@gmail.com', name: 'User Name', role: 'student', email_verified_at: '2026-05-12T...' } } }
Automated Testing with Mocking
The test suite includes a complete OAuth flow test using Mockery:
Test File: tests/Feature/AuthFlowTest.php
Run OAuth tests:
php artisan test tests/Feature/AuthFlowTest.php --filter=googleWhat the test covers:
- Mocked Google user data (email, name, nickname)
- Session-based return URL handling
- New user creation on first OAuth
- Existing user update on repeat OAuth
- Sanctum token generation
- User email auto-verification
- Popup HTML response with postMessage payload
- Database persistence validation
Testing with Postman (Manual)
Step 1: Start OAuth Redirect
GET http://127.0.0.1:8000/auth/google/redirect?return_to=http://localhost:5173- Set
Follow redirects: OFFto capture the redirect location - Google will redirect you to its login page
- Complete Google authentication in browser
Step 2: Callback Simulation (if needed)
If manually testing callback flow without full Google auth:
GET http://127.0.0.1:8000/auth/google/callbackNote: This requires a valid authenticated Google session or Socialite mock setup.
Common Issues & Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
Google Client not configured | Missing GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET | Add credentials to .env and restart server |
Invalid redirect URI | Callback URL doesn't match Google Console config | Update authorized redirect URIs in Google Cloud Console |
Google sign-in did not return an email address | Google account has no public email | Use a different test Google account with public email |
This account is banned | User exists in DB but is banned | Un-ban user via admin dashboard or DB query |
403 Forbidden on callback | Session data corrupted or missing | Clear browser session/cookies and restart OAuth flow |
window.postMessage failed | Frontend origin not whitelisted | Verify return_to parameter matches frontend origin |
Password Fallback Scenario (OAuth Unavailable)
Important: Users created via Google OAuth receive a random password they never see. If Google OAuth becomes unavailable, users can still log in via email/password using the password reset flow.
Test the fallback scenario:
Create a user via Google OAuth (as described above)
User forgets/doesn't know password:
bashcurl -X POST "$BASE_URL/auth/forgot-password" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"email":"user@gmail.com"}'Response:
json{ "message": "We have emailed your password reset link." }User receives password reset email with a unique token (in testing, check the database or mail log)
User resets password:
bashcurl -X POST "$BASE_URL/auth/reset-password" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "email":"user@gmail.com", "token":"RESET_TOKEN_FROM_EMAIL", "password":"mynewpassword123", "password_confirmation":"mynewpassword123" }'User can now log in with email/password:
bashcurl -X POST "$BASE_URL/auth/login" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "email":"user@gmail.com", "password":"mynewpassword123", "token_name":"fallback-login" }'
Why random password for OAuth?
- Prevents security issues where someone knowing your email could brute-force a weak password
- Google handles authentication; you never need the random password
- Password reset provides a secure fallback if OAuth is unavailable
Postman Testing
Environment Variables
Create a Postman environment with:
baseUrl=http://127.0.0.1:8000/apitoken= your Sanctum tokencourseId=1moduleId=1lessonId=1quizId=1
Authorization Setup
At collection level:
- Type: Bearer Token
- Token:
{{token}}
For public endpoints, set Auth to No Auth per request.
Suggested Request Order
GET {{baseUrl}}/coursesPOST {{baseUrl}}/auth/loginGET {{baseUrl}}/auth/mePOST {{baseUrl}}/coursesPOST {{baseUrl}}/courses/{{courseId}}/modulesPOST {{baseUrl}}/modules/{{moduleId}}/lessonsPOST {{baseUrl}}/courses/{{courseId}}/enrollmentsPOST {{baseUrl}}/lessons/{{lessonId}}/progressPOST {{baseUrl}}/courses/{{courseId}}/quizzesPOST {{baseUrl}}/quizzes/{{quizId}}/attemptsPOST {{baseUrl}}/courses/{{courseId}}/reviewsPOST {{baseUrl}}/courses/{{courseId}}/payments
Quick Troubleshooting
401 Unauthorized: missing or invalid bearer token.403 Forbidden: role/enrollment restrictions blocked access.422 Unprocessable Entity: request body failed validation.404 Not Found: nested IDs do not belong to each other.