Files
sam-api/tests/Feature/ParameterBasedBomApiTest.php
kent bf8036a64b feat: DB 연결 오버라이딩 및 대시보드 통계 위젯 추가
- DB 연결: 로컬/Docker 환경 오버라이딩 설정 (.env)
- 테넌트 위젯: redirect 버그 수정 (TenantSelectorWidget)
- 통계 위젯: 사용자/제품/자재/주문 카드 추가 (StatsOverviewWidget)
- 리소스 한국어화: Product, Material 모델 레이블 추가
- 대시보드: 위젯 등록 및 캐시 최적화

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 23:31:14 +09:00

606 lines
19 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\BomConditionRule;
use App\Models\Model;
use App\Models\ModelFormula;
use App\Models\ModelParameter;
use App\Models\User;
use Database\Seeders\ParameterBasedBomTestSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class ParameterBasedBomApiTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Model $kss01Model;
protected function setUp(): void
{
parent::setUp();
// Set up test environment
$this->artisan('migrate');
$this->seed(ParameterBasedBomTestSeeder::class);
// Create test user and authenticate
$this->user = User::factory()->create(['tenant_id' => 1]);
Sanctum::actingAs($this->user);
// Set required headers
$this->withHeaders([
'X-API-KEY' => config('app.api_key', 'test-api-key'),
'Accept' => 'application/json',
'Content-Type' => 'application/json',
]);
$this->kss01Model = Model::where('code', 'KSS01')->first();
}
/** @test */
public function it_can_get_model_parameters()
{
// Act
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters");
// Assert
$response->assertOk()
->assertJsonStructure([
'success',
'message',
'data' => [
'*' => [
'id',
'name',
'label',
'type',
'default_value',
'validation_rules',
'options',
'sort_order',
'is_required',
'is_active',
]
]
]);
$parameters = $response->json('data');
$this->assertCount(5, $parameters); // W0, H0, screen_type, install_type, power_source
// Check parameter order
$this->assertEquals('W0', $parameters[0]['name']);
$this->assertEquals('H0', $parameters[1]['name']);
$this->assertEquals('screen_type', $parameters[2]['name']);
}
/** @test */
public function it_can_get_model_formulas()
{
// Act
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/formulas");
// Assert
$response->assertOk()
->assertJsonStructure([
'success',
'message',
'data' => [
'*' => [
'id',
'name',
'expression',
'description',
'return_type',
'sort_order',
'is_active',
]
]
]);
$formulas = $response->json('data');
$this->assertGreaterThanOrEqual(7, count($formulas)); // W1, H1, area, weight, motor, bracket, guide
// Check formula order
$this->assertEquals('W1', $formulas[0]['name']);
$this->assertEquals('H1', $formulas[1]['name']);
$this->assertEquals('area', $formulas[2]['name']);
}
/** @test */
public function it_can_get_model_condition_rules()
{
// Act
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/rules");
// Assert
$response->assertOk()
->assertJsonStructure([
'success',
'message',
'data' => [
'*' => [
'id',
'name',
'description',
'condition_expression',
'component_code',
'quantity_expression',
'priority',
'is_active',
]
]
]);
$rules = $response->json('data');
$this->assertGreaterThanOrEqual(7, count($rules)); // Various case, bottom, shaft, pipe rules
// Check rules are ordered by priority
$priorities = collect($rules)->pluck('priority');
$this->assertEquals($priorities->sort()->values(), $priorities->values());
}
/** @test */
public function it_can_resolve_bom_for_small_screen()
{
// Arrange
$inputParams = [
'W0' => 1000,
'H0' => 800,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $inputParams
]);
// Assert
$response->assertOk()
->assertJsonStructure([
'success',
'message',
'data' => [
'model_id',
'input_parameters',
'calculated_formulas' => [
'W1',
'H1',
'area',
'weight',
'motor',
'bracket',
'guide',
],
'bom_items' => [
'*' => [
'component_code',
'quantity',
'rule_name',
'condition_expression',
]
],
'summary' => [
'total_components',
'total_weight',
'component_categories',
]
]
]);
$data = $response->json('data');
// Check calculated formulas
$formulas = $data['calculated_formulas'];
$this->assertEquals(1120, $formulas['W1']); // 1000 + 120
$this->assertEquals(900, $formulas['H1']); // 800 + 100
$this->assertEquals(1.008, $formulas['area']); // 1120 * 900 / 1000000
$this->assertEquals('0.5HP', $formulas['motor']); // area <= 3
// Check BOM items
$bomItems = $data['bom_items'];
$this->assertGreaterThan(0, count($bomItems));
// Check specific components
$caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE'));
$this->assertEquals('CASE-SMALL', $caseItem['component_code']);
$screenPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SCREEN');
$this->assertNotNull($screenPipe);
$slatPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SLAT');
$this->assertNull($slatPipe);
}
/** @test */
public function it_can_resolve_bom_for_large_screen()
{
// Arrange
$inputParams = [
'W0' => 2500,
'H0' => 1500,
'screen_type' => 'SCREEN',
'install_type' => 'SIDE',
'power_source' => 'AC',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $inputParams
]);
// Assert
$response->assertOk();
$data = $response->json('data');
$formulas = $data['calculated_formulas'];
// Check large screen calculations
$this->assertEquals(2620, $formulas['W1']); // 2500 + 120
$this->assertEquals(1600, $formulas['H1']); // 1500 + 100
$this->assertEquals(4.192, $formulas['area']); // 2620 * 1600 / 1000000
$this->assertEquals('1HP', $formulas['motor']); // 3 < area <= 6
// Check medium case is selected
$bomItems = $data['bom_items'];
$caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE'));
$this->assertEquals('CASE-MEDIUM', $caseItem['component_code']);
// Check bracket quantity
$bottomItem = collect($bomItems)->first(fn($item) => $item['component_code'] === 'BOTTOM-001');
$this->assertEquals(3, $bottomItem['quantity']); // CEIL(2620 / 1000)
}
/** @test */
public function it_can_resolve_bom_for_slat_type()
{
// Arrange
$inputParams = [
'W0' => 1500,
'H0' => 1000,
'screen_type' => 'SLAT',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $inputParams
]);
// Assert
$response->assertOk();
$bomItems = $response->json('data.bom_items');
// Check that SLAT pipe is used
$screenPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SCREEN');
$slatPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SLAT');
$this->assertNull($screenPipe);
$this->assertNotNull($slatPipe);
$this->assertEquals('PIPE-SLAT', $slatPipe['component_code']);
}
/** @test */
public function it_validates_missing_required_parameters()
{
// Arrange - Missing H0 parameter
$incompleteParams = [
'W0' => 1000,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $incompleteParams
]);
// Assert
$response->assertStatus(422)
->assertJsonValidationErrors(['parameters.H0']);
}
/** @test */
public function it_validates_parameter_ranges()
{
// Arrange - W0 below minimum
$invalidParams = [
'W0' => 100, // Below minimum (500)
'H0' => 800,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $invalidParams
]);
// Assert
$response->assertStatus(422)
->assertJsonValidationErrors(['parameters.W0']);
}
/** @test */
public function it_validates_select_parameter_options()
{
// Arrange - Invalid screen_type
$invalidParams = [
'W0' => 1000,
'H0' => 800,
'screen_type' => 'INVALID_TYPE',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $invalidParams
]);
// Assert
$response->assertStatus(422)
->assertJsonValidationErrors(['parameters.screen_type']);
}
/** @test */
public function it_can_create_product_from_model()
{
// Arrange
$inputParams = [
'W0' => 1200,
'H0' => 900,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
$productData = [
'code' => 'KSS01-TEST-001',
'name' => '테스트 스크린 블라인드 1200x900',
'description' => 'API 테스트로 생성된 제품',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/products", [
'parameters' => $inputParams,
'product' => $productData,
]);
// Assert
$response->assertCreated()
->assertJsonStructure([
'success',
'message',
'data' => [
'product' => [
'id',
'code',
'name',
'description',
'product_type',
'model_id',
'parameter_values',
],
'bom_items' => [
'*' => [
'id',
'product_id',
'component_code',
'component_type',
'quantity',
'unit',
]
],
'summary' => [
'total_components',
'calculated_formulas',
]
]
]);
$data = $response->json('data');
$product = $data['product'];
// Check product data
$this->assertEquals('KSS01-TEST-001', $product['code']);
$this->assertEquals('테스트 스크린 블라인드 1200x900', $product['name']);
$this->assertEquals($this->kss01Model->id, $product['model_id']);
// Check parameter values are stored
$parameterValues = json_decode($product['parameter_values'], true);
$this->assertEquals(1200, $parameterValues['W0']);
$this->assertEquals(900, $parameterValues['H0']);
// Check BOM items
$bomItems = $data['bom_items'];
$this->assertGreaterThan(0, count($bomItems));
foreach ($bomItems as $item) {
$this->assertEquals($product['id'], $item['product_id']);
$this->assertNotEmpty($item['component_code']);
$this->assertGreaterThan(0, $item['quantity']);
}
}
/** @test */
public function it_can_preview_product_without_creating()
{
// Arrange
$inputParams = [
'W0' => 1800,
'H0' => 1200,
'screen_type' => 'SCREEN',
'install_type' => 'SIDE',
'power_source' => 'DC',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/preview", [
'parameters' => $inputParams
]);
// Assert
$response->assertOk()
->assertJsonStructure([
'success',
'message',
'data' => [
'suggested_product' => [
'code',
'name',
'description',
],
'calculated_values' => [
'W1',
'H1',
'area',
'weight',
'motor',
],
'bom_preview' => [
'total_components',
'components' => [
'*' => [
'component_code',
'quantity',
'description',
]
]
],
'cost_estimate' => [
'total_material_cost',
'estimated_labor_cost',
'total_estimated_cost',
]
]
]);
$data = $response->json('data');
// Check suggested product info
$suggestedProduct = $data['suggested_product'];
$this->assertStringContains('KSS01', $suggestedProduct['code']);
$this->assertStringContains('1800x1200', $suggestedProduct['name']);
// Check calculated values
$calculatedValues = $data['calculated_values'];
$this->assertEquals(1920, $calculatedValues['W1']); // 1800 + 120
$this->assertEquals(1300, $calculatedValues['H1']); // 1200 + 100
// Check BOM preview
$bomPreview = $data['bom_preview'];
$this->assertGreaterThan(0, $bomPreview['total_components']);
$this->assertNotEmpty($bomPreview['components']);
}
/** @test */
public function it_handles_authentication_properly()
{
// Arrange - Remove authentication
Sanctum::actingAs(null);
// Act
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters");
// Assert
$response->assertUnauthorized();
}
/** @test */
public function it_handles_tenant_isolation()
{
// Arrange - Create user with different tenant
$otherTenantUser = User::factory()->create(['tenant_id' => 2]);
Sanctum::actingAs($otherTenantUser);
// Act
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters");
// Assert
$response->assertNotFound();
}
/** @test */
public function it_handles_performance_for_large_datasets()
{
// Arrange - Use performance test model with many parameters/formulas/rules
$performanceModel = Model::where('code', 'PERF-TEST')->first();
if (!$performanceModel) {
$this->markTestSkipped('Performance test model not found. Run in testing environment.');
}
$inputParams = array_fill_keys(
ModelParameter::where('model_id', $performanceModel->id)->pluck('name')->toArray(),
100
);
// Act
$startTime = microtime(true);
$response = $this->postJson("/api/v1/design/models/{$performanceModel->id}/bom/resolve", [
'parameters' => $inputParams
]);
$executionTime = microtime(true) - $startTime;
// Assert
$response->assertOk();
$this->assertLessThan(2.0, $executionTime, 'BOM resolution should complete within 2 seconds');
$data = $response->json('data');
$this->assertArrayHasKey('calculated_formulas', $data);
$this->assertArrayHasKey('bom_items', $data);
}
/** @test */
public function it_returns_appropriate_error_for_nonexistent_model()
{
// Act
$response = $this->getJson('/api/v1/design/models/999999/parameters');
// Assert
$response->assertNotFound()
->assertJson([
'success' => false,
'message' => 'error.model_not_found',
]);
}
/** @test */
public function it_handles_concurrent_requests_safely()
{
// Arrange
$inputParams = [
'W0' => 1000,
'H0' => 800,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// Act - Simulate concurrent requests
$promises = [];
for ($i = 0; $i < 5; $i++) {
$promises[] = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $inputParams
]);
}
// Assert - All should succeed with same results
foreach ($promises as $response) {
$response->assertOk();
$formulas = $response->json('data.calculated_formulas');
$this->assertEquals(1120, $formulas['W1']);
$this->assertEquals(900, $formulas['H1']);
}
}
}