""" 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