diff --git a/src/porchlight/store/protocols.py b/src/porchlight/store/protocols.py index 2e21a11..f65588d 100644 --- a/src/porchlight/store/protocols.py +++ b/src/porchlight/store/protocols.py @@ -20,6 +20,10 @@ class UserRepository(Protocol): async def list_users(self, offset: int = 0, limit: int = 100) -> list[User]: ... + async def search_users(self, query: str, offset: int = 0, limit: int = 100) -> list[User]: ... + + async def count_users(self, query: str | None = None) -> int: ... + async def delete(self, userid: str) -> bool: ... diff --git a/src/porchlight/store/sqlite/repositories.py b/src/porchlight/store/sqlite/repositories.py index 2854873..1a2a05f 100644 --- a/src/porchlight/store/sqlite/repositories.py +++ b/src/porchlight/store/sqlite/repositories.py @@ -136,6 +136,32 @@ class SQLiteUserRepository: users.append(self._row_to_user(row, groups)) return users + async def search_users(self, query: str, offset: int = 0, limit: int = 100) -> list[User]: + pattern = f"%{query}%" + async with self._db.execute( + "SELECT * FROM users WHERE username LIKE ? OR email LIKE ? ORDER BY username LIMIT ? OFFSET ?", + (pattern, pattern, limit, offset), + ) as cursor: + rows = await cursor.fetchall() + users = [] + for row in rows: + groups = await self._get_groups(row["userid"]) + users.append(self._row_to_user(row, groups)) + return users + + async def count_users(self, query: str | None = None) -> int: + if query: + pattern = f"%{query}%" + async with self._db.execute( + "SELECT COUNT(*) FROM users WHERE username LIKE ? OR email LIKE ?", + (pattern, pattern), + ) as cursor: + row = await cursor.fetchone() + else: + async with self._db.execute("SELECT COUNT(*) FROM users") as cursor: + row = await cursor.fetchone() + return row[0] if row else 0 + async def delete(self, userid: str) -> bool: cursor = await self._db.execute("DELETE FROM users WHERE userid = ?", (userid,)) await self._db.commit() diff --git a/tests/test_store/test_sqlite_user_repo.py b/tests/test_store/test_sqlite_user_repo.py index ff2c5e6..0239a08 100644 --- a/tests/test_store/test_sqlite_user_repo.py +++ b/tests/test_store/test_sqlite_user_repo.py @@ -159,6 +159,49 @@ async def test_create_duplicate_username(user_repo: SQLiteUserRepository) -> Non await user_repo.create(_make_user(userid="different-id", username="alice")) +async def test_search_users_by_username(user_repo: SQLiteUserRepository) -> None: + await user_repo.create(_make_user(userid="id-1", username="sample_user")) + results = await user_repo.search_users("sample", offset=0, limit=100) + assert len(results) == 1 + assert results[0].userid == "id-1" + + +async def test_search_users_by_email(user_repo: SQLiteUserRepository) -> None: + await user_repo.create(_make_user(email="alice@example.com")) + results = await user_repo.search_users("alice", offset=0, limit=100) + assert len(results) == 1 + + +async def test_search_users_no_match(user_repo: SQLiteUserRepository) -> None: + await user_repo.create(_make_user()) + results = await user_repo.search_users("nonexistent", offset=0, limit=100) + assert len(results) == 0 + + +async def test_search_users_pagination(user_repo: SQLiteUserRepository) -> None: + for i in range(5): + await user_repo.create(_make_user(userid=f"id-{i}", username=f"user{i}", groups=["users"])) + page1 = await user_repo.search_users("user", offset=0, limit=2) + page2 = await user_repo.search_users("user", offset=2, limit=2) + assert len(page1) == 2 + assert len(page2) == 2 + assert page1[0].username != page2[0].username + + +async def test_count_users_no_query(user_repo: SQLiteUserRepository) -> None: + await user_repo.create(_make_user()) + count = await user_repo.count_users() + assert count == 1 + + +async def test_count_users_with_query(user_repo: SQLiteUserRepository) -> None: + await user_repo.create(_make_user()) + count = await user_repo.count_users(query="alice") + assert count == 1 + count = await user_repo.count_users(query="nonexistent") + assert count == 0 + + async def test_roundtrip_preserves_all_fields(user_repo: SQLiteUserRepository) -> None: user = _make_user( preferred_username="ally",