"""Bounded list views, and the links between their pages.
MNT-14. Every list view ran `.all()` on its table. The audit rated the impact
as nil at the scale of a student club and recommended choosing the pattern
now rather than retro-fitting one — which is what this is.
The tests worth having are not "does page 2 exist". They are the three ways
pagination is normally got wrong:
- a link that drops the rest of the query string, silently resetting the
sort or the filter the person was using;
- `?page=999` answering 404 to someone who went one page too far;
- `?per_page=100000`, which hands back by hand exactly the unbounded query
the pagination was added to prevent.
"""
import re
import pytest
from app.extensions import db
from app.pagination import DEFAULT_PER_PAGE, MAX_PER_PAGE
@pytest.fixture
def many_users(app, make_user):
"""Enough accounts to need a second page at a small per_page."""
for _index in range(7):
make_user('player')
return 7
class TestTheUserList:
def test_per_page_bounds_the_rows(self, app, client, as_role, many_users):
as_role('admin')
response = client.get('/users?per_page=3')
assert response.status_code == 200
# One
per user in the body, plus the header row.
rows = re.findall(r'
]', response.get_data(as_text=True))
assert len(rows) <= 4, 'the page returned more rows than per_page allows'
def test_the_second_page_holds_different_people(self, app, client, as_role, many_users):
as_role('admin')
first = client.get('/users?per_page=3').get_data(as_text=True)
second = client.get('/users?per_page=3&page=2').get_data(as_text=True)
assert first != second
def test_a_page_past_the_end_is_empty_not_a_404(self, app, client, as_role, many_users):
"""Page numbers come from the URL, so a stale bookmark is a normal
thing to receive. Flask-SQLAlchemy's default answers it with a 404."""
as_role('admin')
response = client.get('/users?page=999')
assert response.status_code == 200
def test_per_page_is_capped(self, app, client, as_role, many_users):
"""Without the cap, `?per_page=100000` is the unbounded query again."""
as_role('admin')
response = client.get(f'/users?per_page={MAX_PER_PAGE * 100}')
assert response.status_code == 200
def test_a_nonsense_page_does_not_crash(self, app, client, as_role, many_users):
as_role('admin')
assert client.get('/users?page=abc').status_code == 200
assert client.get('/users?page=-4').status_code == 200
assert client.get('/users?per_page=0').status_code == 200
class TestThePageLinksKeepTheQueryString:
"""The failure that is easy to ship and hard to notice: the person sorts
a column, clicks "next", and lands on an unsorted page 2."""
def test_evaluations_links_carry_the_sort(self, app, client, as_role, make_user):
from datetime import date
from app.models import Evaluation, Tryout
# One evaluation per player: the table has a unique constraint on
# (tryout, player, evaluator), which is DB-006 working as intended.
player_ids = [make_user('player') for _ in range(3)]
admin_id = as_role('admin')
with app.app_context():
tryout = Tryout(
title='Spring',
game='Valorant',
date=date(2030, 4, 1),
created_by=admin_id,
)
db.session.add(tryout)
db.session.flush()
for player_id in player_ids:
db.session.add(
Evaluation(
tryout_id=tryout.id,
player_id=player_id,
evaluator_id=admin_id,
overall_score=5,
)
)
db.session.commit()
body = client.get('/evaluations?sort=player&order=asc&per_page=1').get_data(as_text=True)
links = re.findall(r'href="([^"]*page=\d+[^"]*)"', body)
assert links, 'no pagination link was rendered; this test proves nothing'
assert all('sort=player' in link for link in links), (
f'a page link dropped the sort: {links}'
)
assert all('order=asc' in link for link in links)
def test_team_matches_links_carry_the_team_filter(self, app, client, as_role, make_user):
from datetime import date
from app.models import OrgTeam, TeamMatch
admin_id = as_role('admin')
with app.app_context():
team = OrgTeam(name='Varsity', created_by=admin_id)
db.session.add(team)
db.session.flush()
team_id = team.id
for index in range(3):
db.session.add(
TeamMatch(
title=f'Match {index}',
date=date(2030, 5, 1),
org_team_id=team_id,
created_by=admin_id,
)
)
db.session.commit()
body = client.get(f'/team-matches?team_id={team_id}&per_page=1').get_data(as_text=True)
links = re.findall(r'href="([^"]*page=\d+[^"]*)"', body)
assert links, 'no pagination link was rendered; this test proves nothing'
assert all(f'team_id={team_id}' in link for link in links), (
f'a page link dropped the team filter: {links}'
)
class TestTheControlsStayOutOfTheWayWhenThereIsOnePage:
def test_no_controls_for_a_short_list(self, app, client, as_role):
as_role('admin')
body = client.get('/users').get_data(as_text=True)
assert 'aria-label' not in body or 'page=2' not in body
class TestTheDefault:
def test_it_is_a_page_size_not_the_whole_table(self):
assert 0 < DEFAULT_PER_PAGE <= MAX_PER_PAGE