fischer-agentkit/tests/unit/admin/test_models.py

568 lines
23 KiB
Python

"""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 3 in ``auth_meta``
- ``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_v3(self):
"""V3 adds the department-scoped admin tables."""
assert _SCHEMA_VERSION == 3
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_3_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"] == "3"
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