feat(backend): refactor mono repository
This commit is contained in:
93
backend/tests/repositories/test_classification.py
Normal file
93
backend/tests/repositories/test_classification.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# backend/tests/repositories/test_classification.py
|
||||
# ------------------------------------------------------------
|
||||
# Pytest suite for the ClassificationRepository.
|
||||
# ------------------------------------------------------------
|
||||
|
||||
import pytest
|
||||
from backend.models import Classification as ClassificationModel
|
||||
from backend.repositories import ClassificationRepository
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 1️⃣ Basic CRUD – create & get_by_id
|
||||
# ----------------------------------------------------------------------
|
||||
def test_create_and_get_by_id(classification_repo):
|
||||
new = classification_repo.create("Countertenor")
|
||||
assert isinstance(new.ClassificationId, int) and new.ClassificationId > 0
|
||||
assert new.ClassificationName == "Countertenor"
|
||||
|
||||
fetched = classification_repo.get_by_id(new.ClassificationId)
|
||||
assert fetched is not None
|
||||
assert fetched.ClassificationId == new.ClassificationId
|
||||
assert fetched.ClassificationName == "Countertenor"
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 2️⃣ Lookup by name (exact match)
|
||||
# ----------------------------------------------------------------------
|
||||
def test_find_by_name_existing(classification_repo):
|
||||
soprano = classification_repo.find_by_name("Soprano")
|
||||
assert soprano is not None
|
||||
assert soprano.ClassificationName == "Soprano"
|
||||
|
||||
|
||||
def test_find_by_name_missing(classification_repo):
|
||||
missing = classification_repo.find_by_name("Bass")
|
||||
assert missing is None
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 3️⃣ List all classifications (ordered alphabetically)
|
||||
# ----------------------------------------------------------------------
|
||||
def test_list_all(classification_repo):
|
||||
all_rows: list[ClassificationModel] = classification_repo.list_all()
|
||||
assert len(all_rows) == 4 # the four seeded rows
|
||||
names = [row.ClassificationName for row in all_rows]
|
||||
assert names == sorted(names)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 4️⃣ Idempotent helper – ensure_exists
|
||||
# ----------------------------------------------------------------------
|
||||
def test_ensure_exists_creates_when_missing(classification_repo):
|
||||
before = classification_repo.find_by_name("Bass")
|
||||
assert before is None
|
||||
|
||||
bass = classification_repo.ensure_exists("Bass")
|
||||
assert isinstance(bass, ClassificationModel)
|
||||
assert bass.ClassificationName == "Bass"
|
||||
|
||||
# second call returns the same row
|
||||
again = classification_repo.ensure_exists("Bass")
|
||||
assert again.ClassificationId == bass.ClassificationId
|
||||
|
||||
# total rows = 4 seeded + the new “Bass”
|
||||
all_rows = classification_repo.list_all()
|
||||
assert len(all_rows) == 5
|
||||
|
||||
|
||||
def test_ensure_exists_returns_existing(classification_repo):
|
||||
existing = classification_repo.ensure_exists("Alto / Mezzo")
|
||||
assert existing is not None
|
||||
assert existing.ClassificationName == "Alto / Mezzo"
|
||||
|
||||
# no extra rows added
|
||||
all_rows = classification_repo.list_all()
|
||||
assert len(all_rows) == 4
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 5️⃣ Delete (optional – demonstrates that the method works)
|
||||
# ----------------------------------------------------------------------
|
||||
def test_delete(classification_repo):
|
||||
temp = classification_repo.create("TempVoice")
|
||||
assert classification_repo.find_by_name("TempVoice") is not None
|
||||
|
||||
classification_repo.delete(temp.ClassificationId)
|
||||
assert classification_repo.find_by_name("TempVoice") is None
|
||||
|
||||
remaining = classification_repo.list_all()
|
||||
remaining_names = {r.ClassificationName for r in remaining}
|
||||
assert "TempVoice" not in remaining_names
|
||||
# the original four seeded names must still be present
|
||||
assert {"Soprano", "Alto / Mezzo", "Tenor", "Baritone"} <= remaining_names
|
||||
383
backend/tests/repositories/test_member.py
Normal file
383
backend/tests/repositories/test_member.py
Normal file
@@ -0,0 +1,383 @@
|
||||
# tests/repositories/test_member.py
|
||||
import datetime as dt
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.models import Member as MemberModel, ScheduleStatus
|
||||
from backend.repositories import MemberRepository
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Helper: a few sample members we can reuse across tests
|
||||
# ----------------------------------------------------------------------
|
||||
@pytest.fixture
|
||||
def sample_members() -> List[MemberModel]:
|
||||
"""Return a list of MemberModel objects (not yet persisted)."""
|
||||
return [
|
||||
MemberModel(
|
||||
MemberId=-1,
|
||||
FirstName="Alice",
|
||||
LastName="Anderson",
|
||||
Email="alice@example.com",
|
||||
PhoneNumber="555‑1111",
|
||||
ClassificationId=1,
|
||||
Notes=None,
|
||||
IsActive=1,
|
||||
LastScheduledAt=None,
|
||||
LastAcceptedAt=None,
|
||||
LastDeclinedAt=None,
|
||||
DeclineStreak=0,
|
||||
),
|
||||
MemberModel(
|
||||
MemberId=-1,
|
||||
FirstName="Bob",
|
||||
LastName="Baker",
|
||||
Email="bob@example.com",
|
||||
PhoneNumber="555‑2222",
|
||||
ClassificationId=2,
|
||||
Notes="VIP",
|
||||
IsActive=1,
|
||||
LastScheduledAt=dt.datetime(2025, 8, 20, 10, 0, 0),
|
||||
LastAcceptedAt=dt.datetime(2025, 8, 19, 9, 30, 0),
|
||||
LastDeclinedAt=None,
|
||||
DeclineStreak=0,
|
||||
),
|
||||
MemberModel(
|
||||
MemberId=-1,
|
||||
FirstName="Carol",
|
||||
LastName="Carter",
|
||||
Email=None,
|
||||
PhoneNumber=None,
|
||||
ClassificationId=1,
|
||||
Notes=None,
|
||||
IsActive=0, # inactive – useful for filter tests
|
||||
LastScheduledAt=None,
|
||||
LastAcceptedAt=None,
|
||||
LastDeclinedAt=None,
|
||||
DeclineStreak=0,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Fixture to wipe the Members table (used by tests that need a clean slate)
|
||||
# ----------------------------------------------------------------------
|
||||
@pytest.fixture
|
||||
def clean_members(member_repo: MemberRepository):
|
||||
"""
|
||||
Delete *all* rows from the Members table **and** any rows that
|
||||
reference it (ServiceAvailability). The service‑availability tests
|
||||
rely on the seeded Alice/Bob rows, so we only invoke this fixture
|
||||
in the member‑repo tests that need isolation.
|
||||
"""
|
||||
# 1️⃣ Remove dependent rows first – otherwise the FK constraint blocks us.
|
||||
member_repo.db.execute(
|
||||
f"DELETE FROM ServiceAvailability"
|
||||
) # commit happens inside `execute`
|
||||
|
||||
# 2️⃣ Now we can safely delete the members themselves.
|
||||
member_repo.db.execute(
|
||||
f"DELETE FROM {member_repo._TABLE}"
|
||||
)
|
||||
member_repo.db._conn.commit()
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Helper to build a MemberModel with explicit timestamps.
|
||||
# ----------------------------------------------------------------------
|
||||
def make_member(
|
||||
repo: MemberRepository,
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
*,
|
||||
classification_id: int = 1,
|
||||
is_active: int = 1,
|
||||
accepted_at: str | None = None,
|
||||
scheduled_at: str | None = None,
|
||||
declined_at: str | None = None,
|
||||
decline_streak: int = 0,
|
||||
) -> MemberModel:
|
||||
"""Insert a member and then manually set the optional timestamp columns."""
|
||||
m = repo.create(
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
email=None,
|
||||
phone_number=None,
|
||||
classification_id=classification_id,
|
||||
notes=None,
|
||||
is_active=is_active,
|
||||
)
|
||||
# Directly update the row so we can control the timestamps without
|
||||
# invoking the repository’s higher‑level helpers (which would reset
|
||||
# other fields).
|
||||
sql = f"""
|
||||
UPDATE {repo._TABLE}
|
||||
SET
|
||||
LastAcceptedAt = ?,
|
||||
LastScheduledAt = ?,
|
||||
LastDeclinedAt = ?,
|
||||
DeclineStreak = ?
|
||||
WHERE {repo._PK} = ?
|
||||
"""
|
||||
repo.db.execute(
|
||||
sql,
|
||||
(
|
||||
accepted_at,
|
||||
scheduled_at,
|
||||
declined_at,
|
||||
decline_streak,
|
||||
m.MemberId,
|
||||
),
|
||||
)
|
||||
# Refresh the model from the DB so the attributes reflect the changes.
|
||||
return repo.get_by_id(m.MemberId) # type: ignore[return-value]
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 1️⃣ Basic CRUD – create & get_by_id
|
||||
# ----------------------------------------------------------------------
|
||||
def test_create_and_get_by_id(member_repo: MemberRepository):
|
||||
member = member_repo.create(
|
||||
first_name="Diana",
|
||||
last_name="Doe",
|
||||
email="diana@example.com",
|
||||
phone_number="555‑3333",
|
||||
classification_id=3,
|
||||
notes="New recruit",
|
||||
is_active=1,
|
||||
)
|
||||
|
||||
# Primary key should be a positive integer (AUTOINCREMENT starts at 1)
|
||||
assert isinstance(member.MemberId, int) and member.MemberId > 0
|
||||
|
||||
# Retrieve the same row
|
||||
fetched = member_repo.get_by_id(member.MemberId)
|
||||
assert fetched is not None
|
||||
assert fetched.FirstName == "Diana"
|
||||
assert fetched.LastName == "Doe"
|
||||
assert fetched.Email == "diana@example.com"
|
||||
assert fetched.ClassificationId == 3
|
||||
assert fetched.IsActive == 1
|
||||
assert fetched.Notes == "New recruit"
|
||||
|
||||
|
||||
def test_get_by_id_returns_none_when_missing(member_repo: MemberRepository):
|
||||
"""A PK that does not exist must return ``None`` (no exception)."""
|
||||
assert member_repo.get_by_id(9999) is None
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 2️⃣ list_all – bulk insertion + retrieval
|
||||
# ----------------------------------------------------------------------
|
||||
def test_list_all(
|
||||
member_repo: MemberRepository,
|
||||
sample_members: List[MemberModel],
|
||||
clean_members, # ensure we start from an empty table
|
||||
):
|
||||
for m in sample_members:
|
||||
member_repo.create(
|
||||
first_name=m.FirstName,
|
||||
last_name=m.LastName,
|
||||
email=m.Email,
|
||||
phone_number=m.PhoneNumber,
|
||||
classification_id=m.ClassificationId,
|
||||
notes=m.Notes,
|
||||
is_active=m.IsActive,
|
||||
)
|
||||
|
||||
all_members = member_repo.list_all()
|
||||
# Because we cleared the table first, we expect exactly the three we added.
|
||||
assert len(all_members) == 3
|
||||
|
||||
# Spot‑check that each name appears
|
||||
names = {(m.FirstName, m.LastName) for m in all_members}
|
||||
assert ("Alice", "Anderson") in names
|
||||
assert ("Bob", "Baker") in names
|
||||
assert ("Carol", "Carter") in names
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 3️⃣ get_by_classification_ids – filter by classification list
|
||||
# ----------------------------------------------------------------------
|
||||
def test_get_by_classification_ids(
|
||||
member_repo: MemberRepository,
|
||||
sample_members: List[MemberModel],
|
||||
clean_members,
|
||||
):
|
||||
for m in sample_members:
|
||||
member_repo.create(
|
||||
first_name=m.FirstName,
|
||||
last_name=m.LastName,
|
||||
email=m.Email,
|
||||
phone_number=m.PhoneNumber,
|
||||
classification_id=m.ClassificationId,
|
||||
notes=m.Notes,
|
||||
is_active=m.IsActive,
|
||||
)
|
||||
|
||||
# Classification 1 → Alice + Carol (2 rows)
|
||||
result = member_repo.get_by_classification_ids([1])
|
||||
assert len(result) == 2
|
||||
assert {r.FirstName for r in result} == {"Alice", "Carol"}
|
||||
|
||||
# Classification 2 → only Bob
|
||||
result = member_repo.get_by_classification_ids([2])
|
||||
assert len(result) == 1
|
||||
assert result[0].FirstName == "Bob"
|
||||
|
||||
# Both classifications → all three
|
||||
result = member_repo.get_by_classification_ids([1, 2])
|
||||
assert len(result) == 3
|
||||
|
||||
|
||||
def test_candidate_queue_obeys_boost_and_timestamp_sorting(
|
||||
member_repo: MemberRepository,
|
||||
):
|
||||
"""
|
||||
Verify that ``candidate_queue`` respects:
|
||||
|
||||
1️⃣ The boost clause (low ``DeclineStreak`` + recent ``LastDeclinedAt``).
|
||||
2️⃣ ``LastAcceptedAt`` ASC (oldest first, ``NULL`` → far‑past).
|
||||
3️⃣ ``LastScheduledAt`` ASC (same handling).
|
||||
|
||||
The default boost window is 2 days (172 800 seconds).
|
||||
|
||||
Additional rule (as stated in the doc‑string):
|
||||
*Members whose ``LastAcceptedAt`` is NULL should appear **before** members
|
||||
that have a non‑NULL acceptance date.*
|
||||
"""
|
||||
# --------------------------------------------------------------
|
||||
# 0️⃣ Remove any ServiceAvailability rows that reference the seeded
|
||||
# members, then delete the seeded members themselves.
|
||||
# --------------------------------------------------------------
|
||||
member_repo.db.execute("DELETE FROM ServiceAvailability")
|
||||
member_repo.db.execute(
|
||||
f"DELETE FROM {member_repo._TABLE} WHERE MemberId IN (1, 2)"
|
||||
)
|
||||
member_repo.db._conn.commit()
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# 1️⃣ Build a diverse set of members.
|
||||
# --------------------------------------------------------------
|
||||
# ── A – active, no timestamps (baseline, NULL acceptance)
|
||||
a = make_member(member_repo, "Alice", "Anderson")
|
||||
|
||||
# ── B – active, accepted yesterday (non‑NULL acceptance)
|
||||
yesterday = (dt.datetime.utcnow() - dt.timedelta(days=1)).strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
b = make_member(
|
||||
member_repo,
|
||||
"Bob",
|
||||
"Baker",
|
||||
accepted_at=yesterday,
|
||||
)
|
||||
|
||||
# ── C – active, declined **today** with a low streak (boost candidate)
|
||||
today_iso = dt.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
||||
c = make_member(
|
||||
member_repo,
|
||||
"Carol",
|
||||
"Clark",
|
||||
declined_at=today_iso,
|
||||
decline_streak=1, # < 2 → qualifies for boost
|
||||
)
|
||||
|
||||
# ── D – active, declined **3 days ago** (outside the 2‑day boost window,
|
||||
# still NULL acceptance)
|
||||
three_days_ago = (dt.datetime.utcnow() - dt.timedelta(days=3)).strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
d = make_member(
|
||||
member_repo,
|
||||
"Dave",
|
||||
"Davis",
|
||||
declined_at=three_days_ago,
|
||||
decline_streak=1,
|
||||
)
|
||||
|
||||
# ── E – **inactive** member – should never appear when only_active=True.
|
||||
e = make_member(
|
||||
member_repo,
|
||||
"Eve",
|
||||
"Evans",
|
||||
is_active=0,
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# 2️⃣ Pull the queue (default: only_active=True, boost_seconds=2 days)
|
||||
# --------------------------------------------------------------
|
||||
q = member_repo.candidate_queue(classification_ids=[1])
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# 3️⃣ Expected order (explain each step):
|
||||
# --------------------------------------------------------------
|
||||
# • Boosted members first → Carol (recent decline, streak < 2)
|
||||
# • Then all members whose ``LastAcceptedAt`` is NULL,
|
||||
# ordered by ``LastScheduledAt`` (both are NULL, so fallback to PK order):
|
||||
# → Alice, then Dave
|
||||
# • Finally members with a non‑NULL acceptance date → Bob
|
||||
# • Eve is inactive → omitted.
|
||||
expected_first_names = ["Carol", "Alice", "Dave", "Bob"]
|
||||
assert [m.FirstName for m in q] == expected_first_names
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 5️⃣ touch_last_scheduled – updates the timestamp column
|
||||
# ----------------------------------------------------------------------
|
||||
def test_touch_last_scheduled_updates_timestamp(member_repo: MemberRepository):
|
||||
member = member_repo.create(
|
||||
first_name="Eve",
|
||||
last_name="Evans",
|
||||
email=None,
|
||||
phone_number=None,
|
||||
classification_id=4,
|
||||
notes=None,
|
||||
is_active=1,
|
||||
)
|
||||
assert member.LastScheduledAt is None
|
||||
|
||||
# Call the helper – it should set LastScheduledAt to the current UTC time.
|
||||
member_repo.touch_last_scheduled(member.MemberId)
|
||||
|
||||
refreshed = member_repo.get_by_id(member.MemberId)
|
||||
assert refreshed is not None
|
||||
assert refreshed.LastScheduledAt is not None
|
||||
|
||||
# SQLite stores timestamps as ISO‑8601 strings; parsing should succeed.
|
||||
dt.datetime.fromisoformat(refreshed.LastScheduledAt)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 6️⃣ set_last_declined – records decline date and increments streak
|
||||
# ----------------------------------------------------------------------
|
||||
def test_set_last_declined_resets_streak_and_records_date(member_repo: MemberRepository):
|
||||
member = member_repo.create(
|
||||
first_name="Frank",
|
||||
last_name="Foster",
|
||||
email=None,
|
||||
phone_number=None,
|
||||
classification_id=4,
|
||||
notes=None,
|
||||
is_active=1,
|
||||
)
|
||||
# Initial state
|
||||
assert member.DeclineStreak == 0
|
||||
assert member.LastDeclinedAt is None
|
||||
|
||||
# Simulate a decline today.
|
||||
today_iso = dt.date.today().isoformat()
|
||||
member_repo.set_last_declined(member.MemberId, today_iso)
|
||||
|
||||
refreshed = member_repo.get_by_id(member.MemberId)
|
||||
assert refreshed.DeclineStreak == 1
|
||||
assert refreshed.LastDeclinedAt == today_iso
|
||||
|
||||
# Simulate a second decline tomorrow – streak should increase again.
|
||||
tomorrow_iso = (dt.date.today() + dt.timedelta(days=1)).isoformat()
|
||||
member_repo.set_last_declined(member.MemberId, tomorrow_iso)
|
||||
|
||||
refreshed2 = member_repo.get_by_id(member.MemberId)
|
||||
assert refreshed2.DeclineStreak == 2
|
||||
assert refreshed2.LastDeclinedAt == tomorrow_iso
|
||||
0
backend/tests/repositories/test_service.py
Normal file
0
backend/tests/repositories/test_service.py
Normal file
69
backend/tests/repositories/test_service_availability.py
Normal file
69
backend/tests/repositories/test_service_availability.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# tests/test_service_availability.py
|
||||
import pytest
|
||||
|
||||
def test_grant_and_revoke(
|
||||
service_availability_repo,
|
||||
member_repo,
|
||||
service_type_repo,
|
||||
):
|
||||
"""
|
||||
Verify that:
|
||||
• `grant` adds a new (member, service_type) pair idempotently.
|
||||
• `revoke` removes the pair.
|
||||
• The helper `members_for_type` returns the expected IDs.
|
||||
"""
|
||||
# ------------------------------------------------------------------
|
||||
# Arrange – fetch the IDs we know exist from the fixture.
|
||||
# ------------------------------------------------------------------
|
||||
# Alice is member_id 1, Bob is member_id 2 (AUTOINCREMENT order).
|
||||
alice_id = 1
|
||||
bob_id = 2
|
||||
|
||||
# Service type IDs correspond to the order we inserted them:
|
||||
# 9AM → 1, 11AM → 2, 6PM → 3
|
||||
nine_am_id = 1
|
||||
eleven_am_id = 2
|
||||
six_pm_id = 3
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Act – try granting a *new* availability that wasn't seeded.
|
||||
# We'll give Alice the 11AM slot (she didn't have it before).
|
||||
# ------------------------------------------------------------------
|
||||
new_pair = service_availability_repo.grant(alice_id, eleven_am_id)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Assert – the row exists and the helper returns the right member list.
|
||||
# ------------------------------------------------------------------
|
||||
assert new_pair.MemberId == alice_id
|
||||
assert new_pair.ServiceTypeId == eleven_am_id
|
||||
|
||||
# `members_for_type` should now contain Alice (1) **and** Bob (2) for 11AM.
|
||||
members_for_11am = service_availability_repo.members_for_type(eleven_am_id)
|
||||
assert set(members_for_11am) == {alice_id, bob_id}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Revoke the newly added pair and ensure it disappears.
|
||||
# ------------------------------------------------------------------
|
||||
service_availability_repo.revoke(alice_id, eleven_am_id)
|
||||
|
||||
# After revocation the 11AM list should contain **only** Bob.
|
||||
members_after_revoke = service_availability_repo.members_for_type(eleven_am_id)
|
||||
assert members_after_revoke == [bob_id]
|
||||
|
||||
# Also verify that `get` returns None for the removed pair.
|
||||
assert service_availability_repo.get(alice_id, eleven_am_id) is None
|
||||
|
||||
|
||||
def test_list_by_member(service_availability_repo):
|
||||
"""
|
||||
Validate that `list_by_member` returns exactly the slots we seeded.
|
||||
"""
|
||||
# Alice (member_id 1) should have 9AM (1) and 6PM (3)
|
||||
alice_slots = service_availability_repo.list_by_member(1)
|
||||
alice_type_ids = sorted([s.ServiceTypeId for s in alice_slots])
|
||||
assert alice_type_ids == [1, 3]
|
||||
|
||||
# Bob (member_id 2) should have 11AM (2) and 6PM (3)
|
||||
bob_slots = service_availability_repo.list_by_member(2)
|
||||
bob_type_ids = sorted([s.ServiceTypeId for s in bob_slots])
|
||||
assert bob_type_ids == [2, 3]
|
||||
62
backend/tests/repositories/test_service_type.py
Normal file
62
backend/tests/repositories/test_service_type.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# tests/test_service_type_repo.py
|
||||
import pytest
|
||||
|
||||
from backend.models.dataclasses import ServiceType as ServiceTypeModel
|
||||
|
||||
def test_create_and_find(service_type_repo):
|
||||
"""
|
||||
Verify that we can insert a brand‑new ServiceType and retrieve it
|
||||
both by primary key and by name.
|
||||
"""
|
||||
# Create a new slot that wasn't part of the seed data.
|
||||
new_slot = service_type_repo.create("2PM")
|
||||
assert isinstance(new_slot, ServiceTypeModel)
|
||||
assert new_slot.TypeName == "2PM"
|
||||
assert new_slot.ServiceTypeId > 0 # auto‑increment worked
|
||||
|
||||
# Find by primary key.
|
||||
fetched_by_id = service_type_repo.get_by_id(new_slot.ServiceTypeId)
|
||||
assert fetched_by_id == new_slot
|
||||
|
||||
# Find by name.
|
||||
fetched_by_name = service_type_repo.find_by_name("2PM")
|
||||
assert fetched_by_name == new_slot
|
||||
|
||||
|
||||
def test_list_all_contains_seeded_slots(service_type_repo):
|
||||
"""
|
||||
The three seeded slots (9AM, 11AM, 6PM) should be present and sorted
|
||||
alphabetically by the repository implementation.
|
||||
"""
|
||||
all_slots = service_type_repo.list_all()
|
||||
names = [s.TypeName for s in all_slots]
|
||||
|
||||
# The seed fixture inserted exactly these three names.
|
||||
assert set(names) >= {"9AM", "11AM", "6PM"}
|
||||
|
||||
# Because ``list_all`` orders by ``TypeName ASC`` we expect alphabetical order.
|
||||
assert names == sorted(names)
|
||||
|
||||
|
||||
def test_ensure_slots_is_idempotent(service_type_repo):
|
||||
"""
|
||||
``ensure_slots`` should insert missing rows and return the full set,
|
||||
without creating duplicates on subsequent calls.
|
||||
"""
|
||||
# First call – inserts the three seed rows plus a brand‑new one.
|
||||
wanted = ["9AM", "11AM", "6PM", "3PM"]
|
||||
result_first = service_type_repo.ensure_slots(wanted)
|
||||
|
||||
# All four names must now exist.
|
||||
assert {s.TypeName for s in result_first} == set(wanted)
|
||||
|
||||
# Capture the IDs for later comparison.
|
||||
ids_before = {s.TypeName: s.ServiceTypeId for s in result_first}
|
||||
|
||||
# Second call – should *not* create new rows.
|
||||
result_second = service_type_repo.ensure_slots(wanted)
|
||||
ids_after = {s.TypeName: s.ServiceTypeId for s in result_second}
|
||||
|
||||
# IDs must be unchanged (no duplicates were added).
|
||||
assert ids_before == ids_after
|
||||
assert len(result_second) == len(wanted)
|
||||
Reference in New Issue
Block a user