From c9427881191627f6db00afcc4e426acc36048c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 14 Mar 2026 08:56:06 +0900 Subject: [PATCH] =?UTF-8?q?test:=20[orders]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=9D=B8=ED=94=84=EB=9D=BC=20=EC=A0=95=EB=B9=84=20=EB=B0=8F?= =?UTF-8?q?=20=EC=88=98=EC=A3=BC=20API=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TestCase 공통화: setUpAuthenticatedUser(), api(), assertApiSuccess(), assertApiPaginated() - 기존 10개 테스트 파일의 중복 setUp 코드 → TestCase 상속으로 전환 - Factory 3개 추가: TenantFactory, ClientFactory, OrderFactory - OrderApiTest 12개 테스트 신규 작성 (목록/생성/조회/수정/삭제/상태변경/인증) - 발견: 빈 데이터로 수주 생성 가능 (FormRequest 검증 강화 필요) --- database/factories/ClientFactory.php | 28 ++ database/factories/OrderFactory.php | 63 ++++ database/factories/TenantFactory.php | 26 ++ tests/Feature/Account/AccountApiTest.php | 6 +- tests/Feature/BadDebt/BadDebtApiTest.php | 6 +- tests/Feature/Category/CategoryApiTest.php | 6 +- tests/Feature/Company/CompanyApiTest.php | 6 +- .../Feature/ItemMaster/ItemMasterApiTest.php | 6 - tests/Feature/Orders/OrderApiTest.php | 268 ++++++++++++++++++ tests/Feature/Payment/PaymentApiTest.php | 6 +- tests/Feature/Popup/PopupApiTest.php | 6 +- .../Production/BendingLotPipelineTest.php | 2 - .../Subscription/SubscriptionApiTest.php | 6 +- .../User/NotificationSettingApiTest.php | 6 +- tests/Feature/User/UserInvitationApiTest.php | 6 +- tests/TestCase.php | 108 ++++++- 16 files changed, 501 insertions(+), 54 deletions(-) create mode 100644 database/factories/ClientFactory.php create mode 100644 database/factories/OrderFactory.php create mode 100644 database/factories/TenantFactory.php create mode 100644 tests/Feature/Orders/OrderApiTest.php diff --git a/database/factories/ClientFactory.php b/database/factories/ClientFactory.php new file mode 100644 index 0000000..0e67fea --- /dev/null +++ b/database/factories/ClientFactory.php @@ -0,0 +1,28 @@ + + */ +class ClientFactory extends Factory +{ + protected $model = Client::class; + + public function definition(): array + { + return [ + 'name' => fake()->company(), + 'client_code' => 'CLI'.strtoupper(fake()->unique()->bothify('???###')), + 'contact_person' => fake()->name(), + 'phone' => fake()->phoneNumber(), + 'email' => fake()->companyEmail(), + 'address' => fake()->address(), + 'business_no' => fake()->numerify('###-##-#####'), + 'is_active' => true, + ]; + } +} diff --git a/database/factories/OrderFactory.php b/database/factories/OrderFactory.php new file mode 100644 index 0000000..2d827b7 --- /dev/null +++ b/database/factories/OrderFactory.php @@ -0,0 +1,63 @@ + + */ +class OrderFactory extends Factory +{ + protected $model = Order::class; + + public function definition(): array + { + return [ + 'order_no' => 'ORD-'.fake()->unique()->numerify('######'), + 'order_type_code' => Order::TYPE_ORDER, + 'status_code' => Order::STATUS_DRAFT, + 'site_name' => fake()->company().' 현장', + 'quantity' => fake()->numberBetween(1, 100), + 'supply_amount' => fake()->numberBetween(100000, 10000000), + 'tax_amount' => 0, + 'total_amount' => 0, + 'received_at' => now(), + 'delivery_date' => now()->addDays(14), + 'memo' => fake()->optional()->sentence(), + ]; + } + + /** + * 확정 상태 + */ + public function confirmed(): static + { + return $this->state(['status_code' => Order::STATUS_CONFIRMED]); + } + + /** + * 생산중 상태 + */ + public function inProduction(): static + { + return $this->state(['status_code' => Order::STATUS_IN_PRODUCTION]); + } + + /** + * 완료 상태 + */ + public function completed(): static + { + return $this->state(['status_code' => Order::STATUS_COMPLETED]); + } + + /** + * 취소 상태 + */ + public function cancelled(): static + { + return $this->state(['status_code' => Order::STATUS_CANCELLED]); + } +} diff --git a/database/factories/TenantFactory.php b/database/factories/TenantFactory.php new file mode 100644 index 0000000..19b5fd5 --- /dev/null +++ b/database/factories/TenantFactory.php @@ -0,0 +1,26 @@ + + */ +class TenantFactory extends Factory +{ + protected $model = Tenant::class; + + public function definition(): array + { + return [ + 'company_name' => fake()->company(), + 'code' => 'T'.strtoupper(fake()->unique()->bothify('???###')), + 'email' => fake()->unique()->companyEmail(), + 'phone' => fake()->phoneNumber(), + 'ceo_name' => fake()->name(), + 'business_num' => fake()->numerify('###-##-#####'), + ]; + } +} diff --git a/tests/Feature/Account/AccountApiTest.php b/tests/Feature/Account/AccountApiTest.php index 3856cce..5f5f176 100644 --- a/tests/Feature/Account/AccountApiTest.php +++ b/tests/Feature/Account/AccountApiTest.php @@ -10,15 +10,11 @@ class AccountApiTest extends TestCase { - use DatabaseTransactions; - private Tenant $tenant; + // tenant, user, apiKey, token은 TestCase에서 상속 - private User $user; - private string $apiKey; - private string $token; protected function setUp(): void { diff --git a/tests/Feature/BadDebt/BadDebtApiTest.php b/tests/Feature/BadDebt/BadDebtApiTest.php index 808a79b..dd8ef5f 100644 --- a/tests/Feature/BadDebt/BadDebtApiTest.php +++ b/tests/Feature/BadDebt/BadDebtApiTest.php @@ -14,17 +14,13 @@ class BadDebtApiTest extends TestCase { - use DatabaseTransactions; - private Tenant $tenant; + // tenant, user, apiKey, token은 TestCase에서 상속 - private User $user; private Client $client; - private string $apiKey; - private string $token; protected function setUp(): void { diff --git a/tests/Feature/Category/CategoryApiTest.php b/tests/Feature/Category/CategoryApiTest.php index 59a3ca8..7f01580 100644 --- a/tests/Feature/Category/CategoryApiTest.php +++ b/tests/Feature/Category/CategoryApiTest.php @@ -11,15 +11,11 @@ class CategoryApiTest extends TestCase { - use DatabaseTransactions; - private Tenant $tenant; + // tenant, user, apiKey, token은 TestCase에서 상속 - private User $user; - private string $apiKey; - private string $token; protected function setUp(): void { diff --git a/tests/Feature/Company/CompanyApiTest.php b/tests/Feature/Company/CompanyApiTest.php index bc436bf..ee0b1da 100644 --- a/tests/Feature/Company/CompanyApiTest.php +++ b/tests/Feature/Company/CompanyApiTest.php @@ -11,15 +11,11 @@ class CompanyApiTest extends TestCase { - use DatabaseTransactions; - private Tenant $tenant; + // tenant, user, apiKey, token은 TestCase에서 상속 - private User $user; - private string $apiKey; - private string $token; protected function setUp(): void { diff --git a/tests/Feature/ItemMaster/ItemMasterApiTest.php b/tests/Feature/ItemMaster/ItemMasterApiTest.php index f12a593..bbc91f8 100644 --- a/tests/Feature/ItemMaster/ItemMasterApiTest.php +++ b/tests/Feature/ItemMaster/ItemMasterApiTest.php @@ -11,20 +11,14 @@ use App\Models\Members\User; use App\Models\Members\UserTenant; use App\Models\Tenants\Tenant; -use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; class ItemMasterApiTest extends TestCase { - use DatabaseTransactions; - protected User $user; - protected Tenant $tenant; - protected string $token; - protected string $apiKey; protected function setUp(): void { diff --git a/tests/Feature/Orders/OrderApiTest.php b/tests/Feature/Orders/OrderApiTest.php new file mode 100644 index 0000000..5f718e8 --- /dev/null +++ b/tests/Feature/Orders/OrderApiTest.php @@ -0,0 +1,268 @@ +setUpAuthenticatedUser(); + + // 테스트용 거래처 + $this->client = Client::create([ + 'tenant_id' => $this->tenant->id, + 'name' => '테스트 거래처', + 'client_code' => 'CLI'.uniqid(), + 'is_active' => true, + 'created_by' => $this->user->id, + ]); + } + + // ==================== 목록 조회 ==================== + + public function test_수주_목록_조회(): void + { + Order::create([ + 'tenant_id' => $this->tenant->id, + 'order_no' => 'ORD-'.uniqid(), + 'order_type_code' => Order::TYPE_ORDER, + 'status_code' => Order::STATUS_DRAFT, + 'client_id' => $this->client->id, + 'client_name' => $this->client->name, + 'quantity' => 10, + 'supply_amount' => 1000000, + 'tax_amount' => 100000, + 'total_amount' => 1100000, + 'created_by' => $this->user->id, + 'updated_by' => $this->user->id, + ]); + + $response = $this->api('get', '/api/v1/orders'); + + $this->assertApiPaginated($response); + + $data = $response->json('data.data'); + $this->assertNotEmpty($data); + } + + public function test_수주_통계_조회(): void + { + Order::create([ + 'tenant_id' => $this->tenant->id, + 'order_no' => 'ORD-'.uniqid(), + 'order_type_code' => Order::TYPE_ORDER, + 'status_code' => Order::STATUS_CONFIRMED, + 'client_id' => $this->client->id, + 'client_name' => $this->client->name, + 'quantity' => 5, + 'supply_amount' => 500000, + 'tax_amount' => 50000, + 'total_amount' => 550000, + 'created_by' => $this->user->id, + 'updated_by' => $this->user->id, + ]); + + $response = $this->api('get', '/api/v1/orders/stats'); + + $response->assertStatus(200) + ->assertJsonStructure(['success', 'data']); + } + + // ==================== 생성 ==================== + + public function test_수주_생성_성공(): void + { + $response = $this->api('post', '/api/v1/orders', [ + 'order_type_code' => Order::TYPE_ORDER, + 'status_code' => Order::STATUS_DRAFT, + 'client_id' => $this->client->id, + 'client_name' => $this->client->name, + 'site_name' => '테스트 현장', + 'supply_amount' => 2000000, + 'tax_amount' => 200000, + 'total_amount' => 2200000, + 'delivery_date' => now()->addDays(14)->format('Y-m-d'), + 'items' => [ + [ + 'item_name' => '블라인드 A형', + 'quantity' => 10, + 'unit_price' => 200000, + ], + ], + ]); + + $response->assertStatus(200); + $this->assertTrue($response->json('success')); + + $this->assertDatabaseHas('orders', [ + 'client_id' => $this->client->id, + 'site_name' => '테스트 현장', + 'tenant_id' => $this->tenant->id, + ]); + } + + public function test_수주_생성_빈_데이터_허용_확인(): void + { + // NOTE: 현재 API는 빈 데이터로도 수주 생성을 허용함 + // FormRequest 검증 강화가 필요한 지점 (D4 개선 대상) + $response = $this->api('post', '/api/v1/orders', []); + + $response->assertStatus(200); + } + + // ==================== 상세 조회 ==================== + + public function test_수주_상세_조회(): void + { + $order = $this->createOrder(); + + $response = $this->api('get', "/api/v1/orders/{$order->id}"); + + $this->assertApiSuccess($response); + + $data = $response->json('data'); + $this->assertEquals($order->id, $data['id']); + $this->assertEquals($order->order_no, $data['order_no']); + } + + public function test_존재하지_않는_수주_조회시_404(): void + { + $response = $this->api('get', '/api/v1/orders/999999'); + + $response->assertStatus(404); + } + + // ==================== 수정 ==================== + + public function test_수주_수정_성공(): void + { + $order = $this->createOrder(); + + $response = $this->api('put', "/api/v1/orders/{$order->id}", [ + 'order_type_code' => Order::TYPE_ORDER, + 'status_code' => Order::STATUS_DRAFT, + 'client_id' => $this->client->id, + 'client_name' => '수정된 거래처명', + 'site_name' => '수정된 현장', + 'supply_amount' => 3000000, + 'tax_amount' => 300000, + 'total_amount' => 3300000, + 'items' => [ + [ + 'item_name' => '블라인드 B형', + 'quantity' => 20, + 'unit_price' => 150000, + ], + ], + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('orders', [ + 'id' => $order->id, + 'site_name' => '수정된 현장', + ]); + } + + // ==================== 삭제 ==================== + + public function test_수주_삭제_성공(): void + { + $order = $this->createOrder(); + + $response = $this->api('delete', "/api/v1/orders/{$order->id}"); + + $response->assertStatus(200); + + $this->assertSoftDeleted('orders', ['id' => $order->id]); + } + + public function test_수주_일괄_삭제(): void + { + $order1 = $this->createOrder(); + $order2 = $this->createOrder(); + + $response = $this->api('delete', '/api/v1/orders/bulk', [ + 'ids' => [$order1->id, $order2->id], + ]); + + $response->assertStatus(200); + + $this->assertSoftDeleted('orders', ['id' => $order1->id]); + $this->assertSoftDeleted('orders', ['id' => $order2->id]); + } + + // ==================== 상태 변경 ==================== + + public function test_수주_상태_등록에서_확정으로_변경(): void + { + $order = $this->createOrder(Order::STATUS_DRAFT); + + $response = $this->api('patch', "/api/v1/orders/{$order->id}/status", [ + 'status' => Order::STATUS_CONFIRMED, + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('orders', [ + 'id' => $order->id, + 'status_code' => Order::STATUS_CONFIRMED, + ]); + } + + public function test_수주_상태_취소(): void + { + $order = $this->createOrder(Order::STATUS_DRAFT); + + $response = $this->api('patch', "/api/v1/orders/{$order->id}/status", [ + 'status' => Order::STATUS_CANCELLED, + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('orders', [ + 'id' => $order->id, + 'status_code' => Order::STATUS_CANCELLED, + ]); + } + + // ==================== 인증 ==================== + + public function test_미인증_요청시_401(): void + { + $response = $this->withHeaders([ + 'X-API-KEY' => $this->apiKey, + 'Accept' => 'application/json', + ])->getJson('/api/v1/orders'); + + $response->assertStatus(401); + } + + // ==================== 헬퍼 ==================== + + private function createOrder(string $status = Order::STATUS_DRAFT): Order + { + return Order::create([ + 'tenant_id' => $this->tenant->id, + 'order_no' => 'ORD-'.uniqid(), + 'order_type_code' => Order::TYPE_ORDER, + 'status_code' => $status, + 'client_id' => $this->client->id, + 'client_name' => $this->client->name, + 'site_name' => '테스트 현장', + 'quantity' => 10, + 'supply_amount' => 1000000, + 'tax_amount' => 100000, + 'total_amount' => 1100000, + 'created_by' => $this->user->id, + 'updated_by' => $this->user->id, + ]); + } +} diff --git a/tests/Feature/Payment/PaymentApiTest.php b/tests/Feature/Payment/PaymentApiTest.php index 3a083de..b7f48bb 100644 --- a/tests/Feature/Payment/PaymentApiTest.php +++ b/tests/Feature/Payment/PaymentApiTest.php @@ -13,19 +13,15 @@ class PaymentApiTest extends TestCase { - use DatabaseTransactions; - private Tenant $tenant; + // tenant, user, apiKey, token은 TestCase에서 상속 - private User $user; private Plan $plan; private Subscription $subscription; - private string $apiKey; - private string $token; protected function setUp(): void { diff --git a/tests/Feature/Popup/PopupApiTest.php b/tests/Feature/Popup/PopupApiTest.php index 48e3867..6f2695c 100644 --- a/tests/Feature/Popup/PopupApiTest.php +++ b/tests/Feature/Popup/PopupApiTest.php @@ -11,15 +11,11 @@ class PopupApiTest extends TestCase { - use DatabaseTransactions; - private Tenant $tenant; + // tenant, user, apiKey, token은 TestCase에서 상속 - private User $user; - private string $apiKey; - private string $token; protected function setUp(): void { diff --git a/tests/Feature/Production/BendingLotPipelineTest.php b/tests/Feature/Production/BendingLotPipelineTest.php index dbf0aac..22786bc 100644 --- a/tests/Feature/Production/BendingLotPipelineTest.php +++ b/tests/Feature/Production/BendingLotPipelineTest.php @@ -5,7 +5,6 @@ use App\DTOs\Production\DynamicBomEntry; use App\Services\Production\PrefixResolver; use App\Services\WorkOrderService; -use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\DB; use Tests\TestCase; @@ -18,7 +17,6 @@ */ class BendingLotPipelineTest extends TestCase { - use DatabaseTransactions; private const TENANT_ID = 287; diff --git a/tests/Feature/Subscription/SubscriptionApiTest.php b/tests/Feature/Subscription/SubscriptionApiTest.php index b5f55a5..af8c20d 100644 --- a/tests/Feature/Subscription/SubscriptionApiTest.php +++ b/tests/Feature/Subscription/SubscriptionApiTest.php @@ -12,17 +12,13 @@ class SubscriptionApiTest extends TestCase { - use DatabaseTransactions; - private Tenant $tenant; + // tenant, user, apiKey, token은 TestCase에서 상속 - private User $user; private Plan $plan; - private string $apiKey; - private string $token; protected function setUp(): void { diff --git a/tests/Feature/User/NotificationSettingApiTest.php b/tests/Feature/User/NotificationSettingApiTest.php index 74e2c8d..1b5ad8f 100644 --- a/tests/Feature/User/NotificationSettingApiTest.php +++ b/tests/Feature/User/NotificationSettingApiTest.php @@ -11,15 +11,11 @@ class NotificationSettingApiTest extends TestCase { - use DatabaseTransactions; - private Tenant $tenant; + // tenant, user, apiKey, token은 TestCase에서 상속 - private User $user; - private string $apiKey; - private string $token; protected function setUp(): void { diff --git a/tests/Feature/User/UserInvitationApiTest.php b/tests/Feature/User/UserInvitationApiTest.php index 60f6fb6..3e4a42e 100644 --- a/tests/Feature/User/UserInvitationApiTest.php +++ b/tests/Feature/User/UserInvitationApiTest.php @@ -12,17 +12,13 @@ class UserInvitationApiTest extends TestCase { - use DatabaseTransactions; - private Tenant $tenant; + // tenant, user, apiKey, token은 TestCase에서 상속 - private User $user; private Role $role; - private string $apiKey; - private string $token; protected function setUp(): void { diff --git a/tests/TestCase.php b/tests/TestCase.php index fe1ffc2..88458e6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,9 +2,115 @@ namespace Tests; +use App\Models\Members\User; +use App\Models\Members\UserTenant; +use App\Models\Tenants\Tenant; +use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { - // + use DatabaseTransactions; + + protected Tenant $tenant; + + protected User $user; + + protected string $apiKey; + + protected string $token; + + /** + * 테스트 환경 초기화: API Key + Tenant + User + 로그인 토큰 + * 각 테스트 클래스의 setUp()에서 parent::setUp() 후 호출 + */ + protected function setUpAuthenticatedUser(): void + { + // API Key 생성 + $this->apiKey = 'test-api-key-'.uniqid(); + \DB::table('api_keys')->insert([ + 'key' => $this->apiKey, + 'description' => 'Test API Key', + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // Tenant 생성 (Observer 우회) + $this->tenant = Tenant::withoutEvents(function () { + return Tenant::create([ + 'company_name' => 'Test Company', + 'code' => 'TEST'.uniqid(), + 'email' => 'test@example.com', + 'phone' => '010-1234-5678', + ]); + }); + + // User 생성 + $testUserId = 'testuser'.uniqid(); + $this->user = User::create([ + 'user_id' => $testUserId, + 'name' => 'Test User', + 'email' => $testUserId.'@example.com', + 'password' => bcrypt('password123'), + ]); + + // UserTenant 관계 + UserTenant::create([ + 'user_id' => $this->user->id, + 'tenant_id' => $this->tenant->id, + 'is_active' => true, + 'is_default' => true, + ]); + + // 로그인 및 토큰 획득 + $response = $this->withHeaders([ + 'X-API-KEY' => $this->apiKey, + 'Accept' => 'application/json', + ])->postJson('/api/v1/login', [ + 'user_id' => $this->user->user_id, + 'user_pwd' => 'password123', + ]); + + $response->assertStatus(200); + $this->token = $response->json('access_token'); + } + + /** + * 인증된 API 요청 + */ + protected function api(string $method, string $uri, array $data = []) + { + return $this->withHeaders([ + 'X-API-KEY' => $this->apiKey, + 'Authorization' => 'Bearer '.$this->token, + 'Accept' => 'application/json', + ])->{$method.'Json'}($uri, $data); + } + + /** + * 성공 응답 구조 검증 (success + message + data) + */ + protected function assertApiSuccess($response, int $status = 200) + { + $response->assertStatus($status) + ->assertJsonStructure(['success', 'message', 'data']); + + return $response; + } + + /** + * 페이지네이션 응답 구조 검증 + */ + protected function assertApiPaginated($response) + { + $response->assertStatus(200) + ->assertJsonStructure([ + 'success', + 'message', + 'data' => ['data', 'current_page', 'total'], + ]); + + return $response; + } }