Files
led-controller/tests/test_endpoints.py
jimmy 5fdeb57b74 Extend endpoint and browser tests for ESPNow and UI
Add coverage for /presets/send and updated tab/preset UI workflows in HTTP and Selenium tests.
2026-01-28 04:44:41 +13:00

564 lines
22 KiB
Python

#!/usr/bin/env python3
"""
Endpoint tests that mimic web browser requests.
Tests run against the device at 192.168.4.1
"""
import requests
import json
import sys
from typing import Dict, Optional
# Base URL for the device
BASE_URL = "http://192.168.4.1"
class TestClient:
"""HTTP client that mimics a web browser with cookie support."""
def __init__(self, base_url: str = BASE_URL):
self.base_url = base_url
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9',
})
def get(self, path: str, **kwargs) -> requests.Response:
"""GET request."""
url = f"{self.base_url}{path}"
return self.session.get(url, **kwargs)
def post(self, path: str, data: Optional[Dict] = None, json_data: Optional[Dict] = None, **kwargs) -> requests.Response:
"""POST request."""
url = f"{self.base_url}{path}"
if json_data:
return self.session.post(url, json=json_data, **kwargs)
return self.session.post(url, data=data, **kwargs)
def put(self, path: str, json_data: Optional[Dict] = None, **kwargs) -> requests.Response:
"""PUT request."""
url = f"{self.base_url}{path}"
return self.session.put(url, json=json_data, **kwargs)
def delete(self, path: str, **kwargs) -> requests.Response:
"""DELETE request."""
url = f"{self.base_url}{path}"
return self.session.delete(url, **kwargs)
def set_cookie(self, name: str, value: str):
"""Set a cookie manually."""
self.session.cookies.set(name, value, domain='192.168.4.1', path='/')
def get_cookie(self, name: str) -> Optional[str]:
"""Get a cookie value."""
return self.session.cookies.get(name)
def test_connection(client: TestClient) -> bool:
"""Test basic connection to the server."""
print("Testing connection...")
try:
response = client.get('/')
if response.status_code == 200:
print("✓ Connection successful")
return True
else:
print(f"✗ Connection failed: {response.status_code}")
return False
except requests.exceptions.ConnectionError:
print(f"✗ Cannot connect to {BASE_URL}")
print(" Make sure the device is running and accessible at 192.168.4.1")
return False
except Exception as e:
print(f"✗ Connection error: {e}")
return False
def test_tabs(client: TestClient) -> bool:
"""Test tabs endpoints."""
print("\n=== Testing Tabs Endpoints ===")
passed = 0
total = 0
# Test 1: List tabs
total += 1
try:
response = client.get('/tabs')
if response.status_code == 200:
data = response.json()
print(f"✓ GET /tabs - Found {len(data.get('tabs', {}))} tabs")
passed += 1
else:
print(f"✗ GET /tabs - Status: {response.status_code}")
except Exception as e:
print(f"✗ GET /tabs - Error: {e}")
# Test 2: Create tab
total += 1
try:
tab_data = {
"name": "Test Tab",
"names": ["1", "2"]
}
response = client.post('/tabs', json_data=tab_data)
if response.status_code == 201:
created_tab = response.json()
# Response format: {tab_id: {tab_data}}
if isinstance(created_tab, dict):
# Get the first key which should be the tab ID
tab_id = next(iter(created_tab.keys())) if created_tab else None
else:
tab_id = None
print(f"✓ POST /tabs - Created tab: {tab_id}")
passed += 1
# Test 3: Get specific tab
if tab_id:
total += 1
response = client.get(f'/tabs/{tab_id}')
if response.status_code == 200:
print(f"✓ GET /tabs/{tab_id} - Retrieved tab")
passed += 1
else:
print(f"✗ GET /tabs/{tab_id} - Status: {response.status_code}")
# Test 4: Set current tab
total += 1
response = client.post(f'/tabs/{tab_id}/set-current')
if response.status_code == 200:
print(f"✓ POST /tabs/{tab_id}/set-current - Set current tab")
# Check cookie was set
cookie = client.get_cookie('current_tab')
if cookie == tab_id:
print(f" ✓ Cookie 'current_tab' set to {tab_id}")
passed += 1
else:
print(f"✗ POST /tabs/{tab_id}/set-current - Status: {response.status_code}")
# Test 5: Get current tab
total += 1
response = client.get('/tabs/current')
if response.status_code == 200:
data = response.json()
if data.get('tab_id') == tab_id:
print(f"✓ GET /tabs/current - Current tab is {tab_id}")
passed += 1
else:
print(f"✗ GET /tabs/current - Wrong tab ID")
else:
print(f"✗ GET /tabs/current - Status: {response.status_code}")
# Test 6: Update tab (edit functionality)
total += 1
update_data = {
"name": "Updated Test Tab",
"names": ["1", "2", "3"] # Update device IDs too
}
response = client.put(f'/tabs/{tab_id}', json_data=update_data)
if response.status_code == 200:
updated = response.json()
if updated.get('name') == "Updated Test Tab" and updated.get('names') == ["1", "2", "3"]:
print(f"✓ PUT /tabs/{tab_id} - Updated tab (name and device IDs)")
passed += 1
else:
print(f"✗ PUT /tabs/{tab_id} - Update didn't work correctly")
print(f" Expected name='Updated Test Tab', got '{updated.get('name')}'")
print(f" Expected names=['1','2','3'], got {updated.get('names')}")
else:
print(f"✗ PUT /tabs/{tab_id} - Status: {response.status_code}, Response: {response.text}")
# Test 6b: Verify update persisted
total += 1
response = client.get(f'/tabs/{tab_id}')
if response.status_code == 200:
verified = response.json()
if verified.get('name') == "Updated Test Tab":
print(f"✓ GET /tabs/{tab_id} - Verified update persisted")
passed += 1
else:
print(f"✗ GET /tabs/{tab_id} - Update didn't persist")
else:
print(f"✗ GET /tabs/{tab_id} - Status: {response.status_code}")
# Test 7: Delete tab
total += 1
response = client.delete(f'/tabs/{tab_id}')
if response.status_code == 200:
print(f"✓ DELETE /tabs/{tab_id} - Deleted tab")
passed += 1
else:
print(f"✗ DELETE /tabs/{tab_id} - Status: {response.status_code}")
else:
print(f"✗ POST /tabs - Status: {response.status_code}, Response: {response.text}")
except Exception as e:
print(f"✗ POST /tabs - Error: {e}")
import traceback
traceback.print_exc()
print(f"\nTabs tests: {passed}/{total} passed")
return passed == total
def test_profiles(client: TestClient) -> bool:
"""Test profiles endpoints."""
print("\n=== Testing Profiles Endpoints ===")
passed = 0
total = 0
# Test 1: List profiles
total += 1
try:
response = client.get('/profiles')
if response.status_code == 200:
data = response.json()
profiles = data.get('profiles', {})
current_id = data.get('current_profile_id')
print(f"✓ GET /profiles - Found {len(profiles)} profiles, current: {current_id}")
passed += 1
else:
print(f"✗ GET /profiles - Status: {response.status_code}")
except Exception as e:
print(f"✗ GET /profiles - Error: {e}")
# Test 2: Get current profile
total += 1
try:
response = client.get('/profiles/current')
if response.status_code == 200:
data = response.json()
print(f"✓ GET /profiles/current - Current profile: {data.get('id')}")
passed += 1
else:
print(f"✗ GET /profiles/current - Status: {response.status_code}")
except Exception as e:
print(f"✗ GET /profiles/current - Error: {e}")
# Test 3: Create profile
total += 1
try:
profile_data = {"name": "Test Profile"}
response = client.post('/profiles', json_data=profile_data)
if response.status_code == 201:
created = response.json()
# Response format: {profile_id: {profile_data}}
if isinstance(created, dict):
profile_id = next(iter(created.keys())) if created else None
else:
profile_id = None
print(f"✓ POST /profiles - Created profile: {profile_id}")
passed += 1
# Test 4: Apply profile
if profile_id:
total += 1
response = client.post(f'/profiles/{profile_id}/apply')
if response.status_code == 200:
print(f"✓ POST /profiles/{profile_id}/apply - Applied profile")
passed += 1
else:
print(f"✗ POST /profiles/{profile_id}/apply - Status: {response.status_code}")
# Test 5: Delete profile
total += 1
response = client.delete(f'/profiles/{profile_id}')
if response.status_code == 200:
print(f"✓ DELETE /profiles/{profile_id} - Deleted profile")
passed += 1
else:
print(f"✗ DELETE /profiles/{profile_id} - Status: {response.status_code}")
else:
print(f"✗ POST /profiles - Status: {response.status_code}")
except Exception as e:
print(f"✗ POST /profiles - Error: {e}")
print(f"\nProfiles tests: {passed}/{total} passed")
return passed == total
def test_presets(client: TestClient) -> bool:
"""Test presets endpoints."""
print("\n=== Testing Presets Endpoints ===")
passed = 0
total = 0
# Test 1: List presets
total += 1
try:
response = client.get('/presets')
if response.status_code == 200:
data = response.json()
preset_count = len(data) if isinstance(data, dict) else 0
print(f"✓ GET /presets - Found {preset_count} presets")
passed += 1
else:
print(f"✗ GET /presets - Status: {response.status_code}")
except Exception as e:
print(f"✗ GET /presets - Error: {e}")
# Test 2: Create preset
total += 1
try:
preset_data = {
"name": "Test Preset",
"pattern": "on",
"colors": ["#ff0000"],
"brightness": 200
}
response = client.post('/presets', json_data=preset_data)
if response.status_code == 201:
created = response.json()
# Response format: {preset_id: {preset_data}}
if isinstance(created, dict):
preset_id = next(iter(created.keys())) if created else None
else:
preset_id = None
print(f"✓ POST /presets - Created preset: {preset_id}")
passed += 1
# Test 3: Get specific preset
if preset_id:
total += 1
response = client.get(f'/presets/{preset_id}')
if response.status_code == 200:
print(f"✓ GET /presets/{preset_id} - Retrieved preset")
passed += 1
else:
print(f"✗ GET /presets/{preset_id} - Status: {response.status_code}")
# Test 4: Update preset
total += 1
update_data = {"brightness": 150}
response = client.put(f'/presets/{preset_id}', json_data=update_data)
if response.status_code == 200:
print(f"✓ PUT /presets/{preset_id} - Updated preset")
passed += 1
else:
print(f"✗ PUT /presets/{preset_id} - Status: {response.status_code}")
# Test 5: Send preset via /presets/send
total += 1
try:
send_body = {"preset_ids": [preset_id]}
response = client.post('/presets/send', json_data=send_body)
if response.status_code == 200:
data = response.json()
sent = data.get('presets_sent')
print(f"✓ POST /presets/send - Sent presets (presets_sent={sent})")
passed += 1
else:
print(f"✗ POST /presets/send - Status: {response.status_code}, Response: {response.text}")
except Exception as e:
print(f"✗ POST /presets/send - Error: {e}")
# Test 6: Delete preset
total += 1
response = client.delete(f'/presets/{preset_id}')
if response.status_code == 200:
print(f"✓ DELETE /presets/{preset_id} - Deleted preset")
passed += 1
else:
print(f"✗ DELETE /presets/{preset_id} - Status: {response.status_code}")
else:
print(f"✗ POST /presets - Status: {response.status_code}")
except Exception as e:
print(f"✗ POST /presets - Error: {e}")
print(f"\nPresets tests: {passed}/{total} passed")
return passed == total
def test_patterns(client: TestClient) -> bool:
"""Test patterns endpoints."""
print("\n=== Testing Patterns Endpoints ===")
passed = 0
total = 0
# Test 1: List patterns
total += 1
try:
response = client.get('/patterns')
if response.status_code == 200:
data = response.json()
pattern_count = len(data) if isinstance(data, dict) else 0
print(f"✓ GET /patterns - Found {pattern_count} patterns")
passed += 1
else:
print(f"✗ GET /patterns - Status: {response.status_code}")
except Exception as e:
print(f"✗ GET /patterns - Error: {e}")
# Test 2: Get pattern definitions
total += 1
try:
response = client.get('/patterns/definitions')
if response.status_code == 200:
data = response.json()
print(f"✓ GET /patterns/definitions - Retrieved definitions")
passed += 1
else:
print(f"✗ GET /patterns/definitions - Status: {response.status_code}")
except Exception as e:
print(f"✗ GET /patterns/definitions - Error: {e}")
print(f"\nPatterns tests: {passed}/{total} passed")
return passed == total
def test_tab_edit_workflow(client: TestClient) -> bool:
"""Test complete tab edit workflow like a browser would."""
print("\n=== Testing Tab Edit Workflow ===")
passed = 0
total = 0
# Step 1: Create a tab to edit
total += 1
try:
tab_data = {
"name": "Tab to Edit",
"names": ["1"]
}
response = client.post('/tabs', json_data=tab_data)
if response.status_code == 201:
created = response.json()
if isinstance(created, dict):
tab_id = next(iter(created.keys())) if created else None
else:
tab_id = None
if tab_id:
print(f"✓ Created tab {tab_id} for editing")
passed += 1
# Step 2: Get the tab to verify initial state
total += 1
response = client.get(f'/tabs/{tab_id}')
if response.status_code == 200:
original_tab = response.json()
print(f"✓ Retrieved tab - Name: '{original_tab.get('name')}', IDs: {original_tab.get('names')}")
passed += 1
# Step 3: Edit the tab (simulate browser edit form submission)
total += 1
edit_data = {
"name": "Edited Tab Name",
"names": ["2", "3", "4"]
}
response = client.put(f'/tabs/{tab_id}', json_data=edit_data)
if response.status_code == 200:
edited = response.json()
if edited.get('name') == "Edited Tab Name" and edited.get('names') == ["2", "3", "4"]:
print(f"✓ PUT /tabs/{tab_id} - Successfully edited tab")
print(f" New name: '{edited.get('name')}'")
print(f" New device IDs: {edited.get('names')}")
passed += 1
else:
print(f"✗ PUT /tabs/{tab_id} - Edit didn't work correctly")
print(f" Got: {edited}")
else:
print(f"✗ PUT /tabs/{tab_id} - Status: {response.status_code}, Response: {response.text}")
# Step 4: Verify edit persisted by getting the tab again
total += 1
response = client.get(f'/tabs/{tab_id}')
if response.status_code == 200:
verified = response.json()
if verified.get('name') == "Edited Tab Name" and verified.get('names') == ["2", "3", "4"]:
print(f"✓ GET /tabs/{tab_id} - Verified edit persisted")
passed += 1
else:
print(f"✗ GET /tabs/{tab_id} - Edit didn't persist")
print(f" Expected name='Edited Tab Name', got '{verified.get('name')}'")
print(f" Expected names=['2','3','4'], got {verified.get('names')}")
else:
print(f"✗ GET /tabs/{tab_id} - Status: {response.status_code}")
# Step 5: Clean up - delete the test tab
total += 1
response = client.delete(f'/tabs/{tab_id}')
if response.status_code == 200:
print(f"✓ DELETE /tabs/{tab_id} - Cleaned up test tab")
passed += 1
else:
print(f"✗ DELETE /tabs/{tab_id} - Status: {response.status_code}")
else:
print(f"✗ Failed to extract tab ID from create response")
else:
print(f"✗ POST /tabs - Status: {response.status_code}, Response: {response.text}")
except Exception as e:
print(f"✗ Tab edit workflow - Error: {e}")
import traceback
traceback.print_exc()
print(f"\nTab edit workflow tests: {passed}/{total} passed")
return passed == total
def test_static_files(client: TestClient) -> bool:
"""Test static file serving."""
print("\n=== Testing Static Files ===")
passed = 0
total = 0
static_files = [
'/static/style.css',
'/static/app.js',
'/static/tabs.js',
'/static/presets.js',
'/static/profiles.js',
]
for file_path in static_files:
total += 1
try:
response = client.get(file_path)
if response.status_code == 200:
print(f"✓ GET {file_path} - Retrieved")
passed += 1
else:
print(f"✗ GET {file_path} - Status: {response.status_code}")
except Exception as e:
print(f"✗ GET {file_path} - Error: {e}")
print(f"\nStatic files tests: {passed}/{total} passed")
return passed == total
def main():
"""Run all endpoint tests."""
print("=" * 60)
print("LED Controller Endpoint Tests")
print(f"Testing against: {BASE_URL}")
print("=" * 60)
client = TestClient()
# Test connection first
if not test_connection(client):
print("\n✗ Cannot connect to device. Exiting.")
sys.exit(1)
results = []
# Run all tests
results.append(("Tabs", test_tabs(client)))
results.append(("Tab Edit Workflow", test_tab_edit_workflow(client)))
results.append(("Profiles", test_profiles(client)))
results.append(("Presets", test_presets(client)))
results.append(("Patterns", test_patterns(client)))
results.append(("Static Files", test_static_files(client)))
# Summary
print("\n" + "=" * 60)
print("Test Summary")
print("=" * 60)
all_passed = True
for name, passed in results:
status = "✓ PASS" if passed else "✗ FAIL"
print(f"{status} - {name}")
if not passed:
all_passed = False
print("=" * 60)
if all_passed:
print("✓ All tests passed!")
sys.exit(0)
else:
print("✗ Some tests failed")
sys.exit(1)
if __name__ == "__main__":
main()