Files
battery-tracker-app/tests/test_acceptance.py
T
iterminate 6ea3eae981 Initial commit: Flask battery tracker app
- Flask + SQLAlchemy (MariaDB-compatible schema) battery tracking web app
- 40 pre-seeded batteries (Eneloop, BONAI, Energizer NiMH) across 5 devices
- Business rules: block retired assignment, brand-mix warnings, capacity checks
- Mobile-friendly Jinja2 templates with inline CSS
- waitress WSGI server via systemd user service (sbin/install-service.sh)
- SQLite → MariaDB migration script (migrate_to_mariadb.py)
- 26 passing acceptance tests (pytest + Flask test client)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 22:38:16 -05:00

282 lines
9.7 KiB
Python

"""
Acceptance tests for Battery Tracker.
Each test gets a fresh in-memory SQLite database via the fixtures in conftest.py.
The `seeded_client` fixture pre-populates:
Devices: Device A (2 slots), Device B (1 slot)
Batteries: TST-01 (BrandX, available), TST-02 (BrandY, available), TST-03 (BrandX, retired)
"""
import pytest
# ------------------------------------------------------------------ #
# Helpers
# ------------------------------------------------------------------ #
def get_location(resp):
"""Return the redirect Location header (without host)."""
loc = resp.headers.get("Location", "")
# Strip scheme+host if present
if loc.startswith("http"):
from urllib.parse import urlparse
loc = urlparse(loc).path
return loc
def follow(client, resp):
"""Follow a single redirect."""
return client.get(get_location(resp))
# ------------------------------------------------------------------ #
# Dashboard
# ------------------------------------------------------------------ #
def test_dashboard_loads(seeded_client):
resp = seeded_client.get("/")
assert resp.status_code == 200
assert b"TST-01" in resp.data
assert b"TST-02" in resp.data
assert b"TST-03" in resp.data
# ------------------------------------------------------------------ #
# Battery — add
# ------------------------------------------------------------------ #
def test_add_battery(client):
resp = client.post("/battery/add",
data={"label": "NEW-01", "brand": "TestBrand", "status": "available"},
follow_redirects=True)
assert resp.status_code == 200
assert b"NEW-01" in resp.data
def test_add_battery_duplicate_label(seeded_client):
resp = seeded_client.post("/battery/add",
data={"label": "TST-01", "brand": "OtherBrand", "status": "available"})
assert resp.status_code == 400
assert b"already exists" in resp.data
def test_add_battery_missing_fields(client):
resp = client.post("/battery/add", data={"label": "", "brand": ""})
assert resp.status_code == 400
assert b"required" in resp.data
# ------------------------------------------------------------------ #
# Battery — detail
# ------------------------------------------------------------------ #
def test_battery_detail(seeded_client):
resp = seeded_client.get("/battery/1")
assert resp.status_code == 200
assert b"TST-01" in resp.data
assert b"BrandX" in resp.data
def test_battery_detail_not_found(client):
resp = client.get("/battery/9999")
assert resp.status_code == 404
# ------------------------------------------------------------------ #
# Battery — edit notes
# ------------------------------------------------------------------ #
def test_edit_notes(seeded_client):
seeded_client.post("/battery/1/edit-notes", data={"notes": "test note here"})
resp = seeded_client.get("/battery/1")
assert b"test note here" in resp.data
# ------------------------------------------------------------------ #
# Battery — assign
# ------------------------------------------------------------------ #
def test_assign_battery(seeded_client):
# TST-01 is battery id=1, Device A is device id=1
resp = seeded_client.post("/battery/1/assign",
data={"device_id": "1"},
follow_redirects=True)
assert resp.status_code == 200
assert b"Device A" in resp.data
def test_assign_retired_battery_blocked(seeded_client):
# TST-03 is retired (id=3)
resp = seeded_client.post("/battery/3/assign",
data={"device_id": "1"},
follow_redirects=True)
assert resp.status_code == 200
assert b"retired" in resp.data.lower()
def test_assign_over_capacity_blocked(seeded_client):
# Fill Device B (1 slot) with TST-01
seeded_client.post("/battery/1/assign", data={"device_id": "2"})
# Try to assign TST-02 to the same full device
resp = seeded_client.post("/battery/2/assign",
data={"device_id": "2"},
follow_redirects=True)
assert resp.status_code == 200
assert b"full" in resp.data.lower()
def test_brand_mix_warning(seeded_client):
# Assign TST-01 (BrandX) to Device A
seeded_client.post("/battery/1/assign", data={"device_id": "1"})
# Assign TST-02 (BrandY) to same Device A → brand mix warning, but succeeds
resp = seeded_client.post("/battery/2/assign",
data={"device_id": "1"},
follow_redirects=True)
assert resp.status_code == 200
# Warning flash should mention mixing
assert b"mixing" in resp.data.lower() or b"mix" in resp.data.lower() or b"brand" in resp.data.lower()
# ------------------------------------------------------------------ #
# Battery — unassign
# ------------------------------------------------------------------ #
def test_unassign_battery(seeded_client):
seeded_client.post("/battery/1/assign", data={"device_id": "1"})
resp = seeded_client.post("/battery/1/unassign", follow_redirects=True)
assert resp.status_code == 200
# Battery should show as available on dashboard
resp2 = seeded_client.get("/battery/1")
assert b"available" in resp2.data.lower()
# ------------------------------------------------------------------ #
# Battery — retire
# ------------------------------------------------------------------ #
def test_retire_battery(seeded_client):
resp = seeded_client.post("/battery/1/retire", follow_redirects=True)
assert resp.status_code == 200
resp2 = seeded_client.get("/battery/1")
assert b"retired" in resp2.data.lower()
def test_retire_clears_device(seeded_client):
seeded_client.post("/battery/1/assign", data={"device_id": "1"})
seeded_client.post("/battery/1/retire")
resp = seeded_client.get("/battery/1")
assert b"None" in resp.data or b"retired" in resp.data.lower()
def test_retire_already_retired(seeded_client):
# TST-03 is already retired (id=3)
resp = seeded_client.post("/battery/3/retire", follow_redirects=True)
assert resp.status_code == 200
assert b"already retired" in resp.data.lower()
# ------------------------------------------------------------------ #
# Battery — delete
# ------------------------------------------------------------------ #
def test_delete_battery_confirmation_page(seeded_client):
resp = seeded_client.get("/battery/1/delete")
assert resp.status_code == 200
assert b"Confirm Delete" in resp.data
assert b"TST-01" in resp.data
def test_delete_battery(seeded_client):
seeded_client.post("/battery/1/delete")
resp = seeded_client.get("/battery/1")
assert resp.status_code == 404
# ------------------------------------------------------------------ #
# Device — list
# ------------------------------------------------------------------ #
def test_device_list(seeded_client):
resp = seeded_client.get("/device/")
assert resp.status_code == 200
assert b"Device A" in resp.data
assert b"Device B" in resp.data
# ------------------------------------------------------------------ #
# Device — add
# ------------------------------------------------------------------ #
def test_add_device(client):
resp = client.post("/device/add",
data={"name": "My Gadget", "battery_slots": "3"},
follow_redirects=True)
assert resp.status_code == 200
assert b"My Gadget" in resp.data
def test_add_device_duplicate_name(seeded_client):
resp = seeded_client.post("/device/add",
data={"name": "Device A", "battery_slots": "2"})
assert resp.status_code == 400
assert b"already exists" in resp.data
def test_add_device_missing_name(client):
resp = client.post("/device/add", data={"name": "", "battery_slots": "1"})
assert resp.status_code == 400
# ------------------------------------------------------------------ #
# Device — detail
# ------------------------------------------------------------------ #
def test_device_detail(seeded_client):
resp = seeded_client.get("/device/1")
assert resp.status_code == 200
assert b"Device A" in resp.data
assert b"2" in resp.data # battery_slots
def test_device_detail_not_found(client):
resp = client.get("/device/9999")
assert resp.status_code == 404
# ------------------------------------------------------------------ #
# Device — delete
# ------------------------------------------------------------------ #
def test_delete_device_frees_batteries(seeded_client):
# Assign TST-01 to Device A
seeded_client.post("/battery/1/assign", data={"device_id": "1"})
# Delete Device A
resp = seeded_client.post("/device/1/delete", follow_redirects=True)
assert resp.status_code == 200
# TST-01 should now be available
resp2 = seeded_client.get("/battery/1")
assert b"available" in resp2.data.lower()
def test_delete_device_removed(seeded_client):
seeded_client.post("/device/1/delete")
resp = seeded_client.get("/device/1")
assert resp.status_code == 404
# ------------------------------------------------------------------ #
# Full round-trip
# ------------------------------------------------------------------ #
def test_add_assign_delete_battery(client):
# Add a device
client.post("/device/add", data={"name": "Gadget", "battery_slots": "1"})
# Add a battery
client.post("/battery/add", data={"label": "RT-01", "brand": "AcmeBrand", "status": "available"})
# Assign it
resp = client.post("/battery/1/assign", data={"device_id": "1"}, follow_redirects=True)
assert b"Gadget" in resp.data
# Delete it
client.post("/battery/1/delete")
assert client.get("/battery/1").status_code == 404