중첩 라우트의 상위 리소스 ID 를 조회 스코프에 반영하지 않아 A 의 경로로 B 의 하위 리소스를 읽거나 변경할 수 있었다. 방어 위치를 Repository where 절(SSoT)로 통일하고 컨트롤러 사후 비교에 의존하지 않게 했다. 같은 리소스의 형제 엔드포인트가 서로 다른 강도로 검증하던 부분도 맞췄다. 계층 리소스는 자기 자신·자손을 부모로 지정하는 순환을 검증 계층에서 차단하고, 검증을 우회하는 경로(시더/훅/오염 데이터)를 위해 path 재귀에 방문 ID 가드를 뒀다. 설정값이 정하는 깊이 상한을 Service 가 리터럴로 재클램프하던 부분을 제거해 게시판 설정이 실제로 적용되게 했다. 쓰기 경로는 validated 기준으로 좁혀 FormRequest 미정의 필드가 fillable 로 새지 않게 했다. 기존 오염 데이터는 업그레이드 스텝이 정리한다(삭제 없이 최상위로 이동). 재발 방지로 audit 룰 3종을 추가하고, 규정 문서와 트러블슈팅 사례를 함께 갱신했다. 공개 이슈: gnuboard/g7
491 lines
17 KiB
PHP
491 lines
17 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Api\Admin;
|
|
|
|
use App\Enums\ExtensionOwnerType;
|
|
use App\Extension\PluginManager;
|
|
use App\Models\Permission;
|
|
use App\Models\Plugin;
|
|
use App\Models\Role;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\File;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* PluginSettingsController API 테스트
|
|
*
|
|
* 플러그인 설정 조회, 수정, 레이아웃 조회 API 엔드포인트를 테스트합니다.
|
|
* 설정은 파일 기반으로 storage/app/plugins/{identifier}/settings/setting.json에 저장됩니다.
|
|
*/
|
|
class PluginSettingsControllerTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private User $admin;
|
|
|
|
private string $token;
|
|
|
|
private PluginManager $pluginManager;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->admin = $this->createAdminUser();
|
|
$this->token = $this->admin->createToken('test-token')->plainTextToken;
|
|
$this->pluginManager = app(PluginManager::class);
|
|
}
|
|
|
|
/**
|
|
* 관리자 역할 생성 및 할당
|
|
*
|
|
* @param array $permissions 사용자에게 부여할 권한 식별자 목록
|
|
*/
|
|
private function createAdminUser(array $permissions = ['core.plugins.read', 'core.plugins.update']): User
|
|
{
|
|
$user = User::factory()->create();
|
|
|
|
// 권한 생성
|
|
$permissionIds = [];
|
|
foreach ($permissions as $permIdentifier) {
|
|
$permission = Permission::firstOrCreate(
|
|
['identifier' => $permIdentifier],
|
|
[
|
|
'name' => json_encode(['ko' => $permIdentifier, 'en' => $permIdentifier]),
|
|
'description' => json_encode(['ko' => $permIdentifier.' 권한', 'en' => $permIdentifier.' Permission']),
|
|
'extension_type' => ExtensionOwnerType::Core,
|
|
'extension_identifier' => 'core',
|
|
'type' => 'admin',
|
|
]
|
|
);
|
|
$permissionIds[] = $permission->id;
|
|
}
|
|
|
|
// 고유한 식별자로 테스트용 역할 생성
|
|
$roleIdentifier = 'admin_settings_test_'.uniqid();
|
|
$testRole = Role::create([
|
|
'identifier' => $roleIdentifier,
|
|
'name' => json_encode(['ko' => '테스트 관리자', 'en' => 'Test Administrator']),
|
|
'description' => json_encode(['ko' => '테스트 관리자', 'en' => 'Test Administrator']),
|
|
'is_active' => true,
|
|
]);
|
|
|
|
// admin 역할도 추가 (admin 미들웨어 통과용)
|
|
$adminRole = Role::firstOrCreate(
|
|
['identifier' => 'admin'],
|
|
[
|
|
'name' => json_encode(['ko' => '관리자', 'en' => 'Administrator']),
|
|
'description' => json_encode(['ko' => '시스템 관리자', 'en' => 'System Administrator']),
|
|
'extension_type' => ExtensionOwnerType::Core,
|
|
'extension_identifier' => 'core',
|
|
'is_active' => true,
|
|
]
|
|
);
|
|
|
|
// 테스트용 역할에 권한 할당
|
|
$testRole->permissions()->sync($permissionIds);
|
|
|
|
// 사용자에게 admin 역할과 테스트용 역할 모두 할당
|
|
$user->roles()->attach($adminRole->id, [
|
|
'assigned_at' => now(),
|
|
'assigned_by' => null,
|
|
]);
|
|
$user->roles()->attach($testRole->id, [
|
|
'assigned_at' => now(),
|
|
'assigned_by' => null,
|
|
]);
|
|
|
|
return $user->fresh();
|
|
}
|
|
|
|
/**
|
|
* 인증된 요청 헬퍼 메서드
|
|
*/
|
|
private function authRequest(): static
|
|
{
|
|
return $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$this->token,
|
|
'Accept' => 'application/json',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 테스트용 플러그인 생성 (DB 레코드만)
|
|
*/
|
|
private function createTestPlugin(string $identifier = 'test-settings-plugin'): Plugin
|
|
{
|
|
return Plugin::create([
|
|
'identifier' => $identifier,
|
|
'vendor' => 'test',
|
|
'name' => json_encode(['ko' => '테스트 플러그인', 'en' => 'Test Plugin']),
|
|
'version' => '1.0.0',
|
|
'description' => json_encode(['ko' => '테스트용 플러그인', 'en' => 'Test Plugin']),
|
|
'status' => 'active',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 테스트용 설정 파일 생성
|
|
*/
|
|
private function createTestSettingsFile(string $identifier, array $settings): void
|
|
{
|
|
$settingsDir = storage_path("app/plugins/{$identifier}/settings");
|
|
$settingsPath = $settingsDir.'/setting.json';
|
|
|
|
if (! File::isDirectory($settingsDir)) {
|
|
File::makeDirectory($settingsDir, 0755, true);
|
|
}
|
|
|
|
File::put($settingsPath, json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
/**
|
|
* 테스트용 설정 파일 삭제
|
|
*/
|
|
private function cleanupTestSettings(string $identifier): void
|
|
{
|
|
$pluginDir = storage_path("app/plugins/{$identifier}");
|
|
if (File::isDirectory($pluginDir)) {
|
|
File::deleteDirectory($pluginDir);
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// 인증/권한 테스트
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 인증 없이 설정 조회 시 401 반환
|
|
*/
|
|
public function test_show_returns_401_without_authentication(): void
|
|
{
|
|
$response = $this->getJson('/api/admin/plugins/test-plugin/settings');
|
|
|
|
$response->assertStatus(401);
|
|
}
|
|
|
|
/**
|
|
* 권한 없이 설정 조회 시 403 반환
|
|
*/
|
|
public function test_show_returns_403_without_permission(): void
|
|
{
|
|
// 권한 없는 관리자 생성
|
|
$user = User::factory()->create();
|
|
$adminRole = Role::where('identifier', 'admin')->first();
|
|
|
|
$user->roles()->attach($adminRole->id, [
|
|
'assigned_at' => now(),
|
|
'assigned_by' => null,
|
|
]);
|
|
|
|
$token = $user->createToken('test-token')->plainTextToken;
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$token,
|
|
'Accept' => 'application/json',
|
|
])->getJson('/api/admin/plugins/test-plugin/settings');
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
/**
|
|
* 인증 없이 설정 업데이트 시 401 반환
|
|
*/
|
|
public function test_update_returns_401_without_authentication(): void
|
|
{
|
|
$response = $this->putJson('/api/admin/plugins/test-plugin/settings', [
|
|
'key' => 'value',
|
|
]);
|
|
|
|
$response->assertStatus(401);
|
|
}
|
|
|
|
/**
|
|
* 권한 없이 설정 업데이트 시 403 반환
|
|
*/
|
|
public function test_update_returns_403_without_permission(): void
|
|
{
|
|
// read 권한만 있는 관리자 생성
|
|
$user = $this->createAdminUser(['core.plugins.read']);
|
|
$token = $user->createToken('test-token')->plainTextToken;
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$token,
|
|
'Accept' => 'application/json',
|
|
])->putJson('/api/admin/plugins/test-plugin/settings', [
|
|
'key' => 'value',
|
|
]);
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
/**
|
|
* 인증 없이 레이아웃 조회 시 401 반환
|
|
*/
|
|
public function test_layout_returns_401_without_authentication(): void
|
|
{
|
|
$response = $this->getJson('/api/admin/plugins/test-plugin/settings/layout');
|
|
|
|
$response->assertStatus(401);
|
|
}
|
|
|
|
// ========================================================================
|
|
// show() 테스트 - 설정 조회
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 존재하지 않는 플러그인 설정 조회 시 404 반환
|
|
*/
|
|
public function test_show_returns_404_for_nonexistent_plugin(): void
|
|
{
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins/nonexistent-plugin/settings');
|
|
|
|
$response->assertStatus(404)
|
|
->assertJson(['success' => false]);
|
|
}
|
|
|
|
/**
|
|
* 실제 설치된 Daum 우편번호 플러그인 설정 조회 테스트
|
|
*/
|
|
public function test_show_returns_settings_for_installed_plugin(): void
|
|
{
|
|
// Daum 우편번호 플러그인이 설치되어 있는지 확인
|
|
$pluginInstance = $this->pluginManager->getPlugin('sirsoft-daum_postcode');
|
|
|
|
if (! $pluginInstance) {
|
|
$this->markTestSkipped('sirsoft-daum_postcode plugin not installed');
|
|
}
|
|
|
|
// DB에 플러그인 레코드가 있는지 확인
|
|
$plugin = Plugin::where('identifier', 'sirsoft-daum_postcode')->first();
|
|
if (! $plugin) {
|
|
$this->createTestPlugin('sirsoft-daum_postcode');
|
|
}
|
|
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins/sirsoft-daum_postcode/settings');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
|
|
// 기본 설정값이 반환되는지 확인
|
|
$data = $response->json('data');
|
|
$this->assertIsArray($data);
|
|
}
|
|
|
|
// ========================================================================
|
|
// update() 테스트 - 설정 업데이트
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 존재하지 않는 플러그인 설정 업데이트 시 에러 반환
|
|
*/
|
|
public function test_update_returns_error_for_nonexistent_plugin(): void
|
|
{
|
|
$response = $this->authRequest()->putJson('/api/admin/plugins/nonexistent-plugin/settings', [
|
|
'key' => 'value',
|
|
]);
|
|
|
|
// 플러그인 인스턴스가 없으면 저장 실패
|
|
$response->assertStatus(500)
|
|
->assertJson(['success' => false]);
|
|
}
|
|
|
|
/**
|
|
* 실제 설치된 플러그인 설정 업데이트 테스트
|
|
*/
|
|
public function test_update_saves_settings_for_installed_plugin(): void
|
|
{
|
|
$pluginInstance = $this->pluginManager->getPlugin('sirsoft-daum_postcode');
|
|
|
|
if (! $pluginInstance) {
|
|
$this->markTestSkipped('sirsoft-daum_postcode plugin not installed');
|
|
}
|
|
|
|
// DB에 플러그인 레코드 생성
|
|
$plugin = Plugin::where('identifier', 'sirsoft-daum_postcode')->first();
|
|
if (! $plugin) {
|
|
$this->createTestPlugin('sirsoft-daum_postcode');
|
|
}
|
|
|
|
// 플러그인이 선언한 설정 스키마의 실제 키를 사용한다.
|
|
// 스키마 밖의 키는 검증을 통과하지 못해 저장되지 않는다.
|
|
$response = $this->authRequest()->putJson('/api/admin/plugins/sirsoft-daum_postcode/settings', [
|
|
'display_mode' => 'popup',
|
|
'popup_width' => 640,
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
|
|
// 파일에서 값 확인
|
|
$settingsPath = storage_path('app/plugins/sirsoft-daum_postcode/settings/setting.json');
|
|
if (File::exists($settingsPath)) {
|
|
$savedContent = json_decode(File::get($settingsPath), true);
|
|
$this->assertSame('popup', $savedContent['display_mode'] ?? null);
|
|
$this->assertSame(640, $savedContent['popup_width'] ?? null);
|
|
}
|
|
|
|
// 정리
|
|
$this->cleanupTestSettings('sirsoft-daum_postcode');
|
|
}
|
|
|
|
/**
|
|
* 스키마에 없는 필드는 저장되지 않음 (mass-assignment 방지)
|
|
*
|
|
* 과거 구현은 `validated()` 가 비면 `$request->all()` 로 폴백했다. 폴백의 명분이던
|
|
* "PluginManager 미등록 플러그인" 은 `PluginSettingsService::save()` 가 이미 false 로
|
|
* 차단하므로 도달할 수 없고, 실제로는 스키마 밖 키만 담긴 요청이 그대로 설정 파일에
|
|
* 병합되는 경로로만 동작했다.
|
|
*/
|
|
public function test_update_ignores_fields_outside_settings_schema(): void
|
|
{
|
|
$pluginInstance = $this->pluginManager->getPlugin('sirsoft-daum_postcode');
|
|
|
|
if (! $pluginInstance) {
|
|
$this->markTestSkipped('sirsoft-daum_postcode plugin not installed');
|
|
}
|
|
|
|
$plugin = Plugin::where('identifier', 'sirsoft-daum_postcode')->first();
|
|
if (! $plugin) {
|
|
$this->createTestPlugin('sirsoft-daum_postcode');
|
|
}
|
|
|
|
// 스키마에 정의되지 않은 키만 담아 전송 → validated() 가 빈 배열이 되는 조건
|
|
$this->authRequest()->putJson('/api/admin/plugins/sirsoft-daum_postcode/settings', [
|
|
'injected_field' => 'evil',
|
|
'another_unknown' => ['nested' => true],
|
|
]);
|
|
|
|
$settingsPath = storage_path('app/plugins/sirsoft-daum_postcode/settings/setting.json');
|
|
if (File::exists($settingsPath)) {
|
|
$savedContent = json_decode(File::get($settingsPath), true);
|
|
$this->assertArrayNotHasKey('injected_field', $savedContent);
|
|
$this->assertArrayNotHasKey('another_unknown', $savedContent);
|
|
}
|
|
|
|
$this->cleanupTestSettings('sirsoft-daum_postcode');
|
|
}
|
|
|
|
// ========================================================================
|
|
// layout() 테스트 - 레이아웃 조회
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 존재하지 않는 플러그인 레이아웃 조회 시 404 반환
|
|
*/
|
|
public function test_layout_returns_404_for_nonexistent_plugin(): void
|
|
{
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins/nonexistent-plugin/settings/layout');
|
|
|
|
$response->assertStatus(404)
|
|
->assertJson(['success' => false]);
|
|
}
|
|
|
|
/**
|
|
* Daum 우편번호 플러그인 레이아웃 조회 성공
|
|
*/
|
|
public function test_layout_returns_layout_for_daum_postcode_plugin(): void
|
|
{
|
|
$layoutPath = base_path('plugins/sirsoft-daum_postcode/resources/layouts/settings.json');
|
|
|
|
if (! file_exists($layoutPath)) {
|
|
$this->markTestSkipped('Daum postcode plugin layout file not found');
|
|
}
|
|
|
|
$pluginInstance = $this->pluginManager->getPlugin('sirsoft-daum_postcode');
|
|
|
|
if (! $pluginInstance) {
|
|
$this->markTestSkipped('sirsoft-daum_postcode plugin not installed');
|
|
}
|
|
|
|
// DB에 플러그인 레코드 생성
|
|
$plugin = Plugin::where('identifier', 'sirsoft-daum_postcode')->first();
|
|
if (! $plugin) {
|
|
$this->createTestPlugin('sirsoft-daum_postcode');
|
|
}
|
|
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins/sirsoft-daum_postcode/settings/layout');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonStructure([
|
|
'success',
|
|
'data' => [
|
|
'version',
|
|
'meta',
|
|
'schema',
|
|
],
|
|
]);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 응답 구조 테스트
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 설정 조회 응답 구조 검증
|
|
*/
|
|
public function test_show_response_structure(): void
|
|
{
|
|
$pluginInstance = $this->pluginManager->getPlugin('sirsoft-daum_postcode');
|
|
|
|
if (! $pluginInstance) {
|
|
$this->markTestSkipped('sirsoft-daum_postcode plugin not installed');
|
|
}
|
|
|
|
// DB에 플러그인 레코드 생성
|
|
$plugin = Plugin::where('identifier', 'sirsoft-daum_postcode')->first();
|
|
if (! $plugin) {
|
|
$this->createTestPlugin('sirsoft-daum_postcode');
|
|
}
|
|
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins/sirsoft-daum_postcode/settings');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonStructure([
|
|
'success',
|
|
'message',
|
|
'data',
|
|
])
|
|
->assertJson([
|
|
'success' => true,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 설정 업데이트 응답에 업데이트된 설정 포함
|
|
*/
|
|
public function test_update_response_includes_updated_settings(): void
|
|
{
|
|
$pluginInstance = $this->pluginManager->getPlugin('sirsoft-daum_postcode');
|
|
|
|
if (! $pluginInstance) {
|
|
$this->markTestSkipped('sirsoft-daum_postcode plugin not installed');
|
|
}
|
|
|
|
// DB에 플러그인 레코드 생성
|
|
$plugin = Plugin::where('identifier', 'sirsoft-daum_postcode')->first();
|
|
if (! $plugin) {
|
|
$this->createTestPlugin('sirsoft-daum_postcode');
|
|
}
|
|
|
|
$response = $this->authRequest()->putJson('/api/admin/plugins/sirsoft-daum_postcode/settings', [
|
|
'display_mode' => 'popup',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson([
|
|
'success' => true,
|
|
]);
|
|
|
|
// 응답에 업데이트된 설정이 포함되어 있는지 확인
|
|
$data = $response->json('data');
|
|
$this->assertIsArray($data);
|
|
$this->assertArrayHasKey('display_mode', $data);
|
|
$this->assertSame('popup', $data['display_mode']);
|
|
|
|
// 정리
|
|
$this->cleanupTestSettings('sirsoft-daum_postcode');
|
|
}
|
|
}
|