fix(release): 7.0.8 사전점검 전수조사 — 릴리즈 차단 2건 포함 결함 7건 수정 및 출시일 정렬
- pay_nhnkcp 1.0.3 분리: 관리자 API 권한 게이트 변경분이 이미 배포된 1.0.2 섹션에 소급 누적돼 있어 배포본과 어긋나고 업데이트 감지도 불가하던 것을 manifest/package/composer/lock 동기와 함께 정정 - 공개 시나리오 매니페스트의 내부 경로 참조 제거 — 릴리즈 파일 누출 검사가 error 로 발행을 차단하던 상태 해소 (내부 정적 검사 연계는 도구 비귀속 서술로 이관) - 모듈/플러그인/템플릿 수동 설치에서 깨진 zip·manifest 누락 같은 사용자 입력 오류가 500 으로 위장되던 422 계약 회귀 복원 — 서비스 경계에서 도메인 예외로 승격해 상태코드와 실패 사유를 함께 보존 (회귀 테스트 동반) - 본인인증 정책 검색 버튼이 입력 중인 검색어를 반영하지 않고, 초기화 후에도 입력창에 이전 검색어가 남던 문제 수정 — 폼 자동바인딩 옵트아웃 + 명시 상태 전환, E2E 회귀 스펙과 트러블슈팅 사례 동반 - 프록시 URL 검증 비문자열 분기의 :schemes 치환 누락 보정 (회귀 테스트 동반) - 7.0.8 사이클 출시일자를 2026-08-22 로 정렬 (코어·엔진·확장 7종·언어팩 3종, 버전·콘텐츠 무변경) 및 설치본 언어팩 동기
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [7.0.8] - 2026-08-21
|
||||
## [7.0.8] - 2026-08-22
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ class ValidOutboundProxyUrl implements ValidationRule
|
||||
}
|
||||
|
||||
if (! is_string($value)) {
|
||||
$fail(__('validation.settings.outbound_proxy_invalid'));
|
||||
$fail(__('validation.settings.outbound_proxy_invalid', [
|
||||
'schemes' => implode(', ', OutboundProxy::ALLOWED_SCHEMES),
|
||||
]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -783,6 +783,13 @@ class ModuleService
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
} catch (ModuleOperationException $e) {
|
||||
throw $e;
|
||||
} catch (\RuntimeException $e) {
|
||||
// ZipInstallHelper 등 설치 원본 처리의 raw RuntimeException(깨진 zip·manifest
|
||||
// 누락 같은 사용자 입력 오류)을 도메인 예외로 승격한다 — 컨트롤러의 좁혀진
|
||||
// catch 가 인프라 예외와 구분해 종전 422 계약을 유지하고, 사유는 :error 로 보존.
|
||||
throw new ModuleOperationException('modules.errors.install_failed', ['error' => $e->getMessage()], $e);
|
||||
} finally {
|
||||
if (File::exists($extractPath)) {
|
||||
File::deleteDirectory($extractPath);
|
||||
@@ -836,6 +843,12 @@ class ModuleService
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
} catch (ModuleOperationException $e) {
|
||||
throw $e;
|
||||
} catch (\RuntimeException $e) {
|
||||
// GithubHelper·ZipInstallHelper 의 raw RuntimeException(잘못된 URL·다운로드
|
||||
// 실패·manifest 오류)을 도메인 예외로 승격한다 — 종전 422 계약 유지, 사유 보존.
|
||||
throw new ModuleOperationException('modules.errors.install_failed', ['error' => $e->getMessage()], $e);
|
||||
} finally {
|
||||
if (File::exists($extractPath)) {
|
||||
File::deleteDirectory($extractPath);
|
||||
|
||||
@@ -899,6 +899,13 @@ class PluginService
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
} catch (PluginOperationException $e) {
|
||||
throw $e;
|
||||
} catch (\RuntimeException $e) {
|
||||
// ZipInstallHelper 등 설치 원본 처리의 raw RuntimeException(깨진 zip·manifest
|
||||
// 누락 같은 사용자 입력 오류)을 도메인 예외로 승격한다 — 컨트롤러의 좁혀진
|
||||
// catch 가 인프라 예외와 구분해 종전 422 계약을 유지하고, 사유는 :error 로 보존.
|
||||
throw new PluginOperationException('plugins.errors.install_failed', ['error' => $e->getMessage()], $e);
|
||||
} finally {
|
||||
if (File::exists($extractPath)) {
|
||||
File::deleteDirectory($extractPath);
|
||||
@@ -952,6 +959,12 @@ class PluginService
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
} catch (PluginOperationException $e) {
|
||||
throw $e;
|
||||
} catch (\RuntimeException $e) {
|
||||
// GithubHelper·ZipInstallHelper 의 raw RuntimeException(잘못된 URL·다운로드
|
||||
// 실패·manifest 오류)을 도메인 예외로 승격한다 — 종전 422 계약 유지, 사유 보존.
|
||||
throw new PluginOperationException('plugins.errors.install_failed', ['error' => $e->getMessage()], $e);
|
||||
} finally {
|
||||
if (File::exists($extractPath)) {
|
||||
File::deleteDirectory($extractPath);
|
||||
|
||||
@@ -1936,6 +1936,13 @@ class TemplateService
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
} catch (TemplateOperationException $e) {
|
||||
throw $e;
|
||||
} catch (\RuntimeException $e) {
|
||||
// ZipInstallHelper 등 설치 원본 처리의 raw RuntimeException(깨진 zip·manifest
|
||||
// 누락 같은 사용자 입력 오류)을 도메인 예외로 승격한다 — 컨트롤러의 좁혀진
|
||||
// catch 가 인프라 예외와 구분해 종전 422 계약을 유지하고, 사유는 :error 로 보존.
|
||||
throw new TemplateOperationException('templates.errors.install_failed', ['error' => $e->getMessage()], $e);
|
||||
} finally {
|
||||
if (File::exists($extractPath)) {
|
||||
File::deleteDirectory($extractPath);
|
||||
@@ -1989,6 +1996,12 @@ class TemplateService
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
} catch (TemplateOperationException $e) {
|
||||
throw $e;
|
||||
} catch (\RuntimeException $e) {
|
||||
// GithubHelper·ZipInstallHelper 의 raw RuntimeException(잘못된 URL·다운로드
|
||||
// 실패·manifest 오류)을 도메인 예외로 승격한다 — 종전 422 계약 유지, 사유 보존.
|
||||
throw new TemplateOperationException('templates.errors.install_failed', ['error' => $e->getMessage()], $e);
|
||||
} finally {
|
||||
if (File::exists($extractPath)) {
|
||||
File::deleteDirectory($extractPath);
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
| `getMetadata()` | `[]` | 메타데이터 |
|
||||
| `getMiddleware()` | `[]` | 확장 미들웨어 선언 (self-gate targets) — `{class, groups, timing?, targets}` ([middleware.md](../backend/middleware.md)) |
|
||||
| `getBenchmarkProfiles()` | `[]` | 성능 계측 대상 선언 (`g7:bench` 가 수집) — 목록/화면/쓰기/배치 4축 ([benchmark.md](../backend/benchmark.md)) |
|
||||
| `upgrades()` | `[]` | 업그레이드 스텝 (`upgrades/` 디렉토리 자동 발견). **`g7_version >= 7.0.0-beta.5` 인 모듈은 신규 step 이 `AbstractUpgradeStep` 상속 의무** ([upgrade-step-guide §13](upgrade-step-guide.md)) — 미상속 시 `ModuleManager::runUpgradeSteps` 가 `RuntimeException` throw |
|
||||
|
||||
#### 수명주기 훅이 실패를 알리는 방법
|
||||
|
||||
@@ -165,7 +166,6 @@ public function activate(): bool
|
||||
- 사유는 **이미 번역된 문장**이어야 한다 — 확장의 언어 파일 키는 코어가 해석할 수 없다.
|
||||
- 사유를 남기지 않고 `false` 만 돌려주면 코어가 일반 문구로 대체한다(동작은 그대로).
|
||||
- 같은 규칙이 플러그인(`AbstractPlugin`)에도 동일하게 적용된다.
|
||||
| `upgrades()` | `[]` | 업그레이드 스텝 (`upgrades/` 디렉토리 자동 발견). **`g7_version >= 7.0.0-beta.5` 인 모듈은 신규 step 이 `AbstractUpgradeStep` 상속 의무** ([upgrade-step-guide §13](upgrade-step-guide.md)) — 미상속 시 `ModuleManager::runUpgradeSteps` 가 `RuntimeException` throw |
|
||||
|
||||
#### 동적 권한/역할/메뉴 보존 규칙
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.7] - 2026-08-21
|
||||
## [1.0.7] - 2026-08-22
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.1.2] - 2026-08-21
|
||||
## [1.1.2] - 2026-08-22
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.6] - 2026-08-21
|
||||
## [1.0.6] - 2026-08-22
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.5] - 2026-08-21
|
||||
## [1.0.5] - 2026-08-22
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.1.2] - 2026-08-21
|
||||
## [1.1.2] - 2026-08-22
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.3] - 2026-08-21
|
||||
## [1.0.3] - 2026-08-22
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.1.2] - 2026-08-21
|
||||
## [1.1.2] - 2026-08-22
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -4,12 +4,17 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.2] - 2026-08-19
|
||||
## [1.0.3] - 2026-08-22
|
||||
|
||||
### Security
|
||||
|
||||
- 관리자 주문 조회·에스크로 배송등록 API가 세부 권한을 검사하도록 수정했습니다. 이전에는 관리자 계정이면 주문 권한이 없어도 주문번호·결제정보·수령인 연락처와 주소를 조회하고 배송등록까지 할 수 있었습니다. 이제 조회는 주문 조회 권한, 배송등록은 주문 수정 권한이 필요하며, 다른 결제대행사 연동과 같은 기준이 적용됩니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1893)
|
||||
- 가상계좌 입금통보 주소 조회와 시스템 점검 API에도 설정 조회 권한 검사를 추가했습니다.
|
||||
|
||||
## [1.0.2] - 2026-08-19
|
||||
|
||||
### Security
|
||||
|
||||
- 에스크로 배송등록 요청의 운송장번호·택배사 코드에 형식·길이 검증을 추가했습니다.
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "plugins/sirsoft-pay_nhnkcp",
|
||||
"description": "NHN KCP PG Plugin for G7 platform",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"type": "library",
|
||||
"authors": [
|
||||
{
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@g7/sirsoft-pay_nhnkcp",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@g7/sirsoft-pay_nhnkcp",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"devDependencies": {
|
||||
"jsdom": "^27.4.0",
|
||||
"typescript": "^5.3.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@g7/sirsoft-pay_nhnkcp",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"ko": "NHN KCP",
|
||||
"en": "NHN KCP"
|
||||
},
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"license": "MIT",
|
||||
"github_url": "https://github.com/gnuboard/g7-plugin-sirsoft-pay_nhnkcp",
|
||||
"github_changelog_url": "https://github.com/gnuboard/g7-plugin-sirsoft-pay_nhnkcp/blob/main/CHANGELOG.md",
|
||||
|
||||
@@ -4,7 +4,7 @@ All notable changes to this plugin will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [1.0.4] - 2026-08-21
|
||||
## [1.0.4] - 2026-08-22
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
>
|
||||
> 형식: [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)
|
||||
|
||||
## [engine-v1.60.6] - 2026-08-21
|
||||
## [engine-v1.60.6] - 2026-08-22
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -4,13 +4,18 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.6] - 2026-08-21
|
||||
## [1.0.6] - 2026-08-22
|
||||
|
||||
### Added
|
||||
|
||||
- 환경설정 > 고급 > 디버그 카드에 아웃바운드 프록시 설정이 추가되었습니다 — 디버그 모드를 켜면 사이트가 외부로 보내는 요청이 거쳐 갈 프록시 주소와, 프록시를 거치지 않을 주소 목록을 입력할 수 있습니다.
|
||||
- 프록시 주소 옆에 「연결 테스트」 버튼이 추가되었습니다 — 저장하기 전에 연결 여부를 확인하고, 성공 시 외부 서비스에 보이는 IP 주소를 함께 표시합니다.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 환경설정 > 인증 > 정책 관리에서 검색 버튼을 눌러도 입력 중인 검색어가 반영되지 않던 문제를 수정했습니다. Enter 키로는 검색되지만 버튼으로는 직전 검색어로만 다시 조회되어, 검색어를 바꿔 버튼을 눌러도 결과가 그대로였습니다.
|
||||
- 같은 화면의 검색 초기화 버튼을 눌러도 입력창의 검색어가 지워지지 않고 남아 있던 문제를 수정했습니다. 목록은 초기화되는데 입력창만 이전 검색어를 보여 주어 초기화가 안 된 것처럼 보였습니다.
|
||||
|
||||
## [1.0.5] - 2026-08-19
|
||||
|
||||
### Added
|
||||
|
||||
+37
-3
@@ -290,14 +290,24 @@
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"comment": "검색 필터는 설정 폼의 일부가 아니다 — 자동바인딩되면 검색어가 설정 폼 상태(filter.search)에 남아 초기화 후에도 입력창에 되살아나고, 설정 dirty 판정까지 오염시킨다.",
|
||||
"props": {
|
||||
"type": "text",
|
||||
"name": "filter.search",
|
||||
"value": "{{query.search ?? ''}}",
|
||||
"autoBinding": false,
|
||||
"value": "{{_local.identityPolicySearch ?? query.search ?? ''}}",
|
||||
"placeholder": "$t:admin.identity.policies.search_placeholder",
|
||||
"className": "md:flex-1"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"identityPolicySearch": "{{$event.target.value}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "keypress",
|
||||
"key": "Enter",
|
||||
@@ -331,10 +341,25 @@
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"comment": "Enter 키와 동일하게 입력값을 쿼리로 실어 이동한다 — refetch 만 하면 입력 중인 검색어가 반영되지 않는다.",
|
||||
"type": "click",
|
||||
"handler": "refetchDataSource",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"dataSourceId": "policies"
|
||||
"path": "/admin/settings",
|
||||
"replace": true,
|
||||
"mergeQuery": true,
|
||||
"query": {
|
||||
"tab": "identity",
|
||||
"search": "{{(_local.identityPolicySearch ?? query.search) || undefined}}",
|
||||
"page": 1
|
||||
},
|
||||
"transition_overlay_target": "policy_table_card",
|
||||
"scroll": {
|
||||
"container": "#right_content_area",
|
||||
"to": "#identity_section_policies",
|
||||
"offset": 180,
|
||||
"block": "start"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -355,6 +380,15 @@
|
||||
"className": "px-4 py-2 text-sm font-medium bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"comment": "입력창의 미제출 검색어도 함께 비운다 — 상태만 남으면 초기화가 안 된 것으로 보인다.",
|
||||
"type": "click",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"identityPolicySearch": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "audit:allow layout-list-context-navigate-merge-query 필터 초기화 — 걸려 있던 조건을 전부 비우는 것이 이 버튼의 목적이므로 병합하지 않는다.",
|
||||
"type": "click",
|
||||
|
||||
@@ -569,6 +569,40 @@ class ModuleControllerTest extends TestCase
|
||||
$response->assertStatus(500);
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 서비스 경로: module.json 없는 ZIP 업로드는 422 를 유지한다.
|
||||
*
|
||||
* ZipInstallHelper 는 이런 사용자 입력 오류를 raw `\RuntimeException` 으로 던진다.
|
||||
* 컨트롤러 catch 가 도메인 예외로 좁혀지면서 서비스가 이를 승격하지 않으면, 종전
|
||||
* 422 였던 실패가 500(인프라 장애)으로 위장된다 — 서비스 계층의 승격을 고정한다.
|
||||
* (mock 이 아닌 실제 ZIP 으로 서비스 실경로를 태운다.)
|
||||
*/
|
||||
public function test_install_from_file_with_zip_missing_manifest_returns_422(): void
|
||||
{
|
||||
$zipPath = tempnam(sys_get_temp_dir(), 'g7zip').'.zip';
|
||||
$zip = new \ZipArchive;
|
||||
$zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
|
||||
$zip->addFromString('readme.txt', 'manifest 없는 ZIP');
|
||||
$zip->close();
|
||||
|
||||
try {
|
||||
$file = new UploadedFile($zipPath, 'module.zip', 'application/zip', null, true);
|
||||
|
||||
$response = $this->authRequest()->postJson('/api/admin/modules/install-from-file', [
|
||||
'file' => $file,
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$this->assertStringContainsString(
|
||||
__('modules.errors.module_json_not_found'),
|
||||
(string) $response->json('message'),
|
||||
'실패 사유(:error 자리)가 응답 메시지에 보존되지 않았습니다.'
|
||||
);
|
||||
} finally {
|
||||
@unlink($zipPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ModuleService에서 일반 Exception 발생 시 500 반환
|
||||
*/
|
||||
|
||||
@@ -100,6 +100,37 @@ class OutboundProxySettingTest extends TestCase
|
||||
$this->assertArrayHasKey('advanced.outbound_proxy', $validator->errors()->messages());
|
||||
}
|
||||
|
||||
/**
|
||||
* 비문자열 값 거부 시에도 오류 문구의 :schemes 치환 자리가 채워집니다.
|
||||
*
|
||||
* 비문자열 분기가 치환 파라미터 없이 번역을 호출하면 번역기가 자리표시자를 그대로 둔
|
||||
* 문장을 돌려줘 관리자 화면에 `:schemes` 리터럴이 노출됩니다 — 실패했을 때만 드러나는
|
||||
* 결함이라 정상 흐름 테스트로는 잡히지 않습니다.
|
||||
*
|
||||
* @scenario debug_mode=on, proxy_value=invalid, bypass_list=empty
|
||||
*
|
||||
* @effects proxy_setting_rejects_invalid_url
|
||||
*/
|
||||
public function test_non_string_proxy_value_error_message_has_schemes_substituted(): void
|
||||
{
|
||||
$validator = $this->validatorForAdvanced([
|
||||
'outbound_proxy' => ['not', 'a', 'string'],
|
||||
]);
|
||||
|
||||
$this->assertTrue($validator->fails(), '비문자열 프록시 값이 검증을 통과했습니다.');
|
||||
|
||||
$messages = $validator->errors()->get('advanced.outbound_proxy');
|
||||
$this->assertNotEmpty($messages);
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$this->assertStringNotContainsString(
|
||||
':schemes',
|
||||
$message,
|
||||
'치환 자리가 채워지지 않아 :schemes 리터럴이 관리자 화면에 노출됩니다.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 유효한 주소와 빈 값은 검증을 통과합니다.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* E2E 회귀: 환경설정 > 인증 > 정책 관리 — 검색 버튼/검색 초기화
|
||||
*
|
||||
* 결함(2026-08-22 실측): 검색 버튼이 refetchDataSource 만 호출해 입력 중인 검색어가
|
||||
* URL 쿼리로 실리지 않았다 — Enter 로는 검색되는데 버튼으로는 직전 검색어로만 재조회됐다.
|
||||
* 검색 초기화는 URL·목록은 비웠지만 입력창 텍스트가 설정 폼 자동바인딩(filter.search)에
|
||||
* 남아 초기화되지 않은 것처럼 보였다.
|
||||
*
|
||||
* 고정 계약:
|
||||
* 1. 검색 버튼 클릭 = Enter 와 동일하게 입력값을 쿼리로 실어 이동 (재검색 포함)
|
||||
* 2. 검색 초기화 = URL 쿼리 + 입력창 텍스트 모두 비움
|
||||
*/
|
||||
import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth';
|
||||
|
||||
const PERMISSIONS = [
|
||||
'core.settings.read',
|
||||
'core.settings.update',
|
||||
'core.admin.identity.policies.read',
|
||||
] as const;
|
||||
|
||||
const SEARCH_INPUT = 'input[name="filter.search"]';
|
||||
|
||||
async function gotoPoliciesTab(page: import('@playwright/test').Page): Promise<void> {
|
||||
await page.goto('/admin/settings?tab=identity&sub_tab=policies');
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
|
||||
await expect(page.locator(SEARCH_INPUT)).toBeAttached({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
test('정책 검색 버튼이 입력 중인 검색어를 반영하고, 검색어를 바꿔 다시 눌러도 재검색된다', async ({ page }) => {
|
||||
await authenticatePage(page, issueToken(...PERMISSIONS));
|
||||
await gotoPoliciesTab(page);
|
||||
|
||||
const searchButton = page
|
||||
.locator('#tab_content_identity_policies button', { hasText: '검색' })
|
||||
.first();
|
||||
|
||||
await page.locator(SEARCH_INPUT).fill('회원가입');
|
||||
await searchButton.click();
|
||||
await expect(page).toHaveURL(/search=%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85/, { timeout: 15_000 });
|
||||
|
||||
// 회귀 축: 검색어를 바꾼 뒤 버튼으로 재검색 — 종전에는 URL 이 그대로였고
|
||||
// 직전 검색어(회원가입)로만 재조회됐다.
|
||||
await page.locator(SEARCH_INPUT).fill('주문');
|
||||
await searchButton.click();
|
||||
await expect(page).toHaveURL(/search=%EC%A3%BC%EB%AC%B8/, { timeout: 15_000 });
|
||||
});
|
||||
|
||||
test('검색 초기화가 URL 쿼리와 입력창 텍스트를 모두 비운다', async ({ page }) => {
|
||||
await authenticatePage(page, issueToken(...PERMISSIONS));
|
||||
await gotoPoliciesTab(page);
|
||||
|
||||
await page.locator(SEARCH_INPUT).fill('회원가입');
|
||||
await page.locator(SEARCH_INPUT).press('Enter');
|
||||
await expect(page).toHaveURL(/search=/, { timeout: 15_000 });
|
||||
|
||||
// 초기화 직전에 다른 검색어를 입력해 둔다 — 종전에는 이 텍스트가
|
||||
// 설정 폼 자동바인딩에 남아 초기화 후에도 입력창에 되살아났다.
|
||||
await page.locator(SEARCH_INPUT).fill('주문');
|
||||
|
||||
await page
|
||||
.locator('#tab_content_identity_policies button', { hasText: '검색 초기화' })
|
||||
.click();
|
||||
|
||||
await expect(page).not.toHaveURL(/search=/, { timeout: 15_000 });
|
||||
await expect(page.locator(SEARCH_INPUT)).toHaveValue('', { timeout: 10_000 });
|
||||
});
|
||||
@@ -19,7 +19,7 @@ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'
|
||||
*
|
||||
* 이 화면은 하드 로드 시 설정 응답이 도착하기 전에도 폼이 렌더되어 조작할 수 있고,
|
||||
* 그 조작은 뒤늦게 도착한 `initLocal` 시드에 덮인다(실측 노출 창 300~430ms).
|
||||
* 제품 차원의 잠금은 두지 않기로 했으므로(#600 PO 결정), 테스트가 시드 완료를
|
||||
* 제품 차원의 잠금은 두지 않는다는 결정이 확정되어 있으므로, 테스트가 시드 완료를
|
||||
* 기다린 뒤에 조작한다.
|
||||
*/
|
||||
async function gotoAdvancedTab(page: import('@playwright/test').Page): Promise<void> {
|
||||
|
||||
@@ -30,13 +30,11 @@ effects:
|
||||
- incompatible_strings_defined_per_locale
|
||||
- core_source_has_no_lookbehind
|
||||
- variable_extraction_contract_unchanged
|
||||
- shipped_bundle_lookbehind_blocked_by_static_check
|
||||
|
||||
test_files:
|
||||
- tests/Playwright/specs/smoke/bootstrap-parse-failure.spec.ts
|
||||
- tests/Feature/View/BootstrapFallbackDiagnosisTest.php
|
||||
- resources/js/core/template-engine/__tests__/DataBindingEngine.test.ts
|
||||
- .claude/scripts/audit/__tests__/shipped-bundle-browser-baseline.test.cjs
|
||||
|
||||
notes: |
|
||||
로케일 축은 E2E cross product 에 넣지 않는다 — 화면 문구는 서버가 렌더한 문자열을
|
||||
@@ -46,3 +44,6 @@ notes: |
|
||||
|
||||
하한 미만 실기기(iOS 15) 에서 "깨진 채로 이용 가능한가" 는 재현 수단이 없어 이 매니페스트
|
||||
범위 밖이다 — 최신 WebKit(Playwright) 도 lookbehind 를 지원해 결함 자체가 재현되지 않는다.
|
||||
|
||||
배포 번들에 lookbehind 등 파싱 하한 초과 문법이 재유입되는 회귀는 위 테스트와 별개로
|
||||
저장소의 정적 검사가 차단한다 (이 매니페스트의 검증 범위 밖).
|
||||
|
||||
Reference in New Issue
Block a user