update(...)` 를 가로채 * user_overrides 에 수정 필드를 자동 기록하는지 검증합니다. * * 요구사항: `use HasUserOverrides;` 선언만으로 모든 수정 경로(인스턴스 * update/save, mass update) 에서 user_overrides 가 자동 기록되어야 함. */ class HasUserOverridesMassUpdateTest extends TestCase { use RefreshDatabase; /** * 기본 definition 생성 헬퍼. */ private function createDefinition(array $overrides = []): NotificationDefinition { return NotificationDefinition::create(array_merge([ 'type' => 'mass-update-test', 'hook_prefix' => 'core.auth', 'extension_type' => 'core', 'extension_identifier' => 'core', 'name' => ['ko' => '원본'], 'description' => ['ko' => '원본'], 'channels' => ['mail'], 'hooks' => [], 'variables' => [], 'is_active' => true, 'is_default' => true, ], $overrides)); } public function test_mass_update_records_user_overrides_for_trackable_fields(): void { $definition = $this->createDefinition(); // 사용자가 UI 에서 관리자 일괄 저장 시 흔히 발생하는 패턴. NotificationDefinition::where('id', $definition->id)->update([ 'name' => ['ko' => '사용자 수정'], ]); $reloaded = NotificationDefinition::find($definition->id); $this->assertSame(['ko' => '사용자 수정'], $reloaded->name); $this->assertIsArray($reloaded->user_overrides); // 7.0.0-beta.4+ 다국어 컬럼은 sub-key dot-path 단위 기록 $this->assertContains('name.ko', $reloaded->user_overrides, 'mass update 시에도 다국어 sub-key 가 user_overrides 에 자동 기록'); } public function test_mass_update_accumulates_overrides_across_multiple_edits(): void { $definition = $this->createDefinition(); NotificationDefinition::where('id', $definition->id)->update([ 'name' => ['ko' => '수정1'], ]); NotificationDefinition::where('id', $definition->id)->update([ 'is_active' => false, ]); $reloaded = NotificationDefinition::find($definition->id); $this->assertContains('name.ko', $reloaded->user_overrides); $this->assertContains('is_active', $reloaded->user_overrides, '여러 번의 mass update 가 누적 기록'); } public function test_mass_update_ignores_non_trackable_fields(): void { $definition = $this->createDefinition(); // hook_prefix 는 trackableFields 에 없음 NotificationDefinition::where('id', $definition->id)->update([ 'hook_prefix' => 'changed.prefix', ]); $reloaded = NotificationDefinition::find($definition->id); $this->assertSame('changed.prefix', $reloaded->hook_prefix); $this->assertEmpty($reloaded->user_overrides ?? [], 'trackable 외 필드는 user_overrides 기록 안 됨'); } public function test_mass_update_across_multiple_rows_tracks_each_independently(): void { $a = $this->createDefinition(['type' => 'row-a']); $b = $this->createDefinition(['type' => 'row-b']); // 2행 동시 mass update NotificationDefinition::whereIn('id', [$a->id, $b->id])->update([ 'name' => ['ko' => '일괄수정'], ]); $reloadedA = NotificationDefinition::find($a->id); $reloadedB = NotificationDefinition::find($b->id); $this->assertContains('name.ko', $reloadedA->user_overrides ?? []); $this->assertContains('name.ko', $reloadedB->user_overrides ?? []); } public function test_seeder_context_bypasses_tracking_on_mass_update(): void { $definition = $this->createDefinition(); app()->instance('user_overrides.seeding', true); try { NotificationDefinition::where('id', $definition->id)->update([ 'name' => ['ko' => '시더 수정'], ]); } finally { app()->forgetInstance('user_overrides.seeding'); } $reloaded = NotificationDefinition::find($definition->id); $this->assertSame(['ko' => '시더 수정'], $reloaded->name); $this->assertEmpty( $reloaded->user_overrides ?? [], '시더 컨텍스트에서는 user_overrides 기록 안 됨 (성능 유지 + syncFromUpgrade 의미 보존)', ); } public function test_raw_db_facade_update_bypasses_tracking(): void { $definition = $this->createDefinition(); // 실제 대량 처리 성능이 필요한 경우를 위한 escape hatch — DB facade 사용. DB::table('notification_definitions') ->where('id', $definition->id) ->update(['name' => json_encode(['ko' => 'raw 수정'])]); $reloaded = NotificationDefinition::find($definition->id); $this->assertEmpty( $reloaded->user_overrides ?? [], 'DB facade 직접 호출은 Eloquent Builder 를 거치지 않아 trait 미적용 — escape hatch', ); } }