119 خطوط
7.2 KiB
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];
|
|
}
|
|
}
|