route:cache 는 새 앱을 부팅해 라우트를 수집하고, 그 부팅의 확장 라우트 프로바이더는 DB 가 아니라 캐시된 활성 확장 목록을 읽는다. 그래서 rebuild 가 invalidate*StatusCache 보다 앞서면 방금 바뀐 상태가 빠진 채 라우트가 박제되고, 라우트 캐시에는 스캔 폴백이 없어 오류도 로그도 없이 404 가 된다. 무효화를 굽기 직전이 아니라 DB 상태 쓰기 직후로 올려, 같은 목록을 읽는 훅 매핑 캐시까지 함께 바로잡았다. update 경로는 Updating 전이 직후에 비우면 그 창의 오토로드 갱신이 확장을 비활성으로 판정하므로, 상태 복원 직후에 비운 뒤 훅 캐시를 다시 굽는다. 플러그인 라우트 프로바이더에는 활성 게이트가 없어 비활성 플러그인의 API 가 계속 응답했다. 화면·메뉴만 사라지고 기능은 살아 있는 상태였다. 모듈과 같은 기준을 적용했다. 실패 사유가 하위 계층에서 버려져 관리자 화면에 :error 자리표시자가 그대로 노출되던 문제도 고쳤다. 반환 경로를 깨지 않도록 배열 키 reason 과 뒤에 붙인 선택적 out 파라미터로 사유를 실어 올리고, 확장이 수명주기 훅에서 사유를 남길 수 있는 통로를 추가했다. 설치 경로의 광역 RuntimeException catch 는 도메인 예외로 좁혀 원본 키와 파라미터를 응답에 싣는다 — 상태코드 422 는 유지해 사용자 계약을 함께 바꾸지 않는다. 언어팩 화면은 프로덕션에서 예외 원문을 싣지 않는 것이 확정된 계약이므로, 자리를 일반 문구로 채우는 대신 치환 자리 자체를 제거했다. 원문은 종전대로 errors 통로를 거쳐 디버그 모드에서 도달한다.
1034 lines
36 KiB
PHP
1034 lines
36 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Api\Admin;
|
|
|
|
use App\Enums\ExtensionOwnerType;
|
|
use App\Enums\PermissionType;
|
|
use App\Exceptions\PluginOperationException;
|
|
use App\Models\Permission;
|
|
use App\Models\Role;
|
|
use App\Models\User;
|
|
use App\Services\PluginService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* AdminPluginController 테스트
|
|
*
|
|
* 플러그인 관리 API 엔드포인트를 테스트합니다.
|
|
*/
|
|
class PluginControllerTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private User $admin;
|
|
|
|
private string $token;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->admin = $this->createAdminUser();
|
|
$this->token = $this->admin->createToken('test-token')->plainTextToken;
|
|
}
|
|
|
|
/**
|
|
* 관리자 역할 생성 및 할당
|
|
*
|
|
* @param array $permissions 사용자에게 부여할 권한 식별자 목록
|
|
*/
|
|
private function createAdminUser(array $permissions = ['core.plugins.read', 'core.plugins.install', 'core.plugins.activate', 'core.plugins.uninstall']): 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' => PermissionType::Admin,
|
|
]
|
|
);
|
|
$permissionIds[] = $permission->id;
|
|
}
|
|
|
|
// 고유한 식별자로 테스트용 역할 생성 (테스트별 격리를 위해)
|
|
$roleIdentifier = 'admin_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',
|
|
]);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 인증/권한 테스트
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 인증 없이 플러그인 목록 조회 시 401 반환
|
|
*/
|
|
public function test_index_returns_401_without_authentication(): void
|
|
{
|
|
$response = $this->getJson('/api/admin/plugins');
|
|
|
|
$response->assertStatus(401);
|
|
}
|
|
|
|
/**
|
|
* 권한 없이 플러그인 목록 조회 시 403 반환
|
|
*/
|
|
public function test_index_returns_403_without_permission(): void
|
|
{
|
|
// 권한 없는 관리자 생성
|
|
$user = User::factory()->create();
|
|
$adminRole = Role::where('identifier', 'admin')->first();
|
|
|
|
// 기존 권한 분리
|
|
$readPermission = Permission::where('identifier', 'core.plugins.read')->first();
|
|
if ($adminRole && $readPermission) {
|
|
$adminRole->permissions()->detach($readPermission->id);
|
|
}
|
|
|
|
$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');
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
/**
|
|
* 설치 권한 없이 플러그인 설치 시 403 반환
|
|
*/
|
|
public function test_install_returns_403_without_install_permission(): void
|
|
{
|
|
// read 권한만 있는 관리자 생성
|
|
$user = $this->createAdminUser(['core.plugins.read']);
|
|
$token = $user->createToken('test-token')->plainTextToken;
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$token,
|
|
'Accept' => 'application/json',
|
|
])->postJson('/api/admin/plugins/install', [
|
|
'plugin_name' => 'test-plugin',
|
|
]);
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 플러그인 목록 테스트 (index)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 페이지네이션된 플러그인 목록 조회 성공
|
|
*/
|
|
public function test_index_returns_paginated_plugins(): void
|
|
{
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['success' => true])
|
|
->assertJsonStructure([
|
|
'success',
|
|
'message',
|
|
'data' => [
|
|
'data',
|
|
'pagination' => [
|
|
'total',
|
|
'current_page',
|
|
'last_page',
|
|
'per_page',
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 단일 검색어 필터 테스트
|
|
*/
|
|
public function test_index_supports_search_filter(): void
|
|
{
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins?search=test');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* 다중 검색 필터 테스트
|
|
*/
|
|
public function test_index_supports_multiple_filters(): void
|
|
{
|
|
$filters = [
|
|
[
|
|
'field' => 'name',
|
|
'operator' => 'like',
|
|
'value' => 'test',
|
|
],
|
|
];
|
|
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins?'.http_build_query(['filters' => $filters]));
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* 상태 필터 테스트
|
|
*/
|
|
public function test_index_supports_status_filter(): void
|
|
{
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins?status=installed');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 설치된 플러그인 테스트 (installed)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 설치된 플러그인만 조회
|
|
*/
|
|
public function test_installed_returns_only_installed_plugins(): void
|
|
{
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins/installed');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* 전체 권한 유저의 index 응답에 abilities 포함 확인
|
|
*/
|
|
public function test_index_returns_abilities_for_full_permission_user(): void
|
|
{
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('data.abilities.can_install', true)
|
|
->assertJsonPath('data.abilities.can_activate', true)
|
|
->assertJsonPath('data.abilities.can_uninstall', true);
|
|
}
|
|
|
|
/**
|
|
* read 전용 유저의 index 응답에 abilities false 확인
|
|
*/
|
|
public function test_index_returns_false_abilities_for_read_only_user(): void
|
|
{
|
|
$readOnlyUser = $this->createAdminUser(['core.plugins.read']);
|
|
$readOnlyToken = $readOnlyUser->createToken('test-token')->plainTextToken;
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$readOnlyToken,
|
|
'Accept' => 'application/json',
|
|
])->getJson('/api/admin/plugins');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('data.abilities.can_install', false)
|
|
->assertJsonPath('data.abilities.can_activate', false)
|
|
->assertJsonPath('data.abilities.can_uninstall', false);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 플러그인 상세 테스트 (show)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 플러그인 상세 조회 성공
|
|
*/
|
|
public function test_show_returns_plugin_details(): void
|
|
{
|
|
// 실제 플러그인이 설치되어 있지 않으면 404가 반환될 수 있음
|
|
// 테스트 환경에 따라 적절한 플러그인명 사용
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins/test-plugin');
|
|
|
|
// 플러그인이 없으면 404, 있으면 200
|
|
$this->assertContains($response->status(), [200, 404]);
|
|
}
|
|
|
|
/**
|
|
* 존재하지 않는 플러그인 조회 시 404 반환
|
|
*/
|
|
public function test_show_returns_404_for_nonexistent_plugin(): void
|
|
{
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins/nonexistent-plugin-name');
|
|
|
|
$response->assertStatus(404);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 플러그인 설치 테스트 (install)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* plugin_name 필수 검증
|
|
*/
|
|
public function test_install_validates_plugin_name_required(): void
|
|
{
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install', []);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['plugin_name']);
|
|
}
|
|
|
|
/**
|
|
* plugin_name 최대 길이 검증
|
|
*/
|
|
public function test_install_validates_plugin_name_max_length(): void
|
|
{
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install', [
|
|
'plugin_name' => str_repeat('a', 300), // 255자 초과
|
|
]);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['plugin_name']);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 플러그인 활성화 테스트 (activate)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* plugin_name 필수 검증
|
|
*/
|
|
public function test_activate_validates_plugin_name_required(): void
|
|
{
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/activate', []);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['plugin_name']);
|
|
}
|
|
|
|
/**
|
|
* 활성화 요청 성공 (유효한 플러그인이 있는 경우)
|
|
*/
|
|
public function test_activate_returns_success_for_valid_plugin(): void
|
|
{
|
|
// 실제 플러그인이 설치되어 있어야 활성화 가능
|
|
// 테스트 환경에서는 validation만 통과하면 됨
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/activate', [
|
|
'plugin_name' => 'nonexistent-plugin',
|
|
]);
|
|
|
|
// 플러그인이 없으면 실패, 있으면 성공
|
|
$this->assertContains($response->status(), [200, 400, 422, 500]);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 플러그인 비활성화 테스트 (deactivate)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* plugin_name 필수 검증
|
|
*/
|
|
public function test_deactivate_validates_plugin_name_required(): void
|
|
{
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/deactivate', []);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['plugin_name']);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 플러그인 제거 테스트 (uninstall)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* plugin_name 필수 검증
|
|
*/
|
|
public function test_uninstall_validates_plugin_name_required(): void
|
|
{
|
|
$response = $this->authRequest()->deleteJson('/api/admin/plugins/uninstall', []);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['plugin_name']);
|
|
}
|
|
|
|
/**
|
|
* 제거 요청 성공 (유효한 플러그인이 있는 경우)
|
|
*/
|
|
public function test_uninstall_returns_success(): void
|
|
{
|
|
// 실제 플러그인이 설치되어 있어야 제거 가능
|
|
$response = $this->authRequest()->deleteJson('/api/admin/plugins/uninstall', [
|
|
'plugin_name' => 'nonexistent-plugin',
|
|
]);
|
|
|
|
// 플러그인이 없으면 실패, 있으면 성공
|
|
$this->assertContains($response->status(), [200, 400, 422, 500]);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 플러그인 활성화/비활성화 의존성 테스트
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 플러그인 활성화 시 필요한 의존성이 충족되지 않으면 409 경고 응답
|
|
*/
|
|
public function test_activate_returns_409_when_dependencies_not_met(): void
|
|
{
|
|
// Arrange: PluginService를 Mock
|
|
$pluginServiceMock = \Mockery::mock(PluginService::class);
|
|
$pluginServiceMock->shouldReceive('activatePlugin')
|
|
->with('test-plugin', false)
|
|
->andReturn([
|
|
'success' => false,
|
|
'warning' => true,
|
|
'missing_modules' => [
|
|
['identifier' => 'sirsoft-core', 'name' => 'Core Module', 'status' => 'not_installed'],
|
|
],
|
|
'missing_plugins' => [
|
|
['identifier' => 'sirsoft-base', 'name' => 'Base Plugin', 'status' => 'inactive'],
|
|
],
|
|
'message' => '플러그인 활성화를 위해 필요한 의존성이 충족되지 않았습니다.',
|
|
]);
|
|
$this->app->instance(PluginService::class, $pluginServiceMock);
|
|
|
|
// Act
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/activate', [
|
|
'plugin_name' => 'test-plugin',
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertStatus(409)
|
|
->assertJsonStructure([
|
|
'success',
|
|
'message',
|
|
'errors' => [
|
|
'warning',
|
|
'missing_modules',
|
|
'missing_plugins',
|
|
],
|
|
])
|
|
->assertJsonFragment(['warning' => true]);
|
|
|
|
// missing_modules 확인
|
|
$this->assertNotEmpty($response->json('errors.missing_modules'));
|
|
$this->assertEquals('sirsoft-core', $response->json('errors.missing_modules.0.identifier'));
|
|
|
|
// missing_plugins 확인
|
|
$this->assertNotEmpty($response->json('errors.missing_plugins'));
|
|
$this->assertEquals('sirsoft-base', $response->json('errors.missing_plugins.0.identifier'));
|
|
}
|
|
|
|
/**
|
|
* 플러그인 활성화 시 force=true로 의존성 경고 무시하고 강제 활성화
|
|
*/
|
|
public function test_activate_with_force_bypasses_dependency_check(): void
|
|
{
|
|
// Arrange: PluginService를 Mock
|
|
$pluginServiceMock = \Mockery::mock(PluginService::class);
|
|
$pluginServiceMock->shouldReceive('activatePlugin')
|
|
->with('test-plugin', true)
|
|
->andReturn([
|
|
'success' => true,
|
|
'plugin_info' => [
|
|
'identifier' => 'test-plugin',
|
|
'name' => 'Test Plugin',
|
|
'status' => 'active',
|
|
],
|
|
]);
|
|
$this->app->instance(PluginService::class, $pluginServiceMock);
|
|
|
|
// Act
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/activate', [
|
|
'plugin_name' => 'test-plugin',
|
|
'force' => true,
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertStatus(200)
|
|
->assertJsonFragment(['success' => true]);
|
|
}
|
|
|
|
/**
|
|
* 플러그인 비활성화 시 의존하는 확장이 있으면 409 경고 응답
|
|
*/
|
|
public function test_deactivate_returns_409_when_dependents_exist(): void
|
|
{
|
|
// Arrange: PluginService를 Mock
|
|
$pluginServiceMock = \Mockery::mock(PluginService::class);
|
|
$pluginServiceMock->shouldReceive('deactivatePlugin')
|
|
->with('test-plugin', false)
|
|
->andReturn([
|
|
'success' => false,
|
|
'warning' => true,
|
|
'dependent_templates' => ['sirsoft-admin_basic'],
|
|
'dependent_modules' => [
|
|
['identifier' => 'sirsoft-ecommerce', 'name' => 'Ecommerce Module'],
|
|
],
|
|
'dependent_plugins' => [
|
|
['identifier' => 'sirsoft-payment-card', 'name' => 'Card Payment'],
|
|
],
|
|
'message' => '이 플러그인에 의존하는 활성화된 확장이 있습니다.',
|
|
]);
|
|
$this->app->instance(PluginService::class, $pluginServiceMock);
|
|
|
|
// Act
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/deactivate', [
|
|
'plugin_name' => 'test-plugin',
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertStatus(409)
|
|
->assertJsonStructure([
|
|
'success',
|
|
'message',
|
|
'errors' => [
|
|
'warning',
|
|
'dependent_templates',
|
|
'dependent_modules',
|
|
'dependent_plugins',
|
|
],
|
|
])
|
|
->assertJsonFragment(['warning' => true]);
|
|
}
|
|
|
|
/**
|
|
* 플러그인 비활성화 시 force=true로 의존성 경고 무시하고 강제 비활성화
|
|
*/
|
|
public function test_deactivate_with_force_bypasses_dependent_check(): void
|
|
{
|
|
// Arrange: PluginService를 Mock
|
|
$pluginServiceMock = \Mockery::mock(PluginService::class);
|
|
$pluginServiceMock->shouldReceive('deactivatePlugin')
|
|
->with('test-plugin', true)
|
|
->andReturn([
|
|
'success' => true,
|
|
'plugin_info' => [
|
|
'identifier' => 'test-plugin',
|
|
'name' => 'Test Plugin',
|
|
'status' => 'inactive',
|
|
],
|
|
]);
|
|
$this->app->instance(PluginService::class, $pluginServiceMock);
|
|
|
|
// Act
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/deactivate', [
|
|
'plugin_name' => 'test-plugin',
|
|
'force' => true,
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertStatus(200)
|
|
->assertJsonFragment(['success' => true]);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 에셋 정보 포함 테스트
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 플러그인 활성화 응답에 assets 정보가 포함됨
|
|
*/
|
|
public function test_activate_response_includes_assets_when_plugin_has_assets(): void
|
|
{
|
|
// Arrange: PluginService를 Mock
|
|
$pluginServiceMock = \Mockery::mock(PluginService::class);
|
|
$pluginServiceMock->shouldReceive('activatePlugin')
|
|
->with('test-plugin', false)
|
|
->andReturn([
|
|
'success' => true,
|
|
'plugin_info' => [
|
|
'identifier' => 'test-plugin',
|
|
'name' => 'Test Plugin',
|
|
'status' => 'active',
|
|
'assets' => [
|
|
'js' => '/api/plugins/assets/test-plugin/dist/js/plugin.iife.js',
|
|
'css' => '/api/plugins/assets/test-plugin/dist/css/plugin.css',
|
|
'priority' => 100,
|
|
],
|
|
],
|
|
]);
|
|
$this->app->instance(PluginService::class, $pluginServiceMock);
|
|
|
|
// Act
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/activate', [
|
|
'plugin_name' => 'test-plugin',
|
|
]);
|
|
|
|
// Assert
|
|
// 활성화 응답은 data.plugin 으로 PluginResource 를 래핑하고, pending_language_packs 를 동시에 노출
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('data.plugin.assets.js', '/api/plugins/assets/test-plugin/dist/js/plugin.iife.js')
|
|
->assertJsonPath('data.plugin.assets.css', '/api/plugins/assets/test-plugin/dist/css/plugin.css')
|
|
->assertJsonPath('data.plugin.assets.priority', 100);
|
|
}
|
|
|
|
/**
|
|
* 플러그인 비활성화 응답에 assets 정보가 포함됨
|
|
*/
|
|
public function test_deactivate_response_includes_assets_when_plugin_has_assets(): void
|
|
{
|
|
// Arrange: PluginService를 Mock
|
|
$pluginServiceMock = \Mockery::mock(PluginService::class);
|
|
$pluginServiceMock->shouldReceive('deactivatePlugin')
|
|
->with('test-plugin', false)
|
|
->andReturn([
|
|
'success' => true,
|
|
'plugin_info' => [
|
|
'identifier' => 'test-plugin',
|
|
'name' => 'Test Plugin',
|
|
'status' => 'inactive',
|
|
'assets' => [
|
|
'js' => '/api/plugins/assets/test-plugin/dist/js/plugin.iife.js',
|
|
'css' => '/api/plugins/assets/test-plugin/dist/css/plugin.css',
|
|
'priority' => 100,
|
|
],
|
|
],
|
|
]);
|
|
$this->app->instance(PluginService::class, $pluginServiceMock);
|
|
|
|
// Act
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/deactivate', [
|
|
'plugin_name' => 'test-plugin',
|
|
]);
|
|
|
|
// Assert
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('data.assets.js', '/api/plugins/assets/test-plugin/dist/js/plugin.iife.js')
|
|
->assertJsonPath('data.assets.css', '/api/plugins/assets/test-plugin/dist/css/plugin.css')
|
|
->assertJsonPath('data.assets.priority', 100);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Changelog 조회 테스트
|
|
// ========================================================================
|
|
|
|
/**
|
|
* CHANGELOG.md가 있는 플러그인의 changelog를 조회합니다.
|
|
*/
|
|
public function test_changelog_returns_parsed_entries(): void
|
|
{
|
|
$pluginPath = base_path('plugins/test-changelog-plg');
|
|
File::ensureDirectoryExists($pluginPath);
|
|
File::put($pluginPath.'/CHANGELOG.md', "# Changelog\n\n## [0.1.1] - 2026-02-25\n\n### Fixed\n- 버그 수정\n");
|
|
|
|
try {
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins/test-changelog-plg/changelog');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('data.changelog.0.version', '0.1.1')
|
|
->assertJsonPath('data.changelog.0.categories.0.name', 'Fixed');
|
|
} finally {
|
|
File::deleteDirectory($pluginPath);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* CHANGELOG.md가 없는 플러그인은 빈 배열을 반환합니다.
|
|
*/
|
|
public function test_changelog_returns_empty_for_missing_file(): void
|
|
{
|
|
$response = $this->authRequest()->getJson('/api/admin/plugins/nonexistent-plugin/changelog');
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonPath('data.changelog', []);
|
|
}
|
|
|
|
/**
|
|
* 인증 없이 changelog 조회 시 401 반환
|
|
*/
|
|
public function test_changelog_returns_401_without_authentication(): void
|
|
{
|
|
$response = $this->getJson('/api/admin/plugins/test-plugin/changelog');
|
|
$response->assertStatus(401);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 식별자 형식 검증 테스트
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 잘못된 형식의 플러그인 식별자로 설치 요청 시 422 반환
|
|
*/
|
|
public function test_install_returns_422_for_invalid_identifier_format(): void
|
|
{
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install', [
|
|
'plugin_name' => 'sirsoftpayment', // 하이픈 없음
|
|
]);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['plugin_name']);
|
|
}
|
|
|
|
/**
|
|
* 숫자로 시작하는 단어가 포함된 플러그인 식별자로 설치 요청 시 422 반환
|
|
*/
|
|
public function test_install_returns_422_for_digit_starting_identifier(): void
|
|
{
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install', [
|
|
'plugin_name' => 'sirsoft-2payment',
|
|
]);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['plugin_name']);
|
|
}
|
|
|
|
/**
|
|
* 특수문자가 포함된 플러그인 식별자로 설치 요청 시 422 반환
|
|
*/
|
|
public function test_install_returns_422_for_special_char_identifier(): void
|
|
{
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install', [
|
|
'plugin_name' => 'sirsoft-pay@ment',
|
|
]);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['plugin_name']);
|
|
}
|
|
|
|
// ========================================================================
|
|
// 파일 업로드 설치 테스트 (installFromFile)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 인증 없이 파일 업로드 설치 시 401 반환
|
|
*/
|
|
public function test_install_from_file_returns_401_without_authentication(): void
|
|
{
|
|
$response = $this->postJson('/api/admin/plugins/install-from-file', []);
|
|
|
|
$response->assertStatus(401);
|
|
}
|
|
|
|
/**
|
|
* 파일 없이 업로드 설치 요청 시 422 반환
|
|
*/
|
|
public function test_install_from_file_returns_422_without_file(): void
|
|
{
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-file', []);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['file']);
|
|
}
|
|
|
|
/**
|
|
* 잘못된 파일 형식으로 업로드 시 422 반환
|
|
*/
|
|
public function test_install_from_file_returns_422_for_invalid_file_type(): void
|
|
{
|
|
Storage::fake('local');
|
|
|
|
$file = UploadedFile::fake()->create('document.pdf', 100);
|
|
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-file', [
|
|
'file' => $file,
|
|
]);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['file']);
|
|
}
|
|
|
|
/**
|
|
* 파일 크기 초과 시 422 반환
|
|
*/
|
|
public function test_install_from_file_returns_422_for_oversized_file(): void
|
|
{
|
|
Storage::fake('local');
|
|
|
|
// 설정된 최대 크기보다 큰 파일 생성 (기본 50MB)
|
|
$maxSize = config('plugin.upload_max_size', 50);
|
|
$oversizedKb = ($maxSize * 1024) + 100; // 100KB 초과
|
|
|
|
$file = UploadedFile::fake()->create('plugin.zip', $oversizedKb);
|
|
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-file', [
|
|
'file' => $file,
|
|
]);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['file']);
|
|
}
|
|
|
|
/**
|
|
* 설치 권한 없이 파일 업로드 설치 시 403 반환
|
|
*/
|
|
public function test_install_from_file_returns_403_without_install_permission(): void
|
|
{
|
|
$user = $this->createAdminUser(['core.plugins.read']);
|
|
$token = $user->createToken('test-token')->plainTextToken;
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$token,
|
|
'Accept' => 'application/json',
|
|
])->postJson('/api/admin/plugins/install-from-file', []);
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
/**
|
|
* 유효한 ZIP 파일 업로드 시 PluginService::installFromZipFile 호출 확인
|
|
*/
|
|
public function test_install_from_file_calls_service_with_valid_zip(): void
|
|
{
|
|
$pluginServiceMock = \Mockery::mock(PluginService::class);
|
|
$pluginServiceMock->shouldReceive('installFromZipFile')
|
|
->once()
|
|
->andReturn([
|
|
'identifier' => 'sirsoft-testplugin',
|
|
'name' => 'Test Plugin',
|
|
'version' => '1.0.0',
|
|
]);
|
|
$this->app->instance(PluginService::class, $pluginServiceMock);
|
|
|
|
$file = UploadedFile::fake()->create('plugin.zip', 100, 'application/zip');
|
|
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-file', [
|
|
'file' => $file,
|
|
]);
|
|
|
|
$response->assertStatus(201)
|
|
->assertJson(['success' => true])
|
|
->assertJsonPath('data.identifier', 'sirsoft-testplugin');
|
|
}
|
|
|
|
/**
|
|
* PluginService에서 RuntimeException 발생 시 422 반환
|
|
*/
|
|
public function test_install_from_file_returns_422_on_domain_exception(): void
|
|
{
|
|
$pluginServiceMock = \Mockery::mock(PluginService::class);
|
|
$pluginServiceMock->shouldReceive('installFromZipFile')
|
|
->once()
|
|
->andThrow(new PluginOperationException('plugins.errors.plugin_json_not_found'));
|
|
$this->app->instance(PluginService::class, $pluginServiceMock);
|
|
|
|
$file = UploadedFile::fake()->create('plugin.zip', 100, 'application/zip');
|
|
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-file', [
|
|
'file' => $file,
|
|
]);
|
|
|
|
$response->assertStatus(422);
|
|
$response->assertJsonPath('message', __('plugins.errors.plugin_json_not_found'));
|
|
}
|
|
|
|
/**
|
|
* PluginService에서 일반 Exception 발생 시 500 반환
|
|
*/
|
|
public function test_install_from_file_returns_500_on_general_exception(): void
|
|
{
|
|
$pluginServiceMock = \Mockery::mock(PluginService::class);
|
|
$pluginServiceMock->shouldReceive('installFromZipFile')
|
|
->once()
|
|
->andThrow(new \Exception('예상치 못한 오류'));
|
|
$this->app->instance(PluginService::class, $pluginServiceMock);
|
|
|
|
$file = UploadedFile::fake()->create('plugin.zip', 100, 'application/zip');
|
|
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-file', [
|
|
'file' => $file,
|
|
]);
|
|
|
|
$response->assertStatus(500);
|
|
}
|
|
|
|
// ========================================================================
|
|
// GitHub 설치 테스트 (installFromGithub)
|
|
// ========================================================================
|
|
|
|
/**
|
|
* 인증 없이 GitHub 설치 시 401 반환
|
|
*/
|
|
public function test_install_from_github_returns_401_without_authentication(): void
|
|
{
|
|
$response = $this->postJson('/api/admin/plugins/install-from-github', []);
|
|
|
|
$response->assertStatus(401);
|
|
}
|
|
|
|
/**
|
|
* GitHub URL 없이 설치 요청 시 422 반환
|
|
*/
|
|
public function test_install_from_github_returns_422_without_url(): void
|
|
{
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-github', []);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['github_url']);
|
|
}
|
|
|
|
/**
|
|
* 잘못된 GitHub URL 형식으로 요청 시 422 반환
|
|
*/
|
|
public function test_install_from_github_returns_422_for_invalid_url(): void
|
|
{
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-github', [
|
|
'github_url' => 'not-a-valid-url',
|
|
]);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['github_url']);
|
|
}
|
|
|
|
/**
|
|
* GitHub이 아닌 URL로 요청 시 422 반환
|
|
*/
|
|
public function test_install_from_github_returns_422_for_non_github_url(): void
|
|
{
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-github', [
|
|
'github_url' => 'https://gitlab.com/vendor/plugin',
|
|
]);
|
|
|
|
$response->assertStatus(422)
|
|
->assertJsonValidationErrors(['github_url']);
|
|
}
|
|
|
|
/**
|
|
* 유효한 GitHub URL 형식 테스트
|
|
*/
|
|
public function test_install_from_github_accepts_valid_github_url(): void
|
|
{
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-github', [
|
|
'github_url' => 'https://github.com/sirsoft/sample-plugin',
|
|
]);
|
|
|
|
// URL 형식 검증 통과 확인: 422가 아니거나 github_url 관련 validation 에러가 없어야 함
|
|
if ($response->status() === 422) {
|
|
$response->assertJsonMissingValidationErrors(['github_url']);
|
|
} else {
|
|
$this->assertContains($response->status(), [200, 201, 500]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 설치 권한 없이 GitHub 설치 시 403 반환
|
|
*/
|
|
public function test_install_from_github_returns_403_without_install_permission(): void
|
|
{
|
|
$user = $this->createAdminUser(['core.plugins.read']);
|
|
$token = $user->createToken('test-token')->plainTextToken;
|
|
|
|
$response = $this->withHeaders([
|
|
'Authorization' => 'Bearer '.$token,
|
|
'Accept' => 'application/json',
|
|
])->postJson('/api/admin/plugins/install-from-github', [
|
|
'github_url' => 'https://github.com/sirsoft/sample-plugin',
|
|
]);
|
|
|
|
$response->assertStatus(403);
|
|
}
|
|
|
|
/**
|
|
* 유효한 GitHub URL로 설치 시 PluginService::installFromGithub 호출 확인
|
|
*/
|
|
public function test_install_from_github_calls_service_with_valid_url(): void
|
|
{
|
|
$pluginServiceMock = \Mockery::mock(PluginService::class);
|
|
$pluginServiceMock->shouldReceive('installFromGithub')
|
|
->once()
|
|
->with('https://github.com/sirsoft/sample-plugin')
|
|
->andReturn([
|
|
'identifier' => 'sirsoft-sampleplugin',
|
|
'name' => 'Sample Plugin',
|
|
'version' => '1.0.0',
|
|
]);
|
|
$this->app->instance(PluginService::class, $pluginServiceMock);
|
|
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-github', [
|
|
'github_url' => 'https://github.com/sirsoft/sample-plugin',
|
|
]);
|
|
|
|
$response->assertStatus(201)
|
|
->assertJson(['success' => true])
|
|
->assertJsonPath('data.identifier', 'sirsoft-sampleplugin');
|
|
}
|
|
|
|
/**
|
|
* PluginService에서 RuntimeException 발생 시 422 반환
|
|
*/
|
|
public function test_install_from_github_returns_422_on_domain_exception(): void
|
|
{
|
|
$pluginServiceMock = \Mockery::mock(PluginService::class);
|
|
$pluginServiceMock->shouldReceive('installFromGithub')
|
|
->once()
|
|
->andThrow(new PluginOperationException('plugins.errors.github_repo_not_found'));
|
|
$this->app->instance(PluginService::class, $pluginServiceMock);
|
|
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-github', [
|
|
'github_url' => 'https://github.com/sirsoft/sample-plugin',
|
|
]);
|
|
|
|
$response->assertStatus(422);
|
|
$response->assertJsonPath('message', __('plugins.errors.github_repo_not_found'));
|
|
}
|
|
|
|
/**
|
|
* PluginService에서 일반 Exception 발생 시 500 반환
|
|
*/
|
|
public function test_install_from_github_returns_500_on_general_exception(): void
|
|
{
|
|
$pluginServiceMock = \Mockery::mock(PluginService::class);
|
|
$pluginServiceMock->shouldReceive('installFromGithub')
|
|
->once()
|
|
->andThrow(new \Exception('예상치 못한 오류'));
|
|
$this->app->instance(PluginService::class, $pluginServiceMock);
|
|
|
|
$response = $this->authRequest()->postJson('/api/admin/plugins/install-from-github', [
|
|
'github_url' => 'https://github.com/sirsoft/sample-plugin',
|
|
]);
|
|
|
|
$response->assertStatus(500);
|
|
}
|
|
}
|