Files
2026-07-01 10:30:32 +09:00

1303 lines
45 KiB
PHP

<?php
namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Requests;
use Illuminate\Support\Facades\Validator;
use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\StoreEcommerceSettingsRequest;
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
/**
* 이커머스 설정 저장 요청 검증 테스트
*
* - _tab 필드 검증 (탭별 개별 저장 지원)
* - validatedSettings()에서 _tab 제외 확인
* - 탭별 부분 데이터 전송 시 검증 통과 확인
*
* 주의: attributes() 가 `sirsoft-ecommerce::validation.attributes` 번역을 조회하므로
* ModuleTestCase 상속 필수 (코어 TestCase 는 모듈 lang 경로 미등록).
*/
class StoreEcommerceSettingsRequestTest extends ModuleTestCase
{
/**
* 검증 수행
*
* @param array $data 검증 대상 데이터
*/
protected function validate(array $data): \Illuminate\Validation\Validator
{
$request = new StoreEcommerceSettingsRequest;
return Validator::make($data, $request->rules());
}
// ──────────────────────────────────────────────
// _tab 필드 검증
// ──────────────────────────────────────────────
public function test_tab_field_accepts_valid_tab_names(): void
{
$validTabs = ['basic_info', 'language_currency', 'seo', 'order_settings'];
foreach ($validTabs as $tab) {
$validator = $this->validate([
'_tab' => $tab,
$tab => [],
]);
$this->assertFalse(
$validator->errors()->has('_tab'),
"_tab 필드가 '{$tab}' 값을 허용해야 합니다"
);
}
}
public function test_tab_field_rejects_invalid_tab_name(): void
{
$validator = $this->validate([
'_tab' => 'nonexistent_tab',
'basic_info' => [
'shop_name' => '테스트 쇼핑몰',
],
]);
$this->assertTrue(
$validator->errors()->has('_tab'),
'_tab 필드가 유효하지 않은 탭 이름을 거부해야 합니다'
);
}
public function test_tab_field_is_optional(): void
{
$validator = $this->validate([
'basic_info' => [
'shop_name' => '테스트 쇼핑몰',
],
]);
$this->assertFalse(
$validator->errors()->has('_tab'),
'_tab 필드는 선택 사항이어야 합니다'
);
}
// ──────────────────────────────────────────────
// validatedSettings() — _tab 필터링
// ──────────────────────────────────────────────
public function test_validated_settings_excludes_tab_field(): void
{
$request = new StoreEcommerceSettingsRequest;
$validCategories = ['basic_info', 'language_currency', 'seo', 'order_settings'];
// validatedSettings()는 유효 카테고리만 반환하므로 _tab은 제외됨
// 검증 로직을 직접 테스트: validated 결과에서 _tab이 필터링되는지 확인
$inputWithTab = [
'_tab' => 'basic_info',
'basic_info' => ['shop_name' => '테스트'],
'seo' => ['meta_main_title' => 'SEO'],
];
$filtered = array_filter(
$inputWithTab,
fn ($key) => in_array($key, $validCategories),
ARRAY_FILTER_USE_KEY
);
$this->assertArrayNotHasKey('_tab', $filtered);
$this->assertArrayHasKey('basic_info', $filtered);
$this->assertArrayHasKey('seo', $filtered);
}
public function test_validated_settings_returns_only_sent_category(): void
{
$request = new StoreEcommerceSettingsRequest;
$validCategories = ['basic_info', 'language_currency', 'seo', 'order_settings'];
// seo 탭만 전송 시 seo만 포함
$inputSeoOnly = [
'_tab' => 'seo',
'seo' => ['meta_main_title' => '테스트 SEO'],
];
$filtered = array_filter(
$inputSeoOnly,
fn ($key) => in_array($key, $validCategories),
ARRAY_FILTER_USE_KEY
);
$this->assertArrayHasKey('seo', $filtered);
$this->assertArrayNotHasKey('basic_info', $filtered);
$this->assertArrayNotHasKey('language_currency', $filtered);
$this->assertArrayNotHasKey('order_settings', $filtered);
$this->assertArrayNotHasKey('_tab', $filtered);
}
// ──────────────────────────────────────────────
// 탭별 부분 데이터 전송
// ──────────────────────────────────────────────
public function test_basic_info_tab_only_passes_validation(): void
{
$validator = $this->validate([
'_tab' => 'basic_info',
'basic_info' => [
'shop_name' => '테스트 쇼핑몰',
'route_path' => 'shop',
],
]);
$this->assertTrue(
$validator->passes(),
'basic_info 탭만 전송해도 검증을 통과해야 합니다. 오류: '.$validator->errors()->toJson()
);
}
public function test_seo_tab_only_passes_validation(): void
{
$validator = $this->validate([
'_tab' => 'seo',
'seo' => [
'meta_main_title' => '테스트 SEO 타이틀',
],
]);
$this->assertTrue(
$validator->passes(),
'seo 탭만 전송해도 검증을 통과해야 합니다. 오류: '.$validator->errors()->toJson()
);
}
public function test_order_settings_tab_only_passes_validation(): void
{
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'auto_cancel_expired' => true,
'auto_cancel_days' => 3,
],
]);
$this->assertTrue(
$validator->passes(),
'order_settings 탭만 전송해도 검증을 통과해야 합니다. 오류: '.$validator->errors()->toJson()
);
}
public function test_language_currency_tab_only_passes_validation(): void
{
$validator = $this->validate([
'_tab' => 'language_currency',
'language_currency' => [
'default_language' => 'ko',
'default_currency' => 'KRW',
],
]);
$this->assertTrue(
$validator->passes(),
'language_currency 탭만 전송해도 검증을 통과해야 합니다. 오류: '.$validator->errors()->toJson()
);
}
// ──────────────────────────────────────────────
// 통화 기호(symbol) 검증
// ──────────────────────────────────────────────
public function test_currency_symbol_accepts_valid_value(): void
{
$validator = $this->validate([
'_tab' => 'language_currency',
'language_currency' => [
'default_currency' => 'KRW',
'currencies' => [
['code' => 'KRW', 'name' => ['ko' => '원'], 'symbol' => '₩'],
],
],
]);
$this->assertTrue(
$validator->passes(),
'통화 기호(symbol)가 포함된 통화 설정은 검증을 통과해야 합니다. 오류: '.$validator->errors()->toJson()
);
}
public function test_currency_symbol_is_optional(): void
{
$validator = $this->validate([
'_tab' => 'language_currency',
'language_currency' => [
'default_currency' => 'KRW',
'currencies' => [
['code' => 'KRW', 'name' => ['ko' => '원']],
],
],
]);
$this->assertTrue(
$validator->passes(),
'symbol 미전송 시에도 검증을 통과해야 합니다(nullable). 오류: '.$validator->errors()->toJson()
);
}
public function test_currency_symbol_rejects_too_long_value(): void
{
$validator = $this->validate([
'_tab' => 'language_currency',
'language_currency' => [
'default_currency' => 'KRW',
'currencies' => [
['code' => 'KRW', 'name' => ['ko' => '원'], 'symbol' => str_repeat('₩', 9)],
],
],
]);
$this->assertTrue(
$validator->fails(),
'symbol 이 max:8 을 초과하면 검증에 실패해야 합니다.'
);
$this->assertArrayHasKey(
'language_currency.currencies.0.symbol',
$validator->errors()->toArray()
);
}
// ──────────────────────────────────────────────
// basic_info 탭 전송 시 shop_name 필수 확인
// ──────────────────────────────────────────────
public function test_shop_name_required_when_basic_info_present(): void
{
$validator = $this->validate([
'_tab' => 'basic_info',
'basic_info' => [
'company_name' => '테스트 회사',
// shop_name 누락
],
]);
$this->assertTrue(
$validator->errors()->has('basic_info.shop_name'),
'basic_info 탭 전송 시 shop_name은 필수여야 합니다'
);
}
public function test_shop_name_not_required_when_basic_info_absent(): void
{
$validator = $this->validate([
'_tab' => 'seo',
'seo' => [
'meta_main_title' => '테스트',
],
]);
$this->assertFalse(
$validator->errors()->has('basic_info.shop_name'),
'basic_info 탭이 없으면 shop_name 검증을 건너뛰어야 합니다'
);
}
// ──────────────────────────────────────────────
// order_settings boolean 필드 검증
// ──────────────────────────────────────────────
public function test_stock_restore_on_cancel_accepts_boolean(): void
{
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'stock_restore_on_cancel' => false,
],
]);
$this->assertFalse(
$validator->errors()->has('order_settings.stock_restore_on_cancel'),
'stock_restore_on_cancel은 boolean false를 허용해야 합니다'
);
}
public function test_stock_restore_on_cancel_rejects_string_false(): void
{
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'stock_restore_on_cancel' => 'false',
],
]);
$this->assertTrue(
$validator->errors()->has('order_settings.stock_restore_on_cancel'),
'stock_restore_on_cancel은 문자열 "false"를 거부해야 합니다'
);
}
public function test_auto_cancel_expired_accepts_boolean(): void
{
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'auto_cancel_expired' => true,
],
]);
$this->assertFalse(
$validator->errors()->has('order_settings.auto_cancel_expired'),
'auto_cancel_expired는 boolean true를 허용해야 합니다'
);
}
// ──────────────────────────────────────────────
// order_settings 필드 속성 번역 확인
// ──────────────────────────────────────────────
public function test_order_settings_attributes_are_translated(): void
{
$request = new StoreEcommerceSettingsRequest;
$attributes = $request->attributes();
$this->assertArrayHasKey(
'order_settings.stock_restore_on_cancel',
$attributes,
'order_settings.stock_restore_on_cancel 속성 번역이 있어야 합니다'
);
$this->assertArrayHasKey(
'order_settings.auto_cancel_expired',
$attributes,
'order_settings.auto_cancel_expired 속성 번역이 있어야 합니다'
);
$this->assertArrayHasKey(
'order_settings.auto_cancel_days',
$attributes,
'order_settings.auto_cancel_days 속성 번역이 있어야 합니다'
);
}
// ──────────────────────────────────────────────
// order_settings 커스텀 메시지 확인
// ──────────────────────────────────────────────
public function test_order_settings_custom_messages_exist(): void
{
$request = new StoreEcommerceSettingsRequest;
$messages = $request->messages();
$this->assertArrayHasKey(
'order_settings.stock_restore_on_cancel.boolean',
$messages,
'stock_restore_on_cancel.boolean 커스텀 메시지가 있어야 합니다'
);
$this->assertArrayHasKey(
'order_settings.auto_cancel_expired.boolean',
$messages,
'auto_cancel_expired.boolean 커스텀 메시지가 있어야 합니다'
);
$this->assertArrayHasKey(
'order_settings.auto_cancel_days.integer',
$messages,
'auto_cancel_days.integer 커스텀 메시지가 있어야 합니다'
);
}
// ──────────────────────────────────────────────
// 결제수단 최소 1개 활성화 검증
// ──────────────────────────────────────────────
public function test_payment_methods_requires_at_least_one_active(): void
{
$request = new StoreEcommerceSettingsRequest;
$request->merge([
'_tab' => 'order_settings',
'order_settings' => [
'payment_methods' => [
['id' => 'dbank', 'is_active' => false],
['id' => 'point', 'is_active' => false],
],
],
]);
$validator = Validator::make(
$request->all(),
$request->rules()
);
// withValidator의 after 콜백 수동 실행
$validator->after(function ($v) use ($request) {
$reflection = new \ReflectionMethod($request, 'validateAtLeastOneActivePaymentMethod');
$reflection->invoke($request, $v);
});
$validator->passes();
$this->assertTrue(
$validator->errors()->has('order_settings.payment_methods'),
'모든 결제수단이 비활성인 경우 오류가 발생해야 합니다'
);
}
public function test_payment_methods_passes_with_one_active(): void
{
$request = new StoreEcommerceSettingsRequest;
$request->merge([
'_tab' => 'order_settings',
'order_settings' => [
'payment_methods' => [
['id' => 'dbank', 'is_active' => true],
['id' => 'point', 'is_active' => false],
],
],
]);
$validator = Validator::make(
$request->all(),
$request->rules()
);
$validator->after(function ($v) use ($request) {
$reflection = new \ReflectionMethod($request, 'validateAtLeastOneActivePaymentMethod');
$reflection->invoke($request, $v);
});
$validator->passes();
$this->assertFalse(
$validator->errors()->has('order_settings.payment_methods'),
'하나 이상의 결제수단이 활성이면 오류가 없어야 합니다'
);
}
// ──────────────────────────────────────────────
// 재고 차감 시점 - none 옵션 검증
// ──────────────────────────────────────────────
public function test_stock_deduction_timing_accepts_none(): void
{
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'payment_methods' => [
['id' => 'dbank', 'stock_deduction_timing' => 'none'],
],
],
]);
$this->assertFalse(
$validator->errors()->has('order_settings.payment_methods.0.stock_deduction_timing'),
'stock_deduction_timing은 none 값을 허용해야 합니다'
);
}
public function test_stock_deduction_timing_rejects_invalid_value(): void
{
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'payment_methods' => [
['id' => 'dbank', 'stock_deduction_timing' => 'invalid'],
],
],
]);
$this->assertTrue(
$validator->errors()->has('order_settings.payment_methods.0.stock_deduction_timing'),
'stock_deduction_timing은 유효하지 않은 값을 거부해야 합니다'
);
}
// ──────────────────────────────────────────────
// 무통장 계좌 검증
// ──────────────────────────────────────────────
public function test_bank_account_requires_account_number(): void
{
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'bank_accounts' => [
['bank_code' => '004', 'account_holder' => '홍길동', 'is_active' => true, 'is_default' => true],
],
],
]);
$this->assertTrue(
$validator->errors()->has('order_settings.bank_accounts.0.account_number'),
'계좌번호는 필수 항목이어야 합니다'
);
}
public function test_bank_account_requires_account_holder(): void
{
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'bank_accounts' => [
['bank_code' => '004', 'account_number' => '1234567890', 'is_active' => true, 'is_default' => true],
],
],
]);
$this->assertTrue(
$validator->errors()->has('order_settings.bank_accounts.0.account_holder'),
'예금주는 필수 항목이어야 합니다'
);
}
public function test_bank_accounts_requires_at_least_one_active_default(): void
{
$request = new StoreEcommerceSettingsRequest;
$request->merge([
'_tab' => 'order_settings',
'order_settings' => [
'bank_accounts' => [
['bank_code' => '004', 'account_number' => '1234567890', 'account_holder' => '홍길동', 'is_active' => true, 'is_default' => false],
['bank_code' => '088', 'account_number' => '9876543210', 'account_holder' => '김철수', 'is_active' => false, 'is_default' => true],
],
],
]);
$validator = Validator::make(
$request->all(),
$request->rules()
);
$validator->after(function ($v) use ($request) {
$reflection = new \ReflectionMethod($request, 'validateBankAccountDefaults');
$reflection->invoke($request, $v);
});
$validator->passes();
$this->assertTrue(
$validator->errors()->has('order_settings.bank_accounts'),
'기본+사용이 모두 선택된 계좌가 없으면 오류가 발생해야 합니다'
);
}
// ──────────────────────────────────────────────
// order_settings.banks 로케일별 은행명 검증
// ──────────────────────────────────────────────
public function test_banks_current_locale_name_required(): void
{
app()->setLocale('ko');
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'banks' => [
['code' => '004', 'name' => ['en' => 'Kookmin Bank']],
],
],
]);
$this->assertTrue(
$validator->errors()->has('order_settings.banks.0.name.ko'),
'현재 로케일(ko) 은행명이 없으면 실패해야 합니다'
);
}
public function test_banks_current_locale_only_is_valid(): void
{
app()->setLocale('ko');
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'banks' => [
['code' => '004', 'name' => ['ko' => '국민은행']],
],
],
]);
$this->assertTrue(
$validator->passes(),
'현재 로케일(ko)만 입력해도 유효해야 합니다. 오류: '.$validator->errors()->toJson()
);
}
public function test_banks_other_locale_null_is_valid(): void
{
app()->setLocale('ko');
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'banks' => [
['code' => '004', 'name' => ['ko' => '국민은행', 'en' => null]],
],
],
]);
$this->assertTrue(
$validator->passes(),
'다른 로케일이 null이어도 유효해야 합니다. 오류: '.$validator->errors()->toJson()
);
}
public function test_banks_other_locale_validates_max_length_when_provided(): void
{
app()->setLocale('ko');
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'banks' => [
['code' => '004', 'name' => ['ko' => '국민은행', 'en' => str_repeat('A', 101)]],
],
],
]);
$this->assertTrue(
$validator->errors()->has('order_settings.banks.0.name.en'),
'다른 로케일도 입력 시 최대 100자 검증을 적용해야 합니다'
);
}
public function test_banks_en_locale_required_when_locale_is_en(): void
{
app()->setLocale('en');
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'banks' => [
['code' => '004', 'name' => ['ko' => '국민은행']],
],
],
]);
$this->assertTrue(
$validator->errors()->has('order_settings.banks.0.name.en'),
'로케일이 en일 때 영문 은행명이 없으면 실패해야 합니다'
);
}
public function test_banks_accepts_valid_full_data(): void
{
app()->setLocale('ko');
$validator = $this->validate([
'_tab' => 'order_settings',
'order_settings' => [
'banks' => [
['code' => '004', 'name' => ['ko' => '국민은행', 'en' => 'Kookmin Bank']],
['code' => '088', 'name' => ['ko' => '신한은행', 'en' => 'Shinhan Bank']],
],
],
]);
$this->assertTrue(
$validator->passes(),
'유효한 은행 데이터를 허용해야 합니다. 오류: '.$validator->errors()->toJson()
);
}
public function test_banks_custom_messages_exist(): void
{
$request = new StoreEcommerceSettingsRequest;
$messages = $request->messages();
$locale = app()->getLocale();
$this->assertArrayHasKey(
'order_settings.banks.*.code.required_with',
$messages,
'banks.*.code.required_with 커스텀 메시지가 있어야 합니다'
);
$this->assertArrayHasKey(
"order_settings.banks.*.name.{$locale}.required_with",
$messages,
"banks.*.name.{$locale}.required_with 커스텀 메시지가 있어야 합니다"
);
}
public function test_bank_accounts_passes_with_active_default(): void
{
$request = new StoreEcommerceSettingsRequest;
$request->merge([
'_tab' => 'order_settings',
'order_settings' => [
'bank_accounts' => [
['bank_code' => '004', 'account_number' => '1234567890', 'account_holder' => '홍길동', 'is_active' => true, 'is_default' => true],
],
],
]);
$validator = Validator::make(
$request->all(),
$request->rules()
);
$validator->after(function ($v) use ($request) {
$reflection = new \ReflectionMethod($request, 'validateBankAccountDefaults');
$reflection->invoke($request, $v);
});
$validator->passes();
$this->assertFalse(
$validator->errors()->has('order_settings.bank_accounts'),
'기본+사용이 모두 선택된 계좌가 있으면 오류가 없어야 합니다'
);
}
// ========================================================================
// shipping.carriers 검증 테스트
// ========================================================================
public function test_shipping_tab_passes_with_valid_carriers(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
[
'id' => 1,
'code' => 'cj',
'name' => ['ko' => 'CJ대한통운', 'en' => 'CJ Logistics'],
'type' => 'domestic',
'tracking_url' => 'https://example.com/track?no={tracking_number}',
'is_active' => true,
],
[
'code' => 'fedex',
'name' => ['ko' => '페덱스'],
'type' => 'international',
'is_active' => true,
],
],
],
]);
$this->assertFalse(
$validator->fails(),
'유효한 carriers 데이터로 검증이 통과해야 합니다: '.json_encode($validator->errors()->toArray())
);
}
public function test_shipping_tab_passes_with_empty_carriers(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [],
],
]);
$this->assertFalse(
$validator->fails(),
'빈 carriers 배열로 검증이 통과해야 합니다'
);
}
public function test_shipping_tab_passes_without_carriers_key(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'default_country' => 'KR',
],
]);
$this->assertFalse(
$validator->fails(),
'carriers 키 없이도 검증이 통과해야 합니다'
);
}
public function test_carrier_code_is_required(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['name' => ['ko' => 'CJ대한통운'], 'type' => 'domestic'],
],
],
]);
$this->assertTrue(
$validator->errors()->has('shipping.carriers.0.code'),
'carriers가 있으면 code가 필수여야 합니다'
);
}
public function test_carrier_code_accepts_valid_format(): void
{
$validCodes = ['cj', 'fedex', 'ems-express', 'kr-post', 'cj2'];
foreach ($validCodes as $code) {
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => $code, 'name' => ['ko' => '테스트'], 'type' => 'domestic'],
],
],
]);
$this->assertFalse(
$validator->errors()->has('shipping.carriers.0.code'),
"유효한 코드 '{$code}'가 통과해야 합니다"
);
}
}
public function test_carrier_code_rejects_invalid_format(): void
{
$invalidCodes = ['CJ', 'Fedex', 'cj!', '123abc', '-cj', 'cj-'];
foreach ($invalidCodes as $code) {
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => $code, 'name' => ['ko' => '테스트'], 'type' => 'domestic'],
],
],
]);
$this->assertTrue(
$validator->errors()->has('shipping.carriers.0.code'),
"잘못된 코드 '{$code}'는 거부해야 합니다"
);
}
}
public function test_carrier_code_max_length(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => str_repeat('a', 51), 'name' => ['ko' => '테스트'], 'type' => 'domestic'],
],
],
]);
$this->assertTrue(
$validator->errors()->has('shipping.carriers.0.code'),
'code 최대 50자 초과 시 실패해야 합니다'
);
}
public function test_carrier_name_ko_is_required(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => 'cj', 'name' => ['en' => 'CJ Logistics'], 'type' => 'domestic'],
],
],
]);
// LocaleRequiredTranslatable 은 field 레벨에서 실패 — ko 누락 시 shipping.carriers.0.name 에 에러
$this->assertTrue(
$validator->errors()->has('shipping.carriers.0.name'),
'carriers가 있으면 name(primary locale) 이 필수여야 합니다'
);
}
public function test_carrier_name_is_required_as_array(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => 'cj', 'type' => 'domestic'],
],
],
]);
$this->assertTrue(
$validator->errors()->has('shipping.carriers.0.name'),
'carriers가 있으면 name이 필수여야 합니다'
);
}
public function test_carrier_name_ko_max_length(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => 'cj', 'name' => ['ko' => str_repeat('가', 101)], 'type' => 'domestic'],
],
],
]);
// LocaleRequiredTranslatable(maxLength:100) — 초과 시 field 레벨 에러
$this->assertTrue(
$validator->errors()->has('shipping.carriers.0.name'),
'name 최대 100자 초과 시 실패해야 합니다'
);
}
public function test_carrier_type_is_required(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => 'cj', 'name' => ['ko' => 'CJ대한통운']],
],
],
]);
$this->assertTrue(
$validator->errors()->has('shipping.carriers.0.type'),
'carriers가 있으면 type이 필수여야 합니다'
);
}
public function test_carrier_type_rejects_invalid_value(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => 'cj', 'name' => ['ko' => 'CJ대한통운'], 'type' => 'express'],
],
],
]);
$this->assertTrue(
$validator->errors()->has('shipping.carriers.0.type'),
'type은 domestic/international만 허용해야 합니다'
);
}
public function test_carrier_type_accepts_valid_values(): void
{
foreach (['domestic', 'international'] as $type) {
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => 'cj', 'name' => ['ko' => 'CJ대한통운'], 'type' => $type],
],
],
]);
$this->assertFalse(
$validator->errors()->has('shipping.carriers.0.type'),
"유효한 type '{$type}'이 통과해야 합니다"
);
}
}
public function test_carrier_tracking_url_max_length(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
[
'code' => 'cj',
'name' => ['ko' => 'CJ대한통운'],
'type' => 'domestic',
'tracking_url' => str_repeat('a', 501),
],
],
],
]);
$this->assertTrue(
$validator->errors()->has('shipping.carriers.0.tracking_url'),
'tracking_url 최대 500자 초과 시 실패해야 합니다'
);
}
public function test_carrier_tracking_url_is_optional(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => 'cj', 'name' => ['ko' => 'CJ대한통운'], 'type' => 'domestic'],
],
],
]);
$this->assertFalse(
$validator->errors()->has('shipping.carriers.0.tracking_url'),
'tracking_url은 선택 필드여야 합니다'
);
}
public function test_carrier_duplicate_codes_rejected(): void
{
$request = new StoreEcommerceSettingsRequest;
$request->merge([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => 'cj', 'name' => ['ko' => 'CJ대한통운'], 'type' => 'domestic'],
['code' => 'cj', 'name' => ['ko' => 'CJ물류'], 'type' => 'domestic'],
],
],
]);
$validator = Validator::make(
$request->all(),
$request->rules()
);
$validator->after(function ($v) use ($request) {
$reflection = new \ReflectionMethod($request, 'validateUniqueCarrierCodes');
$reflection->invoke($request, $v);
});
$validator->passes();
$this->assertTrue(
$validator->errors()->has('shipping.carriers.1.code'),
'중복 배송사 코드는 거부해야 합니다'
);
}
public function test_carrier_duplicate_codes_case_insensitive(): void
{
$request = new StoreEcommerceSettingsRequest;
$request->merge([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => 'cj', 'name' => ['ko' => 'CJ대한통운'], 'type' => 'domestic'],
['code' => 'cj', 'name' => ['ko' => 'CJ물류'], 'type' => 'domestic'],
],
],
]);
$validator = Validator::make(
$request->all(),
$request->rules()
);
$validator->after(function ($v) use ($request) {
$reflection = new \ReflectionMethod($request, 'validateUniqueCarrierCodes');
$reflection->invoke($request, $v);
});
$validator->passes();
$this->assertTrue(
$validator->errors()->has('shipping.carriers.1.code'),
'대소문자 무관하게 중복 코드를 감지해야 합니다'
);
}
public function test_carrier_empty_name_rejected_by_custom_validator(): void
{
// validateCarrierNames 는 현재 앱 로케일의 배송사명을 검사한다.
// 테스트 데이터가 ko 공백을 제공하므로 환경 로케일(ja 등)과 무관하게 ko 로 고정한다.
app()->setLocale('ko');
$request = new StoreEcommerceSettingsRequest;
$request->merge([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => 'cj', 'name' => ['ko' => ' '], 'type' => 'domestic'],
],
],
]);
$validator = Validator::make(
$request->all(),
$request->rules()
);
$validator->after(function ($v) use ($request) {
$reflection = new \ReflectionMethod($request, 'validateCarrierNames');
$reflection->invoke($request, $v);
});
$validator->passes();
$this->assertTrue(
$validator->errors()->has('shipping.carriers.0.name.ko'),
'공백만 있는 배송사명은 거부해야 합니다'
);
}
public function test_carrier_missing_name_array_rejected_by_custom_validator(): void
{
$request = new StoreEcommerceSettingsRequest;
$request->merge([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => 'cj', 'name' => [], 'type' => 'domestic'],
],
],
]);
$validator = Validator::make(
$request->all(),
$request->rules()
);
$validator->after(function ($v) use ($request) {
$reflection = new \ReflectionMethod($request, 'validateCarrierNames');
$reflection->invoke($request, $v);
});
$validator->passes();
$this->assertTrue(
$validator->errors()->has('shipping.carriers.0.name'),
'빈 name 배열은 거부해야 합니다'
);
}
public function test_carrier_custom_messages_exist(): void
{
$request = new StoreEcommerceSettingsRequest;
$messages = $request->messages();
// LocaleRequiredTranslatable 은 field 레벨에서 보고 — name.ko 분리 키 미사용
$requiredKeys = [
'shipping.carriers.*.code.required_with',
'shipping.carriers.*.code.regex',
'shipping.carriers.*.name.required_with',
'shipping.carriers.*.type.required_with',
'shipping.carriers.*.type.in',
'shipping.carriers.*.tracking_url.max',
];
foreach ($requiredKeys as $key) {
$this->assertArrayHasKey(
$key,
$messages,
"'{$key}' 커스텀 메시지가 있어야 합니다"
);
}
}
public function test_carrier_multiple_items_validated_independently(): void
{
$validator = $this->validate([
'_tab' => 'shipping',
'shipping' => [
'carriers' => [
['code' => 'cj', 'name' => ['ko' => 'CJ대한통운'], 'type' => 'domestic'],
['code' => '', 'name' => ['ko' => ''], 'type' => 'invalid'],
],
],
]);
// 첫 번째 항목은 정상
$this->assertFalse(
$validator->errors()->has('shipping.carriers.0.code'),
'첫 번째 유효한 항목은 오류가 없어야 합니다'
);
// 두 번째 항목만 오류
$this->assertTrue(
$validator->errors()->has('shipping.carriers.1.type'),
'두 번째 항목의 type 오류가 있어야 합니다'
);
}
// ──────────────────────────────────────────────
// mileage 탭 검증 (§8.2)
// ──────────────────────────────────────────────
public function test_tab_field_accepts_mileage(): void
{
$validator = $this->validate(['_tab' => 'mileage', 'mileage' => ['enabled' => true]]);
$this->assertFalse($validator->errors()->has('_tab'), 'mileage 탭이 허용되어야 합니다');
}
public function test_mileage_valid_settings_pass(): void
{
$validator = $this->validate([
'mileage' => [
'enabled' => true,
'default_earn_rate' => 5,
'earn_trigger' => 'confirmed',
'earn_delay_days' => 0,
'currency_rules' => [
['currency_code' => 'KRW', 'point_value' => 1, 'min_use_amount' => 1000, 'use_unit' => 10, 'max_use_type' => 'fixed', 'max_use_percent' => 30, 'max_use_value' => 50000],
],
'expiry_enabled' => true,
'expiry_days' => 365,
'expiry_notification_enabled' => true,
'expiry_notification_days_before' => 7,
],
]);
$this->assertFalse($validator->fails(), '유효한 마일리지 설정은 통과해야 합니다');
}
public function test_mileage_earn_trigger_rejects_invalid(): void
{
$validator = $this->validate(['mileage' => ['enabled' => true, 'earn_trigger' => 'invalid']]);
$this->assertTrue($validator->errors()->has('mileage.earn_trigger'));
}
public function test_mileage_max_use_type_rejects_invalid(): void
{
$validator = $this->validate([
'mileage' => [
'enabled' => true,
'currency_rules' => [['currency_code' => 'KRW', 'max_use_type' => 'percentage']],
],
]);
$this->assertTrue($validator->errors()->has('mileage.currency_rules.0.max_use_type'));
}
public function test_mileage_default_earn_rate_rejects_negative(): void
{
$validator = $this->validate(['mileage' => ['enabled' => true, 'default_earn_rate' => -5]]);
$this->assertTrue($validator->errors()->has('mileage.default_earn_rate'));
}
public function test_mileage_duplicate_currency_code_rejected(): void
{
// withValidator after-검증을 포함한 전체 검증
$request = new StoreEcommerceSettingsRequest;
$request->merge([
'mileage' => [
'enabled' => true,
'currency_rules' => [
['currency_code' => 'KRW', 'max_use_type' => 'fixed'],
['currency_code' => 'KRW', 'max_use_type' => 'fixed'],
],
],
]);
$validator = Validator::make($request->all(), $request->rules());
$request->withValidator($validator);
$validator->passes();
$this->assertTrue(
$validator->errors()->has('mileage.currency_rules.1.currency_code'),
'통화 코드 중복은 거부되어야 합니다'
);
}
public function test_validated_settings_includes_mileage(): void
{
$request = new StoreEcommerceSettingsRequest;
$request->merge([
'_tab' => 'mileage',
'mileage' => ['enabled' => true, 'default_earn_rate' => 3],
]);
$request->setValidator(Validator::make($request->all(), $request->rules()));
$settings = $request->validatedSettings();
$this->assertArrayHasKey('mileage', $settings, 'validatedSettings 에 mileage 카테고리가 포함되어야 합니다');
$this->assertArrayNotHasKey('_tab', $settings, '_tab 은 제외되어야 합니다');
}
}