Adding top-level Scopes table
This commit is contained in:
1 parent
bc8b67d585
commit
3febc15a8f
11 files changed
+470
-1
No files matched your search
@@ -0,0 +1,160 @@
|
||||
import pytest
|
||||
|
||||
from app.models.augmented_part import AugmentedPart
|
||||
from app.models.fit import Fit, FitLabel
|
||||
from app.models.part_type import PartType
|
||||
from app.models.scope_info import ScopeInfo
|
||||
from app.services import scope_guide
|
||||
|
||||
|
||||
async def _seed_guide_data(db):
|
||||
scope_t = PartType(name="Scope")
|
||||
cover_t = PartType(name="Flip Cover")
|
||||
ard_t = PartType(name="ARD")
|
||||
db.add_all([scope_t, cover_t, ard_t])
|
||||
await db.flush()
|
||||
|
||||
def part(desc, type_id):
|
||||
p = AugmentedPart(
|
||||
description=desc, colour_id=1, manufacturer_id=1, part_type_id=type_id
|
||||
)
|
||||
db.add(p)
|
||||
return p
|
||||
|
||||
s1 = part("Alpha 3-18x50", scope_t.id)
|
||||
s2 = part("Zeta 1-6x24", scope_t.id)
|
||||
fc1 = part("Flip cover 50mm", cover_t.id)
|
||||
a1 = part("ARD 50mm", ard_t.id)
|
||||
a2 = part("ARD 56mm", ard_t.id)
|
||||
await db.flush()
|
||||
|
||||
db.add(
|
||||
ScopeInfo(
|
||||
augmented_part_id=s1.id,
|
||||
magnification_min=3,
|
||||
magnification_max=18,
|
||||
objective_thread_pitch="M52x0.75",
|
||||
)
|
||||
)
|
||||
|
||||
def fit(ap_a, ap_b, a, b, label):
|
||||
db.add(
|
||||
Fit(
|
||||
attachment_point_a_id=ap_a,
|
||||
attachment_point_b_id=ap_b,
|
||||
augmented_part_a_id=a.id,
|
||||
augmented_part_b_id=b.id,
|
||||
label=label,
|
||||
)
|
||||
)
|
||||
|
||||
# S1 fits FC1 (exact) via two attachment points -> should dedup to one exact.
|
||||
fit(1, 2, s1, fc1, FitLabel.POSSIBLE)
|
||||
fit(3, 4, s1, fc1, FitLabel.EXACT)
|
||||
# S1 fits A1 (possible); an explicit NONE to A2 must be excluded.
|
||||
fit(5, 6, s1, a1, FitLabel.POSSIBLE)
|
||||
fit(7, 8, s1, a2, FitLabel.NONE)
|
||||
# S2 fits FC1 (possible); order reversed to exercise the b-side branch.
|
||||
fit(9, 10, fc1, s2, FitLabel.POSSIBLE)
|
||||
await db.commit()
|
||||
|
||||
return {"scope": scope_t, "cover": cover_t, "ard": ard_t, "s1": s1, "s2": s2}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_columns_exclude_scope_type(db_sessionmaker):
|
||||
async with db_sessionmaker() as db:
|
||||
await _seed_guide_data(db)
|
||||
guide = await scope_guide.build(db)
|
||||
|
||||
names = [ct.name for ct in guide["column_types"]]
|
||||
assert names == ["ARD", "Flip Cover"] # sorted, Scope excluded
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_rows_group_fits_by_type_and_dedup(db_sessionmaker):
|
||||
async with db_sessionmaker() as db:
|
||||
ids = await _seed_guide_data(db)
|
||||
guide = await scope_guide.build(db)
|
||||
|
||||
cover_id, ard_id = ids["cover"].id, ids["ard"].id
|
||||
rows = {r["scope"].description: r for r in guide["rows"]}
|
||||
assert list(rows) == ["Alpha 3-18x50", "Zeta 1-6x24"] # ordered by description
|
||||
|
||||
s1 = rows["Alpha 3-18x50"]
|
||||
# Two fits to the same cover collapse to one entry, keeping the exact label.
|
||||
cover_cell = s1["cells"][cover_id]
|
||||
assert [(e["part"].description, e["label"]) for e in cover_cell] == [
|
||||
("Flip cover 50mm", "exact")
|
||||
]
|
||||
# Possible ARD present; the NONE-labelled ARD is excluded entirely.
|
||||
ard_cell = s1["cells"][ard_id]
|
||||
assert [(e["part"].description, e["label"]) for e in ard_cell] == [
|
||||
("ARD 50mm", "possible")
|
||||
]
|
||||
assert s1["info"] is not None
|
||||
assert s1["info"].magnification_max == 18
|
||||
|
||||
s2 = rows["Zeta 1-6x24"]
|
||||
assert [e["part"].description for e in s2["cells"][cover_id]] == ["Flip cover 50mm"]
|
||||
assert ard_id not in s2["cells"] # no ARD fits for S2
|
||||
assert s2["info"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_no_scope_type_returns_empty_rows(db_sessionmaker):
|
||||
async with db_sessionmaker() as db:
|
||||
db.add(PartType(name="Flip Cover"))
|
||||
await db.commit()
|
||||
guide = await scope_guide.build(db)
|
||||
assert guide["rows"] == []
|
||||
assert [ct.name for ct in guide["column_types"]] == ["Flip Cover"]
|
||||
|
||||
|
||||
async def _web_login(client, email, password="secret123"):
|
||||
resp = await client.post(
|
||||
"/login", data={"email": email, "password": password}
|
||||
)
|
||||
assert resp.status_code == 303
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scopes_page_renders_for_reader(client, seeded, make_user):
|
||||
await make_user("reader@example.com", role_ids=[seeded["roles"]["user"].id])
|
||||
await _web_login(client, "reader@example.com")
|
||||
resp = await client.get("/scopes")
|
||||
assert resp.status_code == 200
|
||||
assert "Scopes" in resp.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scopes_page_renders_rows_and_fits(
|
||||
client, seeded, make_user, db_sessionmaker
|
||||
):
|
||||
async with db_sessionmaker() as db:
|
||||
await _seed_guide_data(db)
|
||||
await make_user("reader2@example.com", role_ids=[seeded["roles"]["user"].id])
|
||||
await _web_login(client, "reader2@example.com")
|
||||
resp = await client.get("/scopes")
|
||||
assert resp.status_code == 200
|
||||
body = resp.text
|
||||
assert "Alpha 3-18x50" in body # scope row
|
||||
assert "Flip cover 50mm" in body # a matching part in a fit column
|
||||
assert "fit-badge-exact" in body # the exact fit badge
|
||||
assert "M52x0.75" in body # scope info: objective thread
|
||||
assert "3–18×" in body # magnification range, integral values un-padded
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scopes_page_forbidden_without_permission(client, seeded, make_user):
|
||||
await make_user("plain@example.com") # no roles, not a superuser
|
||||
await _web_login(client, "plain@example.com")
|
||||
resp = await client.get("/scopes")
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scopes_page_redirects_when_anonymous(client):
|
||||
resp = await client.get("/scopes")
|
||||
assert resp.status_code == 302
|
||||
assert resp.headers["location"] == "/login"
|
||||
Reference in new issue
Block a user