Files
factoryOps-v2/tests/test_inspections.py
Johngreen 581c845f54
All checks were successful
Deploy to Production / deploy (push) Successful in 1m5s
feat: Phase 4 — inspection sessions (create, execute, complete)
Backend:
- InspectionSession + InspectionRecord models with alembic migration
- 6 API endpoints: create, list, get detail, save records, complete, delete
- Auto pass/fail judgment for numeric (spec range) and boolean items
- Completed inspections are immutable, required items enforced on complete
- 14 new tests (total 53/53 passed)

Frontend:
- Inspection list page with in_progress/completed tabs
- Template select modal for starting new inspections
- Inspection execution page with data-type-specific inputs
- Auto-save with 1.5s debounce, manual save button
- Completion modal with notes and required item validation
- Read-only view for completed inspections
- Pass/fail badges and color-coded item cards

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-02-10 13:46:23 +09:00

412 lines
14 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": "INS-001"},
headers=headers,
)
return resp.json()["id"]
async def _create_template_with_items(
client: AsyncClient,
headers: dict,
tenant_id: str = "test-co",
machine_id: str = None,
) -> dict:
body = {
"name": "일일 설비검사",
"subject_type": "equipment",
"schedule_type": "daily",
"inspection_mode": "measurement",
"items": [
{
"name": "온도 확인",
"data_type": "numeric",
"unit": "°C",
"spec_min": 20.0,
"spec_max": 80.0,
"is_required": True,
},
{
"name": "가동 여부",
"data_type": "boolean",
"is_required": True,
},
{
"name": "비고",
"data_type": "text",
"is_required": False,
},
],
}
if machine_id:
body["machine_id"] = machine_id
resp = await client.post(f"/api/{tenant_id}/templates", json=body, headers=headers)
assert resp.status_code == 200
return resp.json()
async def _start_inspection(
client: AsyncClient, headers: dict, template_id: str, tenant_id: str = "test-co"
) -> dict:
resp = await client.post(
f"/api/{tenant_id}/inspections",
json={"template_id": template_id},
headers=headers,
)
assert resp.status_code == 200
return resp.json()
@pytest.mark.asyncio
async def test_create_inspection(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client)
machine_id = await _create_machine(client, headers)
template = await _create_template_with_items(client, headers, machine_id=machine_id)
inspection = await _start_inspection(client, headers, template["id"])
assert inspection["status"] == "in_progress"
assert inspection["template_id"] == template["id"]
assert inspection["template_name"] == "일일 설비검사"
assert inspection["started_at"] is not None
assert len(inspection["records"]) == 3
for record in inspection["records"]:
assert record["recorded_at"] is None
assert record["is_pass"] is None
@pytest.mark.asyncio
async def test_create_inspection_empty_template_fails(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client)
body = {
"name": "빈 템플릿",
"subject_type": "equipment",
"schedule_type": "daily",
"inspection_mode": "checklist",
}
resp = await client.post("/api/test-co/templates", json=body, headers=headers)
template_id = resp.json()["id"]
resp = await client.post(
"/api/test-co/inspections",
json={"template_id": template_id},
headers=headers,
)
assert resp.status_code == 400
assert "검사 항목이 없는" in resp.json()["detail"]
@pytest.mark.asyncio
async def test_create_inspection_invalid_template(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client)
resp = await client.post(
"/api/test-co/inspections",
json={"template_id": "00000000-0000-0000-0000-000000000000"},
headers=headers,
)
assert resp.status_code == 404
@pytest.mark.asyncio
async def test_list_inspections(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client)
template = await _create_template_with_items(client, headers)
await _start_inspection(client, headers, template["id"])
await _start_inspection(client, headers, template["id"])
resp = await client.get("/api/test-co/inspections", headers=headers)
assert resp.status_code == 200
data = resp.json()
assert len(data["inspections"]) == 2
@pytest.mark.asyncio
async def test_list_inspections_filter_status(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client)
template = await _create_template_with_items(client, headers)
await _start_inspection(client, headers, template["id"])
resp = await client.get(
"/api/test-co/inspections?status=completed", headers=headers
)
assert resp.status_code == 200
assert len(resp.json()["inspections"]) == 0
resp = await client.get(
"/api/test-co/inspections?status=in_progress", headers=headers
)
assert resp.status_code == 200
assert len(resp.json()["inspections"]) == 1
@pytest.mark.asyncio
async def test_get_inspection_detail(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client)
template = await _create_template_with_items(client, headers)
inspection = await _start_inspection(client, headers, template["id"])
resp = await client.get(
f"/api/test-co/inspections/{inspection['id']}", headers=headers
)
assert resp.status_code == 200
data = resp.json()
assert data["id"] == inspection["id"]
assert "items_detail" in data
assert len(data["items_detail"]) == 3
assert data["items_detail"][0]["item"]["name"] == "온도 확인"
assert data["items_detail"][0]["item"]["data_type"] == "numeric"
@pytest.mark.asyncio
async def test_save_records(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client)
template = await _create_template_with_items(client, headers)
inspection = await _start_inspection(client, headers, template["id"])
records = inspection["records"]
numeric_record = next(
r for r in records if r["value_numeric"] is None and r["value_boolean"] is None
)
temp_record = records[0]
resp = await client.put(
f"/api/test-co/inspections/{inspection['id']}/records",
json={
"records": [
{
"template_item_id": temp_record["template_item_id"],
"value_numeric": 45.5,
}
]
},
headers=headers,
)
assert resp.status_code == 200
saved = resp.json()["records"]
assert len(saved) == 1
assert saved[0]["value_numeric"] == 45.5
assert saved[0]["is_pass"] is True
assert saved[0]["recorded_at"] is not None
@pytest.mark.asyncio
async def test_save_records_fail_judgment(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client)
template = await _create_template_with_items(client, headers)
inspection = await _start_inspection(client, headers, template["id"])
temp_record = inspection["records"][0]
resp = await client.put(
f"/api/test-co/inspections/{inspection['id']}/records",
json={
"records": [
{
"template_item_id": temp_record["template_item_id"],
"value_numeric": 100.0,
}
]
},
headers=headers,
)
assert resp.status_code == 200
saved = resp.json()["records"]
assert saved[0]["is_pass"] is False
@pytest.mark.asyncio
async def test_complete_inspection(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client)
template = await _create_template_with_items(client, headers)
inspection = await _start_inspection(client, headers, template["id"])
detail_resp = await client.get(
f"/api/test-co/inspections/{inspection['id']}", headers=headers
)
items_detail = detail_resp.json()["items_detail"]
all_records = []
for item_d in items_detail:
item = item_d["item"]
record = item_d["record"]
if item["data_type"] == "numeric":
all_records.append(
{"template_item_id": record["template_item_id"], "value_numeric": 50.0}
)
elif item["data_type"] == "boolean":
all_records.append(
{"template_item_id": record["template_item_id"], "value_boolean": True}
)
elif item["data_type"] == "text":
all_records.append(
{"template_item_id": record["template_item_id"], "value_text": "정상"}
)
resp = await client.put(
f"/api/test-co/inspections/{inspection['id']}/records",
json={"records": all_records},
headers=headers,
)
assert resp.status_code == 200
resp = await client.post(
f"/api/test-co/inspections/{inspection['id']}/complete",
json={"notes": "검사 완료 메모"},
headers=headers,
)
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "success"
assert data["summary"]["total"] == 3
assert data["summary"]["pass_count"] >= 1
@pytest.mark.asyncio
async def test_complete_inspection_missing_required(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client)
template = await _create_template_with_items(client, headers)
inspection = await _start_inspection(client, headers, template["id"])
resp = await client.post(
f"/api/test-co/inspections/{inspection['id']}/complete",
headers=headers,
)
assert resp.status_code == 400
assert "필수 검사 항목" in resp.json()["detail"]
@pytest.mark.asyncio
async def test_complete_already_completed(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client)
template = await _create_template_with_items(client, headers)
inspection = await _start_inspection(client, headers, template["id"])
detail_resp = await client.get(
f"/api/test-co/inspections/{inspection['id']}", headers=headers
)
items_detail = detail_resp.json()["items_detail"]
all_records = []
for item_d in items_detail:
item = item_d["item"]
record = item_d["record"]
if item["data_type"] == "numeric":
all_records.append(
{"template_item_id": record["template_item_id"], "value_numeric": 50.0}
)
elif item["data_type"] == "boolean":
all_records.append(
{"template_item_id": record["template_item_id"], "value_boolean": True}
)
elif item["data_type"] == "text":
all_records.append(
{"template_item_id": record["template_item_id"], "value_text": "정상"}
)
await client.put(
f"/api/test-co/inspections/{inspection['id']}/records",
json={"records": all_records},
headers=headers,
)
await client.post(
f"/api/test-co/inspections/{inspection['id']}/complete",
headers=headers,
)
resp = await client.post(
f"/api/test-co/inspections/{inspection['id']}/complete",
headers=headers,
)
assert resp.status_code == 400
assert "이미 완료된" in resp.json()["detail"]
@pytest.mark.asyncio
async def test_save_records_completed_fails(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client)
template = await _create_template_with_items(client, headers)
inspection = await _start_inspection(client, headers, template["id"])
detail_resp = await client.get(
f"/api/test-co/inspections/{inspection['id']}", headers=headers
)
items_detail = detail_resp.json()["items_detail"]
all_records = []
for item_d in items_detail:
item = item_d["item"]
record = item_d["record"]
if item["data_type"] == "numeric":
all_records.append(
{"template_item_id": record["template_item_id"], "value_numeric": 50.0}
)
elif item["data_type"] == "boolean":
all_records.append(
{"template_item_id": record["template_item_id"], "value_boolean": True}
)
elif item["data_type"] == "text":
all_records.append(
{"template_item_id": record["template_item_id"], "value_text": "정상"}
)
await client.put(
f"/api/test-co/inspections/{inspection['id']}/records",
json={"records": all_records},
headers=headers,
)
await client.post(
f"/api/test-co/inspections/{inspection['id']}/complete",
headers=headers,
)
resp = await client.put(
f"/api/test-co/inspections/{inspection['id']}/records",
json={
"records": [
{
"template_item_id": all_records[0]["template_item_id"],
"value_numeric": 99.0,
}
]
},
headers=headers,
)
assert resp.status_code == 400
assert "완료된 검사" in resp.json()["detail"]
@pytest.mark.asyncio
async def test_delete_draft_inspection_not_allowed_for_in_progress(
client: AsyncClient, seeded_db
):
headers = await get_auth_headers(client)
template = await _create_template_with_items(client, headers)
inspection = await _start_inspection(client, headers, template["id"])
resp = await client.delete(
f"/api/test-co/inspections/{inspection['id']}",
headers=headers,
)
assert resp.status_code == 400
assert "진행 중이거나" in resp.json()["detail"]
@pytest.mark.asyncio
async def test_tenant_isolation(client: AsyncClient, seeded_db):
headers = await get_auth_headers(client, email="admin@test-co.com")
template = await _create_template_with_items(client, headers)
inspection = await _start_inspection(client, headers, template["id"])
resp = await client.get(
f"/api/other-co/inspections/{inspection['id']}", headers=headers
)
assert resp.status_code in (403, 404)