Files
Gnuboard7/modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/ShippingPolicyResolverCacheTest.php
T
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

200 lines
6.8 KiB
PHP

<?php
namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Services;
use Modules\Sirsoft\Ecommerce\Enums\ChargePolicyEnum;
use Modules\Sirsoft\Ecommerce\Listeners\ShippingPolicyCacheListener;
use Modules\Sirsoft\Ecommerce\Models\ShippingPolicy;
use Modules\Sirsoft\Ecommerce\Services\ShippingPolicyResolver;
use Modules\Sirsoft\Ecommerce\Services\ShippingPolicyService;
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
/**
* 배송정책 변경 후 해석기 캐시 무효화 검증
*
* `ShippingPolicyResolver` 는 싱글톤이라 기본 배송정책을 요청 내 1회만 조회한다.
* 무효화 경로가 없으면 배송정책을 바꾼 요청이 같은 요청 안에서 변경 전 정책을 계속 쓴다
* — 예외도 오류도 남지 않고 배송비만 이전 값으로 계산된다.
*/
class ShippingPolicyResolverCacheTest extends ModuleTestCase
{
/**
* 배송정책을 생성합니다.
*
* @param bool $isDefault 기본 배송정책 여부
* @param int $baseFee 기본 배송비
* @return ShippingPolicy 생성된 배송정책
*/
private function makePolicy(bool $isDefault, int $baseFee = 600): ShippingPolicy
{
$policy = ShippingPolicy::create([
'name' => ['ko' => '배송정책 '.$baseFee, 'en' => 'Policy '.$baseFee],
'is_default' => $isDefault,
'is_active' => true,
]);
$policy->countrySettings()->create([
'country_code' => 'KR',
'shipping_method' => 'parcel',
'currency_code' => 'KRW',
'charge_policy' => ChargePolicyEnum::FIXED,
'base_fee' => $baseFee,
'is_active' => true,
]);
return $policy->load('countrySettings');
}
/**
* 기본 배송정책은 요청 내 1회만 조회됩니다 (캐시가 살아 있는지 확인).
*
* @scenario mutation=none
*
* @effects default_policy_cached_within_request
*/
public function test_default_policy_is_cached_within_request(): void
{
$policy = $this->makePolicy(isDefault: true);
$resolver = app(ShippingPolicyResolver::class);
$first = $resolver->getDefaultPolicy();
$this->assertNotNull($first);
$this->assertSame($policy->id, $first->id);
// 캐시가 없으면 이 단언이 새 기본정책을 집어 캐시 부재를 드러낸다
$this->makePolicy(isDefault: false);
$this->assertSame($first, $resolver->getDefaultPolicy(), '요청 내 재조회가 발생했습니다.');
}
/**
* `flushCache()` 후에는 저장소를 다시 조회합니다. (실패-먼저)
*
* @scenario mutation=manual_flush
*
* @effects resolver_cache_flushed
*/
public function test_flush_cache_forces_refetch(): void
{
$old = $this->makePolicy(isDefault: true, baseFee: 600);
$resolver = app(ShippingPolicyResolver::class);
$this->assertSame($old->id, $resolver->getDefaultPolicy()?->id);
$old->update(['is_default' => false]);
$new = $this->makePolicy(isDefault: true, baseFee: 900);
$resolver->flushCache();
$this->assertSame($new->id, $resolver->getDefaultPolicy()?->id, 'flushCache() 후에도 이전 정책이 반환됩니다.');
}
/**
* 기본 배송정책 지정을 바꾸면 같은 요청 안에서 해석기가 새 정책을 반환합니다. (실패-먼저)
*
* @scenario mutation=set_default
*
* @effects resolver_cache_flushed
*/
public function test_set_default_invalidates_resolver_cache(): void
{
$old = $this->makePolicy(isDefault: true, baseFee: 600);
$candidate = $this->makePolicy(isDefault: false, baseFee: 900);
$resolver = app(ShippingPolicyResolver::class);
$this->assertSame($old->id, $resolver->getDefaultPolicy()?->id);
app(ShippingPolicyService::class)->setDefault($candidate);
$this->assertSame(
$candidate->id,
$resolver->getDefaultPolicy()?->id,
'기본 배송정책을 바꿨는데 해석기가 이전 정책을 반환합니다.'
);
}
/**
* 기본 배송정책 수정도 같은 요청 안에서 반영됩니다. (실패-먼저)
*
* @scenario mutation=update
*
* @effects resolver_cache_flushed
*/
public function test_update_invalidates_resolver_cache(): void
{
$policy = $this->makePolicy(isDefault: true, baseFee: 600);
$resolver = app(ShippingPolicyResolver::class);
$this->assertSame(
600,
(int) $resolver->getDefaultPolicy()?->getCountrySetting('KR')?->base_fee
);
app(ShippingPolicyService::class)->update($policy, [
'name' => ['ko' => '배송정책', 'en' => 'Policy'],
'is_default' => true,
'is_active' => true,
'country_settings' => [[
'country_code' => 'KR',
'shipping_method' => 'parcel',
'currency_code' => 'KRW',
'charge_policy' => ChargePolicyEnum::FIXED->value,
'base_fee' => 1500,
'is_active' => true,
]],
]);
$this->assertSame(
1500,
(int) $resolver->getDefaultPolicy()?->getCountrySetting('KR')?->base_fee,
'배송정책을 수정했는데 해석기가 이전 배송비를 반환합니다.'
);
}
/**
* 기본 배송정책 삭제도 같은 요청 안에서 반영됩니다. (실패-먼저)
*
* @scenario mutation=delete
*
* @effects resolver_cache_flushed
*/
public function test_delete_invalidates_resolver_cache(): void
{
$policy = $this->makePolicy(isDefault: true);
$resolver = app(ShippingPolicyResolver::class);
$this->assertNotNull($resolver->getDefaultPolicy());
app(ShippingPolicyService::class)->delete($policy);
$this->assertNull($resolver->getDefaultPolicy(), '삭제된 배송정책이 계속 반환됩니다.');
}
/**
* 리스너가 배송정책 변경 훅 전부를 구독합니다.
*
* 변경 경로가 늘어나도 같은 훅만 발화하면 무효화가 따라오도록, 구독 목록을 고정한다.
*
* @scenario mutation=hook_subscription
*
* @effects resolver_cache_flushed
*/
public function test_listener_subscribes_every_mutation_hook(): void
{
$hooks = array_keys(ShippingPolicyCacheListener::getSubscribedHooks());
foreach ([
'after_create',
'after_update',
'after_delete',
'after_bulk_delete',
'after_toggle_active',
'after_bulk_toggle_active',
'after_set_default',
] as $event) {
$this->assertContains('sirsoft-ecommerce.shipping_policy.'.$event, $hooks);
}
}
}