New_Micro_Learning/backend/tests/Feature/Assessments/AssessmentAuthoringTest.php

119 خطوط
7.2 KiB
PHP

<?php
namespace Tests\Feature\Assessments;
use App\Models\User;
use App\Modules\Assessments\Domain\Assessment;
use App\Modules\Courses\Domain\Course;
use App\Modules\Courses\Domain\CourseModule;
use App\Modules\Courses\Domain\CourseVersion;
use App\Modules\Courses\Domain\Enums\CourseVersionStatus;
use App\Modules\Courses\Domain\Lesson;
use App\Modules\Identity\Domain\Enums\UserRole;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class AssessmentAuthoringTest extends TestCase
{
use RefreshDatabase;
public function test_designer_creates_bank_question_and_reuses_it_in_configured_assessment(): void
{
[$designer, $version, $lesson] = $this->foundation();
Sanctum::actingAs($designer);
$bankId = $this->postJson('/api/v1/question-bank', [
'type' => 'single_choice', 'prompt' => 'کدام گزینه الزامی است؟', 'topic' => 'ایمنی',
'difficulty' => 'intermediate', 'tags' => ['PPE'], 'explanation' => 'کلاه ایمنی الزامی است.',
'configuration' => ['options' => [
['id' => 'a', 'text' => 'کلاه ایمنی', 'correct' => true, 'feedback' => 'درست'],
['id' => 'b', 'text' => 'عینک آفتابی', 'correct' => false, 'feedback' => 'نادرست'],
]],
])->assertCreated()->assertJsonPath('data.usageCount', 0)->json('data.id');
$assessmentId = $this->postJson('/api/v1/assessments', [
'courseVersionId' => $version->getKey(), 'lessonId' => $lesson->getKey(), 'title' => 'آزمون ایمنی',
'settings' => $this->settings(),
])->assertCreated()->assertJsonPath('data.settings.passingScore', 80)->json('data.id');
$this->postJson("/api/v1/assessments/{$assessmentId}/questions", ['sourceQuestionId' => $bankId])
->assertCreated()->assertJsonPath('data.sourceQuestionId', $bankId)->assertJsonPath('data.position', 1);
$this->getJson('/api/v1/question-bank?topic=ایمنی')->assertOk()
->assertJsonPath('data.0.id', $bankId)->assertJsonPath('data.0.usageCount', 1);
$this->getJson("/api/v1/assessments/{$assessmentId}")->assertOk()
->assertJsonPath('data.questionCount', 1)->assertJsonPath('data.questions.0.prompt', 'کدام گزینه الزامی است؟');
}
public function test_question_schema_rejects_invalid_answers_and_broken_scenario_graph(): void
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
Sanctum::actingAs($designer);
$this->postJson('/api/v1/question-bank', [
'type' => 'single_choice', 'prompt' => 'Invalid',
'configuration' => ['options' => [
['id' => 'a', 'text' => 'A', 'correct' => true], ['id' => 'b', 'text' => 'B', 'correct' => true],
]],
])->assertUnprocessable()->assertJsonValidationErrors('configuration.options');
$this->postJson('/api/v1/question-bank', [
'type' => 'branching_scenario', 'prompt' => 'Broken graph',
'configuration' => [
'startNodeId' => 'start',
'nodes' => [
['id' => 'start', 'type' => 'scene', 'title' => 'شروع', 'choices' => [['text' => 'ادامه', 'targetNodeId' => 'missing']]],
['id' => 'result', 'type' => 'result', 'title' => 'پایان', 'choices' => []],
],
],
])->assertUnprocessable()->assertJsonValidationErrors('configuration.nodes');
}
public function test_designer_creates_two_level_question_categories_and_filters_bank(): void
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
Sanctum::actingAs($designer);
$categoryId = $this->postJson('/api/v1/question-categories', ['name' => 'مهارت'])->assertCreated()->json('data.id');
$subcategoryId = $this->postJson('/api/v1/question-categories', ['name' => 'اکسل', 'parentId' => $categoryId])->assertCreated()->assertJsonPath('data.parentId', $categoryId)->json('data.id');
$questionId = $this->postJson('/api/v1/question-bank', [
'type' => 'true_false', 'prompt' => 'تابع SUM برای جمع است.', 'categoryId' => $categoryId, 'subcategoryId' => $subcategoryId,
'configuration' => ['answer' => true, 'feedback' => 'درست'],
])->assertCreated()->assertJsonPath('data.categoryName', 'مهارت')->assertJsonPath('data.subcategoryName', 'اکسل')->json('data.id');
$this->getJson('/api/v1/question-categories')->assertOk()->assertJsonPath('data.0.children.0.name', 'اکسل');
$this->getJson('/api/v1/question-bank?categoryId='.$categoryId)->assertOk()->assertJsonPath('data.0.id', $questionId);
$this->getJson('/api/v1/question-bank?subcategoryId='.$subcategoryId)->assertOk()->assertJsonPath('data.0.id', $questionId);
$this->deleteJson('/api/v1/question-categories/'.$subcategoryId)->assertStatus(409);
}
public function test_published_assessments_are_immutable_and_tenant_scoped(): void
{
[$designer, $version, $lesson] = $this->foundation(CourseVersionStatus::Published);
$assessment = Assessment::query()->create([
'organization_id' => $designer->organization_id, 'course_version_id' => $version->getKey(),
'lesson_id' => $lesson->getKey(), 'title' => 'Published', 'settings' => $this->settings(),
]);
Sanctum::actingAs($designer);
$this->patchJson('/api/v1/assessments/'.$assessment->getKey(), ['title' => 'Changed'])
->assertUnprocessable()->assertJsonValidationErrors('courseVersionId');
$foreignDesigner = User::factory()->create(['role' => UserRole::CourseDesigner]);
Sanctum::actingAs($foreignDesigner);
$this->getJson('/api/v1/assessments/'.$assessment->getKey())->assertNotFound();
}
/** @return array{User, CourseVersion, Lesson} */
private function foundation(CourseVersionStatus $status = CourseVersionStatus::Draft): array
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
$course = Course::query()->create(['organization_id' => $designer->organization_id, 'title' => 'Safety', 'slug' => 'safety', 'created_by' => $designer->getKey()]);
$version = CourseVersion::query()->create(['organization_id' => $designer->organization_id, 'course_id' => $course->getKey(), 'version_number' => 1, 'status' => $status, 'title' => 'Safety']);
$module = CourseModule::query()->create(['organization_id' => $designer->organization_id, 'course_version_id' => $version->getKey(), 'title' => 'Module', 'position' => 1]);
$lesson = Lesson::query()->create(['organization_id' => $designer->organization_id, 'course_version_id' => $version->getKey(), 'course_module_id' => $module->getKey(), 'title' => 'Lesson', 'position' => 1]);
return [$designer, $version, $lesson];
}
/** @return array<string, mixed> */
private function settings(): array
{
return ['randomSelection' => false, 'questionPoolSize' => null, 'shuffleQuestions' => true, 'shuffleOptions' => true, 'passingScore' => 80, 'attemptLimit' => 2, 'feedbackMode' => 'after_submission', 'timeLimitSeconds' => 600];
}
}