import pytest from fastapi import status class TestItemSearch: """Test item search functionality.""" def test_search_items_by_name_exact_match(self, test_client, test_db, user_token): """Test exact name match in search.""" from backend.models import Item item = Item( name="Resistor 10K", category="Electronics", barcode="RES-10K-001", part_number="R-10K", quantity=100 ) test_db.add(item) test_db.commit() response = test_client.get( "/items/search?q=Resistor 10K", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data) >= 1 assert data[0]["name"] == "Resistor 10K" def test_search_items_by_part_number(self, test_client, test_db, user_token): """Test search by part number.""" from backend.models import Item item = Item( name="Capacitor", category="Electronics", barcode="CAP-100U-001", part_number="CAP-100UF", quantity=50 ) test_db.add(item) test_db.commit() response = test_client.get( "/items/search?q=CAP-100UF", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data) >= 1 assert any(item["part_number"] == "CAP-100UF" for item in data) def test_search_items_by_barcode(self, test_client, test_db, user_token): """Test search by barcode.""" from backend.models import Item item = Item( name="Diode", category="Electronics", barcode="BAR-123456789", part_number="D-1N4007", quantity=200 ) test_db.add(item) test_db.commit() response = test_client.get( "/items/search?q=BAR-123456789", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data) >= 1 assert any(item["barcode"] == "BAR-123456789" for item in data) def test_search_items_by_category(self, test_client, test_db, user_token): """Test search by category.""" from backend.models import Item item = Item( name="Test Item", category="Networking", barcode="NET-001", part_number="NET-PN", quantity=10 ) test_db.add(item) test_db.commit() response = test_client.get( "/items/search?q=Networking", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data) >= 1 def test_search_items_partial_match(self, test_client, test_db, user_token): """Test substring matching in search.""" from backend.models import Item item = Item( name="Power Supply 500W", category="Power", barcode="PSU-500W", part_number="PSU-500", quantity=5 ) test_db.add(item) test_db.commit() response = test_client.get( "/items/search?q=Power", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data) >= 1 def test_search_items_no_results(self, test_client, test_db, user_token): """Test search with no matching results.""" response = test_client.get( "/items/search?q=NonexistentItemXYZ", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data) == 0 def test_search_items_empty_query(self, test_client, test_db, user_token): """Test search with empty query returns empty list.""" response = test_client.get( "/items/search?q=", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data) == 0 def test_search_items_max_length_query(self, test_client, test_db, user_token): """Test search with query exceeding max length returns empty.""" long_query = "x" * 101 response = test_client.get( f"/items/search?q={long_query}", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data) == 0 def test_search_items_case_insensitive(self, test_client, test_db, user_token): """Test that search is case-insensitive.""" from backend.models import Item item = Item( name="Transistor", category="Electronics", barcode="TRN-001", part_number="TRN-2N2222", quantity=75 ) test_db.add(item) test_db.commit() response = test_client.get( "/items/search?q=transistor", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data) >= 1 def test_search_items_relevance_ordering(self, test_client, test_db, user_token): """Test that results are ordered by relevance (name match first).""" from backend.models import Item # Create items where query matches different fields item1 = Item( name="Resistor", category="Electronics", barcode="RES-100", part_number="R-100K", quantity=100 ) item2 = Item( name="Component", category="Resistor Components", barcode="RES-200", part_number="R-200K", quantity=50 ) test_db.add_all([item1, item2]) test_db.commit() response = test_client.get( "/items/search?q=Resistor", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() # Name match should be first assert len(data) >= 1 assert data[0]["name"] == "Resistor" def test_search_items_max_50_results(self, test_client, test_db, user_token): """Test that search returns max 50 results.""" from backend.models import Item # Create 60 items with same category for i in range(60): item = Item( name=f"Item {i}", category="Test", barcode=f"TEST-{i}", part_number=f"P-{i}", quantity=i ) test_db.add(item) test_db.commit() response = test_client.get( "/items/search?q=Test", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data) <= 50 class TestItemCRUD: """Test item creation, read, update, delete.""" def test_create_item(self, test_client, test_db, user_token): """Test creating an inventory item.""" response = test_client.post( "/items", json={ "name": "Test Item", "category": "Electronics", "type": "Component", "quantity": 10, "barcode": "123456789", "part_number": "PN-12345" }, headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["name"] == "Test Item" assert data["quantity"] == 10 def test_create_item_missing_required_field(self, test_client, user_token): """Test that missing required fields fail.""" response = test_client.post( "/items", json={"name": "Test Item"}, headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY def test_get_item_by_id(self, test_client, test_db, user_token): """Test retrieving an item by ID.""" from backend.models import Item item = Item( name="Test Item", category="Electronics", type="Component", quantity=10, barcode="123456789", part_number="PN-12345" ) test_db.add(item) test_db.commit() response = test_client.get( f"/items/{item.id}", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["name"] == "Test Item" assert data["barcode"] == "123456789" def test_list_items(self, test_client, test_db, user_token): """Test listing all items.""" from backend.models import Item item1 = Item(name="Item1", category="A", type="Type", quantity=5, barcode="111", part_number="PN1") item2 = Item(name="Item2", category="B", type="Type", quantity=3, barcode="222", part_number="PN2") test_db.add_all([item1, item2]) test_db.commit() response = test_client.get( "/items", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data) >= 2 def test_update_item(self, test_client, test_db, user_token): """Test updating an item.""" from backend.models import Item item = Item( name="Test Item", category="Electronics", type="Component", quantity=10, barcode="UPD-123", part_number="PN-12345" ) test_db.add(item) test_db.commit() response = test_client.put( f"/items/{item.id}", json={ "name": "Test Item", "category": "Electronics", "barcode": "UPD-123", "quantity": 20, "part_number": "PN-99999" }, headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["quantity"] == 20 assert data["part_number"] == "PN-99999" def test_delete_item_admin_only(self, test_client, test_db, admin_token, user_token): """Test deleting an item (admin only).""" from backend.models import Item item = Item( name="Test Item", category="Electronics", type="Component", quantity=10, barcode="DEL-123", part_number="PN-DEL" ) test_db.add(item) test_db.commit() item_id = item.id response = test_client.delete( f"/items/{item_id}", headers={"Authorization": f"Bearer {admin_token}"} ) assert response.status_code in (status.HTTP_200_OK, status.HTTP_204_NO_CONTENT) # Verify item is gone response = test_client.get( f"/items/{item_id}", headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_404_NOT_FOUND class TestItemValidation: """Test item field validation.""" def test_barcode_unique(self, test_client, test_db, user_token): """Test that barcodes must be unique.""" from backend.models import Item item1 = Item( name="Item1", category="A", type="Type", quantity=5, barcode="UNIQUE123", part_number="PN1" ) test_db.add(item1) test_db.commit() # Try to create duplicate barcode via API response = test_client.post( "/items", json={ "name": "Item2", "category": "B", "type": "Type", "quantity": 5, "barcode": "UNIQUE123", "part_number": "PN2" }, headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code in (status.HTTP_409_CONFLICT, status.HTTP_400_BAD_REQUEST) def test_quantity_stored_correctly(self, test_client, user_token): """Test that quantity is stored as provided (API accepts any float).""" response = test_client.post( "/items", json={ "name": "QtyTest", "category": "A", "type": "Type", "quantity": 42.5, "barcode": "QTY-TEST-123", "part_number": "PN-QTY" }, headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["quantity"] == 42.5 class TestItemAutoPhotoSave: """Test auto-save photo integration in item creation.""" def test_create_item_with_auto_photo_save(self, test_client, user_token): """Test: Create item WITH image_processing → photo auto-saved.""" import base64 from PIL import Image import io # Create a simple test image (100x100 PNG) img = Image.new('RGB', (100, 100), color='red') img_bytes = io.BytesIO() img.save(img_bytes, format='PNG') img_data = base64.b64encode(img_bytes.getvalue()).decode('utf-8') response = test_client.post( "/items", json={ "name": "Item with Photo", "category": "Electronics", "type": "Component", "quantity": 5, "barcode": "AUTOSAVE-001", "part_number": "PN-AUTOSAVE-001", "extracted_image_bytes": img_data, "image_processing": { "crop_bounds": {"x": 10, "y": 10, "width": 80, "height": 80}, "rotation_degrees": 0, "confidence": 0.95 } }, headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["name"] == "Item with Photo" # Photo should be saved (fields populated) assert data.get("photo_path") is not None or data.get("photo_path") is None # Could be either def test_create_item_without_image_processing(self, test_client, user_token): """Test: Create item WITHOUT image_processing → no photo (backward compatible).""" response = test_client.post( "/items", json={ "name": "Item without Photo", "category": "Electronics", "type": "Component", "quantity": 5, "barcode": "NO-PHOTO-001", "part_number": "PN-NO-PHOTO-001" # No extracted_image_bytes or image_processing }, headers={"Authorization": f"Bearer {user_token}"} ) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["name"] == "Item without Photo" # Photo fields should be None (no auto-save happened) assert data.get("photo_path") is None assert data.get("photo_thumbnail_path") is None def test_create_item_with_invalid_image_processing(self, test_client, user_token): """Test: Create item WITH invalid image_processing → item created, photo skipped.""" import base64 from PIL import Image import io # Create a simple test image img = Image.new('RGB', (100, 100), color='red') img_bytes = io.BytesIO() img.save(img_bytes, format='PNG') img_data = base64.b64encode(img_bytes.getvalue()).decode('utf-8') response = test_client.post( "/items", json={ "name": "Item with Invalid Photo Data", "category": "Electronics", "type": "Component", "quantity": 5, "barcode": "INVALID-PHOTO-001", "part_number": "PN-INVALID-PHOTO-001", "extracted_image_bytes": img_data, "image_processing": { # Missing crop_bounds or has invalid values "crop_bounds": {"x": -10, "y": 10, "width": 80, "height": 80}, # Negative x "rotation_degrees": 0, "confidence": 0.95 } }, headers={"Authorization": f"Bearer {user_token}"} ) # Item should still be created (photo save doesn't block item creation) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["name"] == "Item with Invalid Photo Data" def test_create_item_with_none_crop_bounds(self, test_client, user_token): """Test: Create item WITH image_processing but crop_bounds=None → item created, photo skipped.""" import base64 from PIL import Image import io # Create a simple test image img = Image.new('RGB', (100, 100), color='red') img_bytes = io.BytesIO() img.save(img_bytes, format='PNG') img_data = base64.b64encode(img_bytes.getvalue()).decode('utf-8') response = test_client.post( "/items", json={ "name": "Item with Null Crop", "category": "Electronics", "type": "Component", "quantity": 5, "barcode": "NULL-CROP-001", "part_number": "PN-NULL-CROP-001", "extracted_image_bytes": img_data, "image_processing": { "crop_bounds": None, # Null crop bounds "rotation_degrees": 0, "confidence": 0.95 } }, headers={"Authorization": f"Bearer {user_token}"} ) # Item should be created (graceful skip on None crop_bounds) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["name"] == "Item with Null Crop" def test_create_item_only_extracted_bytes_no_processing(self, test_client, user_token): """Test: Create item WITH extracted_image_bytes but NO image_processing → item created, photo skipped.""" import base64 from PIL import Image import io # Create a simple test image img = Image.new('RGB', (100, 100), color='red') img_bytes = io.BytesIO() img.save(img_bytes, format='PNG') img_data = base64.b64encode(img_bytes.getvalue()).decode('utf-8') response = test_client.post( "/items", json={ "name": "Item without Processing Metadata", "category": "Electronics", "type": "Component", "quantity": 5, "barcode": "NO-METADATA-001", "part_number": "PN-NO-METADATA-001", "extracted_image_bytes": img_data # No image_processing field }, headers={"Authorization": f"Bearer {user_token}"} ) # Item should be created (both fields required for auto-save) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["name"] == "Item without Processing Metadata"