New_Micro_Learning/backend/tests/Feature/Manager/ManagerAssignmentTest.php

122 خطوط
8.5 KiB
PHP

<?php
namespace Tests\Feature\Manager;
use App\Models\User;
use App\Modules\Assignments\Domain\Assignment;
use App\Modules\Courses\Domain\Course;
use App\Modules\Courses\Domain\CourseVersion;
use App\Modules\Courses\Domain\Enums\CourseVersionStatus;
use App\Modules\Identity\Domain\Enums\UserRole;
use App\Modules\Teams\Domain\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class ManagerAssignmentTest extends TestCase
{
use RefreshDatabase;
public function test_manager_context_only_contains_managed_audience_and_published_content(): void
{
[$manager, $team, $member, $outsider] = $this->teamContext();
[, $published] = $this->course($manager, CourseVersionStatus::Published, 'ایمنی منتشرشده');
$this->course($manager, CourseVersionStatus::Draft, 'پیش‌نویس خصوصی');
Sanctum::actingAs($manager);
$this->getJson('/api/v1/manager/assignment-contexts')->assertOk()
->assertJsonPath('data.content.0.id', $published->getKey())
->assertJsonPath('data.teams.0.id', $team->getKey())
->assertJsonPath('data.members.0.id', $member->getKey())
->assertJsonMissing(['id' => $outsider->getKey()])
->assertJsonMissing(['title' => 'پیش‌نویس خصوصی']);
}
public function test_manager_assignment_skips_duplicate_recipients_and_schedules_idempotent_notifications(): void
{
[$manager, $team, $member] = $this->teamContext();
$second = User::factory()->create(['organization_id' => $manager->organization_id, 'role' => UserRole::Learner, 'name' => 'عضو دوم']);
$team->members()->attach($second);
[, $version] = $this->course($manager, CourseVersionStatus::Published, 'ایمنی');
$existing = Assignment::query()->create(['organization_id' => $manager->organization_id, 'assignable_type' => 'course', 'assignable_id' => $version->getKey(), 'target_type' => 'individual', 'target_id' => $member->getKey(), 'status' => 'active', 'mandatory' => true, 'assigned_by' => $manager->getKey()]);
$existing->users()->attach($member, ['status' => 'assigned', 'assigned_at' => now(), 'progress' => 0, 'created_at' => now(), 'updated_at' => now()]);
Sanctum::actingAs($manager);
$assignment = $this->postJson('/api/v1/manager/assignments', [
'assignableType' => 'course', 'assignableId' => $version->getKey(), 'audienceType' => 'team', 'teamId' => $team->getKey(),
'mandatory' => true, 'dueAt' => now()->addDays(10)->toISOString(), 'notifyNow' => true,
'reminderDays' => 3, 'reminderOnDeadline' => true, 'onlyIfNotStarted' => true,
])->assertCreated()->assertJsonPath('data.result.assigned', 1)->assertJsonPath('data.result.duplicatesSkipped', 1)
->assertJsonPath('data.result.notificationsSent', 1)->assertJsonPath('data.result.notificationsScheduled', 2)->json('data.assignment.id');
$this->assertDatabaseHas('assignment_users', ['assignment_id' => $assignment, 'user_id' => $second->getKey()]);
$this->assertDatabaseMissing('assignment_users', ['assignment_id' => $assignment, 'user_id' => $member->getKey()]);
$this->assertDatabaseHas('in_app_notifications', ['recipient_id' => $second->getKey(), 'type' => 'learning.assigned']);
$this->assertDatabaseCount('notification_schedules', 2);
$this->assertDatabaseHas('audit_logs', ['actor_id' => $manager->getKey(), 'action' => 'manager.assignment.created', 'entity_id' => $assignment]);
}
public function test_manager_cannot_target_or_modify_people_outside_their_scope(): void
{
[$manager, , , $outsider] = $this->teamContext();
[, $version] = $this->course($manager, CourseVersionStatus::Published, 'ایمنی');
Sanctum::actingAs($manager);
$this->postJson('/api/v1/manager/assignments', [
'assignableType' => 'course', 'assignableId' => $version->getKey(), 'audienceType' => 'employees', 'userIds' => [$outsider->getKey()],
'mandatory' => false, 'notifyNow' => false, 'reminderOnDeadline' => false, 'onlyIfNotStarted' => false,
])->assertUnprocessable()->assertJsonValidationErrors('userIds');
$otherManager = User::factory()->create(['organization_id' => $manager->organization_id, 'role' => UserRole::Manager]);
$foreign = Assignment::query()->create(['organization_id' => $manager->organization_id, 'assignable_type' => 'course', 'assignable_id' => $version->getKey(), 'target_type' => 'individual', 'target_id' => $outsider->getKey(), 'status' => 'active', 'mandatory' => false, 'assigned_by' => $otherManager->getKey()]);
$foreign->users()->attach($outsider, ['status' => 'assigned', 'assigned_at' => now(), 'progress' => 0, 'created_at' => now(), 'updated_at' => now()]);
$this->patchJson('/api/v1/manager/assignments/'.$foreign->getKey().'/deadline', ['dueAt' => now()->addWeek()->toISOString()])->assertNotFound();
$this->postJson('/api/v1/manager/assignments/'.$foreign->getKey().'/reminders')->assertNotFound();
}
public function test_deadline_removal_cancels_schedules_and_manual_reminder_has_daily_cooldown_and_preferences(): void
{
[$manager, , $member] = $this->teamContext();
[, $version] = $this->course($manager, CourseVersionStatus::Published, 'ایمنی');
Sanctum::actingAs($manager);
$assignment = $this->postJson('/api/v1/manager/assignments', [
'assignableType' => 'course', 'assignableId' => $version->getKey(), 'audienceType' => 'employees', 'userIds' => [$member->getKey()],
'mandatory' => false, 'dueAt' => now()->addDays(8)->toISOString(), 'notifyNow' => false,
'reminderDays' => 2, 'reminderOnDeadline' => true, 'onlyIfNotStarted' => false,
])->assertCreated()->json('data.assignment.id');
$this->patchJson('/api/v1/manager/assignments/'.$assignment.'/deadline', ['dueAt' => null])
->assertOk()->assertJsonPath('data.assignment.dueAt', null)->assertJsonPath('data.notificationsScheduled', 0);
$this->assertDatabaseHas('notification_schedules', ['entity_id' => $assignment, 'status' => 'cancelled']);
$this->postJson('/api/v1/manager/assignments/'.$assignment.'/reminders')->assertOk()->assertJsonPath('data.sent', 1);
$this->postJson('/api/v1/manager/assignments/'.$assignment.'/reminders')->assertOk()->assertJsonPath('data.sent', 0)->assertJsonPath('data.skipped', 1);
DB::table('user_preferences')->insert(['id' => (string) str()->ulid(), 'user_id' => $member->getKey(), 'preferences' => json_encode(['deadlineReminders' => false]), 'created_at' => now(), 'updated_at' => now()]);
DB::table('in_app_notifications')->where('recipient_id', $member->getKey())->delete();
$this->travel(1)->day();
$this->postJson('/api/v1/manager/assignments/'.$assignment.'/reminders')->assertOk()->assertJsonPath('data.sent', 0)->assertJsonPath('data.skipped', 1);
}
/** @return array{User, Team, User, User} */
private function teamContext(): array
{
$manager = User::factory()->create(['role' => UserRole::Manager]);
$member = User::factory()->create(['organization_id' => $manager->organization_id, 'role' => UserRole::Learner, 'name' => 'عضو مجاز']);
$outsider = User::factory()->create(['organization_id' => $manager->organization_id, 'role' => UserRole::Learner, 'name' => 'عضو خارج محدوده']);
$team = Team::query()->create(['organization_id' => $manager->organization_id, 'name' => 'عملیات', 'status' => 'active', 'created_by' => $manager->getKey()]);
$team->managers()->attach($manager);
$team->members()->attach($member);
return [$manager, $team, $member, $outsider];
}
/** @return array{Course, CourseVersion} */
private function course(User $owner, CourseVersionStatus $status, string $title): array
{
$course = Course::query()->create(['organization_id' => $owner->organization_id, 'title' => $title, 'slug' => str()->slug($title).'-'.str()->lower(str()->random(5)), 'status' => $status === CourseVersionStatus::Published ? 'published' : 'draft', 'created_by' => $owner->getKey()]);
$version = CourseVersion::query()->create(['organization_id' => $owner->organization_id, 'course_id' => $course->getKey(), 'version_number' => 1, 'status' => $status, 'title' => $title, 'settings' => [], 'completion_rules' => [], 'published_at' => $status === CourseVersionStatus::Published ? now() : null]);
return [$course, $version];
}
}