확장이 훅으로 등록한 카탈로그에서 고른 값은 그 확장이 사라져도 저장값으로 남는다. 그 상태에서 값만 보고 판정하면 이미 제공 불가한 항목이 사용자 화면에 그대로 노출된다. 예외도 경고도 로그도 남지 않아 관리자 화면과 나란히 보지 않으면 드러나지 않는다. - 결제수단에 지정한 PG, 현금영수증 발급사, 검색엔진 드라이버 세 축에 같은 판정을 적용. 공개 응답에서는 제거하고 관리자 응답에는 표시를 남긴다 — 운영자가 고쳐야 할 대상이라 감추면 복구 경로가 사라진다. 서버 검증도 같은 판정을 공유해 화면 우회 제출을 막는다. - 설정 단건 저장이 벌크 저장과 다른 경로를 타던 문제를 코어·이커머스·게시판에서 정리. 저장은 성공하고 파생 값만 갱신되지 않아 어느 화면에서 저장했느냐로 결과가 갈렸다. - 단건 저장의 값 형태 계약 신설. 빈 값으로 되돌릴 수 있게 하고, 타입은 defaults.json 의 기본값을 SSoT 로 삼는다. 해석 불가한 입력은 조용히 캐스팅하지 않고 거절한다. - 드라이버 셀렉트를 서버 카탈로그에 바인딩하고, 저장값이 카탈로그에 없으면 그 값을 드러내는 안내를 8곳 전수에 붙인다. 종전에는 빈 칸으로만 보여 무엇이 저장됐는지 알 수 없었다. 시나리오 매니페스트 8종을 면제 없이 신설했다. `audit:allow` 는 cross product 와 effects 검사를 함께 끄므로, 면제가 걸린 매니페스트에 선언을 추가하면 검증이 회복되지 않는다. 그 아래에서 대응 테스트가 없는 effect 이름 2건이 살아남아 있었고, 함께 정리했다.
260 lines
10 KiB
PHP
260 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services;
|
|
|
|
use Illuminate\Support\Facades\File;
|
|
use Modules\Sirsoft\Ecommerce\Services\CurrencyConversionService;
|
|
use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService;
|
|
use Modules\Sirsoft\Ecommerce\Services\PaymentMethodResolver;
|
|
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
|
|
|
|
/**
|
|
* 설정 서비스 공유 인스턴스화 + 저장 시 리졸버 캐시 무효화 (공개 #116)
|
|
*
|
|
* 형제 서비스 3종(CurrencyConversionService / ShippingPolicyResolver / PaymentMethodResolver)은
|
|
* 싱글톤으로 등록되어 있는데 `EcommerceSettingsService` 만 미등록이라, 주입 지점마다 별개
|
|
* 인스턴스가 만들어지고 각자 자기 설정 캐시를 들고 있었다. 싱글톤 리졸버가 비-싱글톤 설정
|
|
* 서비스를 captive 로 보유하는 비대칭도 함께 생긴다.
|
|
*
|
|
* 싱글톤화만으로는 부족하다 — 리졸버들은 자기 캐시를 따로 들고 있어서, 같은 요청 안에서
|
|
* 설정을 저장해도 이미 해석된 리졸버는 저장 전 카탈로그를 계속 답한다. 저장 지점에서
|
|
* (이미 해석된 경우에만) 무효화를 연동한다.
|
|
*/
|
|
class SettingsSaveFlushesResolversTest extends ModuleTestCase
|
|
{
|
|
private string $storagePath;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->storagePath = storage_path('framework/testing/modules/sirsoft-ecommerce/settings');
|
|
|
|
if (File::isDirectory($this->storagePath)) {
|
|
File::cleanDirectory($this->storagePath);
|
|
}
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if (File::isDirectory($this->storagePath)) {
|
|
File::cleanDirectory($this->storagePath);
|
|
}
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* 카드 결제수단을 지정 상태로 만드는 저장 payload 를 만듭니다.
|
|
*
|
|
* @param bool $cardActive card 결제수단 활성 여부
|
|
* @return array order_settings.payment_methods payload
|
|
*/
|
|
private function paymentMethods(bool $cardActive): array
|
|
{
|
|
return [
|
|
['id' => 'card', 'is_active' => $cardActive, 'sort_order' => 1],
|
|
['id' => 'dbank', 'is_active' => true, 'sort_order' => 2],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 설정 서비스는 컨테이너에서 공유 인스턴스로 해석된다. (실패-먼저)
|
|
*
|
|
* @scenario resolution=container
|
|
*
|
|
* @effects settings_service_shared_instance
|
|
*/
|
|
public function test_settings_service_is_resolved_as_shared_instance(): void
|
|
{
|
|
$this->assertTrue(
|
|
$this->app->bound(EcommerceSettingsService::class),
|
|
'설정 서비스가 컨테이너에 등록되어 있지 않습니다 (형제 3종은 싱글톤 등록).'
|
|
);
|
|
|
|
$first = $this->app->make(EcommerceSettingsService::class);
|
|
$second = $this->app->make(EcommerceSettingsService::class);
|
|
|
|
$this->assertSame($first, $second, '설정 서비스가 해석할 때마다 새 인스턴스로 만들어집니다.');
|
|
}
|
|
|
|
/**
|
|
* 싱글톤 리졸버가 붙든 설정 서비스도 같은 공유 인스턴스다.
|
|
*
|
|
* @scenario resolution=captive_dependency
|
|
*
|
|
* @effects settings_service_shared_instance
|
|
*/
|
|
public function test_resolver_holds_the_same_shared_settings_instance(): void
|
|
{
|
|
$resolver = $this->app->make(PaymentMethodResolver::class);
|
|
|
|
$reflection = new \ReflectionClass($resolver);
|
|
$prop = $reflection->getProperty('settingsService');
|
|
$prop->setAccessible(true);
|
|
|
|
$this->assertSame(
|
|
$this->app->make(EcommerceSettingsService::class),
|
|
$prop->getValue($resolver),
|
|
'싱글톤 리졸버가 별개의 설정 서비스 인스턴스를 붙들고 있습니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 저장 직후 같은 요청에서 결제수단 리졸버가 신값을 답한다. (실패-먼저)
|
|
*
|
|
* @scenario resolution=container
|
|
*
|
|
* @effects payment_resolver_reflects_saved_catalog
|
|
*/
|
|
public function test_bulk_save_flushes_payment_method_resolver(): void
|
|
{
|
|
$settings = $this->app->make(EcommerceSettingsService::class);
|
|
$settings->saveSettings(['order_settings' => ['payment_methods' => $this->paymentMethods(false)]]);
|
|
|
|
// 저장 전 카탈로그를 리졸버가 선점하도록 강제
|
|
$resolver = $this->app->make(PaymentMethodResolver::class);
|
|
$this->assertFalse($this->isActive($resolver, 'card'));
|
|
|
|
$settings->saveSettings(['order_settings' => ['payment_methods' => $this->paymentMethods(true)]]);
|
|
|
|
$this->assertTrue(
|
|
$this->isActive($resolver, 'card'),
|
|
'저장 후에도 리졸버가 저장 전 카탈로그를 답했습니다 (캐시 무효화 미연동).'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 단건 저장(setSetting) 경로도 같은 무효화 지점을 지난다. (실패-먼저)
|
|
*
|
|
* @scenario resolution=container
|
|
*
|
|
* @effects payment_resolver_reflects_saved_catalog
|
|
*/
|
|
public function test_single_key_save_flushes_payment_method_resolver(): void
|
|
{
|
|
$settings = $this->app->make(EcommerceSettingsService::class);
|
|
$settings->setSetting('order_settings.payment_methods', $this->paymentMethods(false));
|
|
|
|
$resolver = $this->app->make(PaymentMethodResolver::class);
|
|
$this->assertFalse($this->isActive($resolver, 'card'));
|
|
|
|
$settings->setSetting('order_settings.payment_methods', $this->paymentMethods(true));
|
|
|
|
$this->assertTrue(
|
|
$this->isActive($resolver, 'card'),
|
|
'단건 저장 후에도 리졸버가 저장 전 카탈로그를 답했습니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 저장 직후 같은 요청에서 통화 서비스도 신값을 답한다. (실패-먼저)
|
|
*
|
|
* 결제수단 리졸버와 함께 무효화 목록에 있으나, 종전에는 "해석 가드가 있는가" 만
|
|
* 확인하고 실제로 신값이 나오는지는 검증하지 않았다.
|
|
*
|
|
* @scenario resolution=container
|
|
*
|
|
* @effects currency_service_reflects_saved_settings
|
|
*/
|
|
public function test_bulk_save_flushes_currency_conversion_service(): void
|
|
{
|
|
$settings = $this->app->make(EcommerceSettingsService::class);
|
|
$settings->saveSettings(['language_currency' => $this->currencySettings('KRW')]);
|
|
|
|
// 저장 전 설정을 통화 서비스가 선점하도록 강제
|
|
$currency = $this->app->make(CurrencyConversionService::class);
|
|
$this->assertSame('KRW', $currency->getDefaultCurrency());
|
|
|
|
$settings->saveSettings(['language_currency' => $this->currencySettings('USD')]);
|
|
|
|
$this->assertSame(
|
|
'USD',
|
|
$currency->getDefaultCurrency(),
|
|
'저장 후에도 통화 서비스가 저장 전 기본 통화를 답했습니다 (캐시 무효화 미연동).'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 단건 저장 경로도 통화 서비스 캐시를 비운다. (실패-먼저)
|
|
*
|
|
* @scenario resolution=container
|
|
*
|
|
* @effects currency_service_reflects_saved_settings
|
|
*/
|
|
public function test_single_key_save_flushes_currency_conversion_service(): void
|
|
{
|
|
$settings = $this->app->make(EcommerceSettingsService::class);
|
|
$settings->saveSettings(['language_currency' => $this->currencySettings('KRW')]);
|
|
|
|
$currency = $this->app->make(CurrencyConversionService::class);
|
|
$this->assertSame('KRW', $currency->getDefaultCurrency());
|
|
|
|
$settings->setSetting('language_currency.default_currency', 'USD');
|
|
|
|
$this->assertSame(
|
|
'USD',
|
|
$currency->getDefaultCurrency(),
|
|
'단건 저장 후에도 통화 서비스가 저장 전 기본 통화를 답했습니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 지정한 기본 통화로 language_currency payload 를 만듭니다.
|
|
*
|
|
* @param string $defaultCurrency 기본 통화 코드
|
|
* @return array language_currency payload
|
|
*/
|
|
private function currencySettings(string $defaultCurrency): array
|
|
{
|
|
return [
|
|
'default_currency' => $defaultCurrency,
|
|
'currencies' => [
|
|
['code' => 'KRW', 'symbol' => '₩', 'decimal_places' => 0, 'exchange_rate' => 1, 'is_active' => true, 'is_default' => $defaultCurrency === 'KRW'],
|
|
['code' => 'USD', 'symbol' => '$', 'decimal_places' => 2, 'exchange_rate' => 0.00075, 'is_active' => true, 'is_default' => $defaultCurrency === 'USD'],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 아직 해석되지 않은 리졸버는 저장이 강제로 인스턴스화하지 않는다.
|
|
*
|
|
* @scenario resolution=not_resolved
|
|
*
|
|
* @effects unresolved_services_not_instantiated_on_save
|
|
*/
|
|
public function test_save_does_not_instantiate_unresolved_resolvers(): void
|
|
{
|
|
$this->app->forgetInstance(PaymentMethodResolver::class);
|
|
$this->app->forgetInstance(CurrencyConversionService::class);
|
|
|
|
$this->app->make(EcommerceSettingsService::class)
|
|
->saveSettings(['order_settings' => ['payment_methods' => $this->paymentMethods(true)]]);
|
|
|
|
$this->assertFalse(
|
|
$this->app->resolved(PaymentMethodResolver::class),
|
|
'저장이 미해석 리졸버를 강제로 인스턴스화했습니다.'
|
|
);
|
|
$this->assertFalse(
|
|
$this->app->resolved(CurrencyConversionService::class),
|
|
'저장이 미해석 통화 서비스를 강제로 인스턴스화했습니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 리졸버가 보는 카탈로그에서 결제수단의 활성 여부를 읽습니다.
|
|
*
|
|
* @param PaymentMethodResolver $resolver 결제수단 리졸버
|
|
* @param string $methodId 결제수단 ID
|
|
* @return bool 활성 여부
|
|
*/
|
|
private function isActive(PaymentMethodResolver $resolver, string $methodId): bool
|
|
{
|
|
$reflection = new \ReflectionClass($resolver);
|
|
$method = $reflection->getMethod('catalogValue');
|
|
$method->setAccessible(true);
|
|
|
|
return (bool) $method->invoke($resolver, $methodId, 'is_active');
|
|
}
|
|
}
|