Skip to content

Commit 7278ee8

Browse files
committed
Refactor getTotals method to clarify API contract and ensure stable response structure #1569 #1561
1 parent fa22523 commit 7278ee8

File tree

2 files changed

+28
-21
lines changed

2 files changed

+28
-21
lines changed

server/models/device_instance.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -327,20 +327,30 @@ def importCSV(self, file_storage=None, json_content=None):
327327
return {"success": True, "inserted": row_count, "skipped_lines": skipped}
328328

329329
def getTotals(self):
330-
"""Get device totals by status."""
330+
"""Get device totals by status.
331+
332+
Returns a list of 6 counts in the documented positional order:
333+
[all, connected, favorites, new, down, archived]
334+
335+
IMPORTANT: This order is a public API contract consumed by:
336+
- presence.php (reads indices 0-5)
337+
- /devices/totals/named (maps indices 0-5 to named fields)
338+
- homepage widget datav2 (reads /devices/totals indices)
339+
DO NOT change the order or add/remove fields without a breaking-change release.
340+
"""
331341
conn = get_temp_db_connection()
332342
sql = conn.cursor()
333343

334-
conditions = get_device_conditions()
344+
all_conditions = get_device_conditions()
335345

336-
# Build sub-selects dynamically for all dictionary entries
337-
sub_queries = []
338-
for key, condition in conditions.items():
339-
# Make sure the alias is SQL-safe (no spaces or special chars)
340-
alias = key.replace(" ", "_").lower()
341-
sub_queries.append(f'(SELECT COUNT(*) FROM DevicesView {condition}) AS "{alias}"')
346+
# Only the 6 public fields, in documented positional order.
347+
# DO NOT change this order — it is a stable API contract.
348+
keys = ["all", "connected", "favorites", "new", "down", "archived"]
349+
sub_queries = [
350+
f'(SELECT COUNT(*) FROM DevicesView {all_conditions[key]}) AS "{key}"'
351+
for key in keys
352+
]
342353

343-
# Join all sub-selects with commas
344354
query = "SELECT\n " + ",\n ".join(sub_queries)
345355
sql.execute(query)
346356
row = sql.fetchone()

test/api_endpoints/test_devices_endpoints.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
from helper import get_setting_value
1010
from api_server.api_server_start import app
11-
from db.db_helper import get_device_conditions
1211

1312

1413
@pytest.fixture(scope="session")
@@ -163,17 +162,15 @@ def test_devices_totals(client, api_token, test_mac):
163162
data = resp.json
164163
assert isinstance(data, list)
165164

166-
# 3. Dynamically get expected length
167-
conditions = get_device_conditions()
168-
expected_length = len(conditions)
169-
assert len(data) == expected_length
170-
171-
# 4. Check that at least 1 device exists when there are any conditions
172-
if expected_length > 0:
173-
assert data[0] >= 1 # 'devices' count includes the dummy device
174-
else:
175-
# no conditions defined; data should be an empty list
176-
assert data == []
165+
# 3. Verify the response has exactly 6 elements in documented order:
166+
# [all, connected, favorites, new, down, archived]
167+
expected_length = 6
168+
assert len(data) == expected_length, (
169+
f"Expected 6 totals (all, connected, favorites, new, down, archived), got {len(data)}"
170+
)
171+
172+
# 4. Check that at least 1 device exists (all count includes the dummy device)
173+
assert data[0] >= 1 # index 0 = 'all'
177174
finally:
178175
delete_dummy(client, api_token, test_mac)
179176

0 commit comments

Comments
 (0)