feat: 2.0 - Backend API - GET /api/habits

This commit is contained in:
Echo
2026-02-10 11:09:58 +00:00
parent ee8727a8df
commit fc5ebf2026
2 changed files with 244 additions and 0 deletions

View File

@@ -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

View 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)