resolver = app(NotificationRecipientResolver::class); } // ────────────────────────────────────────────── // trigger_user 타입 // ────────────────────────────────────────────── public function test_trigger_user_resolves_from_context(): void { $user = User::factory()->create(); $result = $this->resolver->resolve( [['type' => 'trigger_user']], ['trigger_user_id' => $user->id, 'trigger_user' => $user] ); $this->assertCount(1, $result); $this->assertEquals($user->id, $result->first()->id); } public function test_trigger_user_resolves_from_id_only(): void { $user = User::factory()->create(); $result = $this->resolver->resolve( [['type' => 'trigger_user']], ['trigger_user_id' => $user->id] ); $this->assertCount(1, $result); $this->assertEquals($user->id, $result->first()->id); } public function test_trigger_user_returns_empty_without_context(): void { $result = $this->resolver->resolve( [['type' => 'trigger_user']], [] ); $this->assertCount(0, $result); } // ────────────────────────────────────────────── // related_user 타입 // ────────────────────────────────────────────── public function test_related_user_resolves_from_context(): void { $author = User::factory()->create(); $result = $this->resolver->resolve( [['type' => 'related_user', 'relation' => 'author']], ['related_users' => ['author' => $author]] ); $this->assertCount(1, $result); $this->assertEquals($author->id, $result->first()->id); } public function test_related_user_returns_empty_when_relation_missing(): void { $result = $this->resolver->resolve( [['type' => 'related_user', 'relation' => 'author']], ['related_users' => []] ); $this->assertCount(0, $result); } public function test_related_user_supports_collection(): void { $users = User::factory()->count(2)->create(); $result = $this->resolver->resolve( [['type' => 'related_user', 'relation' => 'managers']], ['related_users' => ['managers' => $users]] ); $this->assertCount(2, $result); } // ────────────────────────────────────────────── // role 타입 // ────────────────────────────────────────────── public function test_role_resolves_users_by_role_identifier(): void { $role = Role::factory()->create(['identifier' => 'test_admin']); $admin1 = User::factory()->create(); $admin2 = User::factory()->create(); $role->users()->attach([$admin1->id, $admin2->id]); $result = $this->resolver->resolve( [['type' => 'role', 'value' => 'test_admin']], [] ); $this->assertCount(2, $result); $this->assertTrue($result->contains('id', $admin1->id)); $this->assertTrue($result->contains('id', $admin2->id)); } public function test_role_falls_back_to_super_admin(): void { Role::factory()->create(['identifier' => 'empty_role']); $superAdmin = User::factory()->create(['is_super' => true]); $result = $this->resolver->resolve( [['type' => 'role', 'value' => 'empty_role']], [] ); $this->assertCount(1, $result); $this->assertEquals($superAdmin->id, $result->first()->id); } public function test_role_nonexistent_falls_back_to_super_admin(): void { $superAdmin = User::factory()->create(['is_super' => true]); $result = $this->resolver->resolve( [['type' => 'role', 'value' => 'nonexistent_role']], [] ); $this->assertCount(1, $result); $this->assertEquals($superAdmin->id, $result->first()->id); } // ────────────────────────────────────────────── // specific_users 타입 // ────────────────────────────────────────────── public function test_specific_users_resolves_by_uuid(): void { $user1 = User::factory()->create(); $user2 = User::factory()->create(); User::factory()->create(); $result = $this->resolver->resolve( [['type' => 'specific_users', 'value' => [$user1->uuid, $user2->uuid]]], [] ); $this->assertCount(2, $result); $this->assertTrue($result->contains('uuid', $user1->uuid)); $this->assertTrue($result->contains('uuid', $user2->uuid)); } public function test_specific_users_returns_empty_for_empty_ids(): void { $result = $this->resolver->resolve( [['type' => 'specific_users', 'value' => []]], [] ); $this->assertCount(0, $result); } // ────────────────────────────────────────────── // exclude_trigger_user 필터 // ────────────────────────────────────────────── public function test_exclude_trigger_user_filters_out_trigger(): void { $triggerUser = User::factory()->create(); $role = Role::factory()->create(['identifier' => 'admin_excl_test']); $admin = User::factory()->create(); $role->users()->attach([$triggerUser->id, $admin->id]); $result = $this->resolver->resolve( [['type' => 'role', 'value' => 'admin_excl_test', 'exclude_trigger_user' => true]], ['trigger_user_id' => $triggerUser->id] ); $this->assertCount(1, $result); $this->assertEquals($admin->id, $result->first()->id); $this->assertFalse($result->contains('id', $triggerUser->id)); } public function test_no_exclude_keeps_trigger_user(): void { $triggerUser = User::factory()->create(); $role = Role::factory()->create(['identifier' => 'admin_no_excl']); $role->users()->attach([$triggerUser->id]); $result = $this->resolver->resolve( [['type' => 'role', 'value' => 'admin_no_excl']], ['trigger_user_id' => $triggerUser->id] ); $this->assertCount(1, $result); $this->assertEquals($triggerUser->id, $result->first()->id); } // ────────────────────────────────────────────── // 복합 규칙 // ────────────────────────────────────────────── public function test_multiple_rules_combine_recipients(): void { $triggerUser = User::factory()->create(); $relatedUser = User::factory()->create(); $result = $this->resolver->resolve( [ ['type' => 'trigger_user'], ['type' => 'related_user', 'relation' => 'author'], ], [ 'trigger_user_id' => $triggerUser->id, 'trigger_user' => $triggerUser, 'related_users' => ['author' => $relatedUser], ] ); $this->assertCount(2, $result); $this->assertTrue($result->contains('id', $triggerUser->id)); $this->assertTrue($result->contains('id', $relatedUser->id)); } public function test_duplicate_users_are_deduplicated(): void { $user = User::factory()->create(); $result = $this->resolver->resolve( [ ['type' => 'trigger_user'], ['type' => 'specific_users', 'value' => [$user->uuid]], ], ['trigger_user_id' => $user->id, 'trigger_user' => $user] ); $this->assertCount(1, $result); } public function test_empty_rules_returns_empty(): void { $result = $this->resolver->resolve([], []); $this->assertCount(0, $result); } public function test_unknown_type_is_skipped_gracefully(): void { $user = User::factory()->create(); $result = $this->resolver->resolve( [ ['type' => 'unknown_type'], ['type' => 'trigger_user'], ], ['trigger_user_id' => $user->id, 'trigger_user' => $user] ); $this->assertCount(1, $result); $this->assertEquals($user->id, $result->first()->id); } // ────────────────────────────────────────────── // 비회원(guest) trigger_user 폴백 // ────────────────────────────────────────────── public function test_trigger_user_falls_back_to_guest_recipient_when_no_user_id(): void { $result = $this->resolver->resolve( [['type' => 'trigger_user']], ['guest_recipient' => ['email' => 'guest@example.com', 'name' => '홍길동', 'locale' => 'ko']] ); $this->assertCount(1, $result); $this->assertInstanceOf(\App\Notifications\GuestNotifiable::class, $result->first()); $this->assertSame('guest@example.com', $result->first()->email); } public function test_trigger_user_prefers_member_over_guest_recipient(): void { $user = User::factory()->create(); $result = $this->resolver->resolve( [['type' => 'trigger_user']], [ 'trigger_user_id' => $user->id, 'trigger_user' => $user, 'guest_recipient' => ['email' => 'guest@example.com'], ] ); // 회원이 있으면 게스트 폴백을 타지 않는다 $this->assertCount(1, $result); $this->assertInstanceOf(User::class, $result->first()); $this->assertEquals($user->id, $result->first()->id); } public function test_invalid_guest_recipient_yields_no_recipient(): void { $result = $this->resolver->resolve( [['type' => 'trigger_user']], ['guest_recipient' => ['email' => '']] ); $this->assertCount(0, $result); } public function test_duplicate_guest_email_dedupes_to_one(): void { // 같은 이메일 게스트가 두 규칙(둘 다 trigger_user)으로 들어와도 1명으로 dedupe $result = $this->resolver->resolve( [['type' => 'trigger_user'], ['type' => 'trigger_user']], ['guest_recipient' => ['email' => 'guest@example.com']] ); $this->assertCount(1, $result); } public function test_member_and_guest_coexist_without_collapsing(): void { $user = User::factory()->create(); // role 규칙으로 회원, trigger_user 규칙으로 게스트 — null-id 게스트가 회원과 뭉개지지 않아야 함 $role = Role::factory()->create(['identifier' => 'guest-coexist-role']); $user->roles()->attach($role->id); $result = $this->resolver->resolve( [ ['type' => 'role', 'value' => 'guest-coexist-role'], ['type' => 'trigger_user'], ], ['guest_recipient' => ['email' => 'guest@example.com']] ); $this->assertCount(2, $result); } public function test_exclude_trigger_user_does_not_break_on_guest(): void { // 게스트 수신자 + exclude_trigger_user 규칙이 함께 있어도 예외 없이 통과 $result = $this->resolver->resolve( [ ['type' => 'trigger_user'], ['type' => 'role', 'value' => 'nonexistent-role', 'exclude_trigger_user' => true], ], ['guest_recipient' => ['email' => 'guest@example.com']] ); // 게스트는 user_id 가 없어 exclude 대상이 아니므로 그대로 유지 $this->assertGreaterThanOrEqual(1, $result->count()); $this->assertTrue( $result->contains(fn ($r) => $r instanceof \App\Notifications\GuestNotifiable) ); } }