378 lines
13 KiB
PHP
378 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Services;
|
|
|
|
use App\Models\Role;
|
|
use App\Models\User;
|
|
use App\Services\NotificationRecipientResolver;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* NotificationRecipientResolver 테스트
|
|
*
|
|
* 수신자 타입별(trigger_user, related_user, role, specific_users)
|
|
* 해석 로직과 exclude_trigger_user 필터를 검증합니다.
|
|
*/
|
|
class NotificationRecipientResolverTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private NotificationRecipientResolver $resolver;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->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)
|
|
);
|
|
}
|
|
}
|