feat: 2.0 - Backend API - GET /api/habits
This commit is contained in:
@@ -251,6 +251,8 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
|
|||||||
self.handle_cron_status()
|
self.handle_cron_status()
|
||||||
elif self.path == '/api/activity' or self.path.startswith('/api/activity?'):
|
elif self.path == '/api/activity' or self.path.startswith('/api/activity?'):
|
||||||
self.handle_activity()
|
self.handle_activity()
|
||||||
|
elif self.path == '/api/habits':
|
||||||
|
self.handle_habits_get()
|
||||||
elif self.path.startswith('/api/files'):
|
elif self.path.startswith('/api/files'):
|
||||||
self.handle_files_get()
|
self.handle_files_get()
|
||||||
elif self.path.startswith('/api/diff'):
|
elif self.path.startswith('/api/diff'):
|
||||||
@@ -681,6 +683,41 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.send_json({'error': str(e)}, 500)
|
self.send_json({'error': str(e)}, 500)
|
||||||
|
|
||||||
|
def handle_habits_get(self):
|
||||||
|
"""Get all habits from habits.json."""
|
||||||
|
try:
|
||||||
|
habits_file = KANBAN_DIR / 'habits.json'
|
||||||
|
|
||||||
|
# Handle missing file or empty file gracefully
|
||||||
|
if not habits_file.exists():
|
||||||
|
self.send_json({
|
||||||
|
'habits': [],
|
||||||
|
'lastUpdated': datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Read and parse habits data
|
||||||
|
try:
|
||||||
|
data = json.loads(habits_file.read_text(encoding='utf-8'))
|
||||||
|
except (json.JSONDecodeError, IOError):
|
||||||
|
# Return empty array on parse error instead of 500
|
||||||
|
self.send_json({
|
||||||
|
'habits': [],
|
||||||
|
'lastUpdated': datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ensure required fields exist
|
||||||
|
habits = data.get('habits', [])
|
||||||
|
last_updated = data.get('lastUpdated', datetime.now().isoformat())
|
||||||
|
|
||||||
|
self.send_json({
|
||||||
|
'habits': habits,
|
||||||
|
'lastUpdated': last_updated
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
self.send_json({'error': str(e)}, 500)
|
||||||
|
|
||||||
def handle_files_get(self):
|
def handle_files_get(self):
|
||||||
"""List files or get file content."""
|
"""List files or get file content."""
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|||||||
207
dashboard/test_habits_api.py
Normal file
207
dashboard/test_habits_api.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/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)
|
||||||
Reference in New Issue
Block a user