From ae119ec5e100e4aa480a0dc1e5998f161104335b Mon Sep 17 00:00:00 2001 From: itqop Date: Tue, 11 Nov 2025 22:39:54 +0300 Subject: [PATCH] release: update version to 0.2.0 and add schema to models --- .gitea/workflows/docker-pull.yml | 24 +- pyproject.toml | 2 +- src/hubgw/models/base.py | 7 +- src/hubgw/models/cooldown.py | 3 +- src/hubgw/models/home.py | 3 +- src/hubgw/models/luckperms.py | 3 +- src/hubgw/models/punishment.py | 7 +- src/hubgw/models/teleport_history.py | 2 +- src/hubgw/models/users.py | 5 +- src/hubgw/models/warp.py | 1 + src/hubgw/models/whitelist.py | 1 + src/hubgw/services/whitelist_service.py | 2 + tests/integration/test_whitelist.py | 701 ++++++++++-------------- 13 files changed, 331 insertions(+), 430 deletions(-) diff --git a/.gitea/workflows/docker-pull.yml b/.gitea/workflows/docker-pull.yml index 77a7403..861c0f2 100644 --- a/.gitea/workflows/docker-pull.yml +++ b/.gitea/workflows/docker-pull.yml @@ -10,20 +10,28 @@ jobs: build-and-push: if: contains(github.event.head_commit.message, 'release') runs-on: ubuntu-22.04 - + steps: - name: Checkout code uses: actions/checkout@v4 - + + - name: Set release version manually + id: version + run: echo "version=0.2.0" >> "$GITHUB_OUTPUT" + + - name: Compute short SHA + id: gitvars + run: echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - + - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - + - name: Build and push uses: docker/build-push-action@v6 with: @@ -32,6 +40,12 @@ jobs: push: true tags: | itqop/hubgw:latest - itqop/hubgw:${{ gitea.sha }} + itqop/hubgw:${{ steps.version.outputs.version }} + itqop/hubgw:${{ steps.version.outputs.version }}-${{ steps.gitvars.outputs.short_sha }} + labels: | + org.opencontainers.image.title=hubgw + org.opencontainers.image.version=${{ steps.version.outputs.version }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.source=${{ github.repository }} cache-from: type=registry,ref=itqop/hubgw:buildcache cache-to: type=registry,ref=itqop/hubgw:buildcache,mode=max diff --git a/pyproject.toml b/pyproject.toml index 61f1d60..c8f09bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "hubgw" -version = "0.1.0" +version = "0.2.0" description = "FastAPI Gateway for HubMC" authors = ["itqop "] packages = [ diff --git a/src/hubgw/models/base.py b/src/hubgw/models/base.py index c3dc2cc..091a45b 100644 --- a/src/hubgw/models/base.py +++ b/src/hubgw/models/base.py @@ -2,7 +2,7 @@ from sqlalchemy import Column, DateTime, func -from sqlalchemy.orm import DeclarativeBase +from sqlalchemy.orm import DeclarativeBase, declared_attr class Base(DeclarativeBase): @@ -12,3 +12,8 @@ class Base(DeclarativeBase): updated_at = Column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) + + @declared_attr.directive + def __table_args__(cls): + """Set default schema for all tables.""" + return {"schema": "hubmc"} diff --git a/src/hubgw/models/cooldown.py b/src/hubgw/models/cooldown.py index e76d153..03ea016 100644 --- a/src/hubgw/models/cooldown.py +++ b/src/hubgw/models/cooldown.py @@ -15,6 +15,7 @@ class Cooldown(Base): UniqueConstraint( "player_uuid", "cooldown_type", name="idx_hub_cooldowns_player_type" ), + {"schema": "hubmc"}, ) id = Column( @@ -22,7 +23,7 @@ class Cooldown(Base): ) player_uuid = Column( String(36), - ForeignKey("luckperms_players.uuid", ondelete="CASCADE"), + ForeignKey("hubmc.luckperms_players.uuid", ondelete="CASCADE"), nullable=False, index=True, ) diff --git a/src/hubgw/models/home.py b/src/hubgw/models/home.py index 0c3aaeb..c31d090 100644 --- a/src/hubgw/models/home.py +++ b/src/hubgw/models/home.py @@ -13,6 +13,7 @@ class Home(Base): __tablename__ = "hub_homes" __table_args__ = ( UniqueConstraint("player_uuid", "name", name="idx_hub_homes_player_name"), + {"schema": "hubmc"}, ) id = Column( @@ -20,7 +21,7 @@ class Home(Base): ) player_uuid = Column( String(36), - ForeignKey("luckperms_players.uuid", ondelete="CASCADE"), + ForeignKey("hubmc.luckperms_players.uuid", ondelete="CASCADE"), nullable=False, index=True, ) diff --git a/src/hubgw/models/luckperms.py b/src/hubgw/models/luckperms.py index 3e93541..11f17c8 100644 --- a/src/hubgw/models/luckperms.py +++ b/src/hubgw/models/luckperms.py @@ -41,12 +41,13 @@ class LuckPermsUserPermission(Base): "server", "world", ), + {"schema": "hubmc"}, ) id = Column(Integer, primary_key=True, autoincrement=True) uuid = Column( String(36), - ForeignKey("luckperms_players.uuid", ondelete="CASCADE"), + ForeignKey("hubmc.luckperms_players.uuid", ondelete="CASCADE"), nullable=False, index=True, ) diff --git a/src/hubgw/models/punishment.py b/src/hubgw/models/punishment.py index 2abd20a..a5e982c 100644 --- a/src/hubgw/models/punishment.py +++ b/src/hubgw/models/punishment.py @@ -33,6 +33,7 @@ class Punishment(Base): "player_ip", postgresql_where=Column("player_ip") != None, ), + {"schema": "hubmc"}, ) id = Column( @@ -40,7 +41,7 @@ class Punishment(Base): ) player_uuid = Column( String(36), - ForeignKey("luckperms_players.uuid", ondelete="CASCADE"), + ForeignKey("hubmc.luckperms_players.uuid", ondelete="CASCADE"), nullable=False, index=True, ) @@ -50,7 +51,7 @@ class Punishment(Base): reason = Column(Text, nullable=False) staff_uuid = Column( String(36), - ForeignKey("luckperms_players.uuid", ondelete="SET NULL"), + ForeignKey("hubmc.luckperms_players.uuid", ondelete="SET NULL"), nullable=False, ) staff_name = Column(String(255), nullable=False) @@ -58,7 +59,7 @@ class Punishment(Base): is_active = Column(Boolean, default=True) revoked_at = Column(DateTime(timezone=True)) revoked_by = Column( - String(36), ForeignKey("luckperms_players.uuid", ondelete="SET NULL") + String(36), ForeignKey("hubmc.luckperms_players.uuid", ondelete="SET NULL") ) revoked_reason = Column(Text) evidence_url = Column(Text) diff --git a/src/hubgw/models/teleport_history.py b/src/hubgw/models/teleport_history.py index f333c54..b2bfe8c 100644 --- a/src/hubgw/models/teleport_history.py +++ b/src/hubgw/models/teleport_history.py @@ -17,7 +17,7 @@ class TeleportHistory(Base): ) player_uuid = Column( String(36), - ForeignKey("luckperms_players.uuid", ondelete="CASCADE"), + ForeignKey("hubmc.luckperms_players.uuid", ondelete="CASCADE"), nullable=False, index=True, ) diff --git a/src/hubgw/models/users.py b/src/hubgw/models/users.py index 28a1c35..c24eb33 100644 --- a/src/hubgw/models/users.py +++ b/src/hubgw/models/users.py @@ -32,4 +32,7 @@ class User(Base): deleted_at = Column(DateTime(timezone=True), nullable=True) password_changed_at = Column(DateTime(timezone=True), nullable=True) - __table_args__ = (Index("idx_users_email", "email", unique=True),) + __table_args__ = ( + Index("idx_users_email", "email", unique=True), + {"schema": "public"}, + ) diff --git a/src/hubgw/models/warp.py b/src/hubgw/models/warp.py index 0786027..466a685 100644 --- a/src/hubgw/models/warp.py +++ b/src/hubgw/models/warp.py @@ -15,6 +15,7 @@ class Warp(Base): Index( "idx_hub_warps_public", "is_public", postgresql_where=Column("is_public") ), + {"schema": "hubmc"}, ) id = Column( diff --git a/src/hubgw/models/whitelist.py b/src/hubgw/models/whitelist.py index 1fe09ca..85bca52 100644 --- a/src/hubgw/models/whitelist.py +++ b/src/hubgw/models/whitelist.py @@ -21,6 +21,7 @@ class WhitelistEntry(Base): "expires_at", postgresql_where=Column("expires_at") != None, ), + {"schema": "hubmc"}, ) id = Column( diff --git a/src/hubgw/services/whitelist_service.py b/src/hubgw/services/whitelist_service.py index b0754e6..92d5763 100644 --- a/src/hubgw/services/whitelist_service.py +++ b/src/hubgw/services/whitelist_service.py @@ -42,6 +42,8 @@ class WhitelistService: logger.error( f"Failed to create LuckPerms player '{request.player_name}': {type(e).__name__}: {e}" ) + await self.repo.session.rollback() + raise existing = await self.repo.get_by_player_name(request.player_name) if existing: diff --git a/tests/integration/test_whitelist.py b/tests/integration/test_whitelist.py index 59270c2..0c0c646 100644 --- a/tests/integration/test_whitelist.py +++ b/tests/integration/test_whitelist.py @@ -1,92 +1,48 @@ """Integration tests for whitelist endpoints.""" from datetime import datetime, timedelta, timezone -from unittest.mock import AsyncMock, patch -from uuid import uuid4 +from unittest.mock import AsyncMock import pytest import pytest_asyncio from httpx import AsyncClient, ASGITransport -from sqlalchemy import Column, String, Boolean, DateTime, event -from sqlalchemy.dialects.postgresql import UUID as PG_UUID -from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy import delete +from sqlalchemy.ext.asyncio import AsyncSession from hubgw.context import APP_CTX from hubgw.main import create_app -from hubgw.models.base import Base +from hubgw.models.whitelist import WhitelistEntry @pytest_asyncio.fixture(scope="function") -async def test_engine(): - """Create test database engine and setup tables.""" - from sqlalchemy import Table, MetaData, Index - from uuid import uuid4 as _uuid4 - - engine = create_async_engine( - "sqlite+aiosqlite:///:memory:", - echo=False, - connect_args={"check_same_thread": False}, - ) - - metadata = MetaData() - - whitelist_table = Table( - "hub_whitelist", - metadata, - Column("id", String(36), primary_key=True), - Column("player_name", String(255), nullable=False, unique=True, index=True), - Column("added_by", String(255), nullable=False), - Column("added_at", DateTime(timezone=True), nullable=False), - Column("expires_at", DateTime(timezone=True)), - Column("is_active", Boolean, default=True), - Column("reason", String(500)), - Column("created_at", DateTime(timezone=True)), - Column("updated_at", DateTime(timezone=True)), - ) - - async with engine.begin() as conn: - await conn.run_sync(metadata.create_all) - - yield engine - - await engine.dispose() - - -@pytest_asyncio.fixture(scope="function") -async def test_session(test_engine): - """Create test database session.""" - session_factory = async_sessionmaker( - bind=test_engine, - class_=AsyncSession, - expire_on_commit=False, - ) - - async with session_factory() as session: - yield session - - -@pytest_asyncio.fixture(scope="function") -async def client(test_engine): +async def client(): """Create async test client.""" + from hubgw.services.luckperms_service import LuckPermsService + from hubgw.services.users_service import UserService + app = create_app() - original_engine = APP_CTX._engine - original_factory = APP_CTX._session_factory + mock_luckperms = AsyncMock(spec=LuckPermsService) + mock_luckperms.create_player = AsyncMock() - APP_CTX._engine = test_engine - APP_CTX._session_factory = async_sessionmaker( - bind=test_engine, - class_=AsyncSession, - expire_on_commit=False, - ) + mock_user = AsyncMock(spec=UserService) + + async def get_mock_luckperms(): + return mock_luckperms + + async def get_mock_user(): + return mock_user + + from hubgw.api import deps + app.dependency_overrides[deps.get_luckperms_service] = get_mock_luckperms + app.dependency_overrides[deps.get_user_service] = get_mock_user async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as ac: yield ac - APP_CTX._engine = original_engine - APP_CTX._session_factory = original_factory + app.dependency_overrides.clear() @pytest.fixture @@ -101,36 +57,29 @@ class TestWhitelistAdd: @pytest.mark.asyncio async def test_add_player_success(self, client, api_key): """Test successfully adding a new player to whitelist.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) + payload = { + "player_name": "TestPlayer", + "added_by": "admin", + "added_at": now.isoformat(), + "expires_at": (now + timedelta(days=30)).isoformat(), + "is_active": True, + "reason": "Testing", + } - mock_lp.return_value = AsyncMock() - mock_lp.return_value.create_player = AsyncMock() - mock_us.return_value = AsyncMock() + response = await client.post( + "/api/v1/whitelist/add", + json=payload, + headers={"X-API-Key": api_key}, + ) - now = datetime.now(timezone.utc) - payload = { - "player_name": "TestPlayer", - "added_by": "admin", - "added_at": now.isoformat(), - "expires_at": (now + timedelta(days=30)).isoformat(), - "is_active": True, - "reason": "Testing", - } - - response = await client.post( - "/api/v1/whitelist/add", - json=payload, - headers={"X-API-Key": api_key}, - ) - - assert response.status_code == 201 - data = response.json() - assert data["player_name"] == "TestPlayer" - assert data["added_by"] == "admin" - assert data["is_active"] is True - assert data["reason"] == "Testing" - assert "id" in data + assert response.status_code == 201 + data = response.json() + assert data["player_name"] == "TestPlayer" + assert data["added_by"] == "admin" + assert data["is_active"] is True + assert data["reason"] == "Testing" + assert "id" in data @pytest.mark.asyncio async def test_add_player_invalid_username(self, client, api_key): @@ -172,103 +121,85 @@ class TestWhitelistAdd: @pytest.mark.asyncio async def test_add_player_expires_before_added(self, client, api_key): """Test adding player with expires_at before added_at.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) + payload = { + "player_name": "TestPlayer", + "added_by": "admin", + "added_at": now.isoformat(), + "expires_at": (now - timedelta(minutes=1)).isoformat(), + } - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() + response = await client.post( + "/api/v1/whitelist/add", + json=payload, + headers={"X-API-Key": api_key}, + ) - now = datetime.now(timezone.utc) - payload = { - "player_name": "TestPlayer", - "added_by": "admin", - "added_at": now.isoformat(), - "expires_at": (now - timedelta(minutes=1)).isoformat(), - } - - response = await client.post( - "/api/v1/whitelist/add", - json=payload, - headers={"X-API-Key": api_key}, - ) - - assert response.status_code == 422 - assert "expires_at" in response.text and ("cannot be in the past" in response.text or "must be after added_at" in response.text) + assert response.status_code == 422 + assert "expires_at" in response.text and ("cannot be in the past" in response.text or "must be after added_at" in response.text) @pytest.mark.asyncio async def test_add_player_duplicate_active(self, client, api_key): """Test adding duplicate player when already active.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) + payload = { + "player_name": "DuplicatePlayer", + "added_by": "admin", + "added_at": now.isoformat(), + } - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() + response1 = await client.post( + "/api/v1/whitelist/add", + json=payload, + headers={"X-API-Key": api_key}, + ) + assert response1.status_code == 201 - now = datetime.now(timezone.utc) - payload = { - "player_name": "DuplicatePlayer", - "added_by": "admin", - "added_at": now.isoformat(), - } - - response1 = await client.post( - "/api/v1/whitelist/add", - json=payload, - headers={"X-API-Key": api_key}, - ) - assert response1.status_code == 201 - - response2 = await client.post( - "/api/v1/whitelist/add", - json=payload, - headers={"X-API-Key": api_key}, - ) - assert response2.status_code == 409 - assert "already whitelisted" in response2.text + response2 = await client.post( + "/api/v1/whitelist/add", + json=payload, + headers={"X-API-Key": api_key}, + ) + assert response2.status_code == 409 + assert "already whitelisted" in response2.text @pytest.mark.asyncio - async def test_add_player_reactivate_inactive(self, client, api_key, test_session): + async def test_add_player_reactivate_inactive(self, client, api_key): """Test reactivating an inactive player.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() + payload1 = { + "player_name": "InactivePlayer", + "added_by": "admin", + "added_at": now.isoformat(), + "is_active": False, + } - now = datetime.now(timezone.utc) + response1 = await client.post( + "/api/v1/whitelist/add", + json=payload1, + headers={"X-API-Key": api_key}, + ) + assert response1.status_code == 201 - payload1 = { - "player_name": "InactivePlayer", - "added_by": "admin", - "added_at": now.isoformat(), - "is_active": False, - } + payload2 = { + "player_name": "InactivePlayer", + "added_by": "moderator", + "added_at": now.isoformat(), + "is_active": True, + "reason": "Reactivated", + } - response1 = await client.post( - "/api/v1/whitelist/add", - json=payload1, - headers={"X-API-Key": api_key}, - ) - assert response1.status_code == 201 - - payload2 = { - "player_name": "InactivePlayer", - "added_by": "moderator", - "added_at": now.isoformat(), - "is_active": True, - "reason": "Reactivated", - } - - response2 = await client.post( - "/api/v1/whitelist/add", - json=payload2, - headers={"X-API-Key": api_key}, - ) - assert response2.status_code == 201 - data = response2.json() - assert data["added_by"] == "moderator" - assert data["is_active"] is True - assert data["reason"] == "Reactivated" + response2 = await client.post( + "/api/v1/whitelist/add", + json=payload2, + headers={"X-API-Key": api_key}, + ) + assert response2.status_code == 201 + data = response2.json() + assert data["added_by"] == "moderator" + assert data["is_active"] is True + assert data["reason"] == "Reactivated" @pytest.mark.asyncio async def test_add_player_unauthorized(self, client): @@ -307,33 +238,27 @@ class TestWhitelistRemove: @pytest.mark.asyncio async def test_remove_player_success(self, client, api_key): """Test successfully removing a player.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) + add_payload = { + "player_name": "PlayerToRemove", + "added_by": "admin", + "added_at": now.isoformat(), + } - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() + await client.post( + "/api/v1/whitelist/add", + json=add_payload, + headers={"X-API-Key": api_key}, + ) - now = datetime.now(timezone.utc) - add_payload = { - "player_name": "PlayerToRemove", - "added_by": "admin", - "added_at": now.isoformat(), - } + remove_payload = {"player_name": "PlayerToRemove"} + response = await client.post( + "/api/v1/whitelist/remove", + json=remove_payload, + headers={"X-API-Key": api_key}, + ) - await client.post( - "/api/v1/whitelist/add", - json=add_payload, - headers={"X-API-Key": api_key}, - ) - - remove_payload = {"player_name": "PlayerToRemove"} - response = await client.post( - "/api/v1/whitelist/remove", - json=remove_payload, - headers={"X-API-Key": api_key}, - ) - - assert response.status_code == 204 + assert response.status_code == 204 @pytest.mark.asyncio async def test_remove_player_not_found(self, client, api_key): @@ -367,70 +292,58 @@ class TestWhitelistCheck: @pytest.mark.asyncio async def test_check_player_active(self, client, api_key): """Test checking active whitelisted player.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) + add_payload = { + "player_name": "ActivePlayer", + "added_by": "admin", + "added_at": now.isoformat(), + "is_active": True, + } - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() + await client.post( + "/api/v1/whitelist/add", + json=add_payload, + headers={"X-API-Key": api_key}, + ) - now = datetime.now(timezone.utc) - add_payload = { - "player_name": "ActivePlayer", - "added_by": "admin", - "added_at": now.isoformat(), - "is_active": True, - } + check_payload = {"player_name": "ActivePlayer"} + response = await client.post( + "/api/v1/whitelist/check", + json=check_payload, + headers={"X-API-Key": api_key}, + ) - await client.post( - "/api/v1/whitelist/add", - json=add_payload, - headers={"X-API-Key": api_key}, - ) - - check_payload = {"player_name": "ActivePlayer"} - response = await client.post( - "/api/v1/whitelist/check", - json=check_payload, - headers={"X-API-Key": api_key}, - ) - - assert response.status_code == 200 - data = response.json() - assert data["is_whitelisted"] is True + assert response.status_code == 200 + data = response.json() + assert data["is_whitelisted"] is True @pytest.mark.asyncio async def test_check_player_inactive(self, client, api_key): """Test checking inactive player.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) + add_payload = { + "player_name": "InactivePlayer", + "added_by": "admin", + "added_at": now.isoformat(), + "is_active": False, + } - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() + await client.post( + "/api/v1/whitelist/add", + json=add_payload, + headers={"X-API-Key": api_key}, + ) - now = datetime.now(timezone.utc) - add_payload = { - "player_name": "InactivePlayer", - "added_by": "admin", - "added_at": now.isoformat(), - "is_active": False, - } + check_payload = {"player_name": "InactivePlayer"} + response = await client.post( + "/api/v1/whitelist/check", + json=check_payload, + headers={"X-API-Key": api_key}, + ) - await client.post( - "/api/v1/whitelist/add", - json=add_payload, - headers={"X-API-Key": api_key}, - ) - - check_payload = {"player_name": "InactivePlayer"} - response = await client.post( - "/api/v1/whitelist/check", - json=check_payload, - headers={"X-API-Key": api_key}, - ) - - assert response.status_code == 200 - data = response.json() - assert data["is_whitelisted"] is False + assert response.status_code == 200 + data = response.json() + assert data["is_whitelisted"] is False @pytest.mark.asyncio async def test_check_player_not_found(self, client, api_key): @@ -466,40 +379,34 @@ class TestWhitelistList: @pytest.mark.asyncio async def test_list_multiple_players(self, client, api_key): """Test listing multiple players.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() - - now = datetime.now(timezone.utc) - - for i, name in enumerate(["Player1", "Player2", "Player3"]): - payload = { - "player_name": name, - "added_by": "admin", - "added_at": (now + timedelta(seconds=i)).isoformat(), - "is_active": i % 2 == 0, - } - await client.post( - "/api/v1/whitelist/add", - json=payload, - headers={"X-API-Key": api_key}, - ) - - response = await client.get( - "/api/v1/whitelist/", + for i, name in enumerate(["Player1", "Player2", "Player3"]): + payload = { + "player_name": name, + "added_by": "admin", + "added_at": (now + timedelta(seconds=i)).isoformat(), + "is_active": i % 2 == 0, + } + await client.post( + "/api/v1/whitelist/add", + json=payload, headers={"X-API-Key": api_key}, ) - assert response.status_code == 200 - data = response.json() - assert len(data["entries"]) == 3 - assert data["total"] == 3 + response = await client.get( + "/api/v1/whitelist/", + headers={"X-API-Key": api_key}, + ) - assert data["entries"][0]["player_name"] == "Player3" - assert data["entries"][1]["player_name"] == "Player2" - assert data["entries"][2]["player_name"] == "Player1" + assert response.status_code == 200 + data = response.json() + assert len(data["entries"]) == 3 + assert data["total"] == 3 + + assert data["entries"][0]["player_name"] == "Player3" + assert data["entries"][1]["player_name"] == "Player2" + assert data["entries"][2]["player_name"] == "Player1" class TestWhitelistCount: @@ -520,67 +427,55 @@ class TestWhitelistCount: @pytest.mark.asyncio async def test_count_multiple(self, client, api_key): """Test count with multiple entries.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() - - now = datetime.now(timezone.utc) - - for i in range(5): - payload = { - "player_name": f"Player{i}", - "added_by": "admin", - "added_at": now.isoformat(), - } - await client.post( - "/api/v1/whitelist/add", - json=payload, - headers={"X-API-Key": api_key}, - ) - - response = await client.get( - "/api/v1/whitelist/count", + for i in range(5): + payload = { + "player_name": f"Player{i}", + "added_by": "admin", + "added_at": now.isoformat(), + } + await client.post( + "/api/v1/whitelist/add", + json=payload, headers={"X-API-Key": api_key}, ) - assert response.status_code == 200 - data = response.json() - assert data["total"] == 5 + response = await client.get( + "/api/v1/whitelist/count", + headers={"X-API-Key": api_key}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["total"] == 5 @pytest.mark.asyncio async def test_count_includes_inactive(self, client, api_key): """Test that count includes both active and inactive entries.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() - - now = datetime.now(timezone.utc) - - for i in range(3): - payload = { - "player_name": f"Player{i}", - "added_by": "admin", - "added_at": now.isoformat(), - "is_active": i % 2 == 0, - } - await client.post( - "/api/v1/whitelist/add", - json=payload, - headers={"X-API-Key": api_key}, - ) - - response = await client.get( - "/api/v1/whitelist/count", + for i in range(3): + payload = { + "player_name": f"Player{i}", + "added_by": "admin", + "added_at": now.isoformat(), + "is_active": i % 2 == 0, + } + await client.post( + "/api/v1/whitelist/add", + json=payload, headers={"X-API-Key": api_key}, ) - assert response.status_code == 200 - data = response.json() - assert data["total"] == 3 + response = await client.get( + "/api/v1/whitelist/count", + headers={"X-API-Key": api_key}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["total"] == 3 class TestWhitelistEdgeCases: @@ -589,107 +484,83 @@ class TestWhitelistEdgeCases: @pytest.mark.asyncio async def test_add_player_with_whitespace_reason(self, client, api_key): """Test adding player with whitespace-only reason.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) + payload = { + "player_name": "TestPlayer", + "added_by": "admin", + "added_at": now.isoformat(), + "reason": " ", + } - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() + response = await client.post( + "/api/v1/whitelist/add", + json=payload, + headers={"X-API-Key": api_key}, + ) - now = datetime.now(timezone.utc) - payload = { - "player_name": "TestPlayer", - "added_by": "admin", - "added_at": now.isoformat(), - "reason": " ", - } - - response = await client.post( - "/api/v1/whitelist/add", - json=payload, - headers={"X-API-Key": api_key}, - ) - - assert response.status_code == 201 - data = response.json() - assert data["reason"] is None + assert response.status_code == 201 + data = response.json() + assert data["reason"] is None @pytest.mark.asyncio async def test_add_player_max_length_username(self, client, api_key): """Test adding player with maximum length username.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) + payload = { + "player_name": "A" * 16, + "added_by": "admin", + "added_at": now.isoformat(), + } - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() + response = await client.post( + "/api/v1/whitelist/add", + json=payload, + headers={"X-API-Key": api_key}, + ) - now = datetime.now(timezone.utc) - payload = { - "player_name": "A" * 16, - "added_by": "admin", - "added_at": now.isoformat(), - } - - response = await client.post( - "/api/v1/whitelist/add", - json=payload, - headers={"X-API-Key": api_key}, - ) - - assert response.status_code == 201 + assert response.status_code == 201 @pytest.mark.asyncio async def test_add_player_min_length_username(self, client, api_key): """Test adding player with minimum length username.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) + payload = { + "player_name": "ABC", + "added_by": "admin", + "added_at": now.isoformat(), + } - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() + response = await client.post( + "/api/v1/whitelist/add", + json=payload, + headers={"X-API-Key": api_key}, + ) - now = datetime.now(timezone.utc) - payload = { - "player_name": "ABC", - "added_by": "admin", - "added_at": now.isoformat(), - } - - response = await client.post( - "/api/v1/whitelist/add", - json=payload, - headers={"X-API-Key": api_key}, - ) - - assert response.status_code == 201 + assert response.status_code == 201 @pytest.mark.asyncio async def test_check_player_case_sensitive(self, client, api_key): """Test that player name check is case-sensitive.""" - with patch("hubgw.api.deps.get_luckperms_service") as mock_lp, \ - patch("hubgw.api.deps.get_user_service") as mock_us: + now = datetime.now(timezone.utc) + add_payload = { + "player_name": "TestPlayer", + "added_by": "admin", + "added_at": now.isoformat(), + } - mock_lp.return_value = AsyncMock() - mock_us.return_value = AsyncMock() + await client.post( + "/api/v1/whitelist/add", + json=add_payload, + headers={"X-API-Key": api_key}, + ) - now = datetime.now(timezone.utc) - add_payload = { - "player_name": "TestPlayer", - "added_by": "admin", - "added_at": now.isoformat(), - } + check_payload = {"player_name": "testplayer"} + response = await client.post( + "/api/v1/whitelist/check", + json=check_payload, + headers={"X-API-Key": api_key}, + ) - await client.post( - "/api/v1/whitelist/add", - json=add_payload, - headers={"X-API-Key": api_key}, - ) - - check_payload = {"player_name": "testplayer"} - response = await client.post( - "/api/v1/whitelist/check", - json=check_payload, - headers={"X-API-Key": api_key}, - ) - - assert response.status_code == 200 - data = response.json() - assert data["is_whitelisted"] is False + assert response.status_code == 200 + data = response.json() + assert data["is_whitelisted"] is False