Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/lab_manager/api/routes/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ def document_stats(db: Session = Depends(get_db)):
}


@router.get("/")
@router.get("/", dependencies=[Depends(require_permission("view_documents"))])
def list_documents(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=500),
Expand Down Expand Up @@ -456,7 +456,9 @@ def create_document(body: DocumentCreate, db: Session = Depends(get_db)):
return document


@router.get("/{document_id}")
@router.get(
"/{document_id}", dependencies=[Depends(require_permission("view_documents"))]
)
def get_document(document_id: int, db: Session = Depends(get_db)):
return get_or_404(db, Document, document_id, "Document")

Expand Down
6 changes: 4 additions & 2 deletions src/lab_manager/api/routes/equipment.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def validate_status(cls, v: str | None) -> str | None:
return v


@router.get("/")
@router.get("/", dependencies=[Depends(require_permission("view_equipment"))])
def list_equipment(
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200),
Expand Down Expand Up @@ -134,7 +134,9 @@ def create_equipment(body: EquipmentCreate, db: Session = Depends(get_db)):
return equip


@router.get("/{equipment_id}")
@router.get(
"/{equipment_id}", dependencies=[Depends(require_permission("view_equipment"))]
)
def get_equipment(equipment_id: int, db: Session = Depends(get_db)):
return get_or_404(db, Equipment, equipment_id, "Equipment")

Expand Down
21 changes: 15 additions & 6 deletions src/lab_manager/api/routes/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def _flatten_item(item: InventoryItem) -> dict:
}


@router.get("/")
@router.get("/", dependencies=[Depends(require_permission("view_inventory"))])
def list_inventory(
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200),
Expand Down Expand Up @@ -258,13 +258,13 @@ def create_inventory_item(body: InventoryItemCreate, db: Session = Depends(get_d
return item


@router.get("/low-stock")
@router.get("/low-stock", dependencies=[Depends(require_permission("view_inventory"))])
def low_stock(db: Session = Depends(get_db)):
"""Products below their reorder level."""
return inv_svc.get_low_stock(db)


@router.get("/expiring")
@router.get("/expiring", dependencies=[Depends(require_permission("view_inventory"))])
def expiring(days: int = Query(30, ge=1), db: Session = Depends(get_db)):
"""Items expiring within N days."""
return inv_svc.get_expiring(db, days=days)
Expand All @@ -275,7 +275,11 @@ def expiring(days: int = Query(30, ge=1), db: Session = Depends(get_db)):
# ---------------------------------------------------------------------------


@router.get("/{item_id}", response_model=InventoryItemResponse)
@router.get(
"/{item_id}",
response_model=InventoryItemResponse,
dependencies=[Depends(require_permission("view_inventory"))],
)
def get_inventory_item(item_id: int, db: Session = Depends(get_db)):
return get_or_404(db, InventoryItem, item_id, "Inventory item")

Expand Down Expand Up @@ -324,7 +328,9 @@ def delete_inventory_item(item_id: int, db: Session = Depends(get_db)):
return None


@router.get("/{item_id}/history")
@router.get(
"/{item_id}/history", dependencies=[Depends(require_permission("view_inventory"))]
)
def item_history(item_id: int, db: Session = Depends(get_db)):
"""Consumption log for a specific inventory item."""
return inv_svc.get_item_history(item_id, db)
Expand Down Expand Up @@ -365,7 +371,10 @@ def open_item(item_id: int, body: OpenBody, db: Session = Depends(get_db)):
return inv_svc.open_item(item_id, body.opened_by, db)


@router.get("/{item_id}/reorder-url")
@router.get(
"/{item_id}/reorder-url",
dependencies=[Depends(require_permission("view_inventory"))],
)
def get_reorder_url_endpoint(item_id: int, db: Session = Depends(get_db)):
"""Generate a vendor website URL for reordering this item's product."""
item = get_or_404(db, InventoryItem, item_id, "Inventory item")
Expand Down
91 changes: 91 additions & 0 deletions tests/test_read_endpoints_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Test auth guards on documents, inventory, and equipment GET endpoints."""

import pytest

from lab_manager.config import get_settings


@pytest.fixture(autouse=True)
def _enable_auth(monkeypatch):
monkeypatch.setenv("AUTH_ENABLED", "true")
monkeypatch.setenv("ADMIN_SECRET_KEY", "test-secret-key-for-signing")
monkeypatch.setenv("ADMIN_PASSWORD", "test-admin-password-12345")
monkeypatch.setenv("API_KEY", "test-api-key-12345")
monkeypatch.setenv("SECURE_COOKIES", "false")
get_settings.cache_clear()
yield
monkeypatch.delenv("AUTH_ENABLED", raising=False)
get_settings.cache_clear()


@pytest.fixture
def auth_client(db_session):
import lab_manager.database as db_module

original_engine = db_module._engine
original_factory = db_module._session_factory
db_module._session_factory = None

from lab_manager.api.app import create_app
from lab_manager.api.deps import get_db

app = create_app()

def override_get_db():
yield db_session

app.dependency_overrides[get_db] = override_get_db

from starlette.testclient import TestClient

with TestClient(app) as c:
yield c

db_module._engine = original_engine
db_module._session_factory = original_factory


class TestDocumentsAuthRequired:
def test_list_documents_requires_auth(self, auth_client):
resp = auth_client.get("/api/v1/documents/")
assert resp.status_code == 401

def test_get_document_requires_auth(self, auth_client):
resp = auth_client.get("/api/v1/documents/1")
assert resp.status_code == 401

def test_document_stats_requires_auth(self, auth_client):
resp = auth_client.get("/api/v1/documents/stats")
assert resp.status_code == 401


class TestInventoryAuthRequired:
def test_list_inventory_requires_auth(self, auth_client):
resp = auth_client.get("/api/v1/inventory/")
assert resp.status_code == 401

def test_low_stock_requires_auth(self, auth_client):
resp = auth_client.get("/api/v1/inventory/low-stock")
assert resp.status_code == 401

def test_expiring_requires_auth(self, auth_client):
resp = auth_client.get("/api/v1/inventory/expiring")
assert resp.status_code == 401

def test_get_item_requires_auth(self, auth_client):
resp = auth_client.get("/api/v1/inventory/1")
assert resp.status_code == 401

def test_item_history_requires_auth(self, auth_client):
resp = auth_client.get("/api/v1/inventory/1/history")
assert resp.status_code == 401


class TestEquipmentAuthRequired:
def test_list_equipment_requires_auth(self, auth_client):
resp = auth_client.get("/api/v1/equipment/")
assert resp.status_code == 401

def test_get_equipment_requires_auth(self, auth_client):
resp = auth_client.get("/api/v1/equipment/1")
assert resp.status_code == 401
Loading