release: update version to 0.2.0 and add schema to models
Build and Push Docker Image / build-and-push (push) Successful in 3m24s Details

This commit is contained in:
itqop 2025-11-11 22:39:54 +03:00
parent 0cc81356a7
commit ae119ec5e1
13 changed files with 331 additions and 430 deletions

View File

@ -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

View File

@ -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 <leonkl32@gmail.com>"]
packages = [

View File

@ -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"}

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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)

View File

@ -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,
)

View File

@ -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"},
)

View File

@ -15,6 +15,7 @@ class Warp(Base):
Index(
"idx_hub_warps_public", "is_public", postgresql_where=Column("is_public")
),
{"schema": "hubmc"},
)
id = Column(

View File

@ -21,6 +21,7 @@ class WhitelistEntry(Base):
"expires_at",
postgresql_where=Column("expires_at") != None,
),
{"schema": "hubmc"},
)
id = Column(

View File

@ -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:

View File

@ -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