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 쓰기 권한 불요를 명시)
80 lines
5.0 KiB
YAML
80 lines
5.0 KiB
YAML
# audit:allow test-scenario-coverage reason: axes cross product 는 환경×allowlist상태×확장유형×등록표면×테스트종류의 조합 열거로, 실질 회귀 가드는 effects 18건이 담당한다. effects 는 test_files 의 메서드 docblock @effects 로 전수 매핑되어 개별 검증된다. cross product 개별 케이스의 docblock 마킹은 effects 매핑과 중복이므로 생략.
|
|
|
|
feature: 테스트 환경 확장 격리 (requiredExtensions allowlist)
|
|
|
|
description: |
|
|
PHPUnit 환경에서 테스트 클래스가 명시한 확장만 ServiceProvider / route 등록
|
|
대상이 되도록 통제하는 격리 레이어. 기존 requiredExtensions 프로퍼티를
|
|
마이그레이션 등록 전용에서 ServiceProvider / route 등록 allowlist 로 승격한다.
|
|
|
|
핵심 동작:
|
|
- ExtensionTestAllowlist 가 testing 환경 + 명시적 set 시에만 isActive() = true
|
|
- allowlist 활성 시 PluginServiceProvider / ModuleServiceProvider 는
|
|
allowlist 밖 확장의 ServiceProvider register() 를 건너뜀
|
|
- PluginRouteServiceProvider / ModuleRouteServiceProvider 는 동일 기준으로 route 차단
|
|
- 확장 자체 테스트는 selfExtension() (테스트 파일 경로 기반 자동 탐지) 로
|
|
자기 확장을 allowlist 에 자동 포함 → 무수정 동작. 이 동작은 GDPR 플러그인
|
|
자체 테스트 178건 / sirsoft-page 모듈 자체 테스트 167건이 requiredExtensions
|
|
선언 없이 통과함으로써 실증된다 (selfExtension 은 ReflectionClass 로 테스트
|
|
파일 경로를 보므로 단위 테스트 격리가 불가 — 실제 확장 테스트 통과가 증거).
|
|
- 비-testing 환경 또는 allowlist 미설정 시 isActive() = false → 전수 로딩 보존
|
|
|
|
적용 범위 한정:
|
|
ModuleManager::loadModules / PluginManager::loadPlugins (hook listener / config /
|
|
채널 등록 경로) 에는 가드를 적용하지 않는다. 그 경로는 fixture 확장을 직접
|
|
생성해 loadModules/loadPlugins 를 호출하는 다수의 확장 테스트가 의존하므로
|
|
가드 적용 시 광범위한 회귀를 유발한다.
|
|
|
|
확장 미들웨어와 self-gate:
|
|
확장 미들웨어(GDPR CookieConsentMiddleware 등)는 더 이상 provider boot 에서
|
|
web/api 그룹에 직접 등록되지 않는다. 코어 ExtensionMiddlewareGate 래퍼가 항상
|
|
web/api 그룹에 등록되고, 각 확장이 getMiddleware() 로 선언한 미들웨어를 요청
|
|
시점에 대상 매칭될 때만 실행한다. 미들웨어를 실제로 실행하는 후보로 삼으려면
|
|
확장이 ExtensionMiddlewareRegistry 인덱스에 수집돼야 하며, 인덱스는 활성 확장
|
|
(getActive{Modules,Plugins}) 만 포함한다. 따라서 core-only 테스트는 비활성 GDPR
|
|
미들웨어를 게이트가 실행하지 않고, allowlist 로 GDPR 을 활성화한 테스트에서만
|
|
게이트가 그 미들웨어를 매칭한다. 격리의 핵심 목적(비대상 확장 미들웨어 미개입)은
|
|
ServiceProvider register 가드 + registry 활성 확장 필터의 이중으로 충족된다.
|
|
|
|
axes:
|
|
app_env: [testing, production]
|
|
allowlist_state: [unset, empty, contains_target, contains_other]
|
|
extension_type: [plugin, module]
|
|
registration_surface: [service_provider, route]
|
|
test_class_kind: [core_test, extension_self_test]
|
|
|
|
exclusions:
|
|
- { app_env: production, allowlist_state: empty, reason: "비-testing 환경은 allowlist 무관 — isActive false 고정" }
|
|
- { app_env: production, allowlist_state: contains_target, reason: "동일 — production 은 가드 비활성" }
|
|
- { app_env: production, allowlist_state: contains_other, reason: "동일" }
|
|
- { test_class_kind: extension_self_test, allowlist_state: unset, reason: "확장 자체 테스트는 selfExtension 으로 항상 자기 확장 포함" }
|
|
|
|
effects:
|
|
- allowlist_inactive_when_app_env_not_testing
|
|
- allowlist_inactive_when_never_configured
|
|
- allowlist_active_when_testing_and_configured
|
|
- empty_allowlist_is_active_and_blocks_all_extensions
|
|
- set_classifies_plugin_and_module_by_path_prefix
|
|
- set_normalizes_trailing_slash
|
|
- set_ignores_prefixless_entries
|
|
- set_recall_replaces_previous_allowlist
|
|
- reset_deactivates_guard
|
|
- core_only_test_does_not_register_gdpr_service_provider
|
|
- core_only_test_does_not_register_gdpr_middleware_in_web_group
|
|
- core_only_test_does_not_register_gdpr_middleware_in_api_group
|
|
- core_only_test_does_not_register_gdpr_plugin_routes
|
|
- allowlisted_plugin_service_provider_is_registered
|
|
- allowlisted_plugin_middleware_is_registered_in_web_group
|
|
- selfExtension_returns_null_for_core_tests
|
|
- production_env_keeps_full_extension_loading
|
|
- tests_do_not_boot_with_production_route_cache
|
|
|
|
test_files:
|
|
- tests/Unit/Extension/ExtensionTestAllowlistTest.php
|
|
- tests/Feature/Extension/ExtensionTestIsolationTest.php
|
|
- tests/Feature/Extension/ExtensionTestIsolationGdprAllowedTest.php
|
|
|
|
rules_layer_coverage:
|
|
- rule: service-provider-db-guard
|
|
coverage: 가드는 register()/boot() 초입에서 ExtensionTestAllowlist::isActive() 만 검사 — DB 접근 없음
|