All checks were successful
Deploy to Production / deploy (push) Successful in 1m5s
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>
412 lines
14 KiB
Python
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)
|