feat(backend): create improved tests
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# tests/repositories/test_member.py
|
||||
import datetime as dt
|
||||
from typing import List
|
||||
from typing import List, Any
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -380,4 +380,313 @@ def test_set_last_declined_resets_streak_and_records_date(member_repo: MemberRep
|
||||
|
||||
refreshed2 = member_repo.get_by_id(member.MemberId)
|
||||
assert refreshed2.DeclineStreak == 2
|
||||
assert refreshed2.LastDeclinedAt == tomorrow_iso
|
||||
assert refreshed2.LastDeclinedAt == tomorrow_iso
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 7️⃣ get_active – filter active members only
|
||||
# ----------------------------------------------------------------------
|
||||
def test_get_active_filters_correctly(member_repo: MemberRepository, clean_members):
|
||||
"""Test that get_active returns only active members."""
|
||||
# Create active member
|
||||
active_member = member_repo.create(
|
||||
first_name="Active",
|
||||
last_name="User",
|
||||
email="active@example.com",
|
||||
phone_number="555-1234",
|
||||
classification_id=1,
|
||||
is_active=1,
|
||||
)
|
||||
|
||||
# Create inactive member
|
||||
inactive_member = member_repo.create(
|
||||
first_name="Inactive",
|
||||
last_name="User",
|
||||
email="inactive@example.com",
|
||||
phone_number="555-5678",
|
||||
classification_id=1,
|
||||
is_active=0,
|
||||
)
|
||||
|
||||
active_members = member_repo.get_active()
|
||||
|
||||
# Should only return the active member
|
||||
assert len(active_members) == 1
|
||||
assert active_members[0].MemberId == active_member.MemberId
|
||||
assert active_members[0].FirstName == "Active"
|
||||
assert active_members[0].IsActive == 1
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 8️⃣ set_last_accepted – resets decline data and sets acceptance date
|
||||
# ----------------------------------------------------------------------
|
||||
def test_set_last_accepted_resets_decline_data(member_repo: MemberRepository):
|
||||
"""Test that set_last_accepted clears decline data and sets acceptance timestamp."""
|
||||
member = member_repo.create(
|
||||
first_name="Test",
|
||||
last_name="Member",
|
||||
email="test@example.com",
|
||||
phone_number=None,
|
||||
classification_id=1,
|
||||
is_active=1,
|
||||
)
|
||||
|
||||
# First decline the member to set up decline data
|
||||
yesterday_iso = (dt.date.today() - dt.timedelta(days=1)).isoformat()
|
||||
member_repo.set_last_declined(member.MemberId, yesterday_iso)
|
||||
|
||||
# Verify decline data is set
|
||||
declined_member = member_repo.get_by_id(member.MemberId)
|
||||
assert declined_member.DeclineStreak == 1
|
||||
assert declined_member.LastDeclinedAt == yesterday_iso
|
||||
assert declined_member.LastAcceptedAt is None
|
||||
|
||||
# Now accept
|
||||
member_repo.set_last_accepted(member.MemberId)
|
||||
|
||||
# Verify acceptance resets decline data
|
||||
accepted_member = member_repo.get_by_id(member.MemberId)
|
||||
assert accepted_member.DeclineStreak == 0
|
||||
assert accepted_member.LastDeclinedAt is None
|
||||
assert accepted_member.LastAcceptedAt is not None
|
||||
|
||||
# Verify timestamp is recent (within last 5 seconds)
|
||||
accepted_time = dt.datetime.fromisoformat(accepted_member.LastAcceptedAt.replace('f', '000'))
|
||||
time_diff = dt.datetime.utcnow() - accepted_time
|
||||
assert time_diff.total_seconds() < 5
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 9️⃣ reset_to_queue_front – moves member to front of scheduling queue
|
||||
# ----------------------------------------------------------------------
|
||||
def test_reset_to_queue_front_moves_member_to_front(member_repo: MemberRepository, clean_members):
|
||||
"""Test that reset_to_queue_front properly resets timestamps to move member to front."""
|
||||
# Create two members with different timestamps
|
||||
older_member = make_member(
|
||||
member_repo,
|
||||
"Older",
|
||||
"Member",
|
||||
accepted_at="2025-01-01 10:00:00",
|
||||
scheduled_at="2025-01-01 10:00:00",
|
||||
declined_at="2025-01-01 10:00:00",
|
||||
decline_streak=2,
|
||||
)
|
||||
|
||||
newer_member = make_member(
|
||||
member_repo,
|
||||
"Newer",
|
||||
"Member",
|
||||
accepted_at="2025-08-01 10:00:00",
|
||||
scheduled_at="2025-08-01 10:00:00",
|
||||
)
|
||||
|
||||
# Verify initial queue order (newer should come first due to older accepted date)
|
||||
initial_queue = member_repo.candidate_queue([1])
|
||||
assert len(initial_queue) == 2
|
||||
assert initial_queue[0].FirstName == "Older" # older accepted date comes first
|
||||
assert initial_queue[1].FirstName == "Newer"
|
||||
|
||||
# Reset newer member to queue front
|
||||
member_repo.reset_to_queue_front(newer_member.MemberId)
|
||||
|
||||
# Verify newer member is now at front
|
||||
updated_queue = member_repo.candidate_queue([1])
|
||||
assert len(updated_queue) == 2
|
||||
assert updated_queue[0].FirstName == "Newer" # should now be first
|
||||
assert updated_queue[1].FirstName == "Older"
|
||||
|
||||
# Verify the reset member has expected timestamp values
|
||||
reset_member = member_repo.get_by_id(newer_member.MemberId)
|
||||
assert reset_member.LastAcceptedAt == '1970-01-01 00:00:00'
|
||||
assert reset_member.LastScheduledAt == '1970-01-01 00:00:00'
|
||||
assert reset_member.LastDeclinedAt is None
|
||||
assert reset_member.DeclineStreak == 0
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 🔟 Edge cases and error conditions
|
||||
# ----------------------------------------------------------------------
|
||||
def test_create_with_minimal_data(member_repo: MemberRepository):
|
||||
"""Test creating a member with only required fields."""
|
||||
member = member_repo.create(
|
||||
first_name="Min",
|
||||
last_name="Member"
|
||||
)
|
||||
|
||||
assert member.FirstName == "Min"
|
||||
assert member.LastName == "Member"
|
||||
assert member.Email is None
|
||||
assert member.PhoneNumber is None
|
||||
assert member.ClassificationId is None
|
||||
assert member.Notes is None
|
||||
assert member.IsActive == 1 # default value
|
||||
assert member.DeclineStreak == 0 # default value
|
||||
|
||||
|
||||
def test_get_by_classification_ids_empty_list(member_repo: MemberRepository):
|
||||
"""Test that empty classification list returns empty result without DB query."""
|
||||
result = member_repo.get_by_classification_ids([])
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_get_by_classification_ids_nonexistent_classification(member_repo: MemberRepository):
|
||||
"""Test querying for nonexistent classification IDs."""
|
||||
result = member_repo.get_by_classification_ids([999, 1000])
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_candidate_queue_with_inactive_members(member_repo: MemberRepository, clean_members):
|
||||
"""Test that candidate_queue respects only_active parameter."""
|
||||
# Create active and inactive members
|
||||
active = make_member(member_repo, "Active", "Member", is_active=1)
|
||||
inactive = make_member(member_repo, "Inactive", "Member", is_active=0)
|
||||
|
||||
# Test with only_active=True (default)
|
||||
queue_active_only = member_repo.candidate_queue([1], only_active=True)
|
||||
assert len(queue_active_only) == 1
|
||||
assert queue_active_only[0].FirstName == "Active"
|
||||
|
||||
# Test with only_active=False
|
||||
queue_all = member_repo.candidate_queue([1], only_active=False)
|
||||
assert len(queue_all) == 2
|
||||
names = {m.FirstName for m in queue_all}
|
||||
assert names == {"Active", "Inactive"}
|
||||
|
||||
|
||||
def test_candidate_queue_boost_window_edge_cases(member_repo: MemberRepository, clean_members):
|
||||
"""Test boost logic with edge cases around the boost window."""
|
||||
now = dt.datetime.utcnow()
|
||||
|
||||
# Member declined well outside boost window (3 days ago)
|
||||
outside_window = (now - dt.timedelta(days=3)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
outside_member = make_member(
|
||||
member_repo,
|
||||
"Outside",
|
||||
"Member",
|
||||
declined_at=outside_window,
|
||||
decline_streak=1,
|
||||
)
|
||||
|
||||
# Member declined well inside boost window (6 hours ago)
|
||||
well_inside = (now - dt.timedelta(hours=6)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
inside_member = make_member(
|
||||
member_repo,
|
||||
"Inside",
|
||||
"Member",
|
||||
declined_at=well_inside,
|
||||
decline_streak=1,
|
||||
)
|
||||
|
||||
# Member with high decline streak (should not get boost)
|
||||
high_streak_member = make_member(
|
||||
member_repo,
|
||||
"HighStreak",
|
||||
"Member",
|
||||
declined_at=well_inside,
|
||||
decline_streak=5, # >= 2, so no boost
|
||||
)
|
||||
|
||||
queue = member_repo.candidate_queue([1])
|
||||
|
||||
# Inside member should be boosted to front
|
||||
first_names = [m.FirstName for m in queue]
|
||||
assert "Inside" == first_names[0] # should be boosted
|
||||
assert "HighStreak" in first_names[1:] # should not be boosted
|
||||
assert "Outside" in first_names[1:] # should not be boosted
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"decline_streak,should_boost",
|
||||
[
|
||||
(0, True), # streak < 2
|
||||
(1, True), # streak < 2
|
||||
(2, False), # streak >= 2
|
||||
(5, False), # streak >= 2
|
||||
]
|
||||
)
|
||||
def test_candidate_queue_decline_streak_boost_logic(
|
||||
member_repo: MemberRepository,
|
||||
clean_members,
|
||||
decline_streak: int,
|
||||
should_boost: bool
|
||||
):
|
||||
"""Test boost logic for different decline streak values."""
|
||||
now = dt.datetime.utcnow()
|
||||
recent_decline = (now - dt.timedelta(hours=12)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Create a baseline member (no recent declines)
|
||||
baseline = make_member(member_repo, "Baseline", "Member")
|
||||
|
||||
# Create test member with specific decline streak
|
||||
test_member = make_member(
|
||||
member_repo,
|
||||
"Test",
|
||||
"Member",
|
||||
declined_at=recent_decline,
|
||||
decline_streak=decline_streak,
|
||||
)
|
||||
|
||||
queue = member_repo.candidate_queue([1])
|
||||
first_names = [m.FirstName for m in queue]
|
||||
|
||||
if should_boost:
|
||||
assert first_names[0] == "Test" # boosted to front
|
||||
assert first_names[1] == "Baseline"
|
||||
else:
|
||||
# Order depends on other factors, but Test should not be boosted
|
||||
# Both have NULL LastAcceptedAt, so order by LastScheduledAt (both NULL)
|
||||
# then likely by primary key order
|
||||
assert "Test" in first_names
|
||||
assert "Baseline" in first_names
|
||||
|
||||
|
||||
def test_touch_last_scheduled_with_nonexistent_member(member_repo: MemberRepository):
|
||||
"""Test touch_last_scheduled with nonexistent member ID (should not raise error)."""
|
||||
# This should not raise an exception, just silently do nothing
|
||||
member_repo.touch_last_scheduled(99999)
|
||||
|
||||
|
||||
def test_set_operations_with_nonexistent_member(member_repo: MemberRepository):
|
||||
"""Test set operations with nonexistent member IDs."""
|
||||
# These should not raise exceptions
|
||||
member_repo.set_last_accepted(99999)
|
||||
member_repo.set_last_declined(99999, "2025-08-29")
|
||||
member_repo.reset_to_queue_front(99999)
|
||||
|
||||
|
||||
def test_member_data_integrity_after_operations(member_repo: MemberRepository):
|
||||
"""Test that member data remains consistent after various operations."""
|
||||
member = member_repo.create(
|
||||
first_name="Integrity",
|
||||
last_name="Test",
|
||||
email="integrity@example.com",
|
||||
phone_number="555-0000",
|
||||
classification_id=2,
|
||||
notes="Test member",
|
||||
is_active=1,
|
||||
)
|
||||
|
||||
original_id = member.MemberId
|
||||
original_email = member.Email
|
||||
original_classification = member.ClassificationId
|
||||
|
||||
# Perform various timestamp operations
|
||||
member_repo.touch_last_scheduled(member.MemberId)
|
||||
member_repo.set_last_declined(member.MemberId, "2025-08-29")
|
||||
member_repo.set_last_accepted(member.MemberId)
|
||||
member_repo.reset_to_queue_front(member.MemberId)
|
||||
|
||||
# Verify core data is unchanged
|
||||
final_member = member_repo.get_by_id(member.MemberId)
|
||||
assert final_member.MemberId == original_id
|
||||
assert final_member.FirstName == "Integrity"
|
||||
assert final_member.LastName == "Test"
|
||||
assert final_member.Email == original_email
|
||||
assert final_member.ClassificationId == original_classification
|
||||
assert final_member.IsActive == 1
|
||||
|
||||
# Verify reset operation results
|
||||
assert final_member.LastAcceptedAt == '1970-01-01 00:00:00'
|
||||
assert final_member.LastScheduledAt == '1970-01-01 00:00:00'
|
||||
assert final_member.LastDeclinedAt is None
|
||||
assert final_member.DeclineStreak == 0
|
||||
Reference in New Issue
Block a user