Files
Gnuboard7/tests/Feature/Services/TemplateServiceInstallationTest.php
HeuJung d970117149 fix(core,ecommerce,board,admin_basic): 고아 카탈로그 차단 + 설정 저장 파이프라인 정합
확장이 훅으로 등록한 카탈로그에서 고른 값은 그 확장이 사라져도 저장값으로 남는다.
그 상태에서 값만 보고 판정하면 이미 제공 불가한 항목이 사용자 화면에 그대로 노출된다.
예외도 경고도 로그도 남지 않아 관리자 화면과 나란히 보지 않으면 드러나지 않는다.

- 결제수단에 지정한 PG, 현금영수증 발급사, 검색엔진 드라이버 세 축에 같은 판정을 적용.
 공개 응답에서는 제거하고 관리자 응답에는 표시를 남긴다 — 운영자가 고쳐야 할 대상이라
 감추면 복구 경로가 사라진다. 서버 검증도 같은 판정을 공유해 화면 우회 제출을 막는다.
- 설정 단건 저장이 벌크 저장과 다른 경로를 타던 문제를 코어·이커머스·게시판에서 정리.
 저장은 성공하고 파생 값만 갱신되지 않아 어느 화면에서 저장했느냐로 결과가 갈렸다.
- 단건 저장의 값 형태 계약 신설. 빈 값으로 되돌릴 수 있게 하고, 타입은 defaults.json 의
 기본값을 SSoT 로 삼는다. 해석 불가한 입력은 조용히 캐스팅하지 않고 거절한다.
- 드라이버 셀렉트를 서버 카탈로그에 바인딩하고, 저장값이 카탈로그에 없으면 그 값을
 드러내는 안내를 8곳 전수에 붙인다. 종전에는 빈 칸으로만 보여 무엇이 저장됐는지
 알 수 없었다.

시나리오 매니페스트 8종을 면제 없이 신설했다. `audit:allow` 는 cross product 와 effects
검사를 함께 끄므로, 면제가 걸린 매니페스트에 선언을 추가하면 검증이 회복되지 않는다.
그 아래에서 대응 테스트가 없는 effect 이름 2건이 살아남아 있었고, 함께 정리했다.
2026-08-14 17:28:49 +09:00

436 lines
13 KiB
PHP

