https://github.com/gnuboard/g7/issues/125 — 상품 상세설명을 HTML 로 저장할 때 HTMLPurifier 가 모듈 vendor 폴더 안에 정의 캐시를 만들려다 실패해 저장이 매번 500 으로 끝나던 문제를 고친다. vendor 를 읽기 전용으로 두는 표준 배포에서 그 쓰기는 예외가 아니라 PHP 경고로 나오고 Laravel 이 이를 ErrorException 으로 승격시킨다. 캐시는 설정 해시당 1회만 기록되므로 캐시가 영영 생기지 않아 재시도해도 같은 결과였다. 캐시 경로를 storage 아래로 옮기고, 그 경로마저 확보하지 못하면 캐시만 끄고 정화는 그대로 수행한다 — 캐시는 성능 장치이고 정화는 보안 장치라, 전자의 실패가 후자를 건너뛰게 만들면 안 된다. 저장은 성공하므로 운영자에게 도달하는 흔적이 로그 하나뿐이라 error 수준으로 남긴다 (출하 기본 로그 수준이 error 라 warning 은 기본 설치 상태에서 파일에 남지 않는다). 그 과정에서 갈라져 있던 두 축을 코어 한 곳으로 모은다. - 쓰기 디렉토리 확보: 억제 생성·chmod·setgid·소유권 상속·쓰기 판정 절차가 정적 게시와 정의 캐시 두 곳에 서로 다른 하드닝으로 복제돼 있었다(억제 mkdir·setgid·clearstatcache 가 사본마다 한쪽씩 빠져 있었다). FilePermissionHelper 의 ensureWritableDirectory 와 hardenDirectory 로 통합하고, 실패 사유는 out 파라미터로 올려 정책(조용한 성능 저하 대 시끄러운 실패)은 호출부가 정하게 둔다. - 확장 저장 경로: storage_path('app/modules/…') 손조립이 30곳에 흩어져 있어 테스트 격리 분기를 넣으려면 사본마다 복제해야 했고, 한 곳만 빠뜨려도 그 확장의 테스트가 운영 설정 파일을 덮어쓴다. 디스크 root 를 단일 출처로 읽는 ExtensionStoragePath 로 전환하고 테스트 분기는 config/filesystems.php 한 줄에서 끝낸다. 함께 고친 것 - 테스트가 운영 라우트 캐시로 부팅해 확장 allowlist 가 라우트 축에서 통째로 무력화되던 문제. 삭제가 아니라 경로를 돌린다 — 라우트 캐시는 확장 작업 전까지 재생성되지 않아, 삭제하면 운영 사이트가 그때까지 라우트 파일 스캔 경로로 떨어진다. - PHPUnit 프로세스가 확장 vendor 의 제3자 composer 패키지를 오토로드하지 않아 그 패키지를 쓰는 코드 경로가 통째로 테스트 불가였던 문제. 확장 자신의 오토로더를 그대로 쓰면 활성 디렉토리가 _bundled 를 이기고 base path 유추까지 깨지므로, 생성된 맵에서 제3자 항목만 골라 별도 로더로 등록한다. - 게시 폴더가 setgid 를 갖지 않아, 명령줄과 웹이 번갈아 만든 하위 폴더를 다른 쪽이 쓰지 못하던 문제. - 관리자 템플릿이 HTML 정화 라이브러리를 직접 지정하지 않아 전이 의존으로 딸려온 구버전이 쓰이던 문제. 동반 산출물 - 규정 표(·AGENTS.md) 6행 + storage-driver/service-repository/testing-guide 문서 - audit 룰 2종 + coverage 6항목. 저장소가 이미 전량 전환돼 전수 실행이 공허 통과하므로 판정식은 픽스처 36건이 잠근다 - INSTALL.md 에 설치 후 파일 권한 절 추가 (vendor 쓰기 권한 불요를 명시)
141 lines
5.6 KiB
PHP
141 lines
5.6 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Extension;
|
|
|
|
use App\Extension\Testing\ExtensionTestAllowlist;
|
|
use Illuminate\Contracts\Http\Kernel as HttpKernelContract;
|
|
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Plugins\Sirsoft\Gdpr\Http\Middleware\CookieConsentMiddleware;
|
|
use Plugins\Sirsoft\Gdpr\Providers\GdprServiceProvider;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* 테스트 환경 확장 격리 회귀 테스트
|
|
*
|
|
* requiredExtensions allowlist 가 ServiceProvider / route / middleware 등록
|
|
* 범위를 통제하는지 검증합니다.
|
|
*
|
|
* - core-only 테스트(requiredExtensions 미선언): GDPR 플러그인 미로드
|
|
* - requiredExtensions = ['plugins/sirsoft-gdpr']: GDPR 플러그인 로드
|
|
*
|
|
* 본 클래스 자체는 requiredExtensions 를 선언하지 않으므로 core-only 시나리오다.
|
|
*/
|
|
class ExtensionTestIsolationTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
/**
|
|
* @effects core_only_test_does_not_register_gdpr_service_provider
|
|
*/
|
|
public function test_core_only_test_does_not_register_gdpr_service_provider(): void
|
|
{
|
|
// allowlist 가 활성(testing + 명시적 set)이어야 격리가 동작
|
|
$this->assertTrue(ExtensionTestAllowlist::isActive());
|
|
$this->assertFalse(ExtensionTestAllowlist::isAllowed('plugin', 'sirsoft-gdpr'));
|
|
|
|
$this->assertFalse(
|
|
$this->app->providerIsLoaded(GdprServiceProvider::class),
|
|
'core-only 테스트에서 GdprServiceProvider 가 등록되었습니다 — 격리 실패'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @effects core_only_test_does_not_register_gdpr_middleware_in_web_group, core_only_test_does_not_register_gdpr_middleware_in_api_group
|
|
*/
|
|
public function test_core_only_test_does_not_register_gdpr_middleware_in_groups(): void
|
|
{
|
|
$kernel = $this->app->make(HttpKernelContract::class);
|
|
$this->assertInstanceOf(HttpKernel::class, $kernel);
|
|
|
|
$groups = $kernel->getMiddlewareGroups();
|
|
|
|
$webGroup = $groups['web'] ?? [];
|
|
$apiGroup = $groups['api'] ?? [];
|
|
|
|
$this->assertNotContains(
|
|
CookieConsentMiddleware::class,
|
|
$webGroup,
|
|
'core-only 테스트의 web 미들웨어 그룹에 CookieConsentMiddleware 가 개입했습니다'
|
|
);
|
|
$this->assertNotContains(
|
|
CookieConsentMiddleware::class,
|
|
$apiGroup,
|
|
'core-only 테스트의 api 미들웨어 그룹에 CookieConsentMiddleware 가 개입했습니다'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @effects core_only_test_does_not_register_gdpr_plugin_routes
|
|
*/
|
|
public function test_core_only_test_does_not_register_gdpr_plugin_routes(): void
|
|
{
|
|
$routes = collect($this->app['router']->getRoutes()->getRoutes())
|
|
->map(fn ($route) => $route->uri())
|
|
->filter(fn ($uri) => str_contains($uri, 'plugins/sirsoft-gdpr'));
|
|
|
|
$this->assertCount(
|
|
0,
|
|
$routes,
|
|
'core-only 테스트에 GDPR 플러그인 라우트가 등록되었습니다 — 격리 실패'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 테스트는 운영 라우트 캐시로 부팅하지 않는다.
|
|
*
|
|
* 라우트 캐시가 있으면 `RouteServiceProvider::boot()` 이 캐시 로드로 분기해 **라우트 파일이
|
|
* 실행되지 않는다.** 그러면 allowlist 가 라우트 축에서 통째로 무력화되어, core-only 테스트가
|
|
* 굽던 시점에 활성이던 모든 확장의 라우트를 가진 채 부팅한다.
|
|
*
|
|
* 위의 GDPR 라우트 단언만으로는 이 결함을 안정적으로 잡지 못한다 — GDPR 이 그 머신에
|
|
* 설치돼 있어야만 red 가 되기 때문이다. 여기서는 **메커니즘 자체**를 단언한다.
|
|
*
|
|
* @effects tests_do_not_boot_with_production_route_cache
|
|
*/
|
|
public function test_tests_do_not_boot_with_the_production_route_cache(): void
|
|
{
|
|
$cachedRoutesPath = $this->app->getCachedRoutesPath();
|
|
|
|
// 경로 구분자를 슬래시로 통일해 판정한다 — Windows 는 역슬래시를 섞어 돌려준다.
|
|
$normalized = strtr($cachedRoutesPath, '\\', '/');
|
|
|
|
$this->assertStringNotContainsString(
|
|
'bootstrap/cache',
|
|
$normalized,
|
|
'테스트가 운영 라우트 캐시 경로를 본다 — tests/bootstrap.php 의 APP_ROUTES_CACHE '
|
|
.'리다이렉트가 사라졌다. 라우트 파일이 실행되지 않아 확장 격리가 무력화된다.'
|
|
);
|
|
|
|
$this->assertFalse(
|
|
$this->app->routesAreCached(),
|
|
'테스트가 캐시된 라우트로 부팅했다 — 라우트 파일이 실행되지 않아 allowlist 가 적용되지 않는다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @effects allowlist_inactive_when_never_configured
|
|
*/
|
|
public function test_allowlist_is_inactive_when_never_configured(): void
|
|
{
|
|
// 비-테스트 부팅(또는 set 호출 전)에는 가드가 비활성이어야
|
|
// 운영/개발 환경의 확장 전수 로딩이 보존됨
|
|
ExtensionTestAllowlist::reset();
|
|
|
|
$this->assertFalse(ExtensionTestAllowlist::isActive());
|
|
|
|
// 테스트 격리 보존을 위해 본 클래스의 core-only allowlist 복원
|
|
ExtensionTestAllowlist::set($this->resolveAllowedExtensions());
|
|
}
|
|
|
|
/**
|
|
* @effects selfExtension_returns_null_for_core_tests
|
|
*/
|
|
public function test_self_extension_returns_null_for_core_tests(): void
|
|
{
|
|
// 본 테스트 클래스는 tests/Feature/ 하위 (코어 테스트) →
|
|
// selfExtension() 의 modules/plugins 경로 패턴에 매칭되지 않아 null 반환
|
|
$this->assertNull($this->selfExtension());
|
|
}
|
|
}
|