"""Unit tests for auth.models V3 — department-scoped admin tables (U1). Covers: - ``init_auth_db`` creates the new V3 tables (departments, user_departments, department_skill_bindings, department_kb_bindings, department_quotas) - ``init_auth_db`` is idempotent (calling twice does not error) - ``_SCHEMA_VERSION`` is recorded as 4 in ``auth_meta`` (V4 adds skill_states) - ``departments`` insert + query round-trip - ``user_departments`` many-to-many relationship (one user → many departments, one department → many users) - ``department_skill_bindings`` UNIQUE (department_id, skill_name) constraint - ``department_quotas`` UNIQUE (department_id, quota_type, period) constraint - ``department_row_to_dict`` / ``user_department_row_to_dict`` helpers - Indexes are created for the common access patterns """ from __future__ import annotations import sqlite3 import uuid from datetime import datetime, timezone from pathlib import Path import aiosqlite import pytest from agentkit.server.auth.models import ( DepartmentKbBindingModel, DepartmentModel, DepartmentQuotaModel, DepartmentSkillBindingModel, UserDepartmentModel, _SCHEMA_VERSION, department_row_to_dict, init_auth_db, user_department_row_to_dict, ) # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture async def fresh_db(tmp_path: Path) -> Path: """A brand-new auth DB on a fresh path (no data).""" db_path = tmp_path / "auth.db" await init_auth_db(db_path) return db_path def _now_iso() -> str: return datetime.now(timezone.utc).isoformat() async def _insert_department( db: aiosqlite.Connection, *, dept_id: str | None = None, name: str | None = None, description: str | None = None, is_active: bool = True, ) -> str: """Insert a minimal department row and return its id.""" dept_id = dept_id or str(uuid.uuid4()) name = name or f"dept-{dept_id[:8]}" await db.execute( "INSERT INTO departments (id, name, description, is_active, created_at) " "VALUES (?, ?, ?, ?, ?)", (dept_id, name, description, 1 if is_active else 0, _now_iso()), ) return dept_id async def _insert_user(db: aiosqlite.Connection, *, user_id: str | None = None) -> str: """Insert a minimal user row and return its id.""" user_id = user_id or str(uuid.uuid4()) now_iso = _now_iso() await db.execute( "INSERT INTO users " "(id, username, email, password_hash, role, is_active, " " is_terminal_authorized, is_server_terminal_authorized, " " created_at, updated_at) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ( user_id, f"user-{user_id[:8]}", f"{user_id[:8]}@example.com", "$2b$12$placeholder.hash.placeholder.hash.placeholder.hash", "member", 1, 0, 0, now_iso, now_iso, ), ) return user_id async def _list_index_names(db: aiosqlite.Connection, table: str) -> set[str]: """Return the set of index names for a table.""" db.row_factory = aiosqlite.Row cursor = await db.execute(f"PRAGMA index_list({table})") rows = await cursor.fetchall() return {row["name"] for row in rows} async def _list_table_names(db: aiosqlite.Connection) -> set[str]: """Return the set of table names in the SQLite file.""" cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table'") rows = await cursor.fetchall() return {row[0] for row in rows} # --------------------------------------------------------------------------- # _SCHEMA_VERSION # --------------------------------------------------------------------------- class TestSchemaVersion: def test_schema_version_is_v4(self): """V4 adds the skill_states table (U6 — Admin Console).""" assert _SCHEMA_VERSION == 4 def test_sqlalchemy_model_table_names(self): assert DepartmentModel.__tablename__ == "departments" assert UserDepartmentModel.__tablename__ == "user_departments" assert DepartmentSkillBindingModel.__tablename__ == "department_skill_bindings" assert DepartmentKbBindingModel.__tablename__ == "department_kb_bindings" assert DepartmentQuotaModel.__tablename__ == "department_quotas" # --------------------------------------------------------------------------- # init_auth_db: table creation + idempotency # --------------------------------------------------------------------------- class TestInitAuthDbTables: async def test_creates_departments_table(self, fresh_db: Path): async with aiosqlite.connect(str(fresh_db)) as db: tables = await _list_table_names(db) assert "departments" in tables async def test_creates_user_departments_table(self, fresh_db: Path): async with aiosqlite.connect(str(fresh_db)) as db: tables = await _list_table_names(db) assert "user_departments" in tables async def test_creates_department_skill_bindings_table(self, fresh_db: Path): async with aiosqlite.connect(str(fresh_db)) as db: tables = await _list_table_names(db) assert "department_skill_bindings" in tables async def test_creates_department_kb_bindings_table(self, fresh_db: Path): async with aiosqlite.connect(str(fresh_db)) as db: tables = await _list_table_names(db) assert "department_kb_bindings" in tables async def test_creates_department_quotas_table(self, fresh_db: Path): async with aiosqlite.connect(str(fresh_db)) as db: tables = await _list_table_names(db) assert "department_quotas" in tables async def test_records_schema_version_4_in_auth_meta(self, fresh_db: Path): async with aiosqlite.connect(str(fresh_db)) as db: db.row_factory = aiosqlite.Row cursor = await db.execute("SELECT value FROM auth_meta WHERE key='schema_version'") row = await cursor.fetchone() assert row is not None assert row["value"] == "4" assert row["value"] == str(_SCHEMA_VERSION) async def test_init_auth_db_is_idempotent(self, tmp_path: Path): """Calling init_auth_db twice on the same path must not error.""" db_path = tmp_path / "auth.db" await init_auth_db(db_path) # Second call should be a no-op (CREATE TABLE IF NOT EXISTS + idempotent # meta upsert). Must not raise. await init_auth_db(db_path) async with aiosqlite.connect(str(db_path)) as db: tables = await _list_table_names(db) assert "departments" in tables assert "user_departments" in tables # --------------------------------------------------------------------------- # Indexes # --------------------------------------------------------------------------- class TestDepartmentIndexes: async def test_user_departments_user_id_index(self, fresh_db: Path): async with aiosqlite.connect(str(fresh_db)) as db: indexes = await _list_index_names(db, "user_departments") assert "idx_user_departments_user_id" in indexes async def test_user_departments_department_id_index(self, fresh_db: Path): async with aiosqlite.connect(str(fresh_db)) as db: indexes = await _list_index_names(db, "user_departments") assert "idx_user_departments_department_id" in indexes async def test_department_skill_bindings_department_id_index(self, fresh_db: Path): async with aiosqlite.connect(str(fresh_db)) as db: indexes = await _list_index_names(db, "department_skill_bindings") assert "idx_department_skill_bindings_department_id" in indexes async def test_department_kb_bindings_department_id_index(self, fresh_db: Path): async with aiosqlite.connect(str(fresh_db)) as db: indexes = await _list_index_names(db, "department_kb_bindings") assert "idx_department_kb_bindings_department_id" in indexes async def test_department_quotas_department_id_index(self, fresh_db: Path): async with aiosqlite.connect(str(fresh_db)) as db: indexes = await _list_index_names(db, "department_quotas") assert "idx_department_quotas_department_id" in indexes # --------------------------------------------------------------------------- # departments: insert + query # --------------------------------------------------------------------------- class TestDepartmentsCrud: async def test_insert_and_query_department(self, fresh_db: Path): dept_id = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await _insert_department( db, dept_id=dept_id, name="Engineering", description="Software engineering department", ) await db.commit() db.row_factory = aiosqlite.Row cursor = await db.execute("SELECT * FROM departments WHERE id=?", (dept_id,)) row = await cursor.fetchone() assert row is not None assert row["id"] == dept_id assert row["name"] == "Engineering" assert row["description"] == "Software engineering department" assert bool(row["is_active"]) is True async def test_department_name_is_unique(self, fresh_db: Path): async with aiosqlite.connect(str(fresh_db)) as db: await _insert_department(db, name="HR", description="Human Resources") await db.commit() # Inserting a second department with the same name must fail. with pytest.raises(sqlite3.IntegrityError): await _insert_department(db, name="HR", description="Duplicate") await db.commit() async def test_department_is_active_defaults_to_true(self, fresh_db: Path): """Insert without is_active → column should default to 1 (True).""" dept_id = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await db.execute( "INSERT INTO departments (id, name, created_at) VALUES (?, ?, ?)", (dept_id, "DefaultActive", _now_iso()), ) await db.commit() db.row_factory = aiosqlite.Row cursor = await db.execute("SELECT is_active FROM departments WHERE id=?", (dept_id,)) row = await cursor.fetchone() assert row is not None assert bool(row["is_active"]) is True async def test_department_description_is_nullable(self, fresh_db: Path): dept_id = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await db.execute( "INSERT INTO departments (id, name, created_at) VALUES (?, ?, ?)", (dept_id, "NoDescription", _now_iso()), ) await db.commit() db.row_factory = aiosqlite.Row cursor = await db.execute("SELECT description FROM departments WHERE id=?", (dept_id,)) row = await cursor.fetchone() assert row is not None assert row["description"] is None # --------------------------------------------------------------------------- # user_departments: many-to-many relationship # --------------------------------------------------------------------------- class TestUserDepartmentsManyToMany: async def test_user_can_belong_to_multiple_departments(self, fresh_db: Path): user_id = str(uuid.uuid4()) dept_a = str(uuid.uuid4()) dept_b = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await _insert_user(db, user_id=user_id) await _insert_department(db, dept_id=dept_a, name="DeptA") await _insert_department(db, dept_id=dept_b, name="DeptB") now = _now_iso() await db.executemany( "INSERT INTO user_departments (user_id, department_id, created_at) " "VALUES (?, ?, ?)", [(user_id, dept_a, now), (user_id, dept_b, now)], ) await db.commit() db.row_factory = aiosqlite.Row cursor = await db.execute( "SELECT department_id FROM user_departments WHERE user_id=? ORDER BY department_id", (user_id,), ) rows = await cursor.fetchall() dept_ids = [row["department_id"] for row in rows] assert dept_ids == sorted([dept_a, dept_b]) async def test_department_can_have_multiple_users(self, fresh_db: Path): dept_id = str(uuid.uuid4()) user_a = str(uuid.uuid4()) user_b = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await _insert_department(db, dept_id=dept_id, name="Shared") await _insert_user(db, user_id=user_a) await _insert_user(db, user_id=user_b) now = _now_iso() await db.executemany( "INSERT INTO user_departments (user_id, department_id, created_at) " "VALUES (?, ?, ?)", [(user_a, dept_id, now), (user_b, dept_id, now)], ) await db.commit() db.row_factory = aiosqlite.Row cursor = await db.execute( "SELECT user_id FROM user_departments WHERE department_id=? ORDER BY user_id", (dept_id,), ) rows = await cursor.fetchall() user_ids = [row["user_id"] for row in rows] assert user_ids == sorted([user_a, user_b]) async def test_composite_pk_prevents_duplicate_pair(self, fresh_db: Path): """The (user_id, department_id) composite PK rejects duplicate pairs.""" user_id = str(uuid.uuid4()) dept_id = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await _insert_user(db, user_id=user_id) await _insert_department(db, dept_id=dept_id, name="Unique") now = _now_iso() await db.execute( "INSERT INTO user_departments (user_id, department_id, created_at) " "VALUES (?, ?, ?)", (user_id, dept_id, now), ) await db.commit() with pytest.raises(sqlite3.IntegrityError): await db.execute( "INSERT INTO user_departments (user_id, department_id, created_at) " "VALUES (?, ?, ?)", (user_id, dept_id, now), ) await db.commit() # --------------------------------------------------------------------------- # department_skill_bindings: UNIQUE constraint # --------------------------------------------------------------------------- class TestDepartmentSkillBindingsUnique: async def test_unique_department_skill_pair(self, fresh_db: Path): dept_id = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await _insert_department(db, dept_id=dept_id, name="Bindings") now = _now_iso() await db.execute( "INSERT INTO department_skill_bindings " "(id, department_id, skill_name, created_at) VALUES (?, ?, ?, ?)", (str(uuid.uuid4()), dept_id, "code_review", now), ) await db.commit() # Same (department_id, skill_name) pair must fail, even with a new id. with pytest.raises(sqlite3.IntegrityError): await db.execute( "INSERT INTO department_skill_bindings " "(id, department_id, skill_name, created_at) VALUES (?, ?, ?, ?)", (str(uuid.uuid4()), dept_id, "code_review", now), ) await db.commit() async def test_same_skill_name_in_different_departments_is_allowed(self, fresh_db: Path): dept_a = str(uuid.uuid4()) dept_b = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await _insert_department(db, dept_id=dept_a, name="DeptA") await _insert_department(db, dept_id=dept_b, name="DeptB") now = _now_iso() await db.executemany( "INSERT INTO department_skill_bindings " "(id, department_id, skill_name, created_at) VALUES (?, ?, ?, ?)", [ (str(uuid.uuid4()), dept_a, "shared_skill", now), (str(uuid.uuid4()), dept_b, "shared_skill", now), ], ) await db.commit() db.row_factory = aiosqlite.Row cursor = await db.execute( "SELECT COUNT(*) AS c FROM department_skill_bindings " "WHERE skill_name='shared_skill'" ) row = await cursor.fetchone() assert row["c"] == 2 # --------------------------------------------------------------------------- # department_quotas: UNIQUE constraint # --------------------------------------------------------------------------- class TestDepartmentQuotasUnique: async def test_unique_department_quota_type_period(self, fresh_db: Path): dept_id = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await _insert_department(db, dept_id=dept_id, name="Quota") now = _now_iso() await db.execute( "INSERT INTO department_quotas " "(id, department_id, quota_type, limit_value, period, updated_at) " "VALUES (?, ?, ?, ?, ?, ?)", (str(uuid.uuid4()), dept_id, "token_limit", "10000", "daily", now), ) await db.commit() # Same (department_id, quota_type, period) triple must fail. with pytest.raises(sqlite3.IntegrityError): await db.execute( "INSERT INTO department_quotas " "(id, department_id, quota_type, limit_value, period, updated_at) " "VALUES (?, ?, ?, ?, ?, ?)", (str(uuid.uuid4()), dept_id, "token_limit", "20000", "daily", now), ) await db.commit() async def test_same_quota_type_different_period_is_allowed(self, fresh_db: Path): dept_id = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await _insert_department(db, dept_id=dept_id, name="QuotaPeriods") now = _now_iso() await db.executemany( "INSERT INTO department_quotas " "(id, department_id, quota_type, limit_value, period, updated_at) " "VALUES (?, ?, ?, ?, ?, ?)", [ (str(uuid.uuid4()), dept_id, "token_limit", "10000", "daily", now), (str(uuid.uuid4()), dept_id, "token_limit", "300000", "monthly", now), ], ) await db.commit() db.row_factory = aiosqlite.Row cursor = await db.execute( "SELECT period, limit_value FROM department_quotas " "WHERE department_id=? AND quota_type='token_limit' " "ORDER BY period", (dept_id,), ) rows = await cursor.fetchall() assert len(rows) == 2 assert rows[0]["period"] == "daily" assert rows[0]["limit_value"] == "10000" assert rows[1]["period"] == "monthly" assert rows[1]["limit_value"] == "300000" async def test_quota_period_defaults_to_daily(self, fresh_db: Path): dept_id = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await _insert_department(db, dept_id=dept_id, name="DefaultPeriod") await db.execute( "INSERT INTO department_quotas " "(id, department_id, quota_type, limit_value, updated_at) " "VALUES (?, ?, ?, ?, ?)", (str(uuid.uuid4()), dept_id, "cost_limit", "10.00", _now_iso()), ) await db.commit() db.row_factory = aiosqlite.Row cursor = await db.execute( "SELECT period FROM department_quotas WHERE department_id=?", (dept_id,), ) row = await cursor.fetchone() assert row is not None assert row["period"] == "daily" # --------------------------------------------------------------------------- # row_to_dict helpers # --------------------------------------------------------------------------- class TestRowToDictHelpers: async def test_department_row_to_dict(self, fresh_db: Path): dept_id = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await _insert_department( db, dept_id=dept_id, name="HelperTest", description="Testing the helper", is_active=False, ) await db.commit() db.row_factory = aiosqlite.Row cursor = await db.execute("SELECT * FROM departments WHERE id=?", (dept_id,)) row = await cursor.fetchone() d = department_row_to_dict(row) assert d["id"] == dept_id assert d["name"] == "HelperTest" assert d["description"] == "Testing the helper" assert isinstance(d["is_active"], bool) assert d["is_active"] is False assert "created_at" in d async def test_department_row_to_dict_normalizes_is_active(self, fresh_db: Path): """DB stores 0/1; helper should return Python bool.""" dept_id = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await _insert_department(db, dept_id=dept_id, name="BoolCheck", is_active=True) await db.commit() db.row_factory = aiosqlite.Row cursor = await db.execute("SELECT * FROM departments WHERE id=?", (dept_id,)) row = await cursor.fetchone() d = department_row_to_dict(row) assert isinstance(d["is_active"], bool) assert d["is_active"] is True async def test_user_department_row_to_dict(self, fresh_db: Path): user_id = str(uuid.uuid4()) dept_id = str(uuid.uuid4()) async with aiosqlite.connect(str(fresh_db)) as db: await _insert_user(db, user_id=user_id) await _insert_department(db, dept_id=dept_id, name="UserDeptHelper") now = _now_iso() await db.execute( "INSERT INTO user_departments (user_id, department_id, created_at) " "VALUES (?, ?, ?)", (user_id, dept_id, now), ) await db.commit() db.row_factory = aiosqlite.Row cursor = await db.execute( "SELECT * FROM user_departments WHERE user_id=? AND department_id=?", (user_id, dept_id), ) row = await cursor.fetchone() d = user_department_row_to_dict(row) assert d["user_id"] == user_id assert d["department_id"] == dept_id assert d["created_at"] == now