From 7278ee8cfa20e7c51f02361749eb771e6ee524a4 Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:28:42 +0000 Subject: [PATCH] Refactor getTotals method to clarify API contract and ensure stable response structure #1569 #1561 --- server/models/device_instance.py | 28 +++++++++++++------- test/api_endpoints/test_devices_endpoints.py | 19 ++++++------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/server/models/device_instance.py b/server/models/device_instance.py index 1384a75f..cc8d9d89 100755 --- a/server/models/device_instance.py +++ b/server/models/device_instance.py @@ -327,20 +327,30 @@ class DeviceInstance: return {"success": True, "inserted": row_count, "skipped_lines": skipped} def getTotals(self): - """Get device totals by status.""" + """Get device totals by status. + + Returns a list of 6 counts in the documented positional order: + [all, connected, favorites, new, down, archived] + + IMPORTANT: This order is a public API contract consumed by: + - presence.php (reads indices 0-5) + - /devices/totals/named (maps indices 0-5 to named fields) + - homepage widget datav2 (reads /devices/totals indices) + DO NOT change the order or add/remove fields without a breaking-change release. + """ conn = get_temp_db_connection() sql = conn.cursor() - conditions = get_device_conditions() + all_conditions = get_device_conditions() - # Build sub-selects dynamically for all dictionary entries - sub_queries = [] - for key, condition in conditions.items(): - # Make sure the alias is SQL-safe (no spaces or special chars) - alias = key.replace(" ", "_").lower() - sub_queries.append(f'(SELECT COUNT(*) FROM DevicesView {condition}) AS "{alias}"') + # Only the 6 public fields, in documented positional order. + # DO NOT change this order — it is a stable API contract. + keys = ["all", "connected", "favorites", "new", "down", "archived"] + sub_queries = [ + f'(SELECT COUNT(*) FROM DevicesView {all_conditions[key]}) AS "{key}"' + for key in keys + ] - # Join all sub-selects with commas query = "SELECT\n " + ",\n ".join(sub_queries) sql.execute(query) row = sql.fetchone() diff --git a/test/api_endpoints/test_devices_endpoints.py b/test/api_endpoints/test_devices_endpoints.py index 62a3725c..6517feb6 100644 --- a/test/api_endpoints/test_devices_endpoints.py +++ b/test/api_endpoints/test_devices_endpoints.py @@ -8,7 +8,6 @@ import pytest from helper import get_setting_value from api_server.api_server_start import app -from db.db_helper import get_device_conditions @pytest.fixture(scope="session") @@ -163,17 +162,15 @@ def test_devices_totals(client, api_token, test_mac): data = resp.json assert isinstance(data, list) - # 3. Dynamically get expected length - conditions = get_device_conditions() - expected_length = len(conditions) - assert len(data) == expected_length + # 3. Verify the response has exactly 6 elements in documented order: + # [all, connected, favorites, new, down, archived] + expected_length = 6 + assert len(data) == expected_length, ( + f"Expected 6 totals (all, connected, favorites, new, down, archived), got {len(data)}" + ) - # 4. Check that at least 1 device exists when there are any conditions - if expected_length > 0: - assert data[0] >= 1 # 'devices' count includes the dummy device - else: - # no conditions defined; data should be an empty list - assert data == [] + # 4. Check that at least 1 device exists (all count includes the dummy device) + assert data[0] >= 1 # index 0 = 'all' finally: delete_dummy(client, api_token, test_mac)