""" 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: BrandX 001 (id=1, available), BrandY 001 (id=2, available), BrandX 002 (id=3, retired) """ import pytest # ------------------------------------------------------------------ # # Helpers # ------------------------------------------------------------------ # def get_location(resp): """Return the redirect Location header (without host).""" loc = resp.headers.get("Location", "") 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"BrandX 001" in resp.data assert b"BrandY 001" in resp.data assert b"BrandX 002" in resp.data # ------------------------------------------------------------------ # # Battery — bulk add # ------------------------------------------------------------------ # def test_bulk_add_batteries(client): resp = client.post("/battery/add", data={"brand": "Eneloop", "count": "3"}, follow_redirects=True) assert resp.status_code == 200 assert b"Eneloop 001" in resp.data assert b"Eneloop 002" in resp.data assert b"Eneloop 003" in resp.data def test_bulk_add_autogenerates_sequential_labels(client): client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) resp = client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}, follow_redirects=True) assert b"Eneloop 003" in resp.data assert b"Eneloop 004" in resp.data def test_bulk_add_missing_brand(client): resp = client.post("/battery/add", data={"brand": "", "count": "1"}) assert resp.status_code == 400 assert b"required" in resp.data.lower() # ------------------------------------------------------------------ # # Battery — detail # ------------------------------------------------------------------ # def test_battery_detail(seeded_client): resp = seeded_client.get("/battery/1") assert resp.status_code == 200 assert b"BrandX 001" 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-details", data={"notes": "test note here"}) resp = seeded_client.get("/battery/1") assert b"test note here" in resp.data # ------------------------------------------------------------------ # # Battery — assign (per-battery, kept for special cases) # ------------------------------------------------------------------ # def test_assign_battery(seeded_client): 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): # id=3 is retired 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 id=1 seeded_client.post("/battery/1/assign", data={"device_id": "2"}) # Try to assign id=2 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 BrandX 001 (id=1) to Device A seeded_client.post("/battery/1/assign", data={"device_id": "1"}) # Assign BrandY 001 (id=2) 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 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 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): # id=3 is already retired by fixture 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"BrandX 001" 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 # ------------------------------------------------------------------ # # Battery — bulk actions # ------------------------------------------------------------------ # def test_bulk_retire(client): client.post("/battery/add", data={"brand": "TestBrand", "count": "2"}) resp = client.post("/battery/bulk-action", data={"battery_ids": ["1", "2"], "action": "retire"}, follow_redirects=True) assert resp.status_code == 200 assert b"retired" in client.get("/battery/1").data.lower() assert b"retired" in client.get("/battery/2").data.lower() def test_bulk_delete(client): client.post("/battery/add", data={"brand": "TestBrand", "count": "2"}) resp = client.post("/battery/bulk-action", data={"battery_ids": ["1", "2"], "action": "delete"}, follow_redirects=True) assert resp.status_code == 200 assert client.get("/battery/1").status_code == 404 assert client.get("/battery/2").status_code == 404 def test_bulk_unassign(client): client.post("/device/add", data={"name": "Gadget", "battery_slots": "2"}) client.post("/battery/add", data={"brand": "TestBrand", "count": "2"}) client.post("/device/1/install", data={"brand[]": "TestBrand", "qty[]": "2"}) resp = client.post("/battery/bulk-action", data={"battery_ids": ["1", "2"], "action": "unassign"}, follow_redirects=True) assert resp.status_code == 200 assert b"available" in client.get("/battery/1").data.lower() def test_bulk_set_brand(client): client.post("/battery/add", data={"brand": "OldBrand", "count": "2"}) resp = client.post("/battery/bulk-action", data={"battery_ids": ["1", "2"], "action": "set_brand", "new_brand": "NewBrand"}, follow_redirects=True) assert resp.status_code == 200 assert b"NewBrand" in client.get("/battery/1").data def test_bulk_action_no_selection(client): client.post("/battery/add", data={"brand": "TestBrand", "count": "1"}) resp = client.post("/battery/bulk-action", data={"action": "retire"}, follow_redirects=True) assert resp.status_code == 200 assert b"No batteries selected" in resp.data # ------------------------------------------------------------------ # # Device — install batteries # ------------------------------------------------------------------ # def test_device_install_autoselects(client): client.post("/device/add", data={"name": "Gadget", "battery_slots": "2"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "3"}) resp = client.post("/device/1/install", data={"brand[]": "Eneloop", "qty[]": "2"}, follow_redirects=True) assert resp.status_code == 200 # Device detail should show 2 installed batteries assert b"Eneloop" in resp.data assert b"2 / 2" in resp.data def test_device_install_mixed_brands(client): client.post("/device/add", data={"name": "Remote", "battery_slots": "4"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) client.post("/battery/add", data={"brand": "Energizer", "count": "2"}) resp = client.post("/device/1/install", data={"brand[]": ["Eneloop", "Energizer"], "qty[]": ["2", "2"]}, follow_redirects=True) assert resp.status_code == 200 assert b"Eneloop" in resp.data assert b"Energizer" in resp.data def test_device_install_insufficient_batteries(client): client.post("/device/add", data={"name": "Gadget", "battery_slots": "4"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) resp = client.post("/device/1/install", data={"brand[]": "Eneloop", "qty[]": "2"}, follow_redirects=True) assert resp.status_code == 200 assert b"available" in resp.data.lower() def test_device_install_over_capacity(client): client.post("/device/add", data={"name": "Gadget", "battery_slots": "1"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "3"}) resp = client.post("/device/1/install", data={"brand[]": "Eneloop", "qty[]": "3"}, follow_redirects=True) assert resp.status_code == 200 assert b"slot" in resp.data.lower() def test_bulk_install_device(client): """Select multiple available batteries and install them into a device.""" client.post("/device/add", data={"name": "Box", "battery_slots": "3"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) resp = client.post("/battery/bulk-action", data={"battery_ids": ["1", "2"], "action": "install_device", "device_id": "1"}, follow_redirects=True) assert resp.status_code == 200 assert b"Installed 2" in resp.data def test_bulk_install_device_over_capacity(client): """Bulk install is blocked when device lacks free slots.""" client.post("/device/add", data={"name": "Box", "battery_slots": "1"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) resp = client.post("/battery/bulk-action", data={"battery_ids": ["1", "2"], "action": "install_device", "device_id": "1"}, follow_redirects=True) assert resp.status_code == 200 assert b"slot" in resp.data.lower() def test_bulk_install_device_moves_installed_battery(client): """Bulk install moves a battery already installed in another device.""" client.post("/device/add", data={"name": "Box A", "battery_slots": "2"}) client.post("/device/add", data={"name": "Box B", "battery_slots": "2"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) client.post("/battery/1/assign", data={"device_id": "1"}) resp = client.post("/battery/bulk-action", data={"battery_ids": ["1"], "action": "install_device", "device_id": "2"}, follow_redirects=True) assert resp.status_code == 200 assert b"Installed 1" in resp.data assert b"Box B" in client.get("/battery/1").data # ------------------------------------------------------------------ # # Dashboard — quick-assign # ------------------------------------------------------------------ # def test_dashboard_quick_assign(client): """Quick-assign from dashboard (battery_assign POST) succeeds.""" client.post("/device/add", data={"name": "Box", "battery_slots": "2"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) resp = client.post("/battery/1/assign", data={"device_id": "1"}, follow_redirects=True) assert resp.status_code == 200 assert b"Box" in resp.data def test_dashboard_quick_assign_full_device_blocked(client): """Quick-assign from dashboard to a full device is blocked.""" client.post("/device/add", data={"name": "Box", "battery_slots": "1"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) client.post("/battery/1/assign", data={"device_id": "1"}) # fills the slot resp = client.post("/battery/2/assign", data={"device_id": "1"}, follow_redirects=True) assert resp.status_code == 200 assert b"full" in resp.data.lower() # ------------------------------------------------------------------ # # Device — install-one (specific battery) # ------------------------------------------------------------------ # def test_install_one_specific_battery(client): """device_install_one installs a chosen battery.""" client.post("/device/add", data={"name": "Box", "battery_slots": "2"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) resp = client.post("/device/1/install-one", data={"battery_id": "1"}, follow_redirects=True) assert resp.status_code == 200 assert b"Eneloop 001" in resp.data def test_install_one_over_capacity_blocked(client): """device_install_one to a full device is blocked.""" client.post("/device/add", data={"name": "Box", "battery_slots": "1"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) client.post("/device/1/install-one", data={"battery_id": "1"}) # fills slot resp = client.post("/device/1/install-one", data={"battery_id": "2"}, follow_redirects=True) assert resp.status_code == 200 assert b"full" in resp.data.lower() # ------------------------------------------------------------------ # # 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): seeded_client.post("/battery/1/assign", data={"device_id": "1"}) resp = seeded_client.post("/device/1/delete", follow_redirects=True) assert resp.status_code == 200 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_device_with_type(client): resp = client.post("/device/add", data={"name": "TV Remote", "battery_slots": "2", "device_type": "Remote Control"}, follow_redirects=True) assert resp.status_code == 200 assert b"TV Remote" in resp.data def test_device_detail_shows_type(client): client.post("/device/add", data={"name": "Torch", "battery_slots": "1", "device_type": "Flashlight"}) resp = client.get("/device/1") assert resp.status_code == 200 assert b"Flashlight" in resp.data def test_edit_device_type(client): client.post("/device/add", data={"name": "Torch", "battery_slots": "1"}) resp = client.post("/device/1/edit", data={"name": "Torch", "battery_slots": "1", "device_type": "Flashlight"}, follow_redirects=True) assert resp.status_code == 200 assert b"Flashlight" in resp.data def test_add_capacity_test(client): client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) resp = client.post("/battery/1/capacity-test/add", data={"tested_capacity_mah": "1900", "tested_date": "2026-01-01"}, follow_redirects=True) assert resp.status_code == 200 assert b"1900" in resp.data def test_capacity_test_syncs_battery(client): """Adding a test updates battery.tested_capacity_mah.""" client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) client.post("/battery/1/capacity-test/add", data={"tested_capacity_mah": "1800", "tested_date": "2026-01-01"}) resp = client.get("/battery/1") assert b"1800" in resp.data def test_delete_capacity_test(client): client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) client.post("/battery/1/capacity-test/add", data={"tested_capacity_mah": "1900", "tested_date": "2026-01-01"}) resp = client.post("/battery/1/capacity-test/1/delete", follow_redirects=True) assert resp.status_code == 200 assert b"No test records" in resp.data def test_add_install_delete_battery(client): client.post("/device/add", data={"name": "Gadget", "battery_slots": "1"}) client.post("/battery/add", data={"brand": "AcmeBrand", "count": "1"}) resp = client.post("/device/1/install", data={"brand[]": "AcmeBrand", "qty[]": "1"}, follow_redirects=True) assert b"Gadget" in resp.data client.post("/battery/1/delete") assert client.get("/battery/1").status_code == 404