Files
Gnuboard7/modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/TempOrderServiceSecurityTest.php
T
2026-07-01 10:30:32 +09:00

442 lines
16 KiB
PHP

<?php
namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Services;
use App\Models\User;
use Carbon\Carbon;
use Modules\Sirsoft\Ecommerce\Database\Factories\CartFactory;
use Modules\Sirsoft\Ecommerce\Database\Factories\ProductFactory;
use Modules\Sirsoft\Ecommerce\Database\Factories\ProductOptionFactory;
use Modules\Sirsoft\Ecommerce\Enums\ChargePolicyEnum;
use Modules\Sirsoft\Ecommerce\Enums\CouponDiscountType;
use Modules\Sirsoft\Ecommerce\Enums\CouponIssueRecordStatus;
use Modules\Sirsoft\Ecommerce\Enums\CouponIssueStatus;
use Modules\Sirsoft\Ecommerce\Enums\CouponTargetScope;
use Modules\Sirsoft\Ecommerce\Enums\CouponTargetType;
use Modules\Sirsoft\Ecommerce\Enums\MileageTransactionTypeEnum;
use Modules\Sirsoft\Ecommerce\Models\Cart;
use Modules\Sirsoft\Ecommerce\Models\Coupon;
use Modules\Sirsoft\Ecommerce\Models\CouponIssue;
use Modules\Sirsoft\Ecommerce\Models\MileageTransaction;
use Modules\Sirsoft\Ecommerce\Models\ShippingPolicy;
use Modules\Sirsoft\Ecommerce\Services\TempOrderService;
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
/**
* TempOrderService 보안 테스트
*
* 쿠폰 소유권 검증 및 마일리지 사용 검증을 테스트합니다.
*/
class TempOrderServiceSecurityTest extends ModuleTestCase
{
protected TempOrderService $service;
protected function setUp(): void
{
parent::setUp();
// 실제 서비스 인스턴스 사용 (DI 컨테이너에서 가져옴)
$this->service = app(TempOrderService::class);
}
/**
* 테스트용 배송정책을 생성합니다.
*/
protected function createShippingPolicy(): ShippingPolicy
{
return ShippingPolicy::create([
'name' => ['ko' => '테스트 배송정책', 'en' => 'Test Shipping Policy'],
'shipping_method' => 'parcel',
'charge_policy' => ChargePolicyEnum::FREE,
'base_fee' => 0,
'countries' => ['KR'],
'currency_code' => 'KRW',
'is_default' => false,
'is_active' => true,
]);
}
/**
* 테스트용 쿠폰을 생성합니다.
*
* @param array $overrides 오버라이드할 속성
*/
protected function createCoupon(array $overrides = []): Coupon
{
return Coupon::create(array_merge([
'name' => ['ko' => '테스트 쿠폰', 'en' => 'Test Coupon'],
'description' => ['ko' => '테스트 쿠폰 설명', 'en' => 'Test coupon description'],
'target_type' => CouponTargetType::PRODUCT_AMOUNT,
'discount_type' => CouponDiscountType::FIXED,
'discount_value' => 1000,
'min_order_amount' => 10000,
'issue_status' => CouponIssueStatus::ISSUING,
'is_combinable' => true,
'target_scope' => CouponTargetScope::ALL,
'valid_from' => Carbon::now()->subDay(),
'valid_to' => Carbon::now()->addMonth(),
], $overrides));
}
/**
* 테스트용 쿠폰 발급 내역을 생성합니다.
*
* @param Coupon $coupon 쿠폰
* @param int $userId 사용자 ID
* @param array $overrides 오버라이드할 속성
*/
protected function createCouponIssue(Coupon $coupon, int $userId, array $overrides = []): CouponIssue
{
return CouponIssue::create(array_merge([
'coupon_id' => $coupon->id,
'user_id' => $userId,
'coupon_code' => 'TEST-'.strtoupper(uniqid()),
'status' => CouponIssueRecordStatus::AVAILABLE,
'issued_at' => Carbon::now(),
'expired_at' => Carbon::now()->addMonth(),
], $overrides));
}
// ========================================
// 비회원 쿠폰 사용 불가 테스트
// ========================================
/**
* 비회원이 체크아웃 업데이트 시 쿠폰이 무시됩니다.
*/
public function test_guest_checkout_ignores_coupons(): void
{
// Given: 비회원 장바구니와 쿠폰 (비회원은 쿠폰을 가질 수 없지만 API 우회 시도 가정)
$shippingPolicy = $this->createShippingPolicy();
$product = ProductFactory::new()->create([
'shipping_policy_id' => $shippingPolicy->id,
]);
$option = ProductOptionFactory::new()->forProduct($product)->create([
'selling_price' => 50000,
'stock_quantity' => 100,
]);
$cartKey = 'ck_'.str_repeat('c', 32);
$cart = CartFactory::new()
->forOption($option)
->create([
'user_id' => null,
'cart_key' => $cartKey,
'quantity' => 1,
]);
// 다른 사용자의 쿠폰 ID를 시도 (보안 테스트)
$otherUser = $this->createUser();
$coupon = $this->createCoupon();
$couponIssue = $this->createCouponIssue($coupon, $otherUser->id);
// When: 비회원이 체크아웃 생성
$createResponse = $this->postJson('/api/modules/sirsoft-ecommerce/checkout', [
'item_ids' => [$cart->id],
], [
'X-Cart-Key' => $cartKey,
]);
$createResponse->assertStatus(201);
// When: 비회원이 체크아웃 업데이트 시 쿠폰 ID 포함
$response = $this->putJson('/api/modules/sirsoft-ecommerce/checkout', [
'order_coupon_issue_id' => $couponIssue->id, // 타인의 쿠폰 ID
], [
'X-Cart-Key' => $cartKey,
]);
// Then: 체크아웃 업데이트 성공하지만 쿠폰 할인은 적용되지 않음 (비회원이므로 검증 실패)
$response->assertStatus(200);
// 쿠폰 할인이 적용되지 않았는지 확인 (total_discount가 0 또는 쿠폰 할인 없음)
$this->assertEquals(0, $response->json('data.calculation.summary.coupon_discount') ?? 0);
}
// ========================================
// 타인 쿠폰 사용 불가 테스트
// ========================================
/**
* 사용자가 타인의 쿠폰을 사용하려고 하면 무시됩니다.
*/
public function test_user_cannot_use_other_users_coupon(): void
{
// Given: 두 명의 사용자와 각각의 쿠폰
$user1 = $this->createUser();
$user2 = $this->createUser();
$shippingPolicy = $this->createShippingPolicy();
$product = ProductFactory::new()->create([
'shipping_policy_id' => $shippingPolicy->id,
]);
$option = ProductOptionFactory::new()->forProduct($product)->create([
'selling_price' => 50000,
'stock_quantity' => 100,
]);
$cart = CartFactory::new()
->forUser($user1)
->forOption($option)
->create(['quantity' => 1]);
// user2의 쿠폰 생성
$coupon = $this->createCoupon();
$user2CouponIssue = $this->createCouponIssue($coupon, $user2->id);
// When: user1이 체크아웃 생성
$createResponse = $this->actingAs($user1, 'sanctum')
->postJson('/api/modules/sirsoft-ecommerce/checkout', [
'item_ids' => [$cart->id],
]);
$createResponse->assertStatus(201);
// When: user1이 user2의 쿠폰으로 체크아웃 업데이트 시도
$response = $this->actingAs($user1, 'sanctum')
->putJson('/api/modules/sirsoft-ecommerce/checkout', [
'order_coupon_issue_id' => $user2CouponIssue->id, // user2의 쿠폰
]);
// Then: 체크아웃 업데이트 성공하지만 쿠폰 할인은 적용되지 않음 (소유권 검증 실패)
$response->assertStatus(200);
$this->assertEquals(0, $response->json('data.calculation.summary.coupon_discount') ?? 0);
}
/**
* 사용자가 본인의 쿠폰을 사용하면 정상 적용됩니다.
*/
public function test_user_can_use_own_coupon(): void
{
// Given: 사용자와 본인 쿠폰
$user = $this->createUser();
$shippingPolicy = $this->createShippingPolicy();
$product = ProductFactory::new()->create([
'shipping_policy_id' => $shippingPolicy->id,
]);
$option = ProductOptionFactory::new()->forProduct($product)->create([
'selling_price' => 50000,
'stock_quantity' => 100,
]);
$cart = CartFactory::new()
->forUser($user)
->forOption($option)
->create(['quantity' => 1]);
// 본인 쿠폰 생성 (1000원 할인)
$coupon = $this->createCoupon([
'discount_value' => 1000,
]);
$userCouponIssue = $this->createCouponIssue($coupon, $user->id);
// When: 체크아웃 생성
$createResponse = $this->actingAs($user, 'sanctum')
->postJson('/api/modules/sirsoft-ecommerce/checkout', [
'item_ids' => [$cart->id],
]);
$createResponse->assertStatus(201);
// When: 본인 쿠폰으로 체크아웃 업데이트
$response = $this->actingAs($user, 'sanctum')
->putJson('/api/modules/sirsoft-ecommerce/checkout', [
'order_coupon_issue_id' => $userCouponIssue->id,
]);
// Then: 체크아웃이 업데이트되고 쿠폰 할인이 적용됨
$response->assertStatus(200);
// 참고: 실제 할인 적용 여부는 OrderCalculationService에서 처리됨
// 여기서는 쿠폰이 무시되지 않았는지만 확인
}
/**
* 테스트 사용자에게 마일리지 잔액을 적립합니다 (원장 직접 적립).
*
* @param int $userId 사용자 ID
* @param int $amount 적립 금액
* @param string $currency 통화 코드
*/
protected function grantMileage(int $userId, int $amount, string $currency = 'KRW'): MileageTransaction
{
return MileageTransaction::create([
'user_id' => $userId,
'currency' => $currency,
'type' => MileageTransactionTypeEnum::ADMIN_EARN,
'amount' => $amount,
'remaining_amount' => $amount,
'balance_after' => $amount,
'description' => 'test grant',
'expires_at' => null,
]);
}
/**
* 테스트용 판매중 상품 + 옵션 + 회원 장바구니를 구성합니다.
*
* @param User $user 회원
* @return Cart
*/
protected function setupMemberCart($user)
{
$shippingPolicy = $this->createShippingPolicy();
$product = ProductFactory::new()->create([
'shipping_policy_id' => $shippingPolicy->id,
]);
$option = ProductOptionFactory::new()->forProduct($product)->create([
'selling_price' => 50000,
'stock_quantity' => 100,
]);
return CartFactory::new()
->forUser($user)
->forOption($option)
->create(['quantity' => 1]);
}
// ========================================
// 회원 마일리지 보유잔액 초과 사용 차단 테스트 (U15)
// ========================================
/**
* 회원이 보유 잔액을 초과하는 마일리지 사용을 시도하면 422 로 차단됩니다.
*/
public function test_member_use_points_exceeding_balance_returns_422(): void
{
// Given: 잔액 1000 회원 + 판매중 상품 장바구니
$user = $this->createUser();
$this->grantMileage($user->id, 1000);
$cart = $this->setupMemberCart($user);
// When: 보유 잔액(1000) 초과 use_points(5000) 로 체크아웃 생성
$response = $this->actingAs($user, 'sanctum')
->postJson('/api/modules/sirsoft-ecommerce/checkout', [
'item_ids' => [$cart->id],
'use_points' => 5000,
]);
// Then: 422 + code=mileage_exceeds_balance + 보유잔액 노출
$response->assertStatus(422);
$response->assertJsonPath('errors.code', 'mileage_exceeds_balance');
$this->assertStringContainsString('1000', $response->json('errors.message') ?? '');
}
/**
* update 진입점에서도 보유 잔액 초과 사용은 422 로 차단됩니다 (generic 404 아님).
*/
public function test_member_use_points_exceeding_balance_returns_422_on_update(): void
{
// Given: 잔액 1000 회원 + 체크아웃 생성됨
$user = $this->createUser();
$this->grantMileage($user->id, 1000);
$cart = $this->setupMemberCart($user);
$this->actingAs($user, 'sanctum')
->postJson('/api/modules/sirsoft-ecommerce/checkout', [
'item_ids' => [$cart->id],
])
->assertStatus(201);
// When: update 로 보유 잔액 초과 use_points 시도
$response = $this->actingAs($user, 'sanctum')
->putJson('/api/modules/sirsoft-ecommerce/checkout', [
'use_points' => 5000,
]);
// Then: 422 (generic temp_order_not_found 404 로 가로채이지 않음)
$response->assertStatus(422);
$response->assertJsonPath('errors.code', 'mileage_exceeds_balance');
}
/**
* 보유 잔액과 정확히 같은 마일리지 사용은 통과합니다 (경계).
*/
public function test_member_use_points_exactly_balance_passes(): void
{
// Given: 잔액 5000 회원
$user = $this->createUser();
$this->grantMileage($user->id, 5000);
$cart = $this->setupMemberCart($user);
// When: 잔액과 동일한 use_points(5000)
$response = $this->actingAs($user, 'sanctum')
->postJson('/api/modules/sirsoft-ecommerce/checkout', [
'item_ids' => [$cart->id],
'use_points' => 5000,
]);
// Then: 201 통과
$response->assertStatus(201);
}
/**
* 보유 잔액 미만 마일리지 사용은 통과합니다.
*/
public function test_member_use_points_below_balance_passes(): void
{
// Given: 잔액 5000 회원
$user = $this->createUser();
$this->grantMileage($user->id, 5000);
$cart = $this->setupMemberCart($user);
// When: 잔액 미만 use_points(3000)
$response = $this->actingAs($user, 'sanctum')
->postJson('/api/modules/sirsoft-ecommerce/checkout', [
'item_ids' => [$cart->id],
'use_points' => 3000,
]);
// Then: 201 통과
$response->assertStatus(201);
}
// ========================================
// 비회원 마일리지 사용 불가 테스트
// ========================================
/**
* 비회원이 마일리지를 사용하려고 하면 0으로 처리됩니다.
*/
public function test_guest_checkout_ignores_mileage(): void
{
// Given: 비회원 장바구니
$shippingPolicy = $this->createShippingPolicy();
$product = ProductFactory::new()->create([
'shipping_policy_id' => $shippingPolicy->id,
]);
$option = ProductOptionFactory::new()->forProduct($product)->create([
'selling_price' => 50000,
'stock_quantity' => 100,
]);
$cartKey = 'ck_'.str_repeat('d', 32);
$cart = CartFactory::new()
->forOption($option)
->create([
'user_id' => null,
'cart_key' => $cartKey,
'quantity' => 1,
]);
// When: 비회원이 체크아웃 생성
$createResponse = $this->postJson('/api/modules/sirsoft-ecommerce/checkout', [
'item_ids' => [$cart->id],
], [
'X-Cart-Key' => $cartKey,
]);
$createResponse->assertStatus(201);
// When: 비회원이 마일리지 사용 시도 (업데이트)
$response = $this->putJson('/api/modules/sirsoft-ecommerce/checkout', [
'use_points' => 5000, // 마일리지 사용 시도
], [
'X-Cart-Key' => $cartKey,
]);
// Then: 체크아웃 업데이트 성공하지만 마일리지는 사용되지 않음 (비회원이므로 검증 실패)
$response->assertStatus(200);
$this->assertEquals(0, $response->json('data.calculation.summary.points_used') ?? 0);
}
}