122 خطوط
8.5 KiB
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];
|
|
}
|
|
}
|