577 lines
20 KiB
Python
577 lines
20 KiB
Python
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"
|