v0.1.27
Fix: Throughput API v1 available - Details posted to Irving. POWERBI_KEY was missing from the .ENV file, so was not live. Add: Editor now supports editing a mix's resolved formula directly, with % and kg dual entry on ingredient rows Fix: Mix Editor should bring through correct ingredients. New resolved formula (same logic we use in Mix Calculator). Fix: Security headers on all API responses (hardening) Add: New mix button available on the Mix Editor. Add: New ingredient button available on the Ingredient Editor
This commit is contained in:
@@ -347,3 +347,207 @@ def test_internal_user_can_change_own_password(access_app_and_db):
|
||||
json={"email": admin.email, "password": "new-personal-password"},
|
||||
)
|
||||
assert new_login.status_code == 200
|
||||
|
||||
|
||||
# --- Admin user management --------------------------------------------------
|
||||
|
||||
|
||||
def _admin_headers(db: Session) -> dict[str, str]:
|
||||
admin = db.query(User).filter_by(email="admin@hunterstockfeeds.com").one()
|
||||
return {"Authorization": f"Bearer {_token_for(admin)}"}
|
||||
|
||||
|
||||
def test_manage_users_create_update_password_delete(access_app_and_db):
|
||||
client, db = access_app_and_db
|
||||
headers = _admin_headers(db)
|
||||
full_access_role = db.query(Role).filter_by(name="Full Access").one()
|
||||
|
||||
created = client.post(
|
||||
"/api/access/users",
|
||||
json={"email": "new.user@hunterstockfeeds.com", "name": "New User", "role_id": full_access_role.id},
|
||||
headers=headers,
|
||||
)
|
||||
assert created.status_code == 201
|
||||
body = created.json()
|
||||
assert body["email"] == "new.user@hunterstockfeeds.com"
|
||||
assert body["role"] == "Full Access"
|
||||
assert body["is_protected"] is False
|
||||
user_id = body["id"]
|
||||
|
||||
operations_role = db.query(Role).filter_by(name="Operations").one()
|
||||
updated = client.patch(
|
||||
f"/api/access/users/{user_id}",
|
||||
json={"name": "Renamed", "role_id": operations_role.id, "is_active": False},
|
||||
headers=headers,
|
||||
)
|
||||
assert updated.status_code == 200
|
||||
assert updated.json()["name"] == "Renamed"
|
||||
assert updated.json()["role"] == "Operations"
|
||||
assert updated.json()["is_active"] is False
|
||||
|
||||
pw = client.post(
|
||||
f"/api/access/users/{user_id}/password",
|
||||
json={"new_password": "brand-new-pass"},
|
||||
headers=headers,
|
||||
)
|
||||
assert pw.status_code == 200
|
||||
db.expire_all()
|
||||
target = db.query(User).filter_by(id=user_id).one()
|
||||
assert verify_password("brand-new-pass", target.password_hash)
|
||||
|
||||
deleted = client.delete(f"/api/access/users/{user_id}", headers=headers)
|
||||
assert deleted.status_code == 204
|
||||
assert db.query(User).filter_by(id=user_id).one_or_none() is None
|
||||
|
||||
|
||||
def test_create_user_rejects_duplicate_email(access_app_and_db):
|
||||
client, db = access_app_and_db
|
||||
headers = _admin_headers(db)
|
||||
|
||||
response = client.post(
|
||||
"/api/access/users",
|
||||
json={"email": "admin@hunterstockfeeds.com", "name": "Dup"},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 409
|
||||
|
||||
|
||||
def test_manage_users_requires_permission(access_app_and_db):
|
||||
client, db = access_app_and_db
|
||||
ops = db.query(User).filter_by(email="ops@hunterstockfeeds.com").one()
|
||||
headers = {"Authorization": f"Bearer {_token_for(ops)}"}
|
||||
|
||||
response = client.post(
|
||||
"/api/access/users",
|
||||
json={"email": "x@hunterstockfeeds.com", "name": "X"},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_cannot_deactivate_or_delete_self(access_app_and_db):
|
||||
client, db = access_app_and_db
|
||||
admin = db.query(User).filter_by(email="admin@hunterstockfeeds.com").one()
|
||||
headers = {"Authorization": f"Bearer {_token_for(admin)}"}
|
||||
|
||||
deactivate = client.patch(
|
||||
f"/api/access/users/{admin.id}", json={"is_active": False}, headers=headers
|
||||
)
|
||||
assert deactivate.status_code == 400
|
||||
|
||||
delete = client.delete(f"/api/access/users/{admin.id}", headers=headers)
|
||||
assert delete.status_code == 400
|
||||
|
||||
|
||||
def test_lean_users_cannot_be_deleted(access_app_and_db):
|
||||
client, db = access_app_and_db
|
||||
headers = _admin_headers(db)
|
||||
lean_role = db.query(Role).filter_by(name="lean").one()
|
||||
lean_user = User(email="owner@hunterstockfeeds.com", name="Owner", role_id=lean_role.id, is_active=True)
|
||||
db.add(lean_user)
|
||||
db.commit()
|
||||
|
||||
listed = client.get("/api/access/users", headers=headers)
|
||||
assert listed.status_code == 200
|
||||
owner_row = next(row for row in listed.json() if row["id"] == lean_user.id)
|
||||
assert owner_row["is_protected"] is True
|
||||
|
||||
response = client.delete(f"/api/access/users/{lean_user.id}", headers=headers)
|
||||
assert response.status_code == 403
|
||||
assert db.query(User).filter_by(id=lean_user.id).one_or_none() is not None
|
||||
|
||||
|
||||
def test_assignable_roles_lists_all_roles(access_app_and_db):
|
||||
client, db = access_app_and_db
|
||||
headers = _admin_headers(db)
|
||||
|
||||
response = client.get("/api/access/assignable-roles", headers=headers)
|
||||
assert response.status_code == 200
|
||||
names = {row["name"] for row in response.json()}
|
||||
assert names == set(ROLE_DEFINITIONS.keys())
|
||||
|
||||
|
||||
def test_role_management_lists_modules_and_roles_for_admin(access_app_and_db):
|
||||
client, db = access_app_and_db
|
||||
headers = _admin_headers(db)
|
||||
|
||||
modules = client.get("/api/access/role-modules", headers=headers)
|
||||
assert modules.status_code == 200
|
||||
module_keys = {row["key"] for row in modules.json()}
|
||||
assert {"dashboard", "mix_calculator", "roles", "settings"} <= module_keys
|
||||
|
||||
roles = client.get("/api/access/roles", headers=headers)
|
||||
assert roles.status_code == 200
|
||||
admin_role = next(row for row in roles.json() if row["name"] == "Admin")
|
||||
assert admin_role["is_protected"] is True
|
||||
assert admin_role["module_permissions"]["ordering"] == "manage"
|
||||
assert admin_role["module_permissions"]["roles"] == "manage"
|
||||
|
||||
|
||||
def test_role_management_is_blocked_for_non_admin_non_lean_roles(access_app_and_db):
|
||||
client, db = access_app_and_db
|
||||
ops = db.query(User).filter_by(email="ops@hunterstockfeeds.com").one()
|
||||
headers = {"Authorization": f"Bearer {_token_for(ops)}"}
|
||||
|
||||
response = client.get("/api/access/roles", headers=headers)
|
||||
assert response.status_code == 403
|
||||
assert "lean and admin" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_role_management_create_update_delete_custom_role(access_app_and_db):
|
||||
client, db = access_app_and_db
|
||||
headers = _admin_headers(db)
|
||||
|
||||
created = client.post(
|
||||
"/api/access/roles",
|
||||
json={
|
||||
"name": "Reporting Viewer",
|
||||
"description": "Can review dashboards and reporting inputs",
|
||||
"module_permissions": {
|
||||
"dashboard": "view",
|
||||
"products": "view",
|
||||
"settings": "view",
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
assert created.status_code == 201
|
||||
created_body = created.json()
|
||||
assert created_body["module_permissions"]["dashboard"] == "view"
|
||||
assert created_body["module_permissions"]["products"] == "view"
|
||||
assert created_body["module_permissions"]["settings"] == "view"
|
||||
role_id = created_body["id"]
|
||||
|
||||
updated = client.patch(
|
||||
f"/api/access/roles/{role_id}",
|
||||
json={
|
||||
"description": "Can review and edit product data",
|
||||
"module_permissions": {
|
||||
"dashboard": "view",
|
||||
"products": "edit",
|
||||
"settings": "view",
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
assert updated.status_code == 200
|
||||
updated_body = updated.json()
|
||||
assert updated_body["module_permissions"]["products"] == "edit"
|
||||
assert "edit_products" in updated_body["permissions"]
|
||||
|
||||
deleted = client.delete(f"/api/access/roles/{role_id}", headers=headers)
|
||||
assert deleted.status_code == 204
|
||||
assert db.query(Role).filter_by(id=role_id).one_or_none() is None
|
||||
|
||||
|
||||
def test_protected_or_assigned_roles_cannot_be_deleted(access_app_and_db):
|
||||
client, db = access_app_and_db
|
||||
headers = _admin_headers(db)
|
||||
admin_role = db.query(Role).filter_by(name="Admin").one()
|
||||
|
||||
protected = client.delete(f"/api/access/roles/{admin_role.id}", headers=headers)
|
||||
assert protected.status_code == 403
|
||||
|
||||
full_access_role = db.query(Role).filter_by(name="Full Access").one()
|
||||
assigned = client.delete(f"/api/access/roles/{full_access_role.id}", headers=headers)
|
||||
assert assigned.status_code == 400
|
||||
|
||||
Reference in New Issue
Block a user