568 lines
23 KiB
Python
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
|