$this->shouldDropViews(), '--drop-types' => $this->shouldDropTypes(), '--seed' => $this->shouldSeed(), '--seeder' => $this->seeder(), '--path' => $paths, ]; } /** * HookManager static state 스냅샷 — tearDown 에서 복원하여 테스트 간 훅 격리 보장. * * @var array{hooks: array, filters: array, dispatching: array}|null */ private ?array $hookSnapshot = null; /** * 테스트 환경 설정 */ protected function setUp(): void { parent::setUp(); // 이커머스 모듈 오토로드 등록 $this->registerModuleAutoload(); // 플러그인 오토로드 등록 $this->registerPluginAutoload(); // 이커머스 모듈 ServiceProvider 등록 (Repository 바인딩) $this->app->register(EcommerceServiceProvider::class); // 모듈 라우트를 수동으로 등록 $this->registerModuleRoutes(); // 플러그인 라우트를 수동으로 등록 $this->registerPluginRoutes(); // SettingsServiceProvider 가 storage/app/settings/general.json 의 site_url 로 // app.url 을 override 하면 Laravel 의 assertRedirect (APP_URL 기반) 와 mismatch. // 테스트 환경에서는 APP_URL 그대로 사용하도록 명시 리셋. Config::set('app.url', env('APP_URL', 'http://localhost')); // HookManager 상태 스냅샷 (tearDown 에서 복원) $this->snapshotHookManager(); } /** * tearDown 에 HookManager 상태 복원. */ protected function tearDown(): void { $this->restoreHookManager(); parent::tearDown(); } /** * HookManager static $hooks / $filters / $dispatching 를 스냅샷. */ private function snapshotHookManager(): void { $ref = new \ReflectionClass(HookManager::class); $this->hookSnapshot = [ 'hooks' => $ref->getProperty('hooks')->getValue(), 'filters' => $ref->getProperty('filters')->getValue(), 'dispatching' => $ref->getProperty('dispatching')->getValue(), ]; } /** * 스냅샷 시점으로 HookManager 복원. */ private function restoreHookManager(): void { if ($this->hookSnapshot === null) { return; } $ref = new \ReflectionClass(HookManager::class); $ref->getProperty('hooks')->setValue(null, $this->hookSnapshot['hooks']); $ref->getProperty('filters')->setValue(null, $this->hookSnapshot['filters']); $ref->getProperty('dispatching')->setValue(null, $this->hookSnapshot['dispatching']); $this->hookSnapshot = null; } /** * 이커머스 모듈 오토로드를 등록합니다. */ protected function registerModuleAutoload(): void { $moduleBasePath = base_path('modules/sirsoft-ecommerce/src/'); spl_autoload_register(function ($class) use ($moduleBasePath) { $prefix = 'Modules\\Sirsoft\\Ecommerce\\'; $len = strlen($prefix); if (strncmp($prefix, $class, $len) !== 0) { return; } $relativeClass = substr($class, $len); $file = $moduleBasePath.str_replace('\\', '/', $relativeClass).'.php'; if (file_exists($file) && ! class_exists($class, false) && ! interface_exists($class, false) && ! trait_exists($class, false) && ! enum_exists($class, false)) { // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 require_once $file; } }); // composer.json files 오토로드 (헬퍼 함수 등록) $helpersFile = $moduleBasePath.'Helpers/helpers.php'; if (file_exists($helpersFile)) { require_once $helpersFile; } } /** * 플러그인 오토로드를 등록합니다. */ protected function registerPluginAutoload(): void { // 활성 디렉토리(plugins/sirsoft-tosspayments)가 아닌 자기 자신 기준 경로 — // _bundled 에서 직접 실행할 때도 소스를 찾도록 한다. $pluginBasePath = dirname(__DIR__).'/src/'; spl_autoload_register(function ($class) use ($pluginBasePath) { $prefix = 'Plugins\\Sirsoft\\Tosspayments\\'; $len = strlen($prefix); if (strncmp($prefix, $class, $len) !== 0) { return; } $relativeClass = substr($class, $len); $file = $pluginBasePath.str_replace('\\', '/', $relativeClass).'.php'; if (file_exists($file) && ! class_exists($class, false) && ! interface_exists($class, false) && ! trait_exists($class, false) && ! enum_exists($class, false)) { // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 require_once $file; } }); } /** * 모듈 라우트를 등록합니다. */ protected function registerModuleRoutes(): void { $apiRoutesFile = base_path('modules/sirsoft-ecommerce/src/routes/api.php'); if (file_exists($apiRoutesFile)) { Route::prefix('api/modules/sirsoft-ecommerce') ->name('api.modules.sirsoft-ecommerce.') ->middleware('api') ->group($apiRoutesFile); } } /** * 플러그인 라우트를 등록합니다. */ protected function registerPluginRoutes(): void { $webRoutesFile = dirname(__DIR__).'/src/routes/web.php'; if (file_exists($webRoutesFile)) { Route::prefix('plugins/sirsoft-tosspayments') ->name('plugins.sirsoft-tosspayments.') ->middleware('web') ->group($webRoutesFile); } $apiRoutesFile = dirname(__DIR__).'/src/routes/api.php'; if (file_exists($apiRoutesFile)) { // 프로덕션 PluginRouteServiceProvider 와 동일한 프리픽스·이름 규약을 따른다. Route::prefix('api/plugins/sirsoft-tosspayments') ->name('api.plugins.sirsoft-tosspayments.') ->middleware('api') ->group($apiRoutesFile); } } /** * 관리자 사용자를 생성합니다. * * @param array $permissions 추가 권한 목록 * @return User */ protected function createAdminUser(array $permissions = []): User { $user = User::factory()->create(); $uniqueRoleIdentifier = 'admin-test-'.$user->id.'-'.time(); $userRole = Role::create([ 'identifier' => $uniqueRoleIdentifier, 'name' => ['ko' => '테스트 관리자', 'en' => 'Test Admin'], ]); $user->roles()->attach($userRole->id); $adminAccessPermission = Permission::firstOrCreate( ['identifier' => 'admin.access'], [ 'name' => ['ko' => '관리자 접근', 'en' => 'Admin Access'], 'type' => PermissionType::Admin, ] ); $userRole->permissions()->attach($adminAccessPermission->id); if (! empty($permissions)) { foreach ($permissions as $permissionIdentifier) { $permission = Permission::firstOrCreate( ['identifier' => $permissionIdentifier], [ 'name' => ['ko' => $permissionIdentifier, 'en' => $permissionIdentifier], 'type' => 'admin', ] ); $userRole->permissions()->syncWithoutDetaching([$permission->id]); } } return $user; } /** * 일반 사용자를 생성합니다. * * @return User */ protected function createUser(): User { $userRole = Role::where('identifier', 'user')->first(); $user = User::factory()->create(); if ($userRole) { $user->roles()->attach($userRole->id); } return $user; } }