phase8: expand test coverage and refine docs with gitflow milestones
This commit is contained in:
@ -21,3 +21,20 @@ def test_dashboard_requires_authentication(client):
|
||||
response = client.get(reverse("core:dashboard"))
|
||||
assert response.status_code == 302
|
||||
assert reverse("users:login") in response.url
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_login_logout_flow(client):
|
||||
User.objects.create_user(username="login-user", email="login@example.com", password="StrongPass12345")
|
||||
|
||||
login_response = client.post(
|
||||
reverse("users:login"),
|
||||
data={"username": "login-user", "password": "StrongPass12345"},
|
||||
follow=True,
|
||||
)
|
||||
assert login_response.status_code == 200
|
||||
assert login_response.wsgi_request.user.is_authenticated
|
||||
|
||||
logout_response = client.post(reverse("users:logout"), follow=True)
|
||||
assert logout_response.status_code == 200
|
||||
assert not logout_response.wsgi_request.user.is_authenticated
|
||||
|
||||
@ -26,6 +26,47 @@ def test_run_full_sync_creates_domain_objects(settings):
|
||||
assert PlayerSeasonStats.objects.count() >= 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_full_sync_is_idempotent(settings):
|
||||
settings.PROVIDER_DEFAULT_NAMESPACE = "mvp_demo"
|
||||
|
||||
run_sync_job(provider_namespace="mvp_demo", job_type=IngestionRun.JobType.FULL_SYNC)
|
||||
counts_after_first = {
|
||||
"competition": Competition.objects.count(),
|
||||
"team": Team.objects.count(),
|
||||
"season": Season.objects.count(),
|
||||
"player": Player.objects.count(),
|
||||
"player_season": PlayerSeason.objects.count(),
|
||||
"player_stats": PlayerSeasonStats.objects.count(),
|
||||
}
|
||||
|
||||
run_sync_job(provider_namespace="mvp_demo", job_type=IngestionRun.JobType.FULL_SYNC)
|
||||
counts_after_second = {
|
||||
"competition": Competition.objects.count(),
|
||||
"team": Team.objects.count(),
|
||||
"season": Season.objects.count(),
|
||||
"player": Player.objects.count(),
|
||||
"player_season": PlayerSeason.objects.count(),
|
||||
"player_stats": PlayerSeasonStats.objects.count(),
|
||||
}
|
||||
|
||||
assert counts_after_first == counts_after_second
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_incremental_sync_runs_successfully(settings):
|
||||
settings.PROVIDER_DEFAULT_NAMESPACE = "mvp_demo"
|
||||
|
||||
run = run_sync_job(
|
||||
provider_namespace="mvp_demo",
|
||||
job_type=IngestionRun.JobType.INCREMENTAL,
|
||||
cursor="demo-cursor",
|
||||
)
|
||||
|
||||
assert run.status == IngestionRun.RunStatus.SUCCESS
|
||||
assert run.records_processed > 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_run_sync_handles_rate_limit(settings):
|
||||
settings.PROVIDER_DEFAULT_NAMESPACE = "mvp_demo"
|
||||
|
||||
49
tests/test_integration_paths.py
Normal file
49
tests/test_integration_paths.py
Normal file
@ -0,0 +1,49 @@
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.players.models import Nationality, Player, Position, Role
|
||||
from apps.scouting.models import SavedSearch
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_saved_search_run_filters_player_results(client):
|
||||
user = User.objects.create_user(username="integration", password="pass12345")
|
||||
client.force_login(user)
|
||||
|
||||
nationality = Nationality.objects.create(name="Italy", iso2_code="IT", iso3_code="ITA")
|
||||
position = Position.objects.create(code="PG", name="Point Guard")
|
||||
role = Role.objects.create(code="playmaker", name="Playmaker")
|
||||
|
||||
Player.objects.create(
|
||||
first_name="Marco",
|
||||
last_name="Rossi",
|
||||
full_name="Marco Rossi",
|
||||
birth_date=date(2001, 1, 1),
|
||||
nationality=nationality,
|
||||
nominal_position=position,
|
||||
inferred_role=role,
|
||||
)
|
||||
Player.objects.create(
|
||||
first_name="Luca",
|
||||
last_name="Bianchi",
|
||||
full_name="Luca Bianchi",
|
||||
birth_date=date(2001, 1, 1),
|
||||
nationality=nationality,
|
||||
nominal_position=position,
|
||||
inferred_role=role,
|
||||
)
|
||||
|
||||
saved = SavedSearch.objects.create(
|
||||
user=user,
|
||||
name="Only Marco",
|
||||
filters={"q": "Marco", "sort": "name_asc"},
|
||||
)
|
||||
|
||||
response = client.get(reverse("scouting:saved_search_run", kwargs={"pk": saved.pk}), follow=True)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Marco Rossi" in response.content.decode()
|
||||
assert "Luca Bianchi" not in response.content.decode()
|
||||
92
tests/test_models_domain.py
Normal file
92
tests/test_models_domain.py
Normal file
@ -0,0 +1,92 @@
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import IntegrityError
|
||||
|
||||
from apps.competitions.models import Competition
|
||||
from apps.players.models import Nationality, Player, Position, Role
|
||||
from apps.providers.models import ExternalMapping
|
||||
from apps.scouting.models import FavoritePlayer, SavedSearch
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_player_unique_full_name_birth_date_constraint():
|
||||
nationality = Nationality.objects.create(name="Italy", iso2_code="IT", iso3_code="ITA")
|
||||
position = Position.objects.create(code="PG", name="Point Guard")
|
||||
role = Role.objects.create(code="playmaker", name="Playmaker")
|
||||
|
||||
Player.objects.create(
|
||||
first_name="Marco",
|
||||
last_name="Rossi",
|
||||
full_name="Marco Rossi",
|
||||
birth_date=date(2001, 1, 1),
|
||||
nationality=nationality,
|
||||
nominal_position=position,
|
||||
inferred_role=role,
|
||||
)
|
||||
|
||||
with pytest.raises(IntegrityError):
|
||||
Player.objects.create(
|
||||
first_name="Marco",
|
||||
last_name="Rossi",
|
||||
full_name="Marco Rossi",
|
||||
birth_date=date(2001, 1, 1),
|
||||
nationality=nationality,
|
||||
nominal_position=position,
|
||||
inferred_role=role,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_saved_search_unique_name_per_user_constraint():
|
||||
user = User.objects.create_user(username="u1", password="pass12345")
|
||||
SavedSearch.objects.create(user=user, name="My Search", filters={"q": "rossi"})
|
||||
|
||||
with pytest.raises(IntegrityError):
|
||||
SavedSearch.objects.create(user=user, name="My Search", filters={"q": "martin"})
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_favorite_unique_player_per_user_constraint():
|
||||
user = User.objects.create_user(username="u2", password="pass12345")
|
||||
nationality = Nationality.objects.create(name="Spain", iso2_code="ES", iso3_code="ESP")
|
||||
position = Position.objects.create(code="SF", name="Small Forward")
|
||||
role = Role.objects.create(code="wing", name="Wing")
|
||||
player = Player.objects.create(
|
||||
first_name="Juan",
|
||||
last_name="Perez",
|
||||
full_name="Juan Perez",
|
||||
birth_date=date(2000, 5, 1),
|
||||
nationality=nationality,
|
||||
nominal_position=position,
|
||||
inferred_role=role,
|
||||
)
|
||||
|
||||
FavoritePlayer.objects.create(user=user, player=player)
|
||||
with pytest.raises(IntegrityError):
|
||||
FavoritePlayer.objects.create(user=user, player=player)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_external_mapping_unique_provider_external_id_constraint():
|
||||
competition = Competition.objects.create(
|
||||
name="Liga ACB",
|
||||
slug="liga-acb",
|
||||
competition_type=Competition.CompetitionType.LEAGUE,
|
||||
gender=Competition.Gender.MEN,
|
||||
level=1,
|
||||
)
|
||||
|
||||
ExternalMapping.objects.create(
|
||||
provider_namespace="mvp_demo",
|
||||
external_id="comp-001",
|
||||
content_object=competition,
|
||||
)
|
||||
|
||||
with pytest.raises(IntegrityError):
|
||||
ExternalMapping.objects.create(
|
||||
provider_namespace="mvp_demo",
|
||||
external_id="comp-001",
|
||||
content_object=competition,
|
||||
)
|
||||
83
tests/test_players_filters_advanced.py
Normal file
83
tests/test_players_filters_advanced.py
Normal file
@ -0,0 +1,83 @@
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.competitions.models import Competition, Season
|
||||
from apps.players.models import Nationality, Player, Position, Role
|
||||
from apps.stats.models import PlayerSeason, PlayerSeasonStats
|
||||
from apps.teams.models import Team
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_player_search_stat_filter_and_sorting(client):
|
||||
nationality = Nationality.objects.create(name="France", iso2_code="FR", iso3_code="FRA")
|
||||
position = Position.objects.create(code="SG", name="Shooting Guard")
|
||||
role = Role.objects.create(code="scorer", name="Scorer")
|
||||
season = Season.objects.create(label="2025-2026", start_date=date(2025, 9, 1), end_date=date(2026, 6, 30))
|
||||
competition = Competition.objects.create(
|
||||
name="LNB Pro A",
|
||||
slug="lnb-pro-a",
|
||||
competition_type=Competition.CompetitionType.LEAGUE,
|
||||
gender=Competition.Gender.MEN,
|
||||
country=nationality,
|
||||
)
|
||||
team = Team.objects.create(name="Paris", slug="paris", country=nationality)
|
||||
|
||||
p1 = Player.objects.create(
|
||||
first_name="A",
|
||||
last_name="One",
|
||||
full_name="A One",
|
||||
birth_date=date(2000, 1, 1),
|
||||
nationality=nationality,
|
||||
nominal_position=position,
|
||||
inferred_role=role,
|
||||
)
|
||||
p2 = Player.objects.create(
|
||||
first_name="B",
|
||||
last_name="Two",
|
||||
full_name="B Two",
|
||||
birth_date=date(2000, 1, 1),
|
||||
nationality=nationality,
|
||||
nominal_position=position,
|
||||
inferred_role=role,
|
||||
)
|
||||
|
||||
ps1 = PlayerSeason.objects.create(player=p1, season=season, team=team, competition=competition, games_played=20, minutes_played=500)
|
||||
ps2 = PlayerSeason.objects.create(player=p2, season=season, team=team, competition=competition, games_played=20, minutes_played=700)
|
||||
|
||||
PlayerSeasonStats.objects.create(player_season=ps1, points=10.0, rebounds=3, assists=2, steals=1, blocks=0.2, turnovers=1)
|
||||
PlayerSeasonStats.objects.create(player_season=ps2, points=19.0, rebounds=4, assists=5, steals=1.5, blocks=0.4, turnovers=2)
|
||||
|
||||
response = client.get(
|
||||
reverse("players:index"),
|
||||
data={"points_per_game_min": "15", "sort": "ppg_desc"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
players = list(response.context["players"])
|
||||
assert len(players) == 1
|
||||
assert players[0].full_name == "B Two"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_player_search_pagination_preserves_querystring(client):
|
||||
nationality = Nationality.objects.create(name="Germany", iso2_code="DE", iso3_code="DEU")
|
||||
position = Position.objects.create(code="PF", name="Power Forward")
|
||||
role = Role.objects.create(code="big", name="Big")
|
||||
|
||||
for idx in range(25):
|
||||
Player.objects.create(
|
||||
first_name=f"F{idx}",
|
||||
last_name=f"L{idx}",
|
||||
full_name=f"Player {idx}",
|
||||
birth_date=date(2000, 1, 1),
|
||||
nationality=nationality,
|
||||
nominal_position=position,
|
||||
inferred_role=role,
|
||||
)
|
||||
|
||||
response = client.get(reverse("players:index"), data={"q": "Player", "page_size": 20, "page": 2})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.context["page_obj"].number == 2
|
||||
43
tests/test_provider_adapter.py
Normal file
43
tests/test_provider_adapter.py
Normal file
@ -0,0 +1,43 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from apps.providers.adapters.mvp_provider import MvpDemoProviderAdapter
|
||||
from apps.providers.exceptions import ProviderNotFoundError, ProviderRateLimitError
|
||||
from apps.providers.registry import get_provider
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_mvp_provider_fetch_and_search_players():
|
||||
adapter = MvpDemoProviderAdapter()
|
||||
|
||||
players = adapter.fetch_players()
|
||||
assert len(players) >= 2
|
||||
|
||||
results = adapter.search_players(query="luca")
|
||||
assert any("Luca" in item["full_name"] for item in results)
|
||||
|
||||
detail = adapter.fetch_player(external_player_id="player-001")
|
||||
assert detail is not None
|
||||
assert detail["full_name"] == "Luca Rinaldi"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_mvp_provider_rate_limit_signal():
|
||||
os.environ["PROVIDER_MVP_FORCE_RATE_LIMIT"] = "1"
|
||||
adapter = MvpDemoProviderAdapter()
|
||||
|
||||
with pytest.raises(ProviderRateLimitError):
|
||||
adapter.fetch_players()
|
||||
|
||||
os.environ.pop("PROVIDER_MVP_FORCE_RATE_LIMIT", None)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_provider_registry_resolution(settings):
|
||||
settings.PROVIDER_DEFAULT_NAMESPACE = "mvp_demo"
|
||||
provider = get_provider()
|
||||
assert isinstance(provider, MvpDemoProviderAdapter)
|
||||
|
||||
with pytest.raises(ProviderNotFoundError):
|
||||
get_provider("does-not-exist")
|
||||
@ -85,3 +85,46 @@ def test_favorite_toggle_adds_and_removes(client):
|
||||
remove_resp = client.post(reverse("scouting:favorite_toggle", kwargs={"player_id": player.id}))
|
||||
assert remove_resp.status_code == 302
|
||||
assert not FavoritePlayer.objects.filter(user=user, player=player).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_favorite_toggle_htmx_returns_partial_button(client):
|
||||
user = User.objects.create_user(username="scout4", password="pass12345")
|
||||
client.force_login(user)
|
||||
|
||||
nationality = Nationality.objects.create(name="France", iso2_code="FR", iso3_code="FRA")
|
||||
position = Position.objects.create(code="PF", name="Power Forward")
|
||||
role = Role.objects.create(code="big", name="Big")
|
||||
player = Player.objects.create(
|
||||
first_name="Pierre",
|
||||
last_name="Durand",
|
||||
full_name="Pierre Durand",
|
||||
birth_date=date(2001, 3, 3),
|
||||
nationality=nationality,
|
||||
nominal_position=position,
|
||||
inferred_role=role,
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
reverse("scouting:favorite_toggle", kwargs={"player_id": player.id}),
|
||||
HTTP_HX_REQUEST="true",
|
||||
data={"next": reverse("players:index")},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Remove favorite" in response.content.decode()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_save_search_htmx_feedback(client):
|
||||
user = User.objects.create_user(username="scout5", password="pass12345")
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
reverse("scouting:saved_search_create"),
|
||||
HTTP_HX_REQUEST="true",
|
||||
data={"name": "HTMX Search", "q": "john", "sort": "name_asc"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "created" in response.content.decode().lower()
|
||||
|
||||
Reference in New Issue
Block a user