refactor: comprehensive code quality, security, and release readiness improvements
Some checks failed
Lint & Test / test (push) Failing after 48s
Some checks failed
Lint & Test / test (push) Failing after 48s
Security: tighten CORS defaults, add webhook rate limiting, fix XSS in automations, guard WebSocket JSON.parse, validate ADB address input, seal debug exception leak, URL-encode WS tokens, CSS.escape in selectors. Code quality: add Pydantic models for brightness/power endpoints, fix thread safety and name uniqueness in DeviceStore, immutable update pattern, split 6 oversized files into 16 focused modules, enable TypeScript strictNullChecks (741→102 errors), type state variables, add dom-utils helper, migrate 3 modules from inline onclick to event delegation, ProcessorDependencies dataclass. Performance: async store saves, health endpoint log level, command palette debounce, optimized entity-events comparison, fix service worker precache list. Testing: expand from 45 to 293 passing tests — add store tests (141), route tests (25), core logic tests (42), E2E flow tests (33), organize into tests/api/, tests/storage/, tests/core/, tests/e2e/. DevOps: CI test pipeline, pre-commit config, Dockerfile multi-stage build with non-root user and health check, docker-compose improvements, version bump to 0.2.0. Docs: rewrite CLAUDE.md (202→56 lines), server/CLAUDE.md (212→76), create contexts/server-operations.md, fix .js→.ts references, fix env var prefix in README, rewrite INSTALLATION.md, add CONTRIBUTING.md and .env.example.
This commit is contained in:
125
server/tests/e2e/test_auth_flow.py
Normal file
125
server/tests/e2e/test_auth_flow.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""E2E: Authentication enforcement.
|
||||
|
||||
Tests that protected endpoints require valid auth, and public endpoints work
|
||||
without auth.
|
||||
|
||||
Uses the `client` fixture (which has the correct auth header set), and
|
||||
helpers to make unauthenticated requests by temporarily removing the header.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.e2e.conftest import API_KEY
|
||||
|
||||
|
||||
def _unauth_get(client, url):
|
||||
"""Make a GET request without the Authorization header."""
|
||||
saved = client.headers.pop("Authorization", None)
|
||||
try:
|
||||
return client.get(url)
|
||||
finally:
|
||||
if saved is not None:
|
||||
client.headers["Authorization"] = saved
|
||||
|
||||
|
||||
def _unauth_request(client, method, url, **kwargs):
|
||||
"""Make a request without the Authorization header."""
|
||||
saved = client.headers.pop("Authorization", None)
|
||||
try:
|
||||
return client.request(method, url, **kwargs)
|
||||
finally:
|
||||
if saved is not None:
|
||||
client.headers["Authorization"] = saved
|
||||
|
||||
|
||||
def _with_header(client, method, url, auth_value, **kwargs):
|
||||
"""Make a request with a custom Authorization header."""
|
||||
saved = client.headers.get("Authorization")
|
||||
client.headers["Authorization"] = auth_value
|
||||
try:
|
||||
return client.request(method, url, **kwargs)
|
||||
finally:
|
||||
if saved is not None:
|
||||
client.headers["Authorization"] = saved
|
||||
else:
|
||||
client.headers.pop("Authorization", None)
|
||||
|
||||
|
||||
class TestAuthEnforcement:
|
||||
"""Verify API key authentication is enforced correctly."""
|
||||
|
||||
def test_request_without_auth_returns_401(self, client):
|
||||
"""Protected endpoint without Authorization header returns 401."""
|
||||
resp = _unauth_get(client, "/api/v1/devices")
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_request_with_wrong_key_returns_401(self, client):
|
||||
"""Protected endpoint with an incorrect API key returns 401."""
|
||||
resp = _with_header(
|
||||
client, "GET", "/api/v1/devices",
|
||||
auth_value="Bearer wrong-key-12345",
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_request_with_correct_key_returns_200(self, client):
|
||||
"""Protected endpoint with valid API key succeeds."""
|
||||
resp = client.get("/api/v1/devices")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_health_endpoint_is_public(self, client):
|
||||
"""Health check does not require authentication."""
|
||||
resp = _unauth_get(client, "/health")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "healthy"
|
||||
|
||||
def test_version_endpoint_is_public(self, client):
|
||||
"""Version endpoint does not require authentication."""
|
||||
resp = _unauth_get(client, "/api/v1/version")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "version" in data
|
||||
assert "api_version" in data
|
||||
|
||||
def test_post_without_auth_returns_401(self, client):
|
||||
"""Creating a device without auth fails."""
|
||||
resp = _unauth_request(
|
||||
client, "POST", "/api/v1/devices",
|
||||
json={
|
||||
"name": "Unauthorized Device",
|
||||
"url": "mock://test",
|
||||
"device_type": "mock",
|
||||
"led_count": 10,
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_delete_without_auth_returns_401(self, client):
|
||||
"""Deleting a device without auth fails."""
|
||||
resp = _unauth_request(client, "DELETE", "/api/v1/devices/some_id")
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_backup_without_auth_returns_401(self, client):
|
||||
"""Backup endpoint requires authentication."""
|
||||
resp = _unauth_get(client, "/api/v1/system/backup")
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_color_strip_sources_without_auth_returns_401(self, client):
|
||||
"""Color strip source listing requires authentication."""
|
||||
resp = _unauth_get(client, "/api/v1/color-strip-sources")
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_output_targets_without_auth_returns_401(self, client):
|
||||
"""Output target listing requires authentication."""
|
||||
resp = _unauth_get(client, "/api/v1/output-targets")
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_malformed_bearer_token_returns_401_or_403(self, client):
|
||||
"""A malformed Authorization header is rejected."""
|
||||
resp = _with_header(
|
||||
client, "GET", "/api/v1/devices",
|
||||
auth_value="just-a-key",
|
||||
)
|
||||
# FastAPI's HTTPBearer returns 403 for malformed format,
|
||||
# or 401 depending on auto_error setting. Accept either.
|
||||
assert resp.status_code in (401, 403)
|
||||
Reference in New Issue
Block a user