test: [orders] 테스트 인프라 정비 및 수주 API 테스트 추가

- TestCase 공통화: setUpAuthenticatedUser(), api(), assertApiSuccess(), assertApiPaginated()
- 기존 10개 테스트 파일의 중복 setUp 코드 → TestCase 상속으로 전환
- Factory 3개 추가: TenantFactory, ClientFactory, OrderFactory
- OrderApiTest 12개 테스트 신규 작성 (목록/생성/조회/수정/삭제/상태변경/인증)
- 발견: 빈 데이터로 수주 생성 가능 (FormRequest 검증 강화 필요)
This commit is contained in:
김보곤
2026-03-14 08:56:06 +09:00
parent 2e284f6393
commit c942788119
16 changed files with 501 additions and 54 deletions

View File

@@ -0,0 +1,28 @@
<?php
namespace Database\Factories;
use App\Models\Orders\Client;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Client>
*/
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,
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Database\Factories;
use App\Models\Orders\Order;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Order>
*/
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]);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Database\Factories;
use App\Models\Tenants\Tenant;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Tenant>
*/
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('###-##-#####'),
];
}
}

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -0,0 +1,268 @@
<?php
namespace Tests\Feature\Orders;
use App\Models\Orders\Client;
use App\Models\Orders\Order;
use Tests\TestCase;
class OrderApiTest extends TestCase
{
private Client $client;
protected function setUp(): void
{
parent::setUp();
$this->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,
]);
}
}

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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;
}
}