Add Task 7 edge case and regression tests
This commit is contained in:
312
tests/integration/test_database_edge_cases.py
Normal file
312
tests/integration/test_database_edge_cases.py
Normal file
@@ -0,0 +1,312 @@
|
||||
"""Integration tests for database edge cases.
|
||||
|
||||
Tests boundary values, foreign key constraints, model validation, session
|
||||
lifecycle, and large batch operations on database models.
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from src.server.database.models import (
|
||||
AnimeSeries,
|
||||
DownloadPriority,
|
||||
DownloadQueueItem,
|
||||
DownloadStatus,
|
||||
Episode,
|
||||
SystemSettings,
|
||||
UserSession,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Boundary value tests for AnimeSeries
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAnimeSeriesBoundaries:
|
||||
"""Boundary conditions for AnimeSeries model."""
|
||||
|
||||
def test_empty_key_rejected(self):
|
||||
"""Empty string key triggers validation error."""
|
||||
with pytest.raises((ValueError, Exception)):
|
||||
AnimeSeries(key="", name="Test", site="https://x.com", folder="Test")
|
||||
|
||||
def test_max_length_key(self):
|
||||
"""Key at max length (255) is accepted."""
|
||||
key = "a" * 255
|
||||
a = AnimeSeries(key=key, name="Test", site="https://x.com", folder="Test")
|
||||
assert len(a.key) == 255
|
||||
|
||||
def test_empty_name_rejected(self):
|
||||
"""Empty name triggers validation error."""
|
||||
with pytest.raises((ValueError, Exception)):
|
||||
AnimeSeries(key="k", name="", site="https://x.com", folder="Test")
|
||||
|
||||
def test_long_name(self):
|
||||
"""Name up to 500 chars is accepted."""
|
||||
name = "X" * 500
|
||||
a = AnimeSeries(key="k", name=name, site="https://x.com", folder="F")
|
||||
assert len(a.name) == 500
|
||||
|
||||
def test_unicode_name(self):
|
||||
"""Unicode characters in name are stored correctly."""
|
||||
a = AnimeSeries(
|
||||
key="unicode-test",
|
||||
name="進撃の巨人 Attack on Titan",
|
||||
site="https://x.com",
|
||||
folder="AOT",
|
||||
)
|
||||
assert "進撃の巨人" in a.name
|
||||
|
||||
def test_default_values(self):
|
||||
"""Default booleans and nullables are set correctly."""
|
||||
a = AnimeSeries(key="def", name="Def", site="https://x.com", folder="D")
|
||||
# Before DB insert, mapped_column defaults may not be applied
|
||||
assert a.has_nfo in (None, False)
|
||||
assert a.year is None
|
||||
assert a.tmdb_id is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Episode boundary values
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEpisodeBoundaries:
|
||||
"""Boundary conditions for Episode model."""
|
||||
|
||||
def test_min_season_and_episode(self):
|
||||
"""Season 0, episode 0 are valid (specials/movies)."""
|
||||
ep = Episode(series_id=1, season=0, episode_number=0, title="Special")
|
||||
assert ep.season == 0
|
||||
assert ep.episode_number == 0
|
||||
|
||||
def test_max_season_and_episode(self):
|
||||
"""Maximum allowed values for season and episode."""
|
||||
ep = Episode(series_id=1, season=1000, episode_number=10000, title="Max")
|
||||
assert ep.season == 1000
|
||||
assert ep.episode_number == 10000
|
||||
|
||||
def test_negative_season_rejected(self):
|
||||
"""Negative season triggers validation error."""
|
||||
with pytest.raises((ValueError, Exception)):
|
||||
Episode(series_id=1, season=-1, episode_number=1, title="Bad")
|
||||
|
||||
def test_negative_episode_rejected(self):
|
||||
"""Negative episode number triggers validation error."""
|
||||
with pytest.raises((ValueError, Exception)):
|
||||
Episode(series_id=1, season=1, episode_number=-1, title="Bad")
|
||||
|
||||
def test_empty_title(self):
|
||||
"""Empty title may be allowed (depends on implementation)."""
|
||||
# Some episodes don't have titles
|
||||
ep = Episode(series_id=1, season=1, episode_number=1, title="")
|
||||
assert ep.title == ""
|
||||
|
||||
def test_long_file_path(self):
|
||||
"""file_path up to 1000 chars is accepted."""
|
||||
path = "/a/b/" + "c" * 990
|
||||
ep = Episode(
|
||||
series_id=1, season=1, episode_number=1,
|
||||
title="T", file_path=path
|
||||
)
|
||||
assert len(ep.file_path) == 995
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DownloadQueueItem edge cases
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDownloadQueueItemEdgeCases:
|
||||
"""Edge cases for DownloadQueueItem model."""
|
||||
|
||||
def test_all_status_values(self):
|
||||
"""Every DownloadStatus enum member has a string value."""
|
||||
for st in DownloadStatus:
|
||||
assert isinstance(st.value, str)
|
||||
# Verify expected members exist
|
||||
assert DownloadStatus.PENDING.value == "pending"
|
||||
assert DownloadStatus.DOWNLOADING.value == "downloading"
|
||||
assert DownloadStatus.COMPLETED.value == "completed"
|
||||
assert DownloadStatus.FAILED.value == "failed"
|
||||
|
||||
def test_all_priority_values(self):
|
||||
"""Every DownloadPriority enum member has a string value."""
|
||||
for p in DownloadPriority:
|
||||
assert isinstance(p.value, str)
|
||||
# Verify expected members exist
|
||||
assert DownloadPriority.LOW.value == "low"
|
||||
assert DownloadPriority.NORMAL.value == "normal"
|
||||
assert DownloadPriority.HIGH.value == "high"
|
||||
|
||||
def test_error_message_can_be_none(self):
|
||||
"""error_message defaults to None."""
|
||||
item = DownloadQueueItem(
|
||||
series_id=1,
|
||||
episode_id=1,
|
||||
download_url="https://example.com",
|
||||
file_destination="/tmp/ep.mp4",
|
||||
)
|
||||
assert item.error_message is None
|
||||
|
||||
def test_long_error_message(self):
|
||||
"""A very long error message is stored."""
|
||||
msg = "Error: " + "x" * 2000
|
||||
item = DownloadQueueItem(
|
||||
series_id=1,
|
||||
episode_id=1,
|
||||
download_url="https://example.com",
|
||||
file_destination="/tmp/ep.mp4",
|
||||
error_message=msg,
|
||||
)
|
||||
assert len(item.error_message) > 2000
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UserSession edge cases
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestUserSessionEdgeCases:
|
||||
"""Edge cases for UserSession model."""
|
||||
|
||||
def test_is_expired_property(self):
|
||||
"""Session expired when expires_at is in the past."""
|
||||
session = UserSession(
|
||||
session_id="sess1",
|
||||
token_hash="hash",
|
||||
user_id="user1",
|
||||
ip_address="127.0.0.1",
|
||||
user_agent="test",
|
||||
expires_at=datetime.now(timezone.utc) - timedelta(hours=1),
|
||||
is_active=True,
|
||||
)
|
||||
assert session.is_expired is True
|
||||
|
||||
def test_not_expired(self):
|
||||
"""Session not expired when expires_at is in the future."""
|
||||
session = UserSession(
|
||||
session_id="sess2",
|
||||
token_hash="hash",
|
||||
user_id="user1",
|
||||
ip_address="127.0.0.1",
|
||||
user_agent="test",
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
|
||||
is_active=True,
|
||||
)
|
||||
assert session.is_expired is False
|
||||
|
||||
def test_revoke_sets_inactive(self):
|
||||
"""revoke() sets is_active to False."""
|
||||
session = UserSession(
|
||||
session_id="sess3",
|
||||
token_hash="hash",
|
||||
user_id="user1",
|
||||
ip_address="127.0.0.1",
|
||||
user_agent="test",
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
|
||||
is_active=True,
|
||||
)
|
||||
session.revoke()
|
||||
assert session.is_active is False
|
||||
|
||||
def test_ipv6_address(self):
|
||||
"""IPv6 address fits in ip_address field (max 45 chars)."""
|
||||
session = UserSession(
|
||||
session_id="sess4",
|
||||
token_hash="hash",
|
||||
user_id="user1",
|
||||
ip_address="::ffff:192.168.1.1",
|
||||
user_agent="test",
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
|
||||
is_active=True,
|
||||
)
|
||||
assert session.ip_address == "::ffff:192.168.1.1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SystemSettings edge cases
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSystemSettingsEdgeCases:
|
||||
"""Edge cases for SystemSettings model."""
|
||||
|
||||
def test_default_flags(self):
|
||||
"""Default boolean flags are False or None before DB insert."""
|
||||
ss = SystemSettings()
|
||||
# Before DB insert, mapped_column defaults may not be applied
|
||||
assert ss.initial_scan_completed in (None, False)
|
||||
assert ss.initial_nfo_scan_completed in (None, False)
|
||||
assert ss.initial_media_scan_completed in (None, False)
|
||||
|
||||
def test_last_scan_timestamp_nullable(self):
|
||||
"""last_scan_timestamp can be None."""
|
||||
ss = SystemSettings()
|
||||
assert ss.last_scan_timestamp is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Large batch simulation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestLargeBatch:
|
||||
"""Creating many model instances in a batch."""
|
||||
|
||||
def test_create_100_episodes(self):
|
||||
"""100 Episode objects can be created without error."""
|
||||
episodes = [
|
||||
Episode(
|
||||
series_id=1,
|
||||
season=1,
|
||||
episode_number=i,
|
||||
title=f"Episode {i}",
|
||||
)
|
||||
for i in range(1, 101)
|
||||
]
|
||||
assert len(episodes) == 100
|
||||
assert episodes[-1].episode_number == 100
|
||||
|
||||
def test_create_100_download_items(self):
|
||||
"""100 DownloadQueueItem objects can be created."""
|
||||
items = [
|
||||
DownloadQueueItem(
|
||||
series_id=1,
|
||||
episode_id=i,
|
||||
download_url=f"https://example.com/{i}",
|
||||
file_destination=f"/tmp/ep{i}.mp4",
|
||||
)
|
||||
for i in range(100)
|
||||
]
|
||||
assert len(items) == 100
|
||||
# All URLs unique
|
||||
urls = {item.download_url for item in items}
|
||||
assert len(urls) == 100
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Foreign key reference integrity (model-level)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestForeignKeyReferences:
|
||||
"""Verify FK fields accept valid values."""
|
||||
|
||||
def test_episode_series_id(self):
|
||||
"""Episode.series_id can reference any positive integer."""
|
||||
ep = Episode(series_id=999, season=1, episode_number=1, title="T")
|
||||
assert ep.series_id == 999
|
||||
|
||||
def test_download_item_references(self):
|
||||
"""DownloadQueueItem links to series_id and episode_id."""
|
||||
item = DownloadQueueItem(
|
||||
series_id=42,
|
||||
episode_id=7,
|
||||
download_url="https://example.com",
|
||||
file_destination="/tmp/ep.mp4",
|
||||
)
|
||||
assert item.series_id == 42
|
||||
assert item.episode_id == 7
|
||||
Reference in New Issue
Block a user