New_Micro_Learning/backend/tests/Feature/Taxonomy/ContentMappingTest.php

177 خطوط
7.1 KiB
PHP

<?php
namespace Tests\Feature\Taxonomy;
use App\Models\User;
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 App\Modules\Taxonomy\Domain\ContentTaxonomyMapping;
use App\Modules\Taxonomy\Domain\Enums\TaxonomyStatus;
use App\Modules\Taxonomy\Domain\TaxonomyNode;
use App\Modules\Taxonomy\Domain\TaxonomyType;
use DomainException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class ContentMappingTest extends TestCase
{
use RefreshDatabase;
public function test_designer_can_create_weighted_direct_lesson_mapping(): void
{
[$designer, $version, $lesson, $node] = $this->foundation();
Sanctum::actingAs($designer);
$this->postJson('/api/v1/content-mappings', [
'courseVersionId' => $version->getKey(),
'mappableType' => 'lesson',
'mappableId' => $lesson->getKey(),
'taxonomyNodeId' => $node->getKey(),
'mappingType' => 'develops',
'masteryLevel' => 'applied',
'weight' => 0.7,
'source' => 'manual',
])->assertCreated()
->assertJsonPath('data.weight', 0.7)
->assertJsonPath('data.masteryLevel', 'applied')
->assertJsonPath('data.confirmationStatus', 'confirmed')
->assertJsonPath('data.direct', true);
}
public function test_ai_mapping_stays_draft_until_designer_confirmation(): void
{
[$designer, $version, $lesson, $node] = $this->foundation();
Sanctum::actingAs($designer);
$this->postJson('/api/v1/content-mappings', [
'courseVersionId' => $version->getKey(),
'mappableType' => 'lesson',
'mappableId' => $lesson->getKey(),
'taxonomyNodeId' => $node->getKey(),
'mappingType' => 'related',
'weight' => 0.5,
'source' => 'ai_suggested',
'confidence' => 0.82,
])->assertCreated()
->assertJsonPath('data.confirmationStatus', 'draft')
->assertJsonPath('data.confidence', 0.82);
}
public function test_designer_can_remove_draft_mapping_but_not_published_mapping(): void
{
[$designer, $version, $lesson, $node] = $this->foundation();
Sanctum::actingAs($designer);
$payload = ['courseVersionId' => $version->getKey(), 'mappableType' => 'lesson', 'mappableId' => $lesson->getKey(), 'taxonomyNodeId' => $node->getKey(), 'mappingType' => 'develops', 'weight' => 1, 'source' => 'manual'];
$mapping = $this->postJson('/api/v1/content-mappings', $payload)->assertCreated()->json('data.id');
$this->deleteJson("/api/v1/content-mappings/{$mapping}")->assertNoContent();
$mapping = $this->postJson('/api/v1/content-mappings', $payload)->assertCreated()->json('data.id');
$version->update(['status' => CourseVersionStatus::Published, 'published_at' => now()]);
$this->deleteJson("/api/v1/content-mappings/{$mapping}")->assertUnprocessable()->assertJsonValidationErrors('courseVersionId');
}
public function test_mapping_target_must_belong_to_selected_course_version(): void
{
[$designer, $version, , $node] = $this->foundation();
[, , $otherLesson] = $this->courseStructure($designer, 2);
Sanctum::actingAs($designer);
$this->postJson('/api/v1/content-mappings', [
'courseVersionId' => $version->getKey(),
'mappableType' => 'lesson',
'mappableId' => $otherLesson->getKey(),
'taxonomyNodeId' => $node->getKey(),
'mappingType' => 'develops',
'weight' => 1,
'source' => 'manual',
])->assertUnprocessable()->assertJsonValidationErrors('mappableId');
}
public function test_published_version_and_its_existing_mappings_are_immutable(): void
{
[$designer, $version, $lesson, $node] = $this->foundation();
Sanctum::actingAs($designer);
$mappingId = $this->postJson('/api/v1/content-mappings', [
'courseVersionId' => $version->getKey(),
'mappableType' => 'lesson',
'mappableId' => $lesson->getKey(),
'taxonomyNodeId' => $node->getKey(),
'mappingType' => 'develops',
'weight' => 1,
'source' => 'manual',
])->assertCreated()->json('data.id');
$version->update(['status' => CourseVersionStatus::Published, 'published_at' => now()]);
$this->postJson('/api/v1/content-mappings', [
'courseVersionId' => $version->getKey(),
'mappableType' => 'lesson',
'mappableId' => $lesson->getKey(),
'taxonomyNodeId' => $node->getKey(),
'mappingType' => 'practices',
'weight' => 1,
'source' => 'manual',
])->assertUnprocessable()->assertJsonValidationErrors('courseVersionId');
$this->expectException(DomainException::class);
ContentTaxonomyMapping::query()->findOrFail($mappingId)->update(['weight' => 0.5]);
}
private function foundation(): array
{
$designer = User::factory()->create(['role' => UserRole::CourseDesigner]);
[$course, $version, $lesson] = $this->courseStructure($designer, 1);
$type = TaxonomyType::query()->create([
'organization_id' => $designer->organization_id,
'key' => 'skill',
'name' => 'Skill',
'status' => TaxonomyStatus::Active,
]);
$node = TaxonomyNode::query()->create([
'organization_id' => $designer->organization_id,
'taxonomy_type_id' => $type->getKey(),
'name' => 'Giving feedback',
'status' => TaxonomyStatus::Active,
]);
return [$designer, $version, $lesson, $node, $course];
}
private function courseStructure(User $designer, int $versionNumber): array
{
$course = Course::query()->create([
'organization_id' => $designer->organization_id,
'title' => 'Leadership',
'slug' => 'leadership-'.$versionNumber,
'created_by' => $designer->getKey(),
]);
$version = CourseVersion::query()->create([
'organization_id' => $designer->organization_id,
'course_id' => $course->getKey(),
'version_number' => $versionNumber,
'status' => CourseVersionStatus::Draft,
'title' => 'Leadership',
]);
$module = CourseModule::query()->create([
'organization_id' => $designer->organization_id,
'course_version_id' => $version->getKey(),
'title' => 'Communication',
'position' => 1,
]);
$lesson = Lesson::query()->create([
'organization_id' => $designer->organization_id,
'course_version_id' => $version->getKey(),
'course_module_id' => $module->getKey(),
'title' => 'Feedback',
'position' => 1,
]);
return [$course, $version, $lesson];
}
}