Complete technical documentation for developers working with Test2Go source code.
Test2Go is built on Laravel 13 with a clean MVC architecture enhanced by a service layer. This document provides detailed technical information for developers who need to understand, maintain, or extend the codebase.
test2go/
+-- app/
| +-- Http/
| | +-- Controllers/
| | | +-- Admin/ # Admin panel controllers
| | | | +-- DashboardController.php
| | | | +-- TestController.php
| | | | +-- QuestionController.php
| | | | +-- LiveSessionController.php
| | | | +-- UserController.php
| | | | +-- AnalyticsController.php
| | | +-- Participant/ # Participant controllers
| | | | +-- JoinController.php
| | | | +-- LiveQuizController.php
| | | | +-- AsyncTestController.php
| | | +-- PublicController.php
| | +-- Middleware/
| | | +-- ActiveAdmin.php # Check admin is active
| | | +-- ValidateParticipantToken.php
| | +-- Requests/ # Form requests
| | +-- StoreTestRequest.php
| | +-- StoreQuestionRequest.php
| | +-- SubmitAnswerRequest.php
| +-- Models/
| | +-- User.php # Admin users
| | +-- Test.php
| | +-- Question.php
| | +-- LiveSession.php
| | +-- Participant.php
| | +-- Attempt.php
| | +-- Answer.php
| +-- Services/
| | +-- JoinCodeService.php
| | +-- ScoringService.php
| | +-- LeaderboardService.php
| | +-- LiveQuizService.php
| | +-- AnalyticsService.php
| +-- Providers/
| +-- AppServiceProvider.php
+-- database/
| +-- migrations/
| | +-- 2024_01_01_000000_create_users_table.php
| | +-- 2024_01_01_000001_create_tests_table.php
| | +-- 2024_01_01_000002_create_questions_table.php
| | +-- 2024_01_01_000003_create_live_sessions_table.php
| | +-- 2024_01_01_000004_create_participants_table.php
| | +-- 2024_01_01_000005_create_attempts_table.php
| | +-- 2024_01_01_000006_create_answers_table.php
| +-- seeders/
| | +-- DatabaseSeeder.php
| | +-- AdminUserSeeder.php
| +-- factories/
+-- resources/
| +-- views/
| | +-- admin/ # Admin panel views
| | | +-- dashboard.blade.php
| | | +-- tests/
| | | +-- questions/
| | | +-- live-sessions/
| | | +-- users/
| | | +-- analytics/
| | +-- participant/ # Participant views
| | | +-- join.blade.php
| | | +-- lobby.blade.php
| | | +-- live-quiz.blade.php
| | | +-- async-test.blade.php
| | +-- layouts/
| | | +-- admin.blade.php
| | | +-- participant.blade.php
| | +-- auth/ # Authentication views
| +-- js/
| | +-- admin/
| | | +-- live-session.js
| | +-- participant/
| | +-- live-quiz.js # Polling logic
| | +-- async-test.js # Timer logic
| +-- css/
| +-- app.css
+-- routes/
| +-- web.php # All web routes
| +-- api.php # API routes for polling
+-- config/
| +-- test2go.php # Custom config
+-- public/
| +-- index.php
| +-- css/
| +-- js/
| +-- images/
+-- tests/ # PHPUnit tests
+-- Feature/
+-- Unit/
Test2Go follows a clean MVC architecture with an additional service layer:
+-----------------+
| Routes |
+-----------------+
|
+-----------------+
| Controllers | → Validation, Authorization, Response
+-----------------+
|
+-----------------+
| Services | → Business Logic
+-----------------+
|
+-----------------+
| Models | → Data Access, Relationships
+-----------------+
|
+-----------------+
| Database |
+-----------------+
Admin users table.
CREATE TABLE users (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP,
updated_at TIMESTAMP
);Purpose: Store admin user accounts
Key Fields: - is_active: Prevent admin
from logging in without deletion - email: Unique login
identifier
Test/quiz definitions.
CREATE TABLE tests (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
created_by BIGINT UNSIGNED NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
mode ENUM('live', 'async') NOT NULL,
duration_seconds INT UNSIGNED,
status ENUM('draft', 'published', 'archived') DEFAULT 'draft',
join_code VARCHAR(10) UNIQUE NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id)
);Purpose: Store test configurations
Key Fields: - mode: live or
async determines behavior - join_code: Unique
code for participants to join - status: Control test
availability - duration_seconds: Total time limit (for
async mode)
Test questions with options and answers.
CREATE TABLE questions (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
test_id BIGINT UNSIGNED NOT NULL,
question_text TEXT NOT NULL,
question_type ENUM('single_choice') DEFAULT 'single_choice',
options JSON NOT NULL,
correct_option_key VARCHAR(10) NOT NULL,
score INT UNSIGNED DEFAULT 10,
time_limit_seconds INT UNSIGNED,
order_no INT UNSIGNED NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (test_id) REFERENCES tests(id) ON DELETE CASCADE
);Purpose: Store questions for each test
Key Fields: - options: JSON array of
answer options {"A": "Option A", "B": "Option B"} -
correct_option_key: Key of the correct answer (e.g., “A”) -
score: Points awarded for correct answer -
time_limit_seconds: Optional per-question time limit (live
mode) - order_no: Question sequence
Options JSON Format:
{
"A": "Laravel",
"B": "Django",
"C": "Express.js",
"D": "Spring Boot"
}Live quiz session state.
CREATE TABLE live_sessions (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
test_id BIGINT UNSIGNED NOT NULL,
status ENUM('waiting', 'active', 'finished') DEFAULT 'waiting',
current_question_id BIGINT UNSIGNED NULL,
current_question_order INT UNSIGNED DEFAULT 0,
started_at TIMESTAMP NULL,
ended_at TIMESTAMP NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (test_id) REFERENCES tests(id) ON DELETE CASCADE,
FOREIGN KEY (current_question_id) REFERENCES questions(id)
);Purpose: Track live quiz progression
Key Fields: - status: Session state -
current_question_id: Active question being displayed -
current_question_order: For navigation
Participant records (guest users).
CREATE TABLE participants (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
test_id BIGINT UNSIGNED NOT NULL,
live_session_id BIGINT UNSIGNED NULL,
name VARCHAR(255) NOT NULL,
participant_token VARCHAR(64) UNIQUE NOT NULL,
score INT DEFAULT 0,
joined_at TIMESTAMP NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (test_id) REFERENCES tests(id) ON DELETE CASCADE,
FOREIGN KEY (live_session_id) REFERENCES live_sessions(id) ON DELETE SET NULL
);Purpose: Store participant information
Key Fields: - participant_token: Secure
token for authentication - live_session_id: NULL for async
mode - score: Cached total score
Test attempt records.
CREATE TABLE attempts (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
test_id BIGINT UNSIGNED NOT NULL,
participant_id BIGINT UNSIGNED NOT NULL,
live_session_id BIGINT UNSIGNED NULL,
mode ENUM('live', 'async') NOT NULL,
status ENUM('in_progress', 'submitted', 'auto_submitted') DEFAULT 'in_progress',
total_score INT DEFAULT 0,
correct_count INT DEFAULT 0,
wrong_count INT DEFAULT 0,
started_at TIMESTAMP NOT NULL,
submitted_at TIMESTAMP NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (test_id) REFERENCES tests(id) ON DELETE CASCADE,
FOREIGN KEY (participant_id) REFERENCES participants(id) ON DELETE CASCADE,
FOREIGN KEY (live_session_id) REFERENCES live_sessions(id) ON DELETE SET NULL
);Purpose: Track individual attempts
Key Fields: - status: Attempt state -
total_score: Cached score for leaderboard -
submitted_at: Completion timestamp
Individual answer submissions.
CREATE TABLE answers (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
attempt_id BIGINT UNSIGNED NOT NULL,
participant_id BIGINT UNSIGNED NOT NULL,
question_id BIGINT UNSIGNED NOT NULL,
selected_option_key VARCHAR(10) NOT NULL,
is_correct BOOLEAN DEFAULT FALSE,
score_awarded INT DEFAULT 0,
time_taken_seconds INT NULL,
answered_at TIMESTAMP NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
UNIQUE KEY unique_attempt_question (attempt_id, question_id),
FOREIGN KEY (attempt_id) REFERENCES attempts(id) ON DELETE CASCADE,
FOREIGN KEY (participant_id) REFERENCES participants(id) ON DELETE CASCADE,
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE
);Purpose: Store individual answers
Key Fields: - selected_option_key:
Participant’s answer - is_correct: Pre-calculated
correctness - score_awarded: Points earned -
time_taken_seconds: Response time
Unique Constraint: One answer per question per attempt
Services contain the business logic of Test2Go. Controllers delegate to services to keep code clean and testable.
File:
app/Services/JoinCodeService.php
Responsibilities: - Generate unique join codes - Validate join codes - Check code availability
Key Methods:
class JoinCodeService
{
/**
* Generate a unique join code
*/
public function generate(): string
{
do {
$code = strtoupper(Str::random(6));
} while (Test::where('join_code', $code)->exists());
return $code;
}
/**
* Validate and retrieve test by join code
*/
public function getTestByCode(string $code): ?Test
{
return Test::where('join_code', $code)
->where('status', 'published')
->first();
}
}File:
app/Services/ScoringService.php
Responsibilities: - Calculate answer correctness - Award points - Update attempt totals - Update participant score
Key Methods:
class ScoringService
{
/**
* Score a single answer
*/
public function scoreAnswer(
Attempt $attempt,
Question $question,
string $selectedOption
): Answer {
$isCorrect = $selectedOption === $question->correct_option_key;
$scoreAwarded = $isCorrect ? $question->score : 0;
$answer = Answer::create([
'attempt_id' => $attempt->id,
'participant_id' => $attempt->participant_id,
'question_id' => $question->id,
'selected_option_key' => $selectedOption,
'is_correct' => $isCorrect,
'score_awarded' => $scoreAwarded,
'answered_at' => now(),
]);
$this->updateAttemptScore($attempt);
return $answer;
}
/**
* Recalculate attempt totals
*/
public function updateAttemptScore(Attempt $attempt): void
{
$attempt->total_score = $attempt->answers()->sum('score_awarded');
$attempt->correct_count = $attempt->answers()->where('is_correct', true)->count();
$attempt->wrong_count = $attempt->answers()->where('is_correct', false)->count();
$attempt->save();
// Update participant cached score
$attempt->participant->update([
'score' => $attempt->total_score
]);
}
}File:
app/Services/LeaderboardService.php
Responsibilities: - Generate leaderboards - Rank participants - Provide summary statistics
Key Methods:
class LeaderboardService
{
/**
* Get leaderboard for a test/session
*/
public function getLeaderboard(Test $test, ?LiveSession $session = null): Collection
{
$query = Attempt::with('participant')
->where('test_id', $test->id)
->where('status', '!=', 'in_progress');
if ($session) {
$query->where('live_session_id', $session->id);
}
return $query->orderBy('total_score', 'desc')
->orderBy('submitted_at', 'asc')
->get();
}
/**
* Get live leaderboard (updates in real-time)
*/
public function getLiveLeaderboard(LiveSession $session): Collection
{
return Participant::where('live_session_id', $session->id)
->orderBy('score', 'desc')
->orderBy('joined_at', 'asc')
->get();
}
}File:
app/Services/LiveQuizService.php
Responsibilities: - Manage live session state - Control question progression - Handle participant joining - Provide polling updates
Key Methods:
class LiveQuizService
{
/**
* Start a live session
*/
public function startSession(Test $test): LiveSession
{
return LiveSession::create([
'test_id' => $test->id,
'status' => 'waiting',
'started_at' => now(),
]);
}
/**
* Start a question
*/
public function startQuestion(LiveSession $session, int $questionOrder): void
{
$question = $session->test->questions()
->where('order_no', $questionOrder)
->firstOrFail();
$session->update([
'status' => 'active',
'current_question_id' => $question->id,
'current_question_order' => $questionOrder,
]);
}
/**
* Get live state for polling
*/
public function getLiveState(LiveSession $session): array
{
return [
'status' => $session->status,
'current_question' => $session->currentQuestion,
'participant_count' => $session->participants()->count(),
'time_remaining' => $this->calculateTimeRemaining($session),
];
}
/**
* End session
*/
public function endSession(LiveSession $session): void
{
$session->update([
'status' => 'finished',
'ended_at' => now(),
]);
// Auto-submit all in-progress attempts
Attempt::where('live_session_id', $session->id)
->where('status', 'in_progress')
->update([
'status' => 'auto_submitted',
'submitted_at' => now(),
]);
}
}File:
app/Services/AnalyticsService.php
Responsibilities: - Generate statistics - Provide insights - Calculate metrics
Key Methods:
class AnalyticsService
{
/**
* Get test statistics
*/
public function getTestStats(Test $test): array
{
$attempts = Attempt::where('test_id', $test->id)
->where('status', '!=', 'in_progress')
->get();
return [
'total_participants' => $attempts->count(),
'average_score' => $attempts->avg('total_score'),
'highest_score' => $attempts->max('total_score'),
'lowest_score' => $attempts->min('total_score'),
'pass_rate' => $this->calculatePassRate($attempts, $test),
];
}
/**
* Get question analytics
*/
public function getQuestionStats(Question $question): array
{
$answers = Answer::where('question_id', $question->id)->get();
return [
'total_answers' => $answers->count(),
'correct_count' => $answers->where('is_correct', true)->count(),
'wrong_count' => $answers->where('is_correct', false)->count(),
'accuracy_rate' => $answers->count() > 0
? ($answers->where('is_correct', true)->count() / $answers->count()) * 100
: 0,
'average_time' => $answers->avg('time_taken_seconds'),
];
}
}Controllers handle HTTP requests and delegate to services. They should be thin and focused on request/response handling.
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreTestRequest;
use App\Services\JoinCodeService;
use App\Models\Test;
class TestController extends Controller
{
public function __construct(
private JoinCodeService $joinCodeService
) {}
/**
* Display tests list
*/
public function index()
{
$tests = Test::with('creator')
->latest()
->paginate(15);
return view('admin.tests.index', compact('tests'));
}
/**
* Store new test
*/
public function store(StoreTestRequest $request)
{
$test = Test::create([
...$request->validated(),
'created_by' => auth()->id(),
'join_code' => $this->joinCodeService->generate(),
]);
return redirect()
->route('admin.tests.show', $test)
->with('success', 'Test created successfully');
}
/**
* Update test
*/
public function update(StoreTestRequest $request, Test $test)
{
$test->update($request->validated());
return redirect()
->route('admin.tests.show', $test)
->with('success', 'Test updated successfully');
}
/**
* Delete test
*/
public function destroy(Test $test)
{
$test->delete();
return redirect()
->route('admin.tests.index')
->with('success', 'Test deleted successfully');
}
}<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Test extends Model
{
protected $fillable = [
'created_by',
'title',
'description',
'mode',
'duration_seconds',
'status',
'join_code',
];
protected $casts = [
'duration_seconds' => 'integer',
];
/**
* Creator relationship
*/
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* Questions relationship
*/
public function questions(): HasMany
{
return $this->hasMany(Question::class)->orderBy('order_no');
}
/**
* Live sessions relationship
*/
public function liveSessions(): HasMany
{
return $this->hasMany(LiveSession::class);
}
/**
* Participants relationship
*/
public function participants(): HasMany
{
return $this->hasMany(Participant::class);
}
/**
* Attempts relationship
*/
public function attempts(): HasMany
{
return $this->hasMany(Attempt::class);
}
/**
* Check if test is published
*/
public function isPublished(): bool
{
return $this->status === 'published';
}
/**
* Check if test is live mode
*/
public function isLive(): bool
{
return $this->mode === 'live';
}
/**
* Check if test is async mode
*/
public function isAsync(): bool
{
return $this->mode === 'async';
}
}User (Admin)
├─ hasMany → Test
Test
├─ belongsTo → User (creator)
├─ hasMany → Question
├─ hasMany → LiveSession
├─ hasMany → Participant
└─ hasMany → Attempt
Question
└─ belongsTo → Test
LiveSession
├─ belongsTo → Test
├─ belongsTo → Question (current)
└─ hasMany → Participant
Participant
├─ belongsTo → Test
├─ belongsTo → LiveSession (nullable)
├─ hasMany → Attempt
└─ hasMany → Answer
Attempt
├─ belongsTo → Test
├─ belongsTo → Participant
├─ belongsTo → LiveSession (nullable)
└─ hasMany → Answer
Answer
├─ belongsTo → Attempt
├─ belongsTo → Participant
└─ belongsTo → Question
{{-- resources/views/layouts/admin.blade.php --}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title') - Test2Go Admin</title>
<link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ asset('css/admin.css') }}" rel="stylesheet">
@stack('styles')
</head>
<body>
@include('layouts.partials.admin-header')
<div class="container-fluid">
<div class="row">
@include('layouts.partials.admin-sidebar')
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
@include('layouts.partials.alerts')
@yield('content')
</main>
</div>
</div>
<script src="{{ asset('js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ asset('js/admin.js') }}"></script>
@stack('scripts')
</body>
</html>
File:
resources/js/participant/live-quiz.js
class LiveQuizPoller {
constructor(sessionId, pollingInterval = 1000) {
this.sessionId = sessionId;
this.pollingInterval = pollingInterval;
this.isPolling = false;
this.currentQuestionId = null;
}
start() {
this.isPolling = true;
this.poll();
}
stop() {
this.isPolling = false;
}
async poll() {
if (!this.isPolling) return;
try {
const response = await fetch(`/api/live-quiz/${this.sessionId}/state`, {
headers: {
'Authorization': `Bearer ${this.getParticipantToken()}`,
'Accept': 'application/json',
}
});
if (response.ok) {
const data = await response.json();
this.handleStateUpdate(data);
}
} catch (error) {
console.error('Polling error:', error);
}
// Schedule next poll
setTimeout(() => this.poll(), this.pollingInterval);
}
handleStateUpdate(state) {
// Update UI based on state
if (state.status === 'finished') {
this.stop();
window.location.href = '/results';
return;
}
// New question started
if (state.current_question && state.current_question.id !== this.currentQuestionId) {
this.currentQuestionId = state.current_question.id;
this.displayQuestion(state.current_question);
}
// Update leaderboard
if (state.leaderboard) {
this.updateLeaderboard(state.leaderboard);
}
}
getParticipantToken() {
return localStorage.getItem('participant_token');
}
displayQuestion(question) {
// Render question UI
document.getElementById('question-text').textContent = question.question_text;
// Render options...
}
updateLeaderboard(leaderboard) {
// Update leaderboard UI
}
}
// Initialize
const poller = new LiveQuizPoller(sessionId, parseInt('{{ config("test2go.polling_interval_ms") }}'));
poller.start();Uses Laravel’s built-in authentication system.
Middleware:
app/Http/Middleware/ActiveAdmin.php
public function handle(Request $request, Closure $next)
{
if (!auth()->check()) {
return redirect()->route('login');
}
if (!auth()->user()->is_active) {
auth()->logout();
return redirect()->route('login')
->with('error', 'Your account has been deactivated.');
}
return $next($request);
}Token-based authentication for guest participants.
Middleware:
app/Http/Middleware/ValidateParticipantToken.php
public function handle(Request $request, Closure $next)
{
$token = $request->header('Authorization')
?? $request->input('token')
?? $request->cookie('participant_token');
if (!$token) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$participant = Participant::where('participant_token', $token)->first();
if (!$participant) {
return response()->json(['error' => 'Invalid token'], 401);
}
// Attach to request
$request->merge(['participant' => $participant]);
return $next($request);
}// routes/web.php
// Public routes
Route::get('/', [PublicController::class, 'index'])->name('home');
Route::get('/join', [JoinController::class, 'show'])->name('join.show');
Route::post('/join', [JoinController::class, 'join'])->name('join.submit');
// Admin routes
Route::middleware(['auth', 'active.admin'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::resource('tests', TestController::class);
Route::resource('tests.questions', QuestionController::class);
Route::resource('live-sessions', LiveSessionController::class);
Route::resource('users', UserController::class);
Route::get('/analytics', [AnalyticsController::class, 'index'])->name('analytics');
});
// Participant routes
Route::middleware('participant.token')->prefix('participant')->name('participant.')->group(function () {
Route::get('/lobby', [LiveQuizController::class, 'lobby'])->name('lobby');
Route::get('/live-quiz', [LiveQuizController::class, 'quiz'])->name('live-quiz');
Route::get('/async-test', [AsyncTestController::class, 'test'])->name('async-test');
Route::post('/submit-answer', [AsyncTestController::class, 'submitAnswer'])->name('submit-answer');
});
// Polling API
Route::middleware('participant.token')->prefix('api')->group(function () {
Route::get('/live-quiz/{session}/state', [LiveQuizController::class, 'getState']);
});Endpoint:
GET /api/live-quiz/{session}/state
Authentication: Participant token
Response:
{
"status": "active",
"current_question": {
"id": 5,
"question_text": "What is Laravel?",
"options": {
"A": "PHP Framework",
"B": "JavaScript Library",
"C": "Database",
"D": "Server"
},
"time_limit_seconds": 30
},
"time_remaining": 25,
"participant_count": 15,
"leaderboard": [
{
"name": "John Doe",
"score": 50
},
{
"name": "Jane Smith",
"score": 40
}
]
}Endpoint:
POST /api/participant/submit-answer
Authentication: Participant token
Request:
{
"attempt_id": 10,
"question_id": 5,
"selected_option": "A"
}Response:
{
"success": true,
"is_correct": true,
"score_awarded": 10,
"total_score": 50
}Test2Go uses AJAX polling instead of WebSockets for simplicity and compatibility.
Configuration: .env
REALTIME_MODE=polling
POLLING_INTERVAL_MS=1000
Flow:
Participant Browser Laravel Server
| |
+--- GET /api/live/{session}/state --->|
| +- Get Live State
| +- Check Current Question
| +- Get Leaderboard
|<--- JSON Response -------------------|
| |
+--- Update UI |
| |
+--- Poll (every 1s) ------->|
| |
File: config/test2go.php
<?php
return [
/*
|--------------------------------------------------------------------------
| Real-Time Mode
|--------------------------------------------------------------------------
|
| Determines how live updates are delivered. Options: polling
| Future versions may support websocket
|
*/
'realtime_mode' => env('REALTIME_MODE', 'polling'),
/*
|--------------------------------------------------------------------------
| Polling Interval
|--------------------------------------------------------------------------
|
| Interval in milliseconds between polling requests
|
*/
'polling_interval_ms' => env('POLLING_INTERVAL_MS', 1000),
/*
|--------------------------------------------------------------------------
| Join Code Length
|--------------------------------------------------------------------------
|
| Length of generated join codes
|
*/
'join_code_length' => 6,
/*
|--------------------------------------------------------------------------
| Default Test Duration
|--------------------------------------------------------------------------
|
| Default duration for tests in seconds
|
*/
'default_test_duration' => 1800, // 30 minutes
/*
|--------------------------------------------------------------------------
| Default Question Score
|--------------------------------------------------------------------------
|
| Default score for each question
|
*/
'default_question_score' => 10,
];# Run all tests
php artisan test
# Run specific test
php artisan test --filter TestTest
# With coverage
php artisan test --coveragetests/
+-- Feature/
| +-- AdminAuthTest.php
| +-- JoinTest.php
| +-- LeaderboardServiceTest.php
| +-- LocaleSwitchTest.php
| +-- QuestionImportTest.php
| +-- ScoringServiceTest.php
+-- Unit/
| +-- JoinCodeServiceTest.php
| +-- QuestionImportParserTest.php
+-- TestCase.php
<?php
namespace Tests\Feature\Admin;
use Tests\TestCase;
use App\Models\User;
use App\Models\Test;
use Illuminate\Foundation\Testing\RefreshDatabase;
class TestManagementTest extends TestCase
{
use RefreshDatabase;
public function test_admin_can_create_test()
{
$admin = User::factory()->create(['is_active' => true]);
$response = $this->actingAs($admin)->post(route('admin.tests.store'), [
'title' => 'Laravel Quiz',
'description' => 'Test your Laravel knowledge',
'mode' => 'live',
'duration_seconds' => 1800,
]);
$response->assertRedirect();
$this->assertDatabaseHas('tests', [
'title' => 'Laravel Quiz',
'created_by' => $admin->id,
]);
}
public function test_inactive_admin_cannot_access_dashboard()
{
$admin = User::factory()->create(['is_active' => false]);
$response = $this->actingAs($admin)->get(route('admin.dashboard'));
$response->assertRedirect(route('login'));
$this->assertGuest();
}
}This documentation provides a comprehensive overview of Test2Go’s architecture and implementation. For more information:
Last updated: May 2026