feat: Initial commit for Saqel Platform architecture, backend, Docker, and documentation
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class GuardianSummaryTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
#[Test]
|
||||
public function guardian_can_view_linked_student_summary(): void
|
||||
{
|
||||
$guardian = User::factory()->guardian()->create();
|
||||
$student = User::factory()->create();
|
||||
|
||||
$guardian->students()->attach($student->id);
|
||||
|
||||
$response = $this->getJson("/api/v1/guardian/students/{$student->id}/summary", [
|
||||
'Authorization' => 'Bearer '.$guardian->api_token,
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('student.id', $student->id)
|
||||
->assertJsonStructure([
|
||||
'student' => ['id', 'name'],
|
||||
'lessons_progress',
|
||||
'weakest_concepts',
|
||||
'latest_ai_summary',
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function guardian_cannot_view_unlinked_student(): void
|
||||
{
|
||||
$guardian = User::factory()->guardian()->create();
|
||||
$otherStudent = User::factory()->create();
|
||||
|
||||
$this->getJson("/api/v1/guardian/students/{$otherStudent->id}/summary", [
|
||||
'Authorization' => 'Bearer '.$guardian->api_token,
|
||||
])->assertForbidden();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Contracts\AiService;
|
||||
use App\Jobs\AnalyzeStudentPerformance;
|
||||
use App\Models\Lesson;
|
||||
use App\Models\Question;
|
||||
use App\Models\QuestionOption;
|
||||
use App\Models\Quiz;
|
||||
use App\Models\User;
|
||||
use App\Services\ProgressTrackingService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StudentTrackingTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $student;
|
||||
|
||||
private Lesson $lesson;
|
||||
|
||||
private Quiz $quiz;
|
||||
|
||||
private Question $question;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->student = User::factory()->create();
|
||||
$this->lesson = Lesson::factory()->create(['duration_seconds' => 600]);
|
||||
$this->quiz = Quiz::factory()->for($this->lesson)->create();
|
||||
$this->question = Question::factory()->for($this->quiz)->create([
|
||||
'explanation' => 'لأن القانون ينص على ذلك',
|
||||
]);
|
||||
|
||||
QuestionOption::query()->create(['question_id' => $this->question->id, 'body' => 'A', 'is_correct' => true]);
|
||||
QuestionOption::query()->create(['question_id' => $this->question->id, 'body' => 'B', 'is_correct' => false]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function heartbeat_updates_progress_and_completes_lesson(): void
|
||||
{
|
||||
$response = $this->postJson('/api/v1/progress/heartbeat', [
|
||||
'lesson_id' => $this->lesson->id,
|
||||
'position_seconds' => 300,
|
||||
], $this->authHeaders());
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('watched_percentage', 50)
|
||||
->assertJsonPath('completed', false);
|
||||
|
||||
$this->postJson('/api/v1/progress/heartbeat', [
|
||||
'lesson_id' => $this->lesson->id,
|
||||
'position_seconds' => 580,
|
||||
], $this->authHeaders())->assertOk()->assertJsonPath('completed', true);
|
||||
|
||||
$this->assertDatabaseCount('lesson_progress', 1);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function quiz_attempt_is_graded_server_side_and_updates_mastery(): void
|
||||
{
|
||||
$wrongOption = QuestionOption::query()->where('is_correct', false)->firstOrFail();
|
||||
$rightOption = QuestionOption::query()->where('is_correct', true)->firstOrFail();
|
||||
|
||||
$response = $this->postJson("/api/v1/lessons/{$this->lesson->id}/quizzes/{$this->quiz->id}/attempts", [
|
||||
'answers' => [
|
||||
['question_id' => $this->question->id, 'selected_option_id' => $wrongOption->id, 'time_spent_seconds' => 12],
|
||||
],
|
||||
], $this->authHeaders());
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('score', 0)
|
||||
->assertJsonPath('results.0.is_correct', false)
|
||||
->assertJsonPath('results.0.explanation', 'لأن القانون ينص على ذلك');
|
||||
|
||||
$mastery = $this->student->conceptMastery()->firstOrFail();
|
||||
$this->assertSame(0.0, (float) $mastery->mastery_score);
|
||||
$this->assertSame(1, (int) $mastery->wrong_count);
|
||||
|
||||
$retry = $this->postJson("/api/v1/lessons/{$this->lesson->id}/quizzes/{$this->quiz->id}/attempts", [
|
||||
'answers' => [
|
||||
['question_id' => $this->question->id, 'selected_option_id' => $rightOption->id],
|
||||
],
|
||||
], $this->authHeaders());
|
||||
|
||||
$retry->assertOk()->assertJsonPath('score', 100);
|
||||
|
||||
$mastery->refresh();
|
||||
$this->assertSame(30.0, (float) $mastery->mastery_score);
|
||||
$this->assertSame(1, (int) $mastery->correct_count);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function analysis_endpoint_queues_job(): void
|
||||
{
|
||||
Queue::fake();
|
||||
|
||||
$response = $this->postJson('/api/v1/students/me/analysis', [], $this->authHeaders());
|
||||
|
||||
$response->assertAccepted();
|
||||
Queue::assertPushed(AnalyzeStudentPerformance::class);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function analysis_job_stores_ai_report_via_fake_service(): void
|
||||
{
|
||||
$this->app->bind(AiService::class, fn () => new class implements AiService
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $performance
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function analyzeStudentPerformance(array $performance): array
|
||||
{
|
||||
return [
|
||||
'weak_concepts' => [['name' => 'الاشتقاق', 'reason' => 'أخطاء متكررة', 'severity' => 'high']],
|
||||
'remedial_lesson_ids' => [],
|
||||
'recommended_actions' => ['إعادة مشاهدة الدرس الأول'],
|
||||
'motivation_note' => 'استمر',
|
||||
'summary' => 'ملخص تجريبي',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $activity
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function generateParentWeeklyReport(array $activity): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
(new AnalyzeStudentPerformance($this->student->id))
|
||||
->handle(app(ProgressTrackingService::class), app(AiService::class));
|
||||
|
||||
$this->assertDatabaseHas('ai_reports', [
|
||||
'user_id' => $this->student->id,
|
||||
'type' => 'weakness_analysis',
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function heartbeat_requires_authentication(): void
|
||||
{
|
||||
$this->postJson('/api/v1/progress/heartbeat', [
|
||||
'lesson_id' => $this->lesson->id,
|
||||
'position_seconds' => 10,
|
||||
])->assertUnauthorized();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function authHeaders(): array
|
||||
{
|
||||
return ['Authorization' => 'Bearer '.$this->student->api_token];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user