확장이 훅으로 등록한 카탈로그에서 고른 값은 그 확장이 사라져도 저장값으로 남는다. 그 상태에서 값만 보고 판정하면 이미 제공 불가한 항목이 사용자 화면에 그대로 노출된다. 예외도 경고도 로그도 남지 않아 관리자 화면과 나란히 보지 않으면 드러나지 않는다. - 결제수단에 지정한 PG, 현금영수증 발급사, 검색엔진 드라이버 세 축에 같은 판정을 적용. 공개 응답에서는 제거하고 관리자 응답에는 표시를 남긴다 — 운영자가 고쳐야 할 대상이라 감추면 복구 경로가 사라진다. 서버 검증도 같은 판정을 공유해 화면 우회 제출을 막는다. - 설정 단건 저장이 벌크 저장과 다른 경로를 타던 문제를 코어·이커머스·게시판에서 정리. 저장은 성공하고 파생 값만 갱신되지 않아 어느 화면에서 저장했느냐로 결과가 갈렸다. - 단건 저장의 값 형태 계약 신설. 빈 값으로 되돌릴 수 있게 하고, 타입은 defaults.json 의 기본값을 SSoT 로 삼는다. 해석 불가한 입력은 조용히 캐스팅하지 않고 거절한다. - 드라이버 셀렉트를 서버 카탈로그에 바인딩하고, 저장값이 카탈로그에 없으면 그 값을 드러내는 안내를 8곳 전수에 붙인다. 종전에는 빈 칸으로만 보여 무엇이 저장됐는지 알 수 없었다. 시나리오 매니페스트 8종을 면제 없이 신설했다. `audit:allow` 는 cross product 와 effects 검사를 함께 끄므로, 면제가 걸린 매니페스트에 선언을 추가하면 검증이 회복되지 않는다. 그 아래에서 대응 테스트가 없는 effect 이름 2건이 살아남아 있었고, 함께 정리했다.
95 lines
3.4 KiB
PHP
95 lines
3.4 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Api\Identity;
|
|
|
|
use App\Extension\IdentityVerification\Providers\MailIdentityProvider;
|
|
use App\Models\IdentityPolicy;
|
|
use App\Services\IdentityPolicyService;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* 정책 프리페치 엔드포인트의 provider_id 해석 (A6a)
|
|
*
|
|
* `GET /api/identity/policies/resolve` 는 저장된 `provider_id` 를 그대로 내보냈다.
|
|
* 바로 옆의 428 강제 경로(`IdentityPolicyService::resolveProviderId()`)는 같은 값을
|
|
* 레지스트리와 대조하고 미등록이면 purpose 기반 폴백으로 대체하는데, 이쪽만 raw 였다 —
|
|
* 같은 데이터에 두 개의 게터가 서로 다른 답을 주는 안티패턴이고, 제거된 플러그인의
|
|
* provider ID 가 공개 응답(optional.sanctum)에 그대로 실린다.
|
|
*/
|
|
class ResolvePolicyEndpointTest extends TestCase
|
|
{
|
|
private string $endpoint = '/api/identity/policies/resolve';
|
|
|
|
/**
|
|
* 정책 행을 만듭니다.
|
|
*
|
|
* @param string|null $providerId 저장할 provider ID
|
|
* @return IdentityPolicy 생성된 정책
|
|
*/
|
|
private function makePolicy(?string $providerId): IdentityPolicy
|
|
{
|
|
return IdentityPolicy::updateOrCreate([
|
|
'key' => 'test.resolve.policy',
|
|
], [
|
|
'scope' => 'route',
|
|
'target' => 'api.test.resolve',
|
|
'purpose' => 'sensitive_action',
|
|
'provider_id' => $providerId,
|
|
'enabled' => true,
|
|
'grace_minutes' => 10,
|
|
'applies_to' => 'both',
|
|
'fail_mode' => 'block',
|
|
'source_type' => 'core',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 미등록 provider 는 해석된 값(폴백)으로 대체되어 나간다. (실패-먼저)
|
|
*/
|
|
public function test_unregistered_provider_is_resolved_not_echoed(): void
|
|
{
|
|
$this->makePolicy('ghost_provider');
|
|
|
|
$response = $this->getJson($this->endpoint.'?scope=route&target=api.test.resolve')
|
|
->assertOk();
|
|
|
|
$this->assertNotSame(
|
|
'ghost_provider',
|
|
$response->json('data.provider_id'),
|
|
'제거된 플러그인의 provider ID 가 공개 응답에 그대로 실렸습니다.'
|
|
);
|
|
|
|
// 428 강제 경로와 같은 게터를 경유해야 두 경로의 답이 갈리지 않는다
|
|
$policy = IdentityPolicy::where('key', 'test.resolve.policy')->firstOrFail();
|
|
$this->assertSame(
|
|
app(IdentityPolicyService::class)->resolveProviderId($policy),
|
|
$response->json('data.provider_id'),
|
|
'프리페치 응답과 428 강제 경로의 provider 해석이 어긋납니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 등록된 provider 는 그대로 유지된다. (비회귀 pin)
|
|
*/
|
|
public function test_registered_provider_is_returned_as_is(): void
|
|
{
|
|
// 코어 메일 provider 는 항상 등록되어 있다 (ID 는 provider 자신이 선언한 값)
|
|
$registeredId = app(MailIdentityProvider::class)->getId();
|
|
$this->makePolicy($registeredId);
|
|
|
|
$this->getJson($this->endpoint.'?scope=route&target=api.test.resolve')
|
|
->assertOk()
|
|
->assertJsonPath('data.provider_id', $registeredId);
|
|
}
|
|
|
|
/**
|
|
* 매칭 정책이 없으면 null 을 반환한다. (기존 계약 pin)
|
|
*/
|
|
public function test_no_matching_policy_returns_null(): void
|
|
{
|
|
$this->getJson($this->endpoint.'?scope=route&target=api.no.such.target')
|
|
->assertOk()
|
|
->assertJsonPath('data', null);
|
|
}
|
|
}
|