#!/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)