All checks were successful
Deploy to Production / deploy (push) Successful in 49s
- Seed: 9 machines, 24 parts with counters, 6 inspection templates (per tenant) - Realistic industry data: semiconductor (SpiFox), manufacturing (Enkid), chemical (Alpet) - E2E tests: full lifecycle flow, tenant isolation, spec-based pass/fail validation - 70/70 tests passing Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
354 lines
11 KiB
Python
354 lines
11 KiB
Python
import pytest
|
|
from httpx import AsyncClient
|
|
from tests.conftest import get_auth_headers
|
|
|
|
|
|
async def _login(
|
|
client: AsyncClient, email: str = "super@test.com", pw: str = "pass1234"
|
|
):
|
|
return await get_auth_headers(client, email, pw)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_lifecycle_flow(client: AsyncClient, seeded_db):
|
|
h = await _login(client)
|
|
tid = "test-co"
|
|
|
|
r = await client.post(
|
|
f"/api/{tid}/machines",
|
|
json={
|
|
"name": "E2E 통합 설비",
|
|
"equipment_code": "E2E-001",
|
|
"model": "Test Model X",
|
|
},
|
|
headers=h,
|
|
)
|
|
assert r.status_code == 200
|
|
machine = r.json()
|
|
machine_id = machine["id"]
|
|
assert machine["name"] == "E2E 통합 설비"
|
|
|
|
r = await client.post(
|
|
f"/api/{tid}/machines/{machine_id}/parts",
|
|
json={
|
|
"name": "E2E 부품",
|
|
"part_number": "E2E-P-001",
|
|
"category": "소모품",
|
|
"lifecycle_type": "count",
|
|
"lifecycle_limit": 1000,
|
|
"alarm_threshold": 80,
|
|
"counter_source": "manual",
|
|
},
|
|
headers=h,
|
|
)
|
|
assert r.status_code == 200
|
|
part = r.json()
|
|
part_id = part["id"]
|
|
assert part["counter"]["current_value"] == 0
|
|
|
|
r = await client.put(
|
|
f"/api/{tid}/parts/{part_id}/counter", json={"value": 500}, headers=h
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["counter"]["current_value"] == 500
|
|
assert r.json()["counter"]["lifecycle_pct"] == 50.0
|
|
|
|
r = await client.get(f"/api/{tid}/alarms?is_acknowledged=false", headers=h)
|
|
assert r.status_code == 200
|
|
assert len(r.json()["alarms"]) == 0
|
|
|
|
r = await client.put(
|
|
f"/api/{tid}/parts/{part_id}/counter", json={"value": 850}, headers=h
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["counter"]["lifecycle_pct"] == 85.0
|
|
|
|
r = await client.get(f"/api/{tid}/alarms?is_acknowledged=false", headers=h)
|
|
assert r.status_code == 200
|
|
alarms = r.json()["alarms"]
|
|
assert len(alarms) == 1
|
|
warning_alarm = alarms[0]
|
|
assert warning_alarm["severity"] == "warning"
|
|
assert warning_alarm["alarm_type"] == "threshold"
|
|
warning_alarm_id = warning_alarm["id"]
|
|
|
|
r = await client.put(
|
|
f"/api/{tid}/parts/{part_id}/counter", json={"value": 1050}, headers=h
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["counter"]["lifecycle_pct"] == 105.0
|
|
|
|
r = await client.get(f"/api/{tid}/alarms?is_acknowledged=false", headers=h)
|
|
assert r.status_code == 200
|
|
active = r.json()["alarms"]
|
|
assert len(active) == 2
|
|
severities = {a["severity"] for a in active}
|
|
assert severities == {"warning", "critical"}
|
|
|
|
r = await client.post(
|
|
f"/api/{tid}/alarms/{warning_alarm_id}/acknowledge", headers=h
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["is_acknowledged"] is True
|
|
|
|
r = await client.get(f"/api/{tid}/alarms/summary", headers=h)
|
|
assert r.status_code == 200
|
|
summary = r.json()
|
|
assert summary["active_count"] == 1
|
|
assert summary["critical_count"] == 1
|
|
assert summary["warning_count"] == 0
|
|
|
|
r = await client.post(
|
|
f"/api/{tid}/parts/{part_id}/replace",
|
|
json={
|
|
"reason": "수명 초과",
|
|
"notes": "E2E 테스트 교체",
|
|
},
|
|
headers=h,
|
|
)
|
|
assert r.status_code == 200
|
|
replaced = r.json()
|
|
assert replaced["status"] == "success"
|
|
assert replaced["part"]["counter"]["current_value"] == 0
|
|
assert replaced["part"]["counter"]["lifecycle_pct"] == 0.0
|
|
|
|
r = await client.get(f"/api/{tid}/parts/{part_id}/replacements", headers=h)
|
|
assert r.status_code == 200
|
|
logs = r.json()["replacements"]
|
|
assert len(logs) == 1
|
|
assert logs[0]["counter_at_replacement"] == 1050
|
|
assert logs[0]["reason"] == "수명 초과"
|
|
|
|
r = await client.post(
|
|
f"/api/{tid}/templates",
|
|
json={
|
|
"name": "E2E 설비 점검",
|
|
"subject_type": "equipment",
|
|
"machine_id": machine_id,
|
|
"schedule_type": "daily",
|
|
"inspection_mode": "measurement",
|
|
"items": [
|
|
{
|
|
"name": "온도",
|
|
"data_type": "numeric",
|
|
"unit": "°C",
|
|
"spec_min": 10,
|
|
"spec_max": 50,
|
|
"is_required": True,
|
|
},
|
|
{"name": "가동 상태", "data_type": "boolean", "is_required": True},
|
|
{
|
|
"name": "상태",
|
|
"data_type": "select",
|
|
"select_options": ["정상", "주의", "이상"],
|
|
"is_required": True,
|
|
},
|
|
],
|
|
},
|
|
headers=h,
|
|
)
|
|
assert r.status_code == 200
|
|
template = r.json()
|
|
template_id = template["id"]
|
|
assert len(template["items"]) == 3
|
|
|
|
r = await client.post(
|
|
f"/api/{tid}/inspections",
|
|
json={
|
|
"template_id": template_id,
|
|
},
|
|
headers=h,
|
|
)
|
|
assert r.status_code == 200
|
|
session = r.json()
|
|
session_id = session["id"]
|
|
assert session["status"] == "in_progress"
|
|
assert len(session["records"]) == 3
|
|
|
|
detail_r = await client.get(f"/api/{tid}/inspections/{session_id}", headers=h)
|
|
assert detail_r.status_code == 200
|
|
items_detail = detail_r.json()["items_detail"]
|
|
|
|
record_data = []
|
|
for item_d in items_detail:
|
|
item = item_d["item"]
|
|
record = item_d["record"]
|
|
if item["name"] == "온도":
|
|
record_data.append(
|
|
{"template_item_id": record["template_item_id"], "value_numeric": 25.5}
|
|
)
|
|
elif item["name"] == "가동 상태":
|
|
record_data.append(
|
|
{"template_item_id": record["template_item_id"], "value_boolean": True}
|
|
)
|
|
elif item["name"] == "상태":
|
|
record_data.append(
|
|
{"template_item_id": record["template_item_id"], "value_select": "정상"}
|
|
)
|
|
|
|
r = await client.put(
|
|
f"/api/{tid}/inspections/{session_id}/records",
|
|
json={
|
|
"records": record_data,
|
|
},
|
|
headers=h,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
r = await client.post(f"/api/{tid}/inspections/{session_id}/complete", headers=h)
|
|
assert r.status_code == 200
|
|
completed = r.json()
|
|
assert completed["status"] == "success"
|
|
assert completed["summary"]["pass_count"] >= 2
|
|
|
|
r = await client.get(f"/api/{tid}/machines/{machine_id}", headers=h)
|
|
assert r.status_code == 200
|
|
assert r.json()["parts_count"] == 1
|
|
|
|
r = await client.get(f"/api/{tid}/inspections", headers=h)
|
|
assert r.status_code == 200
|
|
all_sessions = r.json()["inspections"]
|
|
completed_sessions = [s for s in all_sessions if s["status"] == "completed"]
|
|
assert len(completed_sessions) >= 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tenant_isolation_e2e(client: AsyncClient, seeded_db):
|
|
h_super = await _login(client)
|
|
h_tenant = await _login(client, "admin@test-co.com", "pass1234")
|
|
tid = "test-co"
|
|
other = "other-co"
|
|
|
|
r = await client.post(
|
|
f"/api/{tid}/machines",
|
|
json={
|
|
"name": "격리 테스트 설비",
|
|
"equipment_code": "ISO-001",
|
|
},
|
|
headers=h_super,
|
|
)
|
|
assert r.status_code == 200
|
|
machine_id = r.json()["id"]
|
|
|
|
r = await client.post(
|
|
f"/api/{tid}/machines/{machine_id}/parts",
|
|
json={
|
|
"name": "격리 부품",
|
|
"lifecycle_type": "hours",
|
|
"lifecycle_limit": 5000,
|
|
},
|
|
headers=h_super,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
r = await client.post(
|
|
f"/api/{tid}/templates",
|
|
json={
|
|
"name": "격리 템플릿",
|
|
"subject_type": "equipment",
|
|
"schedule_type": "daily",
|
|
"inspection_mode": "checklist",
|
|
"items": [{"name": "체크", "data_type": "boolean", "is_required": True}],
|
|
},
|
|
headers=h_super,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
r = await client.get(f"/api/{other}/machines", headers=h_super)
|
|
assert r.status_code == 200
|
|
assert len(r.json()["machines"]) == 0
|
|
|
|
r = await client.get(f"/api/{other}/templates", headers=h_super)
|
|
assert r.status_code == 200
|
|
assert len(r.json()["templates"]) == 0
|
|
|
|
r = await client.get(f"/api/{other}/alarms", headers=h_super)
|
|
assert r.status_code == 200
|
|
assert len(r.json()["alarms"]) == 0
|
|
|
|
r = await client.get(f"/api/{other}/inspections", headers=h_super)
|
|
assert r.status_code == 200
|
|
assert len(r.json()["inspections"]) == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_inspection_with_spec_validation(client: AsyncClient, seeded_db):
|
|
h = await _login(client)
|
|
tid = "test-co"
|
|
|
|
r = await client.post(
|
|
f"/api/{tid}/machines",
|
|
json={
|
|
"name": "규격 검증 설비",
|
|
"equipment_code": "SPEC-001",
|
|
},
|
|
headers=h,
|
|
)
|
|
machine_id = r.json()["id"]
|
|
|
|
r = await client.post(
|
|
f"/api/{tid}/templates",
|
|
json={
|
|
"name": "규격 검증 점검",
|
|
"subject_type": "equipment",
|
|
"machine_id": machine_id,
|
|
"schedule_type": "ad_hoc",
|
|
"inspection_mode": "measurement",
|
|
"items": [
|
|
{
|
|
"name": "치수 A",
|
|
"data_type": "numeric",
|
|
"unit": "mm",
|
|
"spec_min": 10.0,
|
|
"spec_max": 20.0,
|
|
"is_required": True,
|
|
},
|
|
{
|
|
"name": "치수 B",
|
|
"data_type": "numeric",
|
|
"unit": "mm",
|
|
"spec_min": 5.0,
|
|
"spec_max": 8.0,
|
|
"is_required": True,
|
|
},
|
|
],
|
|
},
|
|
headers=h,
|
|
)
|
|
template_id = r.json()["id"]
|
|
|
|
r = await client.post(
|
|
f"/api/{tid}/inspections", json={"template_id": template_id}, headers=h
|
|
)
|
|
session = r.json()
|
|
session_id = session["id"]
|
|
|
|
detail_r = await client.get(f"/api/{tid}/inspections/{session_id}", headers=h)
|
|
items_detail = detail_r.json()["items_detail"]
|
|
|
|
record_data = []
|
|
for item_d in items_detail:
|
|
item = item_d["item"]
|
|
record = item_d["record"]
|
|
if item["name"] == "치수 A":
|
|
record_data.append(
|
|
{"template_item_id": record["template_item_id"], "value_numeric": 15.0}
|
|
)
|
|
elif item["name"] == "치수 B":
|
|
record_data.append(
|
|
{"template_item_id": record["template_item_id"], "value_numeric": 9.5}
|
|
)
|
|
|
|
await client.put(
|
|
f"/api/{tid}/inspections/{session_id}/records",
|
|
json={"records": record_data},
|
|
headers=h,
|
|
)
|
|
|
|
r = await client.post(f"/api/{tid}/inspections/{session_id}/complete", headers=h)
|
|
assert r.status_code == 200
|
|
completed = r.json()
|
|
assert completed["status"] == "success"
|
|
assert completed["summary"]["pass_count"] == 1
|
|
assert completed["summary"]["fail_count"] == 1
|