v7.0.0-beta.1 release
This commit is contained in:
commit
6595fd0eb5
3967 files changed
+1875383
No files matched your search
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Http\Requests\ActivityLog;
|
||||
|
||||
use App\Enums\ActivityLogType;
|
||||
use App\Http\Requests\ActivityLog\ActivityLogIndexRequest;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* ActivityLogIndexRequest 검증 테스트
|
||||
*
|
||||
* 활동 로그 목록 조회 요청의 유효성 검증 규칙을 테스트합니다.
|
||||
*/
|
||||
class ActivityLogIndexRequestTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* FormRequest 인스턴스의 rules를 사용하여 Validator 생성
|
||||
*
|
||||
* @param array $data 검증할 데이터
|
||||
* @return \Illuminate\Validation\Validator
|
||||
*/
|
||||
private function makeValidator(array $data): \Illuminate\Validation\Validator
|
||||
{
|
||||
$request = new ActivityLogIndexRequest;
|
||||
|
||||
return Validator::make($data, $request->rules());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// authorize
|
||||
// ========================================================================
|
||||
|
||||
public function test_authorize_returns_true(): void
|
||||
{
|
||||
$request = new ActivityLogIndexRequest;
|
||||
$this->assertTrue($request->authorize());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 빈 요청 (모두 optional)
|
||||
// ========================================================================
|
||||
|
||||
public function test_empty_request_passes_validation(): void
|
||||
{
|
||||
$validator = $this->makeValidator([]);
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// log_type
|
||||
// ========================================================================
|
||||
|
||||
public function test_valid_log_type_array_passes(): void
|
||||
{
|
||||
$validator = $this->makeValidator([
|
||||
'log_type' => ['admin', 'user'],
|
||||
]);
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
|
||||
public function test_all_log_type_values_pass(): void
|
||||
{
|
||||
$validator = $this->makeValidator([
|
||||
'log_type' => ActivityLogType::values(),
|
||||
]);
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
|
||||
public function test_invalid_log_type_value_fails(): void
|
||||
{
|
||||
$validator = $this->makeValidator([
|
||||
'log_type' => ['invalid_type'],
|
||||
]);
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// action
|
||||
// ========================================================================
|
||||
|
||||
public function test_valid_action_passes(): void
|
||||
{
|
||||
$validator = $this->makeValidator([
|
||||
'action' => 'user.create',
|
||||
]);
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
|
||||
public function test_action_exceeding_max_length_fails(): void
|
||||
{
|
||||
$validator = $this->makeValidator([
|
||||
'action' => str_repeat('a', 101),
|
||||
]);
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// search
|
||||
// ========================================================================
|
||||
|
||||
public function test_valid_search_passes(): void
|
||||
{
|
||||
$validator = $this->makeValidator([
|
||||
'search' => '홍길동',
|
||||
]);
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
|
||||
public function test_search_exceeding_max_length_fails(): void
|
||||
{
|
||||
$validator = $this->makeValidator([
|
||||
'search' => str_repeat('a', 256),
|
||||
]);
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// search_type
|
||||
// ========================================================================
|
||||
|
||||
public function test_valid_search_type_values_pass(): void
|
||||
{
|
||||
foreach (['all', 'action', 'description', 'ip_address'] as $type) {
|
||||
$validator = $this->makeValidator(['search_type' => $type]);
|
||||
$this->assertTrue($validator->passes(), "search_type={$type} should pass");
|
||||
}
|
||||
}
|
||||
|
||||
public function test_invalid_search_type_fails(): void
|
||||
{
|
||||
$validator = $this->makeValidator(['search_type' => 'invalid']);
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// actor
|
||||
// ========================================================================
|
||||
|
||||
public function test_valid_actor_passes(): void
|
||||
{
|
||||
$validator = $this->makeValidator(['actor' => '홍길동']);
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
|
||||
public function test_actor_exceeding_max_length_fails(): void
|
||||
{
|
||||
$validator = $this->makeValidator(['actor' => str_repeat('a', 256)]);
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// date_from / date_to
|
||||
// ========================================================================
|
||||
|
||||
public function test_valid_date_range_passes(): void
|
||||
{
|
||||
$validator = $this->makeValidator([
|
||||
'date_from' => '2026-01-01',
|
||||
'date_to' => '2026-03-28',
|
||||
]);
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
|
||||
public function test_date_to_before_date_from_fails(): void
|
||||
{
|
||||
$validator = $this->makeValidator([
|
||||
'date_from' => '2026-03-28',
|
||||
'date_to' => '2026-01-01',
|
||||
]);
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
public function test_date_from_only_passes(): void
|
||||
{
|
||||
$validator = $this->makeValidator([
|
||||
'date_from' => '2026-01-01',
|
||||
]);
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// per_page
|
||||
// ========================================================================
|
||||
|
||||
public function test_valid_per_page_passes(): void
|
||||
{
|
||||
$validator = $this->makeValidator(['per_page' => 50]);
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
|
||||
public function test_per_page_zero_fails(): void
|
||||
{
|
||||
$validator = $this->makeValidator(['per_page' => 0]);
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
public function test_per_page_exceeding_max_fails(): void
|
||||
{
|
||||
$validator = $this->makeValidator(['per_page' => 101]);
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
public function test_per_page_min_boundary_passes(): void
|
||||
{
|
||||
$validator = $this->makeValidator(['per_page' => 1]);
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
|
||||
public function test_per_page_max_boundary_passes(): void
|
||||
{
|
||||
$validator = $this->makeValidator(['per_page' => 100]);
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// sort_by / sort_order
|
||||
// ========================================================================
|
||||
|
||||
public function test_valid_sort_by_values_pass(): void
|
||||
{
|
||||
foreach (['created_at', 'action', 'log_type'] as $sortBy) {
|
||||
$validator = $this->makeValidator(['sort_by' => $sortBy]);
|
||||
$this->assertTrue($validator->passes(), "sort_by={$sortBy} should pass");
|
||||
}
|
||||
}
|
||||
|
||||
public function test_invalid_sort_by_fails(): void
|
||||
{
|
||||
$validator = $this->makeValidator(['sort_by' => 'invalid_column']);
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
public function test_valid_sort_order_values_pass(): void
|
||||
{
|
||||
foreach (['asc', 'desc'] as $order) {
|
||||
$validator = $this->makeValidator(['sort_order' => $order]);
|
||||
$this->assertTrue($validator->passes(), "sort_order={$order} should pass");
|
||||
}
|
||||
}
|
||||
|
||||
public function test_invalid_sort_order_fails(): void
|
||||
{
|
||||
$validator = $this->makeValidator(['sort_order' => 'random']);
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// user_id
|
||||
// ========================================================================
|
||||
|
||||
public function test_valid_user_id_passes(): void
|
||||
{
|
||||
$user = \App\Models\User::factory()->create();
|
||||
$validator = $this->makeValidator(['user_id' => $user->id]);
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
|
||||
public function test_nonexistent_user_id_fails(): void
|
||||
{
|
||||
$validator = $this->makeValidator(['user_id' => 99999]);
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 복합 검증
|
||||
// ========================================================================
|
||||
|
||||
public function test_all_valid_filters_combined_passes(): void
|
||||
{
|
||||
$user = \App\Models\User::factory()->create();
|
||||
|
||||
$validator = $this->makeValidator([
|
||||
'log_type' => ['admin', 'system'],
|
||||
'action' => 'user.update',
|
||||
'user_id' => $user->id,
|
||||
'loggable_type' => 'App\\Models\\User',
|
||||
'search' => '검색어',
|
||||
'search_type' => 'action',
|
||||
'actor' => '홍길동',
|
||||
'date_from' => '2026-01-01',
|
||||
'date_to' => '2026-03-28',
|
||||
'per_page' => 20,
|
||||
'sort_by' => 'created_at',
|
||||
'sort_order' => 'desc',
|
||||
]);
|
||||
|
||||
$this->assertTrue($validator->passes());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Http\Requests\Module;
|
||||
|
||||
use App\Http\Requests\Module\IndexModuleRequest;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* IndexModuleRequest 유닛 테스트
|
||||
*
|
||||
* with[] 파라미터 검증 규칙을 테스트합니다.
|
||||
*/
|
||||
class IndexModuleRequestTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* 유효한 with 파라미터가 통과함
|
||||
*/
|
||||
public function test_valid_with_param_passes(): void
|
||||
{
|
||||
$request = new IndexModuleRequest();
|
||||
$rules = $request->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'with' => ['custom_menus'],
|
||||
], $rules);
|
||||
|
||||
$this->assertFalse($validator->fails());
|
||||
}
|
||||
|
||||
/**
|
||||
* 유효하지 않은 with 파라미터가 실패함
|
||||
*/
|
||||
public function test_invalid_with_param_fails(): void
|
||||
{
|
||||
$request = new IndexModuleRequest();
|
||||
$rules = $request->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'with' => ['invalid_option'],
|
||||
], $rules);
|
||||
|
||||
$this->assertTrue($validator->fails());
|
||||
$this->assertArrayHasKey('with.0', $validator->errors()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 빈 with 파라미터가 통과함
|
||||
*/
|
||||
public function test_empty_with_param_passes(): void
|
||||
{
|
||||
$request = new IndexModuleRequest();
|
||||
$rules = $request->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'with' => [],
|
||||
], $rules);
|
||||
|
||||
$this->assertFalse($validator->fails());
|
||||
}
|
||||
|
||||
/**
|
||||
* with 파라미터 없이 요청이 통과함
|
||||
*/
|
||||
public function test_request_without_with_param_passes(): void
|
||||
{
|
||||
$request = new IndexModuleRequest();
|
||||
$rules = $request->rules();
|
||||
|
||||
$validator = Validator::make([], $rules);
|
||||
|
||||
$this->assertFalse($validator->fails());
|
||||
}
|
||||
|
||||
/**
|
||||
* ALLOWED_WITH_OPTIONS 상수가 정의됨
|
||||
*/
|
||||
public function test_allowed_with_options_constant_exists(): void
|
||||
{
|
||||
$this->assertTrue(defined(IndexModuleRequest::class.'::ALLOWED_WITH_OPTIONS'));
|
||||
$this->assertContains('custom_menus', IndexModuleRequest::ALLOWED_WITH_OPTIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* hasWithOption 메서드가 존재함
|
||||
*/
|
||||
public function test_has_with_option_method_exists(): void
|
||||
{
|
||||
$request = new IndexModuleRequest();
|
||||
$this->assertTrue(method_exists($request, 'hasWithOption'));
|
||||
}
|
||||
|
||||
/**
|
||||
* with 배열이 최대 5개까지 허용됨
|
||||
*/
|
||||
public function test_with_param_max_5_items(): void
|
||||
{
|
||||
$request = new IndexModuleRequest();
|
||||
$rules = $request->rules();
|
||||
|
||||
// 6개 항목 - 실패해야 함
|
||||
$validator = Validator::make([
|
||||
'with' => ['custom_menus', 'item2', 'item3', 'item4', 'item5', 'item6'],
|
||||
], $rules);
|
||||
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
/**
|
||||
* search 파라미터 검증이 유지됨
|
||||
*/
|
||||
public function test_search_param_validation(): void
|
||||
{
|
||||
$request = new IndexModuleRequest();
|
||||
$rules = $request->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'search' => 'test query',
|
||||
], $rules);
|
||||
|
||||
$this->assertFalse($validator->fails());
|
||||
}
|
||||
|
||||
/**
|
||||
* status 파라미터 검증이 유지됨
|
||||
*/
|
||||
public function test_status_param_validation(): void
|
||||
{
|
||||
$request = new IndexModuleRequest();
|
||||
$rules = $request->rules();
|
||||
|
||||
// 유효한 상태
|
||||
$validator = Validator::make([
|
||||
'status' => 'active',
|
||||
], $rules);
|
||||
$this->assertFalse($validator->fails());
|
||||
|
||||
// 유효하지 않은 상태
|
||||
$validator = Validator::make([
|
||||
'status' => 'invalid_status',
|
||||
], $rules);
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,709 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Http\Resources;
|
||||
|
||||
use App\Enums\ActivityLogType;
|
||||
use App\Http\Resources\ActivityLogResource;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* ActivityLogResource API 리소스 테스트
|
||||
*
|
||||
* 리소스 변환 결과에 i18n 필드 및 변경 이력이 올바르게 포함되는지 검증합니다.
|
||||
*/
|
||||
class ActivityLogResourceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* 테스트용 Request 인스턴스를 생성합니다.
|
||||
*
|
||||
* @return Request
|
||||
*/
|
||||
private function makeRequest(): Request
|
||||
{
|
||||
return Request::create('/api/admin/activity-logs', 'GET');
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 기본 필드 검증
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 리소스에 모든 표준 필드가 포함됨
|
||||
*/
|
||||
public function test_resource_includes_standard_fields(): void
|
||||
{
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'user.create',
|
||||
'description_key' => 'activity_log.description.user_create',
|
||||
'description_params' => ['user_id' => 'abc-123'],
|
||||
'properties' => ['key' => 'value'],
|
||||
'changes' => [['field' => 'name', 'old' => null, 'new' => 'John']],
|
||||
'ip_address' => '127.0.0.1',
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertArrayHasKey('id', $resource);
|
||||
$this->assertArrayHasKey('log_type', $resource);
|
||||
$this->assertArrayHasKey('log_type_label', $resource);
|
||||
$this->assertArrayHasKey('loggable_type', $resource);
|
||||
$this->assertArrayHasKey('loggable_id', $resource);
|
||||
$this->assertArrayHasKey('action', $resource);
|
||||
$this->assertArrayHasKey('action_label', $resource);
|
||||
$this->assertArrayHasKey('localized_description', $resource);
|
||||
$this->assertArrayHasKey('description_key', $resource);
|
||||
$this->assertArrayHasKey('properties', $resource);
|
||||
$this->assertArrayHasKey('changes', $resource);
|
||||
$this->assertArrayHasKey('bulk_changes', $resource);
|
||||
$this->assertArrayHasKey('actor_name', $resource);
|
||||
$this->assertArrayHasKey('user', $resource);
|
||||
$this->assertArrayHasKey('ip_address', $resource);
|
||||
$this->assertArrayHasKey('created_at', $resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스에 description 키가 아닌 localized_description이 포함됨
|
||||
*/
|
||||
public function test_resource_uses_localized_description_not_description(): void
|
||||
{
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'test',
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertArrayHasKey('localized_description', $resource);
|
||||
$this->assertArrayNotHasKey('description', $resource);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// i18n 필드 검증
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* localized_description에 번역된 문자열이 반환됨
|
||||
*/
|
||||
public function test_resource_returns_translated_description(): void
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
|
||||
app('translator')->addLines([
|
||||
'activity_log.description.user_create' => ':user_id 사용자가 생성되었습니다',
|
||||
], $locale);
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'user.create',
|
||||
'description_key' => 'activity_log.description.user_create',
|
||||
'description_params' => ['user_id' => 'test-user'],
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertEquals('test-user 사용자가 생성되었습니다', $resource['localized_description']);
|
||||
}
|
||||
|
||||
/**
|
||||
* description_key에 raw 키가 그대로 반환됨
|
||||
*/
|
||||
public function test_resource_returns_raw_description_key(): void
|
||||
{
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'user.create',
|
||||
'description_key' => 'activity_log.description.user_create',
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertEquals('activity_log.description.user_create', $resource['description_key']);
|
||||
}
|
||||
|
||||
/**
|
||||
* action_label에 번역된 액션 라벨이 반환됨
|
||||
*/
|
||||
public function test_resource_returns_action_label(): void
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
|
||||
app('translator')->addLines([
|
||||
'activity_log.action.user.create' => '사용자 생성',
|
||||
], $locale);
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'user.create',
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertEquals('사용자 생성', $resource['action_label']);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// changes 필드 검증
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* changes에 구조화된 변경 이력이 포함되고 label이 번역됨
|
||||
*/
|
||||
public function test_resource_includes_changes_with_translated_label(): void
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
app('translator')->addLines([
|
||||
'activity_log.fields.email' => '이메일',
|
||||
'activity_log.fields.name' => '이름',
|
||||
], $locale);
|
||||
|
||||
$changes = [
|
||||
['field' => 'email', 'label_key' => 'activity_log.fields.email', 'old' => 'old@test.com', 'new' => 'new@test.com', 'type' => 'text'],
|
||||
['field' => 'name', 'label_key' => 'activity_log.fields.name', 'old' => 'Old Name', 'new' => 'New Name', 'type' => 'text'],
|
||||
];
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'user.update',
|
||||
'changes' => $changes,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertCount(2, $resource['changes']);
|
||||
$this->assertEquals('이메일', $resource['changes'][0]['label']);
|
||||
$this->assertEquals('이름', $resource['changes'][1]['label']);
|
||||
$this->assertEquals('old@test.com', $resource['changes'][0]['old']);
|
||||
$this->assertEquals('new@test.com', $resource['changes'][0]['new']);
|
||||
}
|
||||
|
||||
/**
|
||||
* changes에 enum 타입 필드의 old_label/new_label이 번역됨
|
||||
*/
|
||||
public function test_resource_translates_enum_labels_in_changes(): void
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
app('translator')->addLines([
|
||||
'activity_log.fields.status' => '상태',
|
||||
'enums.status.active' => '활성',
|
||||
'enums.status.inactive' => '비활성',
|
||||
], $locale);
|
||||
|
||||
$changes = [
|
||||
[
|
||||
'field' => 'status',
|
||||
'label_key' => 'activity_log.fields.status',
|
||||
'old' => 'inactive',
|
||||
'new' => 'active',
|
||||
'type' => 'enum',
|
||||
'old_label_key' => 'enums.status.inactive',
|
||||
'new_label_key' => 'enums.status.active',
|
||||
],
|
||||
];
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'user.update',
|
||||
'changes' => $changes,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertEquals('상태', $resource['changes'][0]['label']);
|
||||
$this->assertEquals('비활성', $resource['changes'][0]['old_label']);
|
||||
$this->assertEquals('활성', $resource['changes'][0]['new_label']);
|
||||
}
|
||||
|
||||
/**
|
||||
* changes에 boolean 타입 필드의 old_label/new_label이 예/아니오로 변환됨
|
||||
*/
|
||||
public function test_resource_translates_boolean_labels_in_changes(): void
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
app('translator')->addLines([
|
||||
'activity_log.fields.is_active' => '활성 여부',
|
||||
], $locale);
|
||||
|
||||
$changes = [
|
||||
[
|
||||
'field' => 'is_active',
|
||||
'label_key' => 'activity_log.fields.is_active',
|
||||
'old' => false,
|
||||
'new' => true,
|
||||
'type' => 'boolean',
|
||||
],
|
||||
];
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'user.update',
|
||||
'changes' => $changes,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertEquals('활성 여부', $resource['changes'][0]['label']);
|
||||
$this->assertEquals(__('common.no'), $resource['changes'][0]['old_label']);
|
||||
$this->assertEquals(__('common.yes'), $resource['changes'][0]['new_label']);
|
||||
}
|
||||
|
||||
/**
|
||||
* changes에 json 타입 필드(다국어)가 현재 로케일 값으로 변환됨
|
||||
*/
|
||||
public function test_resource_extracts_locale_value_from_json_changes(): void
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
app('translator')->addLines([
|
||||
'activity_log.fields.option_name' => '옵션명',
|
||||
], $locale);
|
||||
|
||||
$changes = [
|
||||
[
|
||||
'field' => 'option_name',
|
||||
'label_key' => 'activity_log.fields.option_name',
|
||||
'old' => ['en' => 'Brown', 'ko' => '하나하나'],
|
||||
'new' => ['en' => 'Brown', 'ko' => '원'],
|
||||
'type' => 'json',
|
||||
],
|
||||
];
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'product_option.update',
|
||||
'changes' => $changes,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertEquals('옵션명', $resource['changes'][0]['label']);
|
||||
// 현재 로케일(ko)에 맞는 값 추출
|
||||
$this->assertEquals('하나하나', $resource['changes'][0]['old']);
|
||||
$this->assertEquals('원', $resource['changes'][0]['new']);
|
||||
}
|
||||
|
||||
/**
|
||||
* changes에 json 타입 필드가 한국어 로케일로 변환됨
|
||||
*/
|
||||
public function test_resource_extracts_korean_locale_value_from_json_changes(): void
|
||||
{
|
||||
app()->setLocale('ko');
|
||||
|
||||
app('translator')->addLines([
|
||||
'activity_log.fields.option_name' => '옵션명',
|
||||
], 'ko');
|
||||
|
||||
$changes = [
|
||||
[
|
||||
'field' => 'option_name',
|
||||
'label_key' => 'activity_log.fields.option_name',
|
||||
'old' => ['en' => 'Brown', 'ko' => '하나하나'],
|
||||
'new' => ['en' => 'Brown', 'ko' => '원'],
|
||||
'type' => 'json',
|
||||
],
|
||||
];
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'product_option.update',
|
||||
'changes' => $changes,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertEquals('하나하나', $resource['changes'][0]['old']);
|
||||
$this->assertEquals('원', $resource['changes'][0]['new']);
|
||||
|
||||
// 로케일 복원
|
||||
app()->setLocale(config('app.locale'));
|
||||
}
|
||||
|
||||
/**
|
||||
* changes에 json 타입 필드의 old가 null이면 null 그대로 반환
|
||||
*/
|
||||
public function test_resource_handles_null_json_change_values(): void
|
||||
{
|
||||
$changes = [
|
||||
[
|
||||
'field' => 'option_name',
|
||||
'label_key' => 'activity_log.fields.option_name',
|
||||
'old' => null,
|
||||
'new' => ['en' => 'New', 'ko' => '새로운'],
|
||||
'type' => 'json',
|
||||
],
|
||||
];
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'product_option.update',
|
||||
'changes' => $changes,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertNull($resource['changes'][0]['old']);
|
||||
$this->assertIsString($resource['changes'][0]['new']);
|
||||
}
|
||||
|
||||
/**
|
||||
* bulk_changes에서도 json 타입 필드가 로케일 변환됨
|
||||
*/
|
||||
public function test_resource_extracts_locale_value_from_bulk_json_changes(): void
|
||||
{
|
||||
app()->setLocale('ko');
|
||||
|
||||
$bulkChanges = [
|
||||
503 => [
|
||||
[
|
||||
'field' => 'option_name',
|
||||
'label_key' => 'activity_log.fields.option_name',
|
||||
'old' => ['en' => 'Brown', 'ko' => '하나하나'],
|
||||
'new' => ['en' => 'Brown', 'ko' => '원'],
|
||||
'type' => 'json',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'product_option.bulk_update',
|
||||
'changes' => $bulkChanges,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertNotNull($resource['bulk_changes']);
|
||||
$this->assertEquals('하나하나', $resource['bulk_changes'][0]['changes'][0]['old']);
|
||||
$this->assertEquals('원', $resource['bulk_changes'][0]['changes'][0]['new']);
|
||||
|
||||
app()->setLocale(config('app.locale'));
|
||||
}
|
||||
|
||||
/**
|
||||
* changes가 null이면 changes, bulk_changes 모두 null로 반환됨
|
||||
*/
|
||||
public function test_resource_returns_null_changes_when_empty(): void
|
||||
{
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'login',
|
||||
'changes' => null,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertNull($resource['changes']);
|
||||
$this->assertNull($resource['bulk_changes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 수정 changes는 changes에 반환, bulk_changes는 null
|
||||
*/
|
||||
public function test_resource_returns_single_changes_in_changes_field(): void
|
||||
{
|
||||
$changes = [
|
||||
['field' => 'name', 'label_key' => 'activity_log.fields.name', 'old' => 'Old', 'new' => 'New', 'type' => 'text'],
|
||||
];
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'user.update',
|
||||
'changes' => $changes,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertNotNull($resource['changes']);
|
||||
$this->assertNull($resource['bulk_changes']);
|
||||
$this->assertCount(1, $resource['changes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 수정 changes (모델ID 기준 그룹핑)는 bulk_changes에 반환, changes는 null
|
||||
*/
|
||||
public function test_resource_returns_bulk_changes_in_bulk_changes_field(): void
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
app('translator')->addLines([
|
||||
'activity_log.fields.status' => '상태',
|
||||
], $locale);
|
||||
|
||||
$bulkChanges = [
|
||||
1 => [
|
||||
['field' => 'status', 'label_key' => 'activity_log.fields.status', 'old' => 'active', 'new' => 'inactive', 'type' => 'text'],
|
||||
],
|
||||
2 => [
|
||||
['field' => 'status', 'label_key' => 'activity_log.fields.status', 'old' => 'active', 'new' => 'inactive', 'type' => 'text'],
|
||||
],
|
||||
];
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'user.bulk_update',
|
||||
'changes' => $bulkChanges,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertNull($resource['changes']);
|
||||
$this->assertNotNull($resource['bulk_changes']);
|
||||
$this->assertCount(2, $resource['bulk_changes']);
|
||||
|
||||
// 각 그룹에 model_id와 번역된 changes 포함
|
||||
$this->assertEquals(1, $resource['bulk_changes'][0]['model_id']);
|
||||
$this->assertEquals(2, $resource['bulk_changes'][1]['model_id']);
|
||||
$this->assertEquals('상태', $resource['bulk_changes'][0]['changes'][0]['label']);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 하위 호환: 기존 DB 레코드 처리
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 기존 DB 레코드에서 type이 'text'로 저장되었지만 모델의 현재
|
||||
* $activityLogFields에서 'enum'으로 정의된 필드는 동적으로 enum 라벨을 해석함
|
||||
*/
|
||||
public function test_resource_resolves_enum_labels_from_legacy_text_type(): void
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
app('translator')->addLines([
|
||||
'activity_log.fields.status' => '상태',
|
||||
'user.status.active' => '활성',
|
||||
'user.status.blocked' => '차단',
|
||||
], $locale);
|
||||
|
||||
// 기존 DB 레코드: type이 'text'로 저장됨 (old_label_key/new_label_key 없음)
|
||||
$changes = [
|
||||
[
|
||||
'field' => 'status',
|
||||
'label_key' => 'activity_log.fields.status',
|
||||
'old' => 'active',
|
||||
'new' => 'blocked',
|
||||
'type' => 'text',
|
||||
],
|
||||
];
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'loggable_type' => User::class,
|
||||
'loggable_id' => 1,
|
||||
'action' => 'user.update',
|
||||
'changes' => $changes,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
// type이 동적으로 'enum'으로 보정됨
|
||||
$this->assertEquals('enum', $resource['changes'][0]['type']);
|
||||
// enum 라벨이 동적으로 해석됨
|
||||
$this->assertEquals('활성', $resource['changes'][0]['old_label']);
|
||||
$this->assertEquals('차단', $resource['changes'][0]['new_label']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 DB 레코드에서 type이 'text'이고 모델에서도 'text'인 필드는 변경 없이 유지됨
|
||||
*/
|
||||
public function test_resource_does_not_alter_genuine_text_type_fields(): void
|
||||
{
|
||||
$changes = [
|
||||
[
|
||||
'field' => 'name',
|
||||
'label_key' => 'activity_log.fields.name',
|
||||
'old' => 'Old Name',
|
||||
'new' => 'New Name',
|
||||
'type' => 'text',
|
||||
],
|
||||
];
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'loggable_type' => User::class,
|
||||
'loggable_id' => 1,
|
||||
'action' => 'user.update',
|
||||
'changes' => $changes,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertEquals('text', $resource['changes'][0]['type']);
|
||||
$this->assertArrayNotHasKey('old_label', $resource['changes'][0]);
|
||||
$this->assertArrayNotHasKey('new_label', $resource['changes'][0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 변경 로그에서 description_params에 count가 누락된 경우
|
||||
* bulk changes 개수로 :count 플레이스홀더를 보정함
|
||||
*/
|
||||
public function test_resource_resolves_unreplaced_count_from_bulk_changes(): void
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
app('translator')->addLines([
|
||||
'sirsoft-ecommerce::activity_log.description.order_bulk_status_update' => '주문 일괄 상태 변경 (:count건)',
|
||||
], $locale);
|
||||
|
||||
$bulkChanges = [
|
||||
101 => [
|
||||
['field' => 'status', 'label_key' => 'activity_log.fields.status', 'old' => 'pending', 'new' => 'shipping', 'type' => 'text'],
|
||||
],
|
||||
102 => [
|
||||
['field' => 'status', 'label_key' => 'activity_log.fields.status', 'old' => 'pending', 'new' => 'shipping', 'type' => 'text'],
|
||||
],
|
||||
103 => [
|
||||
['field' => 'status', 'label_key' => 'activity_log.fields.status', 'old' => 'pending', 'new' => 'shipping', 'type' => 'text'],
|
||||
],
|
||||
];
|
||||
|
||||
// 기존 DB 레코드: description_params에 count 없음
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'order.bulk_status_update',
|
||||
'description_key' => 'sirsoft-ecommerce::activity_log.description.order_bulk_status_update',
|
||||
'description_params' => ['order_id' => 'ORD-001'],
|
||||
'changes' => $bulkChanges,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertEquals('주문 일괄 상태 변경 (3건)', $resource['localized_description']);
|
||||
}
|
||||
|
||||
/**
|
||||
* description_params에 count가 이미 있으면 그대로 사용됨 (이중 치환 없음)
|
||||
*/
|
||||
public function test_resource_does_not_double_replace_count(): void
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
app('translator')->addLines([
|
||||
'sirsoft-ecommerce::activity_log.description.order_bulk_status_update' => '주문 일괄 상태 변경 (:count건)',
|
||||
], $locale);
|
||||
|
||||
$bulkChanges = [
|
||||
101 => [
|
||||
['field' => 'status', 'label_key' => 'activity_log.fields.status', 'old' => 'pending', 'new' => 'shipping', 'type' => 'text'],
|
||||
],
|
||||
];
|
||||
|
||||
// 새 DB 레코드: description_params에 count가 이미 있음
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'order.bulk_status_update',
|
||||
'description_key' => 'sirsoft-ecommerce::activity_log.description.order_bulk_status_update',
|
||||
'description_params' => ['order_id' => 'ORD-001', 'count' => 5],
|
||||
'changes' => $bulkChanges,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
// count가 이미 치환되어 5건으로 표시 (bulk_changes 개수 1이 아닌 5)
|
||||
$this->assertEquals('주문 일괄 상태 변경 (5건)', $resource['localized_description']);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// user 필드 검증
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* user가 있으면 uuid, name, email이 포함된 객체 반환
|
||||
*/
|
||||
public function test_resource_includes_user_info_when_user_exists(): void
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'user.create',
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
$log->load('user');
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertIsArray($resource['user']);
|
||||
$this->assertEquals($user->uuid, $resource['user']['uuid']);
|
||||
$this->assertEquals('Test User', $resource['user']['name']);
|
||||
$this->assertEquals('test@example.com', $resource['user']['email']);
|
||||
}
|
||||
|
||||
/**
|
||||
* user가 없으면 시스템 라벨만 포함된 객체 반환
|
||||
*/
|
||||
public function test_resource_returns_system_user_when_no_user(): void
|
||||
{
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::System,
|
||||
'action' => 'maintenance',
|
||||
'user_id' => null,
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertIsArray($resource['user']);
|
||||
$this->assertEquals(__('common.system'), $resource['user']['name']);
|
||||
$this->assertArrayNotHasKey('uuid', $resource['user']);
|
||||
$this->assertArrayNotHasKey('email', $resource['user']);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 기타 필드
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* log_type에 enum value 문자열이 반환됨
|
||||
*/
|
||||
public function test_resource_returns_log_type_as_string_value(): void
|
||||
{
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'test',
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertEquals('admin', $resource['log_type']);
|
||||
}
|
||||
|
||||
/**
|
||||
* ip_address가 올바르게 반환됨
|
||||
*/
|
||||
public function test_resource_returns_ip_address(): void
|
||||
{
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'login',
|
||||
'ip_address' => '192.168.1.100',
|
||||
]);
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertEquals('192.168.1.100', $resource['ip_address']);
|
||||
}
|
||||
|
||||
/**
|
||||
* actor_name이 리소스에 포함됨
|
||||
*/
|
||||
public function test_resource_includes_actor_name(): void
|
||||
{
|
||||
$user = User::factory()->create(['name' => 'Admin User']);
|
||||
|
||||
$log = ActivityLog::create([
|
||||
'log_type' => ActivityLogType::Admin,
|
||||
'action' => 'test',
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
$log->load('user');
|
||||
|
||||
$resource = (new ActivityLogResource($log))->toArray($this->makeRequest());
|
||||
|
||||
$this->assertEquals('Admin User', $resource['actor_name']);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user