Multi-tenant factory inspection system (SpiFox, Enkid, Alpet): - FastAPI backend with JWT auth, PostgreSQL (asyncpg) - Next.js 16 frontend with App Router, SWR data fetching - Machines CRUD with equipment parts management - Part lifecycle tracking (hours/count/date) with counters - Partial unique index for soft-delete support - 24 pytest tests passing, E2E verified Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
226 lines
6.8 KiB
Python
226 lines
6.8 KiB
Python
import pytest
|
|
from httpx import AsyncClient
|
|
from tests.conftest import get_auth_headers
|
|
|
|
|
|
async def _create_machine(
|
|
client: AsyncClient, headers: dict, tenant_id: str = "test-co"
|
|
) -> str:
|
|
resp = await client.post(
|
|
f"/api/{tenant_id}/machines",
|
|
json={"name": "테스트 설비", "equipment_code": "T-001"},
|
|
headers=headers,
|
|
)
|
|
return resp.json()["id"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_part(client: AsyncClient, seeded_db):
|
|
headers = await get_auth_headers(client)
|
|
machine_id = await _create_machine(client, headers)
|
|
|
|
resp = await client.post(
|
|
f"/api/test-co/machines/{machine_id}/parts",
|
|
json={
|
|
"name": "상부 금형",
|
|
"category": "mold",
|
|
"lifecycle_type": "count",
|
|
"lifecycle_limit": 10000,
|
|
"alarm_threshold": 90.0,
|
|
"counter_source": "auto_plc",
|
|
"installed_at": "2026-02-01T00:00:00+09:00",
|
|
},
|
|
headers=headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["name"] == "상부 금형"
|
|
assert data["lifecycle_type"] == "count"
|
|
assert data["lifecycle_limit"] == 10000
|
|
assert data["counter"]["current_value"] == 0
|
|
assert data["counter"]["lifecycle_pct"] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_part_auto_creates_counter(client: AsyncClient, seeded_db):
|
|
headers = await get_auth_headers(client)
|
|
machine_id = await _create_machine(client, headers)
|
|
|
|
resp = await client.post(
|
|
f"/api/test-co/machines/{machine_id}/parts",
|
|
json={
|
|
"name": "히터",
|
|
"lifecycle_type": "hours",
|
|
"lifecycle_limit": 5000,
|
|
"installed_at": "2026-01-15T00:00:00Z",
|
|
},
|
|
headers=headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "counter" in data
|
|
assert data["counter"]["current_value"] == 0
|
|
assert data["counter"]["version"] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_duplicate_part_name_rejected(client: AsyncClient, seeded_db):
|
|
headers = await get_auth_headers(client)
|
|
machine_id = await _create_machine(client, headers)
|
|
|
|
part_data = {
|
|
"name": "동일 부품",
|
|
"lifecycle_type": "count",
|
|
"lifecycle_limit": 1000,
|
|
"installed_at": "2026-02-01T00:00:00Z",
|
|
}
|
|
|
|
resp1 = await client.post(
|
|
f"/api/test-co/machines/{machine_id}/parts", json=part_data, headers=headers
|
|
)
|
|
assert resp1.status_code == 200
|
|
|
|
resp2 = await client.post(
|
|
f"/api/test-co/machines/{machine_id}/parts", json=part_data, headers=headers
|
|
)
|
|
assert resp2.status_code == 409
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_parts(client: AsyncClient, seeded_db):
|
|
headers = await get_auth_headers(client)
|
|
machine_id = await _create_machine(client, headers)
|
|
|
|
for name in ["부품A", "부품B", "부품C"]:
|
|
await client.post(
|
|
f"/api/test-co/machines/{machine_id}/parts",
|
|
json={
|
|
"name": name,
|
|
"lifecycle_type": "count",
|
|
"lifecycle_limit": 1000,
|
|
"installed_at": "2026-02-01T00:00:00Z",
|
|
},
|
|
headers=headers,
|
|
)
|
|
|
|
resp = await client.get(
|
|
f"/api/test-co/machines/{machine_id}/parts", headers=headers
|
|
)
|
|
assert resp.status_code == 200
|
|
assert len(resp.json()["parts"]) == 3
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_part_detail(client: AsyncClient, seeded_db):
|
|
headers = await get_auth_headers(client)
|
|
machine_id = await _create_machine(client, headers)
|
|
|
|
create_resp = await client.post(
|
|
f"/api/test-co/machines/{machine_id}/parts",
|
|
json={
|
|
"name": "센서",
|
|
"lifecycle_type": "hours",
|
|
"lifecycle_limit": 8000,
|
|
"installed_at": "2026-02-01T00:00:00Z",
|
|
},
|
|
headers=headers,
|
|
)
|
|
part_id = create_resp.json()["id"]
|
|
|
|
resp = await client.get(f"/api/test-co/parts/{part_id}", headers=headers)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["name"] == "센서"
|
|
assert resp.json()["lifecycle_limit"] == 8000
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_part(client: AsyncClient, seeded_db):
|
|
headers = await get_auth_headers(client)
|
|
machine_id = await _create_machine(client, headers)
|
|
|
|
create_resp = await client.post(
|
|
f"/api/test-co/machines/{machine_id}/parts",
|
|
json={
|
|
"name": "금형",
|
|
"lifecycle_type": "count",
|
|
"lifecycle_limit": 5000,
|
|
"installed_at": "2026-02-01T00:00:00Z",
|
|
},
|
|
headers=headers,
|
|
)
|
|
part_id = create_resp.json()["id"]
|
|
|
|
resp = await client.put(
|
|
f"/api/test-co/parts/{part_id}",
|
|
json={"name": "상부 금형", "lifecycle_limit": 8000},
|
|
headers=headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["name"] == "상부 금형"
|
|
assert resp.json()["lifecycle_limit"] == 8000
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_part(client: AsyncClient, seeded_db):
|
|
headers = await get_auth_headers(client)
|
|
machine_id = await _create_machine(client, headers)
|
|
|
|
create_resp = await client.post(
|
|
f"/api/test-co/machines/{machine_id}/parts",
|
|
json={
|
|
"name": "삭제 부품",
|
|
"lifecycle_type": "date",
|
|
"lifecycle_limit": 30,
|
|
"installed_at": "2026-02-01T00:00:00Z",
|
|
},
|
|
headers=headers,
|
|
)
|
|
part_id = create_resp.json()["id"]
|
|
|
|
resp = await client.delete(f"/api/test-co/parts/{part_id}", headers=headers)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["status"] == "success"
|
|
|
|
list_resp = await client.get(
|
|
f"/api/test-co/machines/{machine_id}/parts", headers=headers
|
|
)
|
|
assert len(list_resp.json()["parts"]) == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_lifecycle_type(client: AsyncClient, seeded_db):
|
|
headers = await get_auth_headers(client)
|
|
machine_id = await _create_machine(client, headers)
|
|
|
|
resp = await client.post(
|
|
f"/api/test-co/machines/{machine_id}/parts",
|
|
json={
|
|
"name": "잘못된 부품",
|
|
"lifecycle_type": "invalid",
|
|
"lifecycle_limit": 100,
|
|
"installed_at": "2026-02-01T00:00:00Z",
|
|
},
|
|
headers=headers,
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_machine_with_parts_rejected(client: AsyncClient, seeded_db):
|
|
headers = await get_auth_headers(client)
|
|
machine_id = await _create_machine(client, headers)
|
|
|
|
await client.post(
|
|
f"/api/test-co/machines/{machine_id}/parts",
|
|
json={
|
|
"name": "부품",
|
|
"lifecycle_type": "count",
|
|
"lifecycle_limit": 1000,
|
|
"installed_at": "2026-02-01T00:00:00Z",
|
|
},
|
|
headers=headers,
|
|
)
|
|
|
|
resp = await client.delete(f"/api/test-co/machines/{machine_id}", headers=headers)
|
|
assert resp.status_code == 409
|