810 lines
32 KiB
Python
810 lines
32 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: 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 add — label preview data
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def test_battery_add_get_renders(client):
|
|
resp = client.get("/battery/add")
|
|
assert resp.status_code == 200
|
|
assert b"prefixMaxNums" in resp.data
|
|
|
|
|
|
def test_battery_add_brand_counts_reflects_existing(seeded_client):
|
|
# seeded_client has BrandX x2, BrandY x1
|
|
resp = seeded_client.get("/battery/add")
|
|
assert resp.status_code == 200
|
|
assert b'"BrandX": 2' in resp.data or b'"BrandX":2' in resp.data
|
|
assert b'"BrandY": 1' in resp.data or b'"BrandY":1' 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", "battery_size": "AA"})
|
|
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", "battery_size": "AA"})
|
|
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", "battery_size": "AA"})
|
|
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", "battery_size": "AA"})
|
|
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", "battery_size": "AA"})
|
|
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", "battery_size": "AA"})
|
|
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", "battery_size": "AA"})
|
|
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", "battery_size": "AA"})
|
|
client.post("/device/add", data={"name": "Box B", "battery_slots": "2", "battery_size": "AA"})
|
|
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", "battery_size": "AA"})
|
|
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", "battery_size": "AA"})
|
|
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", "battery_size": "AA"})
|
|
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", "battery_size": "AA"})
|
|
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", "battery_size": "AA"},
|
|
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", "battery_size": "AA"})
|
|
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", "battery_size": "AA",
|
|
"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", "battery_size": "AA", "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", "battery_size": "AA"})
|
|
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_device_with_location(client):
|
|
resp = client.post("/device/add",
|
|
data={"name": "Bedroom Remote", "battery_slots": "2", "battery_size": "AA",
|
|
"location": "Bedroom"},
|
|
follow_redirects=True)
|
|
assert resp.status_code == 200
|
|
assert b"Bedroom Remote" in resp.data
|
|
resp = client.get("/device/1")
|
|
assert b"Bedroom" in resp.data
|
|
|
|
|
|
def test_edit_device_location(client):
|
|
client.post("/device/add", data={"name": "Living Room Clock", "battery_slots": "1", "battery_size": "AA"})
|
|
resp = client.post("/device/1/edit",
|
|
data={"name": "Living Room Clock", "battery_slots": "1", "location": "Living Room"},
|
|
follow_redirects=True)
|
|
assert resp.status_code == 200
|
|
assert b"Living Room" 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", "battery_size": "AA"})
|
|
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
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Device — battery_size
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def test_add_device_requires_battery_size(client):
|
|
"""POST without battery_size returns 400."""
|
|
resp = client.post("/device/add", data={"name": "No Size Device", "battery_slots": "2"})
|
|
assert resp.status_code == 400
|
|
assert b"Battery size is required" in resp.data
|
|
|
|
|
|
def test_add_device_with_battery_size(client):
|
|
"""Device with battery_size shows it on detail page."""
|
|
resp = client.post("/device/add",
|
|
data={"name": "AA Remote", "battery_slots": "2", "battery_size": "AA"},
|
|
follow_redirects=True)
|
|
assert resp.status_code == 200
|
|
resp2 = client.get("/device/1")
|
|
assert b"AA" in resp2.data
|
|
|
|
|
|
def test_install_one_filters_by_size(client):
|
|
"""Single-battery install dropdown excludes batteries with wrong size."""
|
|
client.post("/device/add", data={"name": "AA Device", "battery_slots": "2", "battery_size": "AA"})
|
|
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) # id=1
|
|
client.post("/battery/1/edit-details", data={"size": "AA"})
|
|
client.post("/battery/add", data={"brand": "Energizer", "count": "1"}) # id=2
|
|
client.post("/battery/2/edit-details", data={"size": "AAA"})
|
|
resp = client.get("/device/1")
|
|
assert resp.status_code == 200
|
|
# AA battery should appear in the dropdown, AAA should not
|
|
assert b"Eneloop 001" in resp.data
|
|
assert b"Energizer 001" not in resp.data
|
|
|
|
|
|
def test_bulk_install_filters_by_size(client):
|
|
"""Bulk install with a brand that only has wrong-size batteries fails with 0 available."""
|
|
client.post("/device/add", data={"name": "AA Device", "battery_slots": "2", "battery_size": "AA"})
|
|
client.post("/battery/add", data={"brand": "Energizer", "count": "2"})
|
|
# Set both batteries to AAA (explicitly incompatible)
|
|
client.post("/battery/1/edit-details", data={"size": "AAA"})
|
|
client.post("/battery/2/edit-details", data={"size": "AAA"})
|
|
resp = client.post("/device/1/install",
|
|
data={"brand[]": "Energizer", "qty[]": "1"},
|
|
follow_redirects=True)
|
|
assert resp.status_code == 200
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Import
|
|
# ------------------------------------------------------------------ #
|
|
|
|
import io
|
|
import json as _json
|
|
|
|
|
|
def _make_import_payload(devices=None, batteries=None,
|
|
charge_logs=None, capacity_tests=None, pct_logs=None):
|
|
data = {
|
|
"exported_at": "2026-01-01T00:00:00",
|
|
"devices": devices or [],
|
|
"batteries": batteries or [],
|
|
"charge_logs": charge_logs or [],
|
|
"capacity_tests": capacity_tests or [],
|
|
"pct_logs": pct_logs or [],
|
|
}
|
|
return (io.BytesIO(_json.dumps(data).encode()), "export.json")
|
|
|
|
|
|
def _post_import(client, payload_tuple):
|
|
buf, fname = payload_tuple
|
|
return client.post(
|
|
"/import",
|
|
data={"file": (buf, fname)},
|
|
content_type="multipart/form-data",
|
|
)
|
|
|
|
|
|
def _bat(id, label, brand="Brand", status="available", device_id=None, **kw):
|
|
return {"id": id, "label": label, "brand": brand, "status": status,
|
|
"device_id": device_id, "size": None, "chemistry": None, "capacity_mah": None,
|
|
"tested_capacity_mah": None, "tested_date": None, "charge_cycles": None,
|
|
"purchase_date": None, "storage_location": None, "battery_percentage": None,
|
|
"notes": None, **kw}
|
|
|
|
|
|
def _dev(id, name, battery_slots=2, battery_size="AA", **kw):
|
|
return {"id": id, "name": name, "battery_slots": battery_slots,
|
|
"battery_size": battery_size, "device_type": None,
|
|
"location": None, "ha_entity_id": None, "notes": None, **kw}
|
|
|
|
|
|
def test_import_page_loads(client):
|
|
resp = client.get("/import")
|
|
assert resp.status_code == 200
|
|
assert b"Import" in resp.data
|
|
assert b'enctype="multipart/form-data"' in resp.data
|
|
|
|
|
|
def test_import_no_file_returns_400(client):
|
|
resp = client.post("/import", data={}, content_type="multipart/form-data")
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_import_invalid_json_returns_400(client):
|
|
buf = io.BytesIO(b"this is not json")
|
|
resp = client.post("/import",
|
|
data={"file": (buf, "bad.json")},
|
|
content_type="multipart/form-data")
|
|
assert resp.status_code == 400
|
|
assert b"Invalid JSON" in resp.data
|
|
|
|
|
|
def test_import_missing_required_keys_returns_400(client):
|
|
buf = io.BytesIO(_json.dumps({"exported_at": "2026-01-01"}).encode())
|
|
resp = client.post("/import",
|
|
data={"file": (buf, "bad.json")},
|
|
content_type="multipart/form-data")
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_import_creates_devices_and_batteries(client):
|
|
payload = _make_import_payload(
|
|
devices=[_dev(1, "RC Car")],
|
|
batteries=[_bat(1, "Eneloop 001", brand="Eneloop")],
|
|
)
|
|
resp = _post_import(client, payload)
|
|
assert resp.status_code == 200
|
|
assert b"Import Results" in resp.data
|
|
dash = client.get("/")
|
|
assert b"Eneloop 001" in dash.data
|
|
assert b"RC Car" in dash.data
|
|
|
|
|
|
def test_import_results_show_correct_counts(client):
|
|
payload = _make_import_payload(
|
|
devices=[_dev(10, "DevX")],
|
|
batteries=[_bat(10, "BrandX 001"), _bat(11, "BrandX 002")],
|
|
)
|
|
resp = _post_import(client, payload)
|
|
assert resp.status_code == 200
|
|
# devices_created=1, batteries_created=2 appear in the results table
|
|
assert b"Import Results" in resp.data
|
|
|
|
|
|
def test_import_charge_logs_remapped_correctly(client):
|
|
payload = _make_import_payload(
|
|
batteries=[_bat(99, "Eneloop 001", brand="Eneloop")],
|
|
charge_logs=[{"id": 1, "battery_id": 99, "battery_label": "Eneloop 001",
|
|
"charged_date": "2026-01-15", "increment_cycles": 1, "notes": None}],
|
|
)
|
|
resp = _post_import(client, payload)
|
|
assert resp.status_code == 200
|
|
detail = client.get("/battery/1")
|
|
assert b"2026-01-15" in detail.data
|
|
|
|
|
|
def test_import_capacity_tests_remapped(client):
|
|
payload = _make_import_payload(
|
|
batteries=[_bat(5, "Test 001", capacity_mah=2000)],
|
|
capacity_tests=[{"id": 1, "battery_id": 5, "battery_label": "Test 001",
|
|
"tested_capacity_mah": 1850, "tested_date": "2026-02-01", "notes": None}],
|
|
)
|
|
resp = _post_import(client, payload)
|
|
assert resp.status_code == 200
|
|
detail = client.get("/battery/1")
|
|
assert b"1850" in detail.data
|
|
|
|
|
|
def test_import_skips_existing_device(client):
|
|
client.post("/device/add", data={"name": "RC Car", "battery_slots": "2", "battery_size": "AA"})
|
|
payload = _make_import_payload(
|
|
devices=[_dev(1, "RC Car", battery_slots=4)],
|
|
)
|
|
resp = _post_import(client, payload)
|
|
assert resp.status_code == 200
|
|
# Original 2-slot device not overwritten
|
|
detail = client.get("/device/1")
|
|
assert b"RC Car" in detail.data
|
|
|
|
|
|
def test_import_skips_existing_battery(client):
|
|
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
|
|
payload = _make_import_payload(
|
|
batteries=[_bat(999, "Eneloop 001", brand="Eneloop", status="retired")],
|
|
)
|
|
resp = _post_import(client, payload)
|
|
assert resp.status_code == 200
|
|
detail = client.get("/battery/1")
|
|
assert b"available" in detail.data.lower()
|
|
|
|
|
|
def test_import_log_skipped_when_battery_not_in_map(client):
|
|
payload = _make_import_payload(
|
|
batteries=[],
|
|
charge_logs=[{"id": 1, "battery_id": 42, "battery_label": "Ghost",
|
|
"charged_date": "2026-01-01", "increment_cycles": 1, "notes": None}],
|
|
)
|
|
resp = _post_import(client, payload)
|
|
assert resp.status_code == 200
|
|
assert b"Import Results" in resp.data
|
|
|
|
|
|
def test_import_battery_gets_installed_when_device_maps(client):
|
|
payload = _make_import_payload(
|
|
devices=[_dev(7, "My Device")],
|
|
batteries=[_bat(7, "Linked 001", status="installed", device_id=7)],
|
|
)
|
|
resp = _post_import(client, payload)
|
|
assert resp.status_code == 200
|
|
detail = client.get("/battery/1")
|
|
assert b"installed" in detail.data.lower()
|
|
assert b"My Device" in detail.data
|
|
|
|
|
|
def test_import_battery_retired_preserved_when_no_device(client):
|
|
payload = _make_import_payload(
|
|
batteries=[_bat(3, "Old 001", status="retired")],
|
|
)
|
|
resp = _post_import(client, payload)
|
|
assert resp.status_code == 200
|
|
detail = client.get("/battery/1")
|
|
assert b"retired" in detail.data.lower()
|
|
|
|
|
|
def test_import_battery_device_id_unknown_becomes_available(client):
|
|
payload = _make_import_payload(
|
|
devices=[],
|
|
batteries=[_bat(5, "Orphan 001", status="installed", device_id=99)],
|
|
)
|
|
resp = _post_import(client, payload)
|
|
assert resp.status_code == 200
|
|
detail = client.get("/battery/1")
|
|
assert b"available" in detail.data.lower()
|
|
|
|
|
|
def test_full_roundtrip_export_import(client):
|
|
client.post("/device/add", data={"name": "Router", "battery_slots": "1", "battery_size": "AA"})
|
|
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
|
|
client.post("/battery/1/assign", data={"device_id": "1"})
|
|
|
|
export_resp = client.get("/export/all.json")
|
|
assert export_resp.status_code == 200
|
|
|
|
buf = io.BytesIO(export_resp.data)
|
|
resp = client.post("/import",
|
|
data={"file": (buf, "export.json")},
|
|
content_type="multipart/form-data")
|
|
assert resp.status_code == 200
|
|
assert b"Import Results" in resp.data
|