177 خطوط
7.1 KiB
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];
|
|
}
|
|
}
|