"""D-1: Draft store stub — POST /api/v1/slabs contract test. Tests via httpx TestClient. Each test gets a fresh app instance (FUND-8). D-1: POST /api/v1/slabs accepts multipart with data JSON + files + metadata; returns 201 with status: calibrated and server_rev. FUND-8: App factory creates fresh instance per test. FUND-9: TestClient context manager manages lifespan. FUND-10: dependency_overrides cleared after every test. """ import json import uuid import pytest from fastapi.testclient import TestClient from app.config import Settings from app.main import create_app class TestCreateSlab: """D-1: POST /api/v1/slabs multipart contract.""" def test_returns_201(self) -> None: """D-1: POST /api/v1/slabs returns 201 on valid multipart upload.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) data_json = json.dumps({ "id": slab_id, "sku": "BW-0042", "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) assert response.status_code == 201 app.dependency_overrides.clear() def test_returns_status_calibrated(self) -> None: """D-1: Response contains status: calibrated.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) data_json = json.dumps({ "id": slab_id, "sku": "BW-0099", "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) assert response.status_code == 201 body = response.json() assert body["status"] == "calibrated" app.dependency_overrides.clear() def test_returns_server_rev(self) -> None: """D-1: Response contains server_rev.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) data_json = json.dumps({ "id": slab_id, "sku": "BW-0077", "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) assert response.status_code == 201 body = response.json() assert "server_rev" in body assert body["server_rev"] == 1 app.dependency_overrides.clear() def test_response_shape(self) -> None: """D-1: Response contains required fields.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) data_json = json.dumps({ "id": slab_id, "sku": "BW-0033", "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) assert response.status_code == 201 body = response.json() # Required fields per OPENAPI.md Slab read model required_fields = [ "id", "sku", "status", "length_in", "thickness_in", "sqft", "bdft", "width_min_in", "width_max_in", "width_avg_in", "species_id", "wood_category_ids", "edge_type_term_id", "figure_term_ids", "grade_term_ids", "thickness_term_id", "moisture_term_id", "fig_tag_ids", "feat_tag_ids", "price_per_bdft", "price", "price_source", "title", "short_title", "description", "content_source", "client_rev", "server_rev", "created_at", "updated_at", "photos", "inference_status", ] for field in required_fields: assert field in body, f"Missing field: {field}" # Verify stored values match sent values (server stores as-is) assert body["id"] == slab_id assert body["sku"] == "BW-0033" assert body["sqft"] == 0.64 assert body["bdft"] == 0.96 assert body["width_min_in"] == 14.0 assert body["width_max_in"] == 22.0 assert body["width_avg_in"] == 18.2 assert body["price"] == 12.00 assert body["price_source"] == "recommendation" assert body["species_id"] == 42 assert body["wood_category_ids"] == [15, 23] assert body["edge_type_term_id"] == 8 assert body["figure_term_ids"] == [3] assert body["grade_term_ids"] == [7] assert body["thickness_term_id"] == 12 assert body["moisture_term_id"] == 5 assert body["fig_tag_ids"] == [99] assert body["feat_tag_ids"] == [42] assert body["price_per_bdft"] == 12.50 app.dependency_overrides.clear() def test_multiple_photos(self) -> None: """D-1: Multiple files[] and meta[] pairs are accepted.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) data_json = json.dumps({ "id": slab_id, "sku": "BW-0055", "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ("files[]", ("extra1.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) # Note: httpx TestClient with multiple files[] + single meta[] # will pair them. The meta[] for the second file defaults. assert response.status_code == 201 body = response.json() assert len(body["photos"]) >= 2 app.dependency_overrides.clear() def test_missing_data_field_returns_400(self) -> None: """D-1: Missing data field returns 400.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) with TestClient(app) as client: response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) assert response.status_code == 400 body = response.json() assert body["detail"]["code"] == "validation" app.dependency_overrides.clear() def test_duplicate_sku_returns_409(self) -> None: """D-1: Duplicate SKU returns 409.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) sku = "DUPLICATE-TEST" data_json = json.dumps({ "id": slab_id, "sku": sku, "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: # Create first slab response1 = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) assert response1.status_code == 201 # Try to create second slab with same SKU response2 = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) assert response2.status_code == 409 body = response2.json() assert body["detail"]["code"] == "duplicate_sku" app.dependency_overrides.clear() def test_with_test_client_lifespan(self) -> None: """FUND-9: TestClient with app manages lifespan correctly for slabs.""" settings = Settings( aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", woo_base_url="https://test.example.com", ) app = create_app(settings) slab_id = str(uuid.uuid4()) data_json = json.dumps({ "id": slab_id, "sku": "BW-0011", "length_in": 96.0, "thickness_in": 1.5, "sqft": 0.64, "bdft": 0.96, "width_min_in": 14.0, "width_max_in": 22.0, "width_avg_in": 18.2, "species_id": 42, "wood_category_ids": [15, 23], "edge_type_term_id": 8, "figure_term_ids": [3], "grade_term_ids": [7], "thickness_term_id": 12, "moisture_term_id": 5, "fig_tag_ids": [99], "feat_tag_ids": [42], "price_per_bdft": 12.50, "price": 12.00, "price_source": "recommendation", "title": None, "short_title": None, "description": None, "content_source": None, "client_rev": 1, }) with TestClient(app) as client: response = client.post( "/api/v1/slabs", files=[ ("files[]", ("topdown.jpg", b"\xff\xd8\xff\xe0", "image/jpeg")), ], data={ "data": data_json, "meta[]": json.dumps({ "kind": "inventory", "role": "topdown", "seq": 1, }), }, ) assert response.status_code == 201 assert response.json()["status"] == "calibrated" app.dependency_overrides.clear()