"""Integration tests for #2554 team-management API — employees CRUD, quotas, history. Same pattern as `tests/test_auth_api.py`: real `rbac_guard` + real `auth.router` / `team.router` wired into an isolated FastAPI test app, with an in-memory fake DB (`_Store`/`_FakeDB`) dispatching on SQL text standing in for `tradein_users` / `tradein_sessions` / `account_quota_overrides` / `account_estimate_usage` / `user_events` / `trade_in_estimates`. `app.core.rbac.SessionLocal` (middleware, no FastAPI DI) and `app.core.db.get_db` (auth.router / team.router `Depends(get_db)`) both point at the SAME `_Store` instance per test — a session created via POST /login is immediately visible to rbac_guard's own DB round trip AND to `current_team_actor`. """ from __future__ import annotations import os from datetime import UTC, datetime, timedelta from types import SimpleNamespace from typing import Any from uuid import uuid4 os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from app.api.v1 import auth as auth_router from app.api.v1 import team as team_router from app.core import auth as auth_mod from app.core import config from app.core.db import get_db from app.core.password import hash_password from app.core.rbac import rbac_guard # --------------------------------------------------------------------------- # Fake DB backing tradein_users / tradein_sessions / quota / user_events # --------------------------------------------------------------------------- class _Store: def __init__(self) -> None: self.users: dict[str, dict[str, Any]] = {} # username -> user dict self.sessions: dict[str, dict[str, Any]] = {} self.quota_overrides: dict[str, dict[str, Any]] = {} self.usage: dict[tuple[str, str], int] = {} self.estimates: dict[str, dict[str, Any]] = {} # estimate_id -> result fields self.events: list[dict[str, Any]] = [] # user_events rows (history source) self._next_id = 1 def add_user( self, username: str, password_hash: str | None, *, role: str = "employee", manager_id: int | None = None, is_active: bool = True, display_name: str | None = None, org_name: str | None = None, email: str | None = None, ) -> int: uid = self._next_id self._next_id += 1 self.users[username] = { "id": uid, "username": username, "password_hash": password_hash, "role": role, "manager_id": manager_id, "display_name": display_name, "org_name": org_name, "email": email, "is_active": is_active, "created_at": datetime.now(UTC), } return uid def user_by_id(self, uid: int) -> dict[str, Any] | None: for u in self.users.values(): if u["id"] == uid: return u return None def add_estimate_event( self, username: str, *, address: str | None = "ул. Ленина, 1", area_m2: str | None = "45", rooms: str | None = "2", estimate_id: str | None = None, median_price: int | None = None, confidence: str | None = None, n_analogs: int | None = None, created_at: datetime | None = None, ) -> None: eid = estimate_id or str(uuid4()) self.events.append( { "username": username, "event_type": "estimate_request", "estimate_id": eid, "payload": {"address": address, "area_m2": area_m2, "rooms": rooms}, "created_at": created_at or datetime.now(UTC), } ) if median_price is not None or confidence is not None or n_analogs is not None: self.estimates[eid] = { "median_price": median_price, "confidence": confidence, "n_analogs": n_analogs, } class _Result: """Minimal cursor-result stand-in: `.fetchone()`/`.fetchall()` (attribute-style Row) AND `.mappings().fetchone()`/`.all()` (dict-style RowMapping) — enough surface for the SQL used by app.api.v1.team / app.api.v1.auth / app.services.auth_session / app.services.account_quota.""" def __init__(self, rows: list[dict[str, Any]]) -> None: self._rows = rows def fetchone(self) -> SimpleNamespace | None: return SimpleNamespace(**self._rows[0]) if self._rows else None def fetchall(self) -> list[SimpleNamespace]: return [SimpleNamespace(**r) for r in self._rows] def mappings(self) -> _Mappings: return _Mappings(self._rows) class _Mappings: def __init__(self, rows: list[dict[str, Any]]) -> None: self._rows = rows def fetchone(self) -> dict[str, Any] | None: return dict(self._rows[0]) if self._rows else None def all(self) -> list[dict[str, Any]]: return [dict(r) for r in self._rows] class _FakeDB: """Minimal Session stand-in dispatching on SQL text — see module docstring.""" def __init__(self, store: _Store) -> None: self.store = store def __enter__(self) -> _FakeDB: return self def __exit__(self, *exc: object) -> bool: return False def close(self) -> None: pass def commit(self) -> None: pass def rollback(self) -> None: pass def execute(self, stmt: object, params: dict[str, Any] | None = None) -> _Result: sql = str(stmt) p = params or {} s = self.store # ---- tradein_sessions ---- if "INSERT INTO tradein_sessions" in sql: now = datetime.now(UTC) s.sessions[p["token"]] = { "user_id": p["user_id"], "expires_at": now + timedelta(hours=p["ttl_hours"]), "last_seen_at": now, } return _Result([]) if "UPDATE tradein_sessions" in sql and "SET last_seen_at" in sql: sess = s.sessions.get(p["token"]) if sess is not None: now = datetime.now(UTC) sess["last_seen_at"] = now sess["expires_at"] = now + timedelta(hours=p["ttl_hours"]) return _Result([]) if "DELETE FROM tradein_sessions WHERE token" in sql: s.sessions.pop(p["token"], None) return _Result([]) if "DELETE FROM tradein_sessions WHERE user_id" in sql: uid = p["user_id"] for tok in [t for t, sess in s.sessions.items() if sess["user_id"] == uid]: del s.sessions[tok] return _Result([]) if "FROM tradein_sessions s" in sql and "JOIN tradein_users u" in sql: sess = s.sessions.get(p["token"]) if sess is None: return _Result([]) user = s.user_by_id(sess["user_id"]) if user is None: return _Result([]) return _Result( [ { "user_id": sess["user_id"], "expires_at": sess["expires_at"], "last_seen_at": sess["last_seen_at"], "username": user["username"], "role": user["role"], "display_name": user["display_name"], "org_name": user["org_name"], "email": user["email"], "is_active": user["is_active"], } ] ) # ---- tradein_users: login lookup (get_user_by_username) ---- if "password_hash, role, is_active" in sql and "FROM tradein_users" in sql: user = s.users.get(p["username"]) return _Result([user] if user is not None else []) # ---- tradein_users: create ---- if "INSERT INTO tradein_users" in sql: uid = s._next_id s._next_id += 1 created_at = datetime.now(UTC) row = { "id": uid, "username": p["username"], "password_hash": p["password_hash"], "role": "employee", "manager_id": p["manager_id"], "display_name": p["display_name"], "org_name": p["org_name"], "email": p["email"], "is_active": True, "created_at": created_at, } s.users[p["username"]] = row return _Result([dict(row)]) # ---- tradein_users: manager_id validation ---- if "role = 'manager'" in sql: user = s.user_by_id(p["id"]) match = user is not None and user["role"] == "manager" return _Result([{"id": user["id"]}] if match else []) # ---- tradein_users: list employees (has explicit ORDER BY) ---- if "role = 'employee'" in sql and "ORDER BY created_at DESC" in sql: rows = [u for u in s.users.values() if u["role"] == "employee"] if "manager_id" in p: rows = [u for u in rows if u["manager_id"] == p["manager_id"]] rows = sorted(rows, key=lambda u: u["created_at"], reverse=True) return _Result( [ { "id": u["id"], "username": u["username"], "display_name": u["display_name"], "org_name": u["org_name"], "email": u["email"], "is_active": u["is_active"], "manager_id": u["manager_id"], "created_at": u["created_at"], } for u in rows ] ) # ---- tradein_users: fetch single employee by id ---- if "role = 'employee'" in sql: user = s.user_by_id(p["id"]) if user is None or user["role"] != "employee": return _Result([]) return _Result( [ { "id": user["id"], "username": user["username"], "display_name": user["display_name"], "org_name": user["org_name"], "email": user["email"], "is_active": user["is_active"], "manager_id": user["manager_id"], "created_at": user["created_at"], } ] ) # ---- tradein_users: uniqueness pre-check ---- if sql.strip().startswith("SELECT id FROM tradein_users WHERE username"): user = s.users.get(p["u"]) return _Result([{"id": user["id"]}] if user is not None else []) # ---- tradein_users: update (PATCH) ---- if "UPDATE tradein_users" in sql and "SET display_name = COALESCE" in sql: user = s.user_by_id(p["id"]) assert user is not None if p.get("display_name") is not None: user["display_name"] = p["display_name"] if p.get("org_name") is not None: user["org_name"] = p["org_name"] if p.get("email") is not None: user["email"] = p["email"] if p.get("is_active") is not None: user["is_active"] = p["is_active"] if p.get("password_hash") is not None: user["password_hash"] = p["password_hash"] return _Result([]) # ---- account_quota_overrides upsert ---- if "INSERT INTO account_quota_overrides" in sql: s.quota_overrides[p["username"]] = { "monthly_limit": p["monthly_limit"], "unlimited": False, "note": p["note"], } return _Result([]) if "SELECT unlimited FROM account_quota_overrides" in sql: override = s.quota_overrides.get(p["u"]) return _Result([{"unlimited": override["unlimited"]}] if override else []) if "SELECT monthly_limit FROM account_quota_overrides" in sql: override = s.quota_overrides.get(p["u"]) return _Result([{"monthly_limit": override["monthly_limit"]}] if override else []) # ---- account_estimate_usage ---- if "SELECT used FROM account_estimate_usage" in sql: used = s.usage.get((p["u"], p["p"])) return _Result([{"used": used}] if used is not None else []) # ---- user_events + trade_in_estimates (history) ---- if "FROM user_events ue" in sql: matches = [ e for e in s.events if e["username"] == p["username"] and e["event_type"] == "estimate_request" ] matches.sort(key=lambda e: e["created_at"], reverse=True) page = matches[p["offset"] : p["offset"] + p["limit"]] rows = [] for e in page: extra = s.estimates.get(e["estimate_id"], {}) rows.append( { "estimate_id": e["estimate_id"], "address": e["payload"].get("address"), "area_m2": e["payload"].get("area_m2"), "rooms": e["payload"].get("rooms"), "median_price": extra.get("median_price"), "confidence": extra.get("confidence"), "n_analogs": extra.get("n_analogs"), "created_at": e["created_at"], } ) return _Result(rows) raise AssertionError(f"unhandled fake SQL in test_team_api: {sql!r}") # --------------------------------------------------------------------------- # Test app # --------------------------------------------------------------------------- def _build_test_app(store: _Store) -> FastAPI: app = FastAPI() app.middleware("http")(rbac_guard) app.include_router(auth_router.router, prefix="/api/v1/auth", tags=["auth"]) app.include_router(team_router.router, prefix="/api/v1/team", tags=["team"]) def _override_get_db(): # generator dependency — matches app.core.db.get_db shape yield _FakeDB(store) app.dependency_overrides[get_db] = _override_get_db return app @pytest.fixture(autouse=True) def _reset_state(monkeypatch: pytest.MonkeyPatch) -> None: auth_mod.reset_cache_for_tests() auth_router._LOGIN_LIMITER._hits.clear() monkeypatch.setattr(config.settings, "auth_mode", "dual") # team.py / auth.py events go through schedule_event (own SessionLocal(), fire- # and-forget) — captured into a list instead of hitting a real DB. monkeypatch.setattr(team_router, "schedule_event", lambda **kw: _EVENTS.append(kw)) monkeypatch.setattr(auth_router, "schedule_event", lambda **kw: None) _EVENTS.clear() _EVENTS: list[dict[str, Any]] = [] @pytest.fixture def store() -> _Store: return _Store() @pytest.fixture def client(store: _Store, monkeypatch: pytest.MonkeyPatch) -> TestClient: monkeypatch.setattr("app.core.rbac.SessionLocal", lambda: _FakeDB(store)) # base_url=https:// — login sets a Secure cookie; see test_auth_api.py for why # a plain-http TestClient would silently drop it. return TestClient(_build_test_app(store), base_url="https://testserver") def _login(client: TestClient, username: str, password: str) -> None: resp = client.post("/api/v1/auth/login", json={"username": username, "password": password}) assert resp.status_code == 200, resp.text # --------------------------------------------------------------------------- # POST /employees — happy path + validation # --------------------------------------------------------------------------- def test_manager_creates_employee_forces_own_manager_id(client: TestClient, store: _Store) -> None: mgr_id = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") _login(client, "mgr_a", "Secret123!") resp = client.post( "/api/v1/team/employees", json={"username": "emp_alice", "password": "Secret123!", "display_name": "Алиса"}, ) assert resp.status_code == 201, resp.text body = resp.json() assert body["username"] == "emp_alice" assert body["manager_id"] == mgr_id assert body["quota"]["limit"] > 0 assert store.users["emp_alice"]["password_hash"] is not None assert any(e["event_type"] == "employee_created" for e in _EVENTS) created_event = next(e for e in _EVENTS if e["event_type"] == "employee_created") # Пароль никогда не попадает в аудит-событие. assert "Secret123!" not in str(created_event) def test_admin_creates_employee_with_explicit_manager_id(client: TestClient, store: _Store) -> None: store.add_user("admin1", hash_password("Secret123!"), role="admin") mgr_id = store.add_user("mgr_b", hash_password("Secret123!"), role="manager") _login(client, "admin1", "Secret123!") resp = client.post( "/api/v1/team/employees", json={"username": "emp_bob", "password": "Secret123!", "manager_id": mgr_id}, ) assert resp.status_code == 201, resp.text assert resp.json()["manager_id"] == mgr_id def test_admin_creates_employee_without_manager_id(client: TestClient, store: _Store) -> None: store.add_user("admin1", hash_password("Secret123!"), role="admin") _login(client, "admin1", "Secret123!") resp = client.post( "/api/v1/team/employees", json={"username": "emp_free", "password": "Secret123!"} ) assert resp.status_code == 201, resp.text assert resp.json()["manager_id"] is None def test_admin_create_employee_invalid_manager_id_422(client: TestClient, store: _Store) -> None: store.add_user("admin1", hash_password("Secret123!"), role="admin") _login(client, "admin1", "Secret123!") resp = client.post( "/api/v1/team/employees", json={"username": "emp_x", "password": "Secret123!", "manager_id": 999}, ) assert resp.status_code == 422 def test_create_employee_non_ascii_username_422(client: TestClient, store: _Store) -> None: store.add_user("mgr_a", hash_password("Secret123!"), role="manager") _login(client, "mgr_a", "Secret123!") resp = client.post( "/api/v1/team/employees", json={"username": "сотрудник", "password": "Secret123!"}, ) assert resp.status_code == 422 def test_create_employee_duplicate_username_409(client: TestClient, store: _Store) -> None: store.add_user("mgr_a", hash_password("Secret123!"), role="manager") store.add_user("emp_dup", hash_password("Secret123!"), role="employee") _login(client, "mgr_a", "Secret123!") resp = client.post( "/api/v1/team/employees", json={"username": "emp_dup", "password": "Secret123!"}, ) assert resp.status_code == 409 def test_create_employee_no_session_401(client: TestClient) -> None: resp = client.post( "/api/v1/team/employees", json={"username": "emp_x", "password": "Secret123!"} ) assert resp.status_code == 401 def test_create_employee_employee_role_403(client: TestClient, store: _Store) -> None: store.add_user("emp_a", hash_password("Secret123!"), role="employee") _login(client, "emp_a", "Secret123!") resp = client.post( "/api/v1/team/employees", json={"username": "emp_x", "password": "Secret123!"} ) assert resp.status_code == 403 def test_employee_role_403_on_all_team_routes(client: TestClient, store: _Store) -> None: store.add_user("emp_a", hash_password("Secret123!"), role="employee") _login(client, "emp_a", "Secret123!") assert client.get("/api/v1/team/employees").status_code == 403 assert client.patch("/api/v1/team/employees/1", json={}).status_code == 403 assert client.get("/api/v1/team/employees/1/history").status_code == 403 # --------------------------------------------------------------------------- # Org isolation — manager A vs manager B # --------------------------------------------------------------------------- def test_manager_a_cannot_see_manager_b_employee(client: TestClient, store: _Store) -> None: mgr_a = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") mgr_b = store.add_user("mgr_b", hash_password("Secret123!"), role="manager") store.add_user("emp_a", hash_password("Secret123!"), role="employee", manager_id=mgr_a) store.add_user("emp_b", hash_password("Secret123!"), role="employee", manager_id=mgr_b) _login(client, "mgr_a", "Secret123!") resp = client.get("/api/v1/team/employees") assert resp.status_code == 200 usernames = {e["username"] for e in resp.json()} assert usernames == {"emp_a"} def test_manager_a_patch_manager_b_employee_404(client: TestClient, store: _Store) -> None: mgr_a = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") mgr_b = store.add_user("mgr_b", hash_password("Secret123!"), role="manager") emp_b_id = store.add_user( "emp_b", hash_password("Secret123!"), role="employee", manager_id=mgr_b ) assert mgr_a # used only to seed manager_id != mgr_b _login(client, "mgr_a", "Secret123!") resp = client.patch(f"/api/v1/team/employees/{emp_b_id}", json={"display_name": "hacked"}) assert resp.status_code == 404 def test_manager_a_history_manager_b_employee_404(client: TestClient, store: _Store) -> None: mgr_a = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") mgr_b = store.add_user("mgr_b", hash_password("Secret123!"), role="manager") emp_b_id = store.add_user( "emp_b", hash_password("Secret123!"), role="employee", manager_id=mgr_b ) assert mgr_a _login(client, "mgr_a", "Secret123!") resp = client.get(f"/api/v1/team/employees/{emp_b_id}/history") assert resp.status_code == 404 def test_manager_post_with_foreign_manager_id_creates_under_self( client: TestClient, store: _Store ) -> None: """POST с чужим manager_id в теле от лица manager — ИГНОРИРУЕТСЯ, сотрудник создаётся под ЕГО собственным manager_id, не под указанным чужим.""" mgr_a_id = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") mgr_b_id = store.add_user("mgr_b", hash_password("Secret123!"), role="manager") assert mgr_a_id != mgr_b_id _login(client, "mgr_a", "Secret123!") resp = client.post( "/api/v1/team/employees", json={ "username": "emp_spoof", "password": "Secret123!", "manager_id": mgr_b_id, }, ) assert resp.status_code == 201, resp.text assert resp.json()["manager_id"] == mgr_a_id assert store.users["emp_spoof"]["manager_id"] == mgr_a_id # --------------------------------------------------------------------------- # PATCH /employees/{id} — block/unblock/quota/profile/password # --------------------------------------------------------------------------- def test_block_employee_revokes_sessions(client: TestClient, store: _Store) -> None: mgr_id = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") emp_id = store.add_user( "emp_a", hash_password("EmpSecret1!"), role="employee", manager_id=mgr_id ) # Employee logs in first — real session created via the real auth flow. emp_client = TestClient(client.app, base_url="https://testserver") _login(emp_client, "emp_a", "EmpSecret1!") assert any(sess["user_id"] == emp_id for sess in store.sessions.values()) _login(client, "mgr_a", "Secret123!") resp = client.patch(f"/api/v1/team/employees/{emp_id}", json={"is_active": False}) assert resp.status_code == 200, resp.text assert resp.json()["is_active"] is False # Blocking must have revoked ALL of emp_a's sessions. assert not any(sess["user_id"] == emp_id for sess in store.sessions.values()) assert any(e["event_type"] == "employee_blocked" for e in _EVENTS) def test_unblock_employee_event(client: TestClient, store: _Store) -> None: mgr_id = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") emp_id = store.add_user( "emp_a", hash_password("Secret123!"), role="employee", manager_id=mgr_id, is_active=False ) _login(client, "mgr_a", "Secret123!") resp = client.patch(f"/api/v1/team/employees/{emp_id}", json={"is_active": True}) assert resp.status_code == 200 assert resp.json()["is_active"] is True assert any(e["event_type"] == "employee_unblocked" for e in _EVENTS) def test_patch_monthly_limit_reflected_in_quota_status(client: TestClient, store: _Store) -> None: mgr_id = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") emp_id = store.add_user( "emp_a", hash_password("Secret123!"), role="employee", manager_id=mgr_id ) _login(client, "mgr_a", "Secret123!") resp = client.patch(f"/api/v1/team/employees/{emp_id}", json={"monthly_limit": 30}) assert resp.status_code == 200, resp.text assert resp.json()["quota"]["limit"] == 30 assert any(e["event_type"] == "quota_changed" for e in _EVENTS) listing = client.get("/api/v1/team/employees") assert listing.status_code == 200 entry = next(e for e in listing.json() if e["id"] == emp_id) assert entry["quota"]["limit"] == 30 def test_patch_reset_password_no_password_in_events(client: TestClient, store: _Store) -> None: mgr_id = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") emp_id = store.add_user( "emp_a", hash_password("OldSecret1!"), role="employee", manager_id=mgr_id ) _login(client, "mgr_a", "Secret123!") resp = client.patch(f"/api/v1/team/employees/{emp_id}", json={"new_password": "NewSecret2!"}) assert resp.status_code == 200, resp.text assert any(e["event_type"] == "employee_password_reset" for e in _EVENTS) assert "NewSecret2!" not in str(_EVENTS) emp_client = TestClient(client.app, base_url="https://testserver") login_resp = emp_client.post( "/api/v1/auth/login", json={"username": "emp_a", "password": "NewSecret2!"} ) assert login_resp.status_code == 200 def test_patch_no_session_401(client: TestClient, store: _Store) -> None: mgr_id = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") emp_id = store.add_user( "emp_a", hash_password("Secret123!"), role="employee", manager_id=mgr_id ) resp = client.patch(f"/api/v1/team/employees/{emp_id}", json={"is_active": False}) assert resp.status_code == 401 # --------------------------------------------------------------------------- # GET /employees — list + admin filter # --------------------------------------------------------------------------- def test_admin_sees_all_employees_and_filters_by_manager(client: TestClient, store: _Store) -> None: store.add_user("admin1", hash_password("Secret123!"), role="admin") mgr_a = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") mgr_b = store.add_user("mgr_b", hash_password("Secret123!"), role="manager") store.add_user("emp_a", hash_password("Secret123!"), role="employee", manager_id=mgr_a) store.add_user("emp_b", hash_password("Secret123!"), role="employee", manager_id=mgr_b) _login(client, "admin1", "Secret123!") resp_all = client.get("/api/v1/team/employees") assert resp_all.status_code == 200 assert {e["username"] for e in resp_all.json()} == {"emp_a", "emp_b"} resp_filtered = client.get("/api/v1/team/employees", params={"manager_id": mgr_a}) assert resp_filtered.status_code == 200 assert {e["username"] for e in resp_filtered.json()} == {"emp_a"} # --------------------------------------------------------------------------- # GET /employees/{id}/history # --------------------------------------------------------------------------- def test_employee_history_happy_path(client: TestClient, store: _Store) -> None: mgr_id = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") emp_id = store.add_user( "emp_a", hash_password("Secret123!"), role="employee", manager_id=mgr_id ) now = datetime.now(UTC) store.add_estimate_event( "emp_a", address="ул. Малышева, 10", median_price=6_500_000, confidence="high", n_analogs=12, created_at=now - timedelta(minutes=5), ) store.add_estimate_event("emp_a", address="ул. Мамина-Сибиряка, 5", created_at=now) _login(client, "mgr_a", "Secret123!") resp = client.get(f"/api/v1/team/employees/{emp_id}/history") assert resp.status_code == 200, resp.text body = resp.json() assert len(body) == 2 assert body[0]["address"] == "ул. Мамина-Сибиряка, 5" # most recent first priced = next(e for e in body if e["address"] == "ул. Малышева, 10") assert priced["median_price"] == 6_500_000 assert priced["confidence"] == "high" def test_employee_history_pagination(client: TestClient, store: _Store) -> None: mgr_id = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") emp_id = store.add_user( "emp_a", hash_password("Secret123!"), role="employee", manager_id=mgr_id ) for i in range(5): store.add_estimate_event("emp_a", address=f"адрес-{i}") _login(client, "mgr_a", "Secret123!") resp = client.get(f"/api/v1/team/employees/{emp_id}/history", params={"limit": 2, "offset": 1}) assert resp.status_code == 200 assert len(resp.json()) == 2 def test_employee_history_limit_max_200(client: TestClient, store: _Store) -> None: mgr_id = store.add_user("mgr_a", hash_password("Secret123!"), role="manager") emp_id = store.add_user( "emp_a", hash_password("Secret123!"), role="employee", manager_id=mgr_id ) _login(client, "mgr_a", "Secret123!") resp = client.get(f"/api/v1/team/employees/{emp_id}/history", params={"limit": 500}) assert resp.status_code == 422