Updates
This commit is contained in:
@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.core.config import settings
|
||||
from app.db.migrations import bootstrap_schema, sync_tenant_ids
|
||||
from app.db.migrations import bootstrap_schema, sync_product_visibility, sync_tenant_ids
|
||||
from app.db.session import Base
|
||||
from app.main import app
|
||||
from app.models.assumption import FreightCostRule, PackagingCostRule, ProcessCostRule
|
||||
@@ -17,7 +17,7 @@ from app.models.product import Product
|
||||
from app.models.raw_material import RawMaterial, RawMaterialPriceVersion
|
||||
from app.services.client_access_service import build_client_access_export, ensure_user_module_permissions, serialize_client_account
|
||||
from app.services.costing_engine import calculate_mix_cost, calculate_product_cost, calculate_raw_material_cost, serialize_raw_material
|
||||
from app.services.mix_calculator_service import calculate_mix_calculator_preview
|
||||
from app.services.mix_calculator_service import build_mix_calculator_options, calculate_mix_calculator_preview
|
||||
|
||||
|
||||
def build_session() -> Session:
|
||||
@@ -151,6 +151,94 @@ def test_mix_calculator_preview_scales_saved_mix_and_warns_on_fractional_bags():
|
||||
assert "not a whole-bag quantity" in preview["warnings"][0]
|
||||
|
||||
|
||||
def test_mix_calculator_options_hide_invisible_products_and_clients():
|
||||
db = build_session()
|
||||
|
||||
maize = RawMaterial(name="Maize", unit_of_measure="tonne", kg_per_unit=1000, status="active")
|
||||
db.add(maize)
|
||||
db.flush()
|
||||
|
||||
visible_mix = Mix(tenant_id="hunter-premium-produce", client_name="Peckish", name="Visible Mix", status="active", version=1)
|
||||
hidden_mix = Mix(tenant_id="hunter-premium-produce", client_name="Chaff", name="Hidden Mix", status="active", version=1)
|
||||
db.add_all([visible_mix, hidden_mix])
|
||||
db.flush()
|
||||
|
||||
db.add_all(
|
||||
[
|
||||
MixIngredient(tenant_id="hunter-premium-produce", mix_id=visible_mix.id, raw_material_id=maize.id, quantity_kg=20),
|
||||
MixIngredient(tenant_id="hunter-premium-produce", mix_id=hidden_mix.id, raw_material_id=maize.id, quantity_kg=20),
|
||||
]
|
||||
)
|
||||
db.flush()
|
||||
|
||||
db.add_all(
|
||||
[
|
||||
Product(
|
||||
tenant_id="hunter-premium-produce",
|
||||
client_name="Peckish",
|
||||
name="Visible Product",
|
||||
mix_id=visible_mix.id,
|
||||
visible=True,
|
||||
sale_type="standard",
|
||||
own_bag=False,
|
||||
unit_of_measure="20kg bag",
|
||||
items_per_pallet=50,
|
||||
),
|
||||
Product(
|
||||
tenant_id="hunter-premium-produce",
|
||||
client_name="Chaff",
|
||||
name="Hidden Product",
|
||||
mix_id=hidden_mix.id,
|
||||
visible=False,
|
||||
sale_type="standard",
|
||||
own_bag=False,
|
||||
unit_of_measure="20kg bag",
|
||||
items_per_pallet=50,
|
||||
),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
options = build_mix_calculator_options(db, tenant_id="hunter-premium-produce")
|
||||
|
||||
assert options["clients"] == ["Peckish"]
|
||||
assert [product["product_name"] for product in options["products"]] == ["Visible Product"]
|
||||
|
||||
|
||||
def test_sync_product_visibility_hides_configured_clients():
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
with engine.begin() as connection:
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE products (
|
||||
id INTEGER PRIMARY KEY,
|
||||
client_name VARCHAR(255),
|
||||
visible BOOLEAN NOT NULL DEFAULT TRUE
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO products (id, client_name, visible)
|
||||
VALUES
|
||||
(1, 'Chaff', TRUE),
|
||||
(2, 'Peckish', TRUE),
|
||||
(3, 'Uncategorized', TRUE)
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
updated = sync_product_visibility(engine)
|
||||
|
||||
assert updated == 2
|
||||
with engine.connect() as connection:
|
||||
rows = connection.execute(text("SELECT client_name, visible FROM products ORDER BY id")).all()
|
||||
assert rows == [("Chaff", 0), ("Peckish", 1), ("Uncategorized", 0)]
|
||||
|
||||
|
||||
def test_root_and_login_endpoints():
|
||||
with TestClient(app) as client:
|
||||
root_response = client.get("/")
|
||||
@@ -260,16 +348,15 @@ def test_client_access_endpoints():
|
||||
"/api/auth/admin/login",
|
||||
json={"email": settings.admin_email, "password": settings.admin_password},
|
||||
)
|
||||
token = login_response.json()["token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
admin_cookies = {settings.admin_session_cookie_name: login_response.cookies.get(settings.admin_session_cookie_name)}
|
||||
|
||||
access_response = client.get("/api/client-access", headers=headers)
|
||||
access_response = client.get("/api/client-access", cookies=admin_cookies)
|
||||
assert access_response.status_code == 200
|
||||
assert len(access_response.json()) >= 1
|
||||
assert "audit_history" in access_response.json()[0]
|
||||
assert "module_permissions" in access_response.json()[0]["users"][0]
|
||||
|
||||
export_response = client.get("/api/powerbi/client-access", headers=headers)
|
||||
export_response = client.get("/api/powerbi/client-access", cookies=admin_cookies)
|
||||
assert export_response.status_code == 200
|
||||
assert "client_rows" in export_response.json()
|
||||
assert "permission_rows" in export_response.json()
|
||||
@@ -278,8 +365,8 @@ def test_client_access_endpoints():
|
||||
"/api/auth/client/login",
|
||||
json={"email": settings.client_email, "password": settings.client_password},
|
||||
)
|
||||
client_headers = {"Authorization": f"Bearer {client_login_response.json()['token']}"}
|
||||
superadmin_access_response = client.get("/api/client-access", headers=client_headers)
|
||||
client_cookies = {settings.session_cookie_name: client_login_response.cookies.get(settings.session_cookie_name)}
|
||||
superadmin_access_response = client.get("/api/client-access", cookies=client_cookies)
|
||||
assert superadmin_access_response.status_code == 200
|
||||
assert len(superadmin_access_response.json()) == 1
|
||||
|
||||
@@ -291,9 +378,9 @@ def test_mix_calculator_endpoints_respect_owner_visibility():
|
||||
json={"email": settings.client_email, "password": settings.client_password},
|
||||
)
|
||||
assert superadmin_login.status_code == 200
|
||||
superadmin_headers = {"Authorization": f"Bearer {superadmin_login.json()['token']}"}
|
||||
superadmin_cookies = {settings.session_cookie_name: superadmin_login.cookies.get(settings.session_cookie_name)}
|
||||
|
||||
options_response = client.get("/api/mix-calculator/options", headers=superadmin_headers)
|
||||
options_response = client.get("/api/mix-calculator/options", cookies=superadmin_cookies)
|
||||
assert options_response.status_code == 200
|
||||
options_payload = options_response.json()
|
||||
assert len(options_payload["products"]) >= 100
|
||||
@@ -310,7 +397,7 @@ def test_mix_calculator_endpoints_respect_owner_visibility():
|
||||
"prepared_by_name": "Amelia Hart",
|
||||
"notes": "Morning production run",
|
||||
},
|
||||
headers=superadmin_headers,
|
||||
cookies=superadmin_cookies,
|
||||
)
|
||||
assert create_response.status_code == 201
|
||||
created = create_response.json()
|
||||
@@ -323,7 +410,7 @@ def test_mix_calculator_endpoints_respect_owner_visibility():
|
||||
patch_response = client.patch(
|
||||
f"/api/mix-calculator/{created['id']}",
|
||||
json={"batch_size_kg": 550},
|
||||
headers=superadmin_headers,
|
||||
cookies=superadmin_cookies,
|
||||
)
|
||||
assert patch_response.status_code == 200
|
||||
assert patch_response.json()["total_bags"] == 27.5
|
||||
@@ -334,13 +421,13 @@ def test_mix_calculator_endpoints_respect_owner_visibility():
|
||||
json={"email": "ethan.cole@hunterpremiumproduce.example", "password": settings.client_password},
|
||||
)
|
||||
assert operator_login.status_code == 200
|
||||
operator_headers = {"Authorization": f"Bearer {operator_login.json()['token']}"}
|
||||
operator_cookies = {settings.session_cookie_name: operator_login.cookies.get(settings.session_cookie_name)}
|
||||
|
||||
operator_list_response = client.get("/api/mix-calculator", headers=operator_headers)
|
||||
operator_list_response = client.get("/api/mix-calculator", cookies=operator_cookies)
|
||||
assert operator_list_response.status_code == 200
|
||||
assert operator_list_response.json() == []
|
||||
|
||||
operator_detail_response = client.get(f"/api/mix-calculator/{created['id']}", headers=operator_headers)
|
||||
operator_detail_response = client.get(f"/api/mix-calculator/{created['id']}", cookies=operator_cookies)
|
||||
assert operator_detail_response.status_code == 404
|
||||
|
||||
|
||||
@@ -350,9 +437,9 @@ def test_mix_calculator_pdf_endpoint_returns_pdf():
|
||||
"/api/auth/client/login",
|
||||
json={"email": settings.client_email, "password": settings.client_password},
|
||||
)
|
||||
headers = {"Authorization": f"Bearer {superadmin_login.json()['token']}"}
|
||||
superadmin_cookies = {settings.session_cookie_name: superadmin_login.cookies.get(settings.session_cookie_name)}
|
||||
|
||||
options_response = client.get("/api/mix-calculator/options", headers=headers)
|
||||
options_response = client.get("/api/mix-calculator/options", cookies=superadmin_cookies)
|
||||
seeded_product = next(
|
||||
product for product in options_response.json()["products"] if product["product_name"] == "Specialty Pigeon Breeder 20kg"
|
||||
)
|
||||
@@ -367,11 +454,11 @@ def test_mix_calculator_pdf_endpoint_returns_pdf():
|
||||
"prepared_by_name": "Amelia Hart",
|
||||
"notes": "Morning production run",
|
||||
},
|
||||
headers=headers,
|
||||
cookies=superadmin_cookies,
|
||||
)
|
||||
created = create_response.json()
|
||||
|
||||
pdf_response = client.get(f"/api/mix-calculator/{created['id']}/pdf", headers=headers)
|
||||
pdf_response = client.get(f"/api/mix-calculator/{created['id']}/pdf", cookies=superadmin_cookies)
|
||||
|
||||
assert pdf_response.status_code == 200
|
||||
assert pdf_response.headers["content-type"] == "application/pdf"
|
||||
@@ -385,8 +472,8 @@ def test_module_permission_blocks_client_module_access():
|
||||
"/api/auth/admin/login",
|
||||
json={"email": settings.admin_email, "password": settings.admin_password},
|
||||
)
|
||||
admin_headers = {"Authorization": f"Bearer {admin_login_response.json()['token']}"}
|
||||
access_response = client.get("/api/client-access", headers=admin_headers)
|
||||
admin_cookies = {settings.admin_session_cookie_name: admin_login_response.cookies.get(settings.admin_session_cookie_name)}
|
||||
access_response = client.get("/api/client-access", cookies=admin_cookies)
|
||||
first_client = access_response.json()[0]
|
||||
first_user = next(user for user in first_client["users"] if user["email"] == settings.client_email)
|
||||
|
||||
@@ -396,15 +483,15 @@ def test_module_permission_blocks_client_module_access():
|
||||
client.patch(
|
||||
f"/api/client-access/users/{first_user['id']}/module-permissions/{permission['module_key']}",
|
||||
json={"access_level": "none"},
|
||||
headers=admin_headers,
|
||||
cookies=admin_cookies,
|
||||
)
|
||||
|
||||
client_login_response = client.post(
|
||||
"/api/auth/client/login",
|
||||
json={"email": settings.client_email, "password": settings.client_password},
|
||||
)
|
||||
client_headers = {"Authorization": f"Bearer {client_login_response.json()['token']}"}
|
||||
raw_materials_response = client.get("/api/raw-materials", headers=client_headers)
|
||||
client_cookies = {settings.session_cookie_name: client_login_response.cookies.get(settings.session_cookie_name)}
|
||||
raw_materials_response = client.get("/api/raw-materials", cookies=client_cookies)
|
||||
|
||||
assert raw_materials_response.status_code == 403
|
||||
|
||||
|
||||
Reference in New Issue
Block a user