<?php
namespace Tests\Feature\Services;
use App\Contracts\Extension\ModuleManagerInterface;
use App\Contracts\Extension\PluginManagerInterface;
use App\Contracts\Extension\TemplateManagerInterface;
use App\Contracts\Repositories\LayoutVersionRepositoryInterface;
use App\Enums\ExtensionStatus;
use App\Models\Template;
use App\Repositories\TemplateRepository;
use App\Services\TemplateService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Validation\ValidationException;
use Mockery;
use Tests\TestCase;
class TemplateServiceInstallationTest extends TestCase
{
use RefreshDatabase;
private TemplateService $templateService;
private TemplateRepository $templateRepository;
private TemplateManagerInterface $templateManager;
private ModuleManagerInterface $moduleManager;
private PluginManagerInterface $pluginManager;
protected function setUp(): void
{
parent::setUp();
// TemplateManager Mock 생성
$this->templateManager = Mockery::mock(TemplateManagerInterface::class);
// loadTemplates()/ensureLoaded() 호출 허용 (생성자에서 ensureLoaded 호출됨)
$this->templateManager->shouldReceive('loadTemplates')
->zeroOrMoreTimes()
->andReturnNull();
$this->templateManager->shouldReceive('ensureLoaded')
->zeroOrMoreTimes()
->andReturnNull();
// ModuleManager Mock 생성
$this->moduleManager = Mockery::mock(ModuleManagerInterface::class);
// PluginManager Mock 생성
$this->pluginManager = Mockery::mock(PluginManagerInterface::class);
// TemplateRepository 인스턴스 생성
$this->templateRepository = new TemplateRepository;
// TemplateService 인스턴스 생성 (Mock Manager 주입)
$this->templateService = new TemplateService(
$this->templateRepository,
$this->templateManager,
$this->moduleManager,
$this->pluginManager,
app(LayoutVersionRepositoryInterface::class)
);
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
/**
* getAllTemplates 메서드가 설치된 템플릿과 미설치 템플릿을 모두 반환하는지 테스트
*/
public function test_get_all_templates_returns_installed_and_uninstalled(): void
{
// Arrange
$installedTemplates = [
[
'identifier' => 'installed-template',
'vendor' => 'test-vendor',
'name' => 'Installed Template',
'type' => 'admin',
'version' => '1.0.0',
'status' => ExtensionStatus::Active->value,
],
];
$uninstalledTemplates = [
[
'identifier' => 'uninstalled-template',
'vendor' => 'test-vendor',
'name' => 'Uninstalled Template',
'type' => 'admin',
'version' => '1.0.0',
],
];
$this->templateManager
->shouldReceive('getInstalledTemplatesWithDetails')
->once()
->andReturn($installedTemplates);
$this->templateManager
->shouldReceive('getUninstalledTemplates')
->once()
->andReturn($uninstalledTemplates);
// Act
$result = $this->templateService->getAllTemplates();
// Assert
$this->assertIsArray($result);
$this->assertArrayHasKey('installed', $result);
$this->assertArrayHasKey('uninstalled', $result);
$this->assertArrayHasKey('total', $result);
$this->assertCount(1, $result['installed']);
$this->assertCount(1, $result['uninstalled']);
$this->assertEquals(2, $result['total']);
}
/**
* getAllTemplates 메서드가 타입별로 필터링하는지 테스트
*/
public function test_get_all_templates_filters_by_type(): void
{
// Arrange
$installedTemplates = [
[
'identifier' => 'admin-template',
'type' => 'admin',
],
[
'identifier' => 'user-template',
'type' => 'user',
],
];
$this->templateManager
->shouldReceive('getInstalledTemplatesWithDetails')
->once()
->andReturn($installedTemplates);
$this->templateManager
->shouldReceive('getUninstalledTemplates')
->once()
->andReturn([]);
// Act
$result = $this->templateService->getAllTemplates('admin');
// Assert
$this->assertCount(1, $result['installed']);
$this->assertEquals('admin-template', $result['installed'][0]['identifier']);
}
/**
* getInstalledTemplatesOnly 메서드가 설치된 템플릿만 반환하는지 테스트
*/
public function test_get_installed_templates_only(): void
{
// Arrange
$installedTemplates = [
['identifier' => 'template1', 'type' => 'admin'],
['identifier' => 'template2', 'type' => 'user'],
];
$this->templateManager
->shouldReceive('getInstalledTemplatesWithDetails')
->once()
->andReturn($installedTemplates);
// Act
$result = $this->templateService->getInstalledTemplatesOnly();
// Assert
$this->assertIsArray($result);
$this->assertCount(2, $result);
}
/**
* getUninstalledTemplatesOnly 메서드가 미설치 템플릿만 반환하는지 테스트
*/
public function test_get_uninstalled_templates_only(): void
{
// Arrange
$uninstalledTemplates = [
['identifier' => 'template3', 'type' => 'admin'],
];
$this->templateManager
->shouldReceive('getUninstalledTemplates')
->once()
->andReturn($uninstalledTemplates);
// Act
$result = $this->templateService->getUninstalledTemplatesOnly();
// Assert
$this->assertIsArray($result);
$this->assertCount(1, $result);
}
/**
* getTemplateInfo 메서드가 특정 템플릿 정보를 반환하는지 테스트
*/
public function test_get_template_info(): void
{
// Arrange
$identifier = 'test-template';
$templateInfo = [
'identifier' => $identifier,
'vendor' => 'test-vendor',
'name' => 'Test Template',
'type' => 'admin',
'version' => '1.0.0',
];
$this->templateManager
->shouldReceive('getTemplateInfo')
->once()
->with($identifier)
->andReturn($templateInfo);
// Act
$result = $this->templateService->getTemplateInfo($identifier);
// Assert
$this->assertIsArray($result);
$this->assertEquals($identifier, $result['identifier']);
}
/**
* installTemplate 메서드가 템플릿을 설치하는지 테스트
*/
public function test_install_template_successfully(): void
{
// Arrange
$identifier = 'test-template';
$templateInfo = [
'identifier' => $identifier,
'vendor' => 'test-vendor',
'name' => 'Test Template',
'type' => 'admin',
];
$this->templateManager
->shouldReceive('installTemplate')
->once()
->with($identifier, null, false)
->andReturn(true);
// installTemplate은 getTemplateInfo를 호출함
$this->templateManager
->shouldReceive('getTemplateInfo')
->once()
->with($identifier)
->andReturn($templateInfo);
// Act
$result = $this->templateService->installTemplate($identifier);
// Assert: installTemplate은 배열을 반환함 (templateInfo)
$this->assertIsArray($result);
$this->assertEquals($identifier, $result['identifier']);
}
/**
* installTemplate 메서드가 실패 시 예외를 발생시키는지 테스트
*/
public function test_install_template_throws_exception_on_failure(): void
{
// Arrange
$identifier = 'test-template';
$this->templateManager
->shouldReceive('installTemplate')
->once()
->with($identifier, null, false)
->andThrow(new \Exception('Installation failed'));
// Act & Assert
$this->expectException(ValidationException::class);
$this->templateService->installTemplate($identifier);
}
/**
* uninstallTemplate 메서드가 템플릿을 제거하는지 테스트
*/
public function test_uninstall_template_successfully(): void
{
// Arrange
$identifier = 'test-template';
$templateInfo = [
'identifier' => $identifier,
'vendor' => 'test-vendor',
'name' => 'Test Template',
'type' => 'admin',
];
// uninstallTemplate은 getTemplateInfo를 먼저 호출함
$this->templateManager
->shouldReceive('getTemplateInfo')
->once()
->with($identifier)
->andReturn($templateInfo);
$this->templateManager
->shouldReceive('uninstallTemplate')
->once()
->with($identifier)
->andReturn(true);
// Act
$result = $this->templateService->uninstallTemplate($identifier);
// Assert: uninstallTemplate은 배열을 반환함 (templateInfo)
$this->assertIsArray($result);
$this->assertEquals($identifier, $result['identifier']);
}
/**
* uninstallTemplate 메서드가 실패 시 예외를 발생시키는지 테스트
*/
public function test_uninstall_template_throws_exception_on_failure(): void
{
// Arrange
$identifier = 'test-template';
$templateInfo = [
'identifier' => $identifier,
'vendor' => 'test-vendor',
'name' => 'Test Template',
'type' => 'admin',
];
// getTemplateInfo가 먼저 호출됨
$this->templateManager
->shouldReceive('getTemplateInfo')
->once()
->with($identifier)
->andReturn($templateInfo);
$this->templateManager
->shouldReceive('uninstallTemplate')
->once()
->with($identifier)
->andThrow(new \Exception('Uninstallation failed'));
// Act & Assert
$this->expectException(ValidationException::class);
$this->templateService->uninstallTemplate($identifier);
}
/**
* deactivateTemplate 메서드가 템플릿을 비활성화하는지 테스트
*
* 참고: 실제 deactivate 로직은 TemplateServiceActivationTest에서 테스트됨
* 이 테스트는 Mock 기반으로 Manager 호출이 올바르게 이루어지는지 확인
*/
public function test_deactivate_template_successfully(): void
{
// Arrange
$identifier = 'test-template';
$template = Template::create([
'identifier' => $identifier,
'vendor' => 'test-vendor',
'name' => 'Test Template',
'type' => 'admin',
'version' => '1.0.0',
'status' => ExtensionStatus::Active->value,
]);
$templateInfo = [
'identifier' => $identifier,
'vendor' => 'test-vendor',
'name' => 'Test Template',
'type' => 'admin',
'status' => ExtensionStatus::Inactive->value,
];
$this->templateManager
->shouldReceive('deactivateTemplate')
->once()
->with($identifier)
->andReturnUsing(function () use ($template) {
$template->update(['status' => ExtensionStatus::Inactive->value]);
return true;
});
// deactivateTemplate은 getTemplateInfo()를 호출함
// loadTemplates()는 setUp에서 zeroOrMoreTimes로 설정됨
$this->templateManager
->shouldReceive('getTemplateInfo')
->once()
->with($identifier)
->andReturn($templateInfo);
// Act
$result = $this->templateService->deactivateTemplate($identifier);
// Assert: deactivateTemplate은 배열을 반환함
$this->assertIsArray($result);
$this->assertEquals(ExtensionStatus::Inactive->value, $result['status']);
$this->assertEquals(ExtensionStatus::Inactive->value, $template->fresh()->status);
}
/**
* deactivateTemplate 메서드가 실패 시 예외를 발생시키는지 테스트
*/
public function test_deactivate_template_throws_exception_on_failure(): void
{
// Arrange
$identifier = 'test-template';
Template::create([
'identifier' => $identifier,
'vendor' => 'test-vendor',
'name' => 'Test Template',
'type' => 'admin',
'version' => '1.0.0',
'status' => ExtensionStatus::Active->value,
]);
$this->templateManager
->shouldReceive('deactivateTemplate')
->once()
->with($identifier)
->andThrow(new \Exception('Deactivation failed'));
// Act & Assert
$this->expectException(ValidationException::class);
$this->templateService->deactivateTemplate($identifier);
}
}