208 lines
6.5 KiB
Python
208 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for GET /api/habits endpoint.
|
|
Validates API response structure, status codes, and error handling.
|
|
"""
|
|
|
|
import json
|
|
import urllib.request
|
|
import urllib.error
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
# API endpoint - assumes server is running on localhost:8088
|
|
API_BASE = 'http://localhost:8088'
|
|
HABITS_FILE = Path(__file__).parent / 'habits.json'
|
|
|
|
|
|
def test_habits_endpoint_exists():
|
|
"""Test that GET /api/habits endpoint exists and returns 200."""
|
|
print("Testing endpoint exists and returns 200...")
|
|
|
|
try:
|
|
response = urllib.request.urlopen(f'{API_BASE}/api/habits', timeout=5)
|
|
status_code = response.getcode()
|
|
|
|
assert status_code == 200, f"Expected status 200, got {status_code}"
|
|
print("✓ Endpoint returns 200 status")
|
|
except urllib.error.HTTPError as e:
|
|
raise AssertionError(f"Endpoint returned HTTP {e.code}: {e.reason}")
|
|
except urllib.error.URLError as e:
|
|
raise AssertionError(f"Could not connect to API server: {e.reason}")
|
|
|
|
|
|
def test_habits_response_is_json():
|
|
"""Test that response is valid JSON."""
|
|
print("Testing response is valid JSON...")
|
|
|
|
try:
|
|
response = urllib.request.urlopen(f'{API_BASE}/api/habits', timeout=5)
|
|
content = response.read().decode('utf-8')
|
|
|
|
try:
|
|
data = json.loads(content)
|
|
print("✓ Response is valid JSON")
|
|
return data
|
|
except json.JSONDecodeError as e:
|
|
raise AssertionError(f"Response is not valid JSON: {e}")
|
|
except urllib.error.URLError as e:
|
|
raise AssertionError(f"Could not connect to API server: {e.reason}")
|
|
|
|
|
|
def test_habits_response_structure():
|
|
"""Test that response has correct structure: habits array and lastUpdated."""
|
|
print("Testing response structure...")
|
|
|
|
data = test_habits_response_is_json()
|
|
|
|
# Check for habits array
|
|
assert 'habits' in data, "Response missing 'habits' field"
|
|
assert isinstance(data['habits'], list), "'habits' field must be an array"
|
|
print("✓ Response contains 'habits' array")
|
|
|
|
# Check for lastUpdated timestamp
|
|
assert 'lastUpdated' in data, "Response missing 'lastUpdated' field"
|
|
print("✓ Response contains 'lastUpdated' field")
|
|
|
|
|
|
def test_habits_lastupdated_is_iso():
|
|
"""Test that lastUpdated is a valid ISO timestamp."""
|
|
print("Testing lastUpdated is valid ISO timestamp...")
|
|
|
|
data = test_habits_response_is_json()
|
|
last_updated = data.get('lastUpdated')
|
|
|
|
assert last_updated, "lastUpdated field is empty"
|
|
|
|
try:
|
|
# Try to parse as ISO datetime
|
|
dt = datetime.fromisoformat(last_updated.replace('Z', '+00:00'))
|
|
print(f"✓ lastUpdated is valid ISO timestamp: {last_updated}")
|
|
except (ValueError, AttributeError) as e:
|
|
raise AssertionError(f"lastUpdated is not a valid ISO timestamp: {e}")
|
|
|
|
|
|
def test_empty_habits_returns_empty_array():
|
|
"""Test that empty habits.json returns empty array, not error."""
|
|
print("Testing empty habits file returns empty array...")
|
|
|
|
# Backup original file
|
|
backup = None
|
|
if HABITS_FILE.exists():
|
|
backup = HABITS_FILE.read_text()
|
|
|
|
try:
|
|
# Write empty habits file
|
|
HABITS_FILE.write_text(json.dumps({
|
|
'lastUpdated': datetime.now().isoformat(),
|
|
'habits': []
|
|
}))
|
|
|
|
# Request habits
|
|
response = urllib.request.urlopen(f'{API_BASE}/api/habits', timeout=5)
|
|
data = json.loads(response.read().decode('utf-8'))
|
|
|
|
assert data['habits'] == [], "Empty habits.json should return empty array"
|
|
print("✓ Empty habits.json returns empty array (not error)")
|
|
|
|
finally:
|
|
# Restore backup
|
|
if backup:
|
|
HABITS_FILE.write_text(backup)
|
|
|
|
|
|
def test_habits_with_data():
|
|
"""Test that habits with data are returned correctly."""
|
|
print("Testing habits with data are returned...")
|
|
|
|
# Backup original file
|
|
backup = None
|
|
if HABITS_FILE.exists():
|
|
backup = HABITS_FILE.read_text()
|
|
|
|
try:
|
|
# Write test habits
|
|
test_data = {
|
|
'lastUpdated': '2026-02-10T10:00:00.000Z',
|
|
'habits': [
|
|
{
|
|
'id': 'test-habit-1',
|
|
'name': 'Bazin',
|
|
'frequency': 'daily',
|
|
'createdAt': '2026-02-10T10:00:00.000Z',
|
|
'completions': ['2026-02-10T10:00:00.000Z']
|
|
}
|
|
]
|
|
}
|
|
HABITS_FILE.write_text(json.dumps(test_data, indent=2))
|
|
|
|
# Request habits
|
|
response = urllib.request.urlopen(f'{API_BASE}/api/habits', timeout=5)
|
|
data = json.loads(response.read().decode('utf-8'))
|
|
|
|
assert len(data['habits']) == 1, "Should return 1 habit"
|
|
habit = data['habits'][0]
|
|
assert habit['name'] == 'Bazin', f"Expected habit name 'Bazin', got '{habit['name']}'"
|
|
assert habit['frequency'] == 'daily', f"Expected frequency 'daily', got '{habit['frequency']}'"
|
|
print("✓ Habits with data are returned correctly")
|
|
|
|
finally:
|
|
# Restore backup
|
|
if backup:
|
|
HABITS_FILE.write_text(backup)
|
|
|
|
|
|
def run_all_tests():
|
|
"""Run all tests and report results."""
|
|
print("=" * 60)
|
|
print("Running GET /api/habits endpoint tests")
|
|
print("=" * 60)
|
|
print()
|
|
|
|
tests = [
|
|
test_habits_endpoint_exists,
|
|
test_habits_response_is_json,
|
|
test_habits_response_structure,
|
|
test_habits_lastupdated_is_iso,
|
|
test_empty_habits_returns_empty_array,
|
|
test_habits_with_data,
|
|
]
|
|
|
|
passed = 0
|
|
failed = 0
|
|
|
|
for test in tests:
|
|
try:
|
|
test()
|
|
passed += 1
|
|
print()
|
|
except AssertionError as e:
|
|
print(f"✗ FAILED: {e}")
|
|
print()
|
|
failed += 1
|
|
except Exception as e:
|
|
print(f"✗ ERROR: {e}")
|
|
print()
|
|
failed += 1
|
|
|
|
print("=" * 60)
|
|
print(f"Results: {passed} passed, {failed} failed")
|
|
print("=" * 60)
|
|
|
|
return failed == 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
|
|
# Check if API server is running
|
|
try:
|
|
urllib.request.urlopen(f'{API_BASE}/api/status', timeout=2)
|
|
except urllib.error.URLError:
|
|
print("ERROR: API server is not running on localhost:8088")
|
|
print("Start the server with: python3 dashboard/api.py")
|
|
sys.exit(1)
|
|
|
|
success = run_all_tests()
|
|
sys.exit(0 if success else 1)
|