Skip to content

Backend Testing Guide

Comprehensive guide to testing the Tech Tutor backend, including automated tests, test setup, and manual testing.

Automated Testing

Running Tests

bash
# 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/Feature

Test 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 bootstrap

Test Coverage Summary

FeatureTestsLocation
AuthenticationRegister, login, OAuth, password reset, email verificationAuthFlowTest
Course ManagementCRUD, publishing, enrollment, progressCourseFlowTest
Learning PathEnrollment → Progress → CertificateCourseFlowTest
PaymentsInternal payment, Stripe checkout, receipt generationCommerceFlowTest
QuizzesCreation, attempts, scoring, analyticsQuizFlowTest
Instructor DashboardMetrics, analytics, certificatesInstructorDashboardFlowTest
Admin PanelUser management, moderation, dashboardAdminPanelFlowTest
CommentsCreation, threads, replies, moderationLessonCommentFlowTest
User InvitesInvite creation, acceptance, role assignmentUserInviteFlowTest

Test Environment Setup

Test configuration in phpunit.xml:

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

php
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

php
// 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

php
// 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

php
// 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

php
$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

php
// 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

php
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

php
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

php
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

php
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

bash
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:

bash
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:

bash
-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:

bash
BASE_URL="http://127.0.0.1:8000/api"
TOKEN="YOUR_SANCTUM_TOKEN"

For PowerShell:

powershell
$BASE_URL = "http://127.0.0.1:8000/api"
$TOKEN = "YOUR_SANCTUM_TOKEN"

Public Endpoints

Check runtime config:

bash
curl -X GET "$BASE_URL/app-config" \
  -H "Accept: application/json"

List courses:

bash
curl -X GET "$BASE_URL/courses"

Search/filter the catalog:

bash
curl -X GET "$BASE_URL/courses?q=laravel&category=backend&price_type=paid&sort=price_desc"

MeiliSearch smoke test with live indexing:

  1. Keep a queue worker running in another terminal:
bash
php artisan queue:listen --tries=1 --timeout=0
  1. Create or update a published course through the API using an instructor token:
bash
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
  }'
  1. Search the catalog for the new course title or category:
bash
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:

bash
curl -X GET "$BASE_URL/courses/1"

Register a new user with CAPTCHA token when CAPTCHA is enabled:

bash
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:

bash
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:

bash
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:

bash
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:

bash
curl -X GET "$BASE_URL/auth/me" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"

Resend email verification:

bash
curl -X POST "$BASE_URL/auth/email/resend" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"

Logout the current token:

bash
curl -X POST "$BASE_URL/auth/logout" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"

Fetch instructor dashboard metrics:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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:

bash
curl -X GET "$BASE_URL/payments/1" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"

Create a Stripe Checkout session for a paid course:

bash
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:

bash
stripe login
stripe listen --forward-to http://127.0.0.1:8000/api/stripe/webhook --events checkout.session.completed

Copy the printed whsec_... signing secret into backend .env:

env
STRIPE_WEBHOOK_SECRET=whsec_...

Then clear cached config if needed:

bash
php artisan config:clear

After 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:

txt
https://your-domain.example/api/stripe/webhook

Use the Workbench endpoint secret as STRIPE_WEBHOOK_SECRET on that deployed environment.

Update lesson progress:

bash
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:

bash
curl -X GET "$BASE_URL/certificates" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"

Manually check certificate eligibility for a course:

bash
curl -X POST "$BASE_URL/courses/1/certificate" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"

Create quiz:

bash
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:

bash
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:

bash
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:

bash
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:

bash
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() and config('app.debug') must be true.
  • If DEV_TOKEN_KEY is set, include header X-Dev-Key: <key> when requesting a dev token.
  1. Mint a dev token for the instructor (use seeded instructor email/password):
bash
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"}'
  1. Instructor creates a draft and requests publishing:
bash
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}'
  1. Admin mints a token and accepts the publish request by publishing the course:
bash
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}'
  1. Admin declines a pending publish request (optional reason):
bash
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 user and role in the response so you can mint tokens for different roles.
  • The publish-request records are stored in the publish_requests table 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:

bash
# 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/callback

Obtain credentials from Google Cloud Console:

  1. Create a new OAuth 2.0 credential (Web Application)
  2. Set authorized redirect URIs: http://127.0.0.1:8000/auth/google/callback
  3. 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:

  1. 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
  2. Authenticate with Google:

    • Sign in with a test Google account
    • Approve the requested permissions
  3. 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
  4. 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:

bash
php artisan test tests/Feature/AuthFlowTest.php --filter=google

What 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: OFF to 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/callback

Note: This requires a valid authenticated Google session or Socialite mock setup.

Common Issues & Troubleshooting

IssueCauseSolution
Google Client not configuredMissing GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRETAdd credentials to .env and restart server
Invalid redirect URICallback URL doesn't match Google Console configUpdate authorized redirect URIs in Google Cloud Console
Google sign-in did not return an email addressGoogle account has no public emailUse a different test Google account with public email
This account is bannedUser exists in DB but is bannedUn-ban user via admin dashboard or DB query
403 Forbidden on callbackSession data corrupted or missingClear browser session/cookies and restart OAuth flow
window.postMessage failedFrontend origin not whitelistedVerify 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:

  1. Create a user via Google OAuth (as described above)

  2. User forgets/doesn't know password:

    bash
    curl -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."
    }
  3. User receives password reset email with a unique token (in testing, check the database or mail log)

  4. User resets password:

    bash
    curl -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"
      }'
  5. User can now log in with email/password:

    bash
    curl -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/api
  • token = your Sanctum token
  • courseId = 1
  • moduleId = 1
  • lessonId = 1
  • quizId = 1

Authorization Setup

At collection level:

  • Type: Bearer Token
  • Token: {{token}}

For public endpoints, set Auth to No Auth per request.

Suggested Request Order

  1. GET {{baseUrl}}/courses
  2. POST {{baseUrl}}/auth/login
  3. GET {{baseUrl}}/auth/me
  4. POST {{baseUrl}}/courses
  5. POST {{baseUrl}}/courses/{{courseId}}/modules
  6. POST {{baseUrl}}/modules/{{moduleId}}/lessons
  7. POST {{baseUrl}}/courses/{{courseId}}/enrollments
  8. POST {{baseUrl}}/lessons/{{lessonId}}/progress
  9. POST {{baseUrl}}/courses/{{courseId}}/quizzes
  10. POST {{baseUrl}}/quizzes/{{quizId}}/attempts
  11. POST {{baseUrl}}/courses/{{courseId}}/reviews
  12. POST {{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.