diff --git a/dashboard/test_habits_integration.py b/dashboard/test_habits_integration.py new file mode 100644 index 0000000..aa9fdc5 --- /dev/null +++ b/dashboard/test_habits_integration.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +""" +Integration test for complete habit lifecycle. +Tests: create, check multiple days, view streak, delete +""" + +import json +import os +import sys +import time +from datetime import datetime, timedelta +from http.server import HTTPServer +import threading +import urllib.request +import urllib.error + +# Add parent directory to path for imports +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from api import TaskBoardHandler + +# Test configuration +PORT = 8765 +BASE_URL = f"http://localhost:{PORT}" +HABITS_FILE = os.path.join(os.path.dirname(__file__), 'habits.json') + +# Global server instance +server = None +server_thread = None + + +def setup_server(): + """Start test server in background thread""" + global server, server_thread + server = HTTPServer(('localhost', PORT), TaskBoardHandler) + server_thread = threading.Thread(target=server.serve_forever, daemon=True) + server_thread.start() + time.sleep(0.5) # Give server time to start + + +def teardown_server(): + """Stop test server""" + global server + if server: + server.shutdown() + server.server_close() + + +def reset_habits_file(): + """Reset habits.json to empty state""" + data = { + "lastUpdated": datetime.utcnow().isoformat() + 'Z', + "habits": [] + } + with open(HABITS_FILE, 'w') as f: + json.dump(data, f, indent=2) + + +def make_request(method, path, body=None): + """Make HTTP request to test server""" + url = BASE_URL + path + headers = {'Content-Type': 'application/json'} if body else {} + data = json.dumps(body).encode('utf-8') if body else None + + req = urllib.request.Request(url, data=data, headers=headers, method=method) + + try: + with urllib.request.urlopen(req) as response: + response_data = response.read().decode('utf-8') + return response.status, json.loads(response_data) if response_data else None + except urllib.error.HTTPError as e: + response_data = e.read().decode('utf-8') + return e.code, json.loads(response_data) if response_data else None + + +def get_today(): + """Get today's date in YYYY-MM-DD format""" + return datetime.utcnow().strftime('%Y-%m-%d') + + +def get_yesterday(): + """Get yesterday's date in YYYY-MM-DD format""" + return (datetime.utcnow() - timedelta(days=1)).strftime('%Y-%m-%d') + + +def test_complete_habit_lifecycle(): + """ + Integration test: Complete habit flow from creation to deletion + """ + print("\n=== Integration Test: Complete Habit Lifecycle ===\n") + + # Step 1: Create daily habit 'Bazin' + print("Step 1: Creating daily habit 'Bazin'...") + status, response = make_request('POST', '/api/habits', { + 'name': 'Bazin', + 'frequency': 'daily' + }) + + assert status == 201, f"Expected 201, got {status}" + assert response['name'] == 'Bazin', "Habit name mismatch" + assert response['frequency'] == 'daily', "Frequency mismatch" + assert 'id' in response, "Missing habit ID" + habit_id = response['id'] + print(f"✓ Created habit: {habit_id}") + + # Step 2: Check it today (streak should be 1) + print("\nStep 2: Checking habit today (expecting streak = 1)...") + status, response = make_request('POST', f'/api/habits/{habit_id}/check', {}) + + assert status == 200, f"Expected 200, got {status}" + assert response['streak'] == 1, f"Expected streak=1, got {response['streak']}" + assert get_today() in response['completions'], "Today's date not in completions" + print(f"✓ Checked today, streak = {response['streak']}") + + # Step 3: Simulate checking yesterday (manually add to completions) + print("\nStep 3: Simulating yesterday's check (expecting streak = 2)...") + # Read current habits.json + with open(HABITS_FILE, 'r') as f: + data = json.load(f) + + # Find the habit and add yesterday's date + for habit in data['habits']: + if habit['id'] == habit_id: + habit['completions'].append(get_yesterday()) + habit['completions'].sort() # Keep chronological order + break + + # Write back to file + data['lastUpdated'] = datetime.utcnow().isoformat() + 'Z' + with open(HABITS_FILE, 'w') as f: + json.dump(data, f, indent=2) + + # Verify streak calculation by fetching habits + status, response = make_request('GET', '/api/habits', None) + assert status == 200, f"Expected 200, got {status}" + + habit = next((h for h in response['habits'] if h['id'] == habit_id), None) + assert habit is not None, "Habit not found in response" + assert habit['streak'] == 2, f"Expected streak=2 after adding yesterday, got {habit['streak']}" + print(f"✓ Added yesterday's completion, streak = {habit['streak']}") + + # Step 4: Verify streak calculation is correct + print("\nStep 4: Verifying streak calculation...") + assert len(habit['completions']) == 2, f"Expected 2 completions, got {len(habit['completions'])}" + assert get_yesterday() in habit['completions'], "Yesterday not in completions" + assert get_today() in habit['completions'], "Today not in completions" + assert habit['checkedToday'] == True, "checkedToday should be True" + print("✓ Streak calculation verified: 2 consecutive days") + + # Step 5: Delete habit successfully + print("\nStep 5: Deleting habit...") + status, response = make_request('DELETE', f'/api/habits/{habit_id}', None) + + assert status == 200, f"Expected 200, got {status}" + assert 'message' in response, "Missing success message" + print(f"✓ Deleted habit: {response['message']}") + + # Verify habit is gone + status, response = make_request('GET', '/api/habits', None) + assert status == 200, f"Expected 200, got {status}" + + habit = next((h for h in response['habits'] if h['id'] == habit_id), None) + assert habit is None, "Habit still exists after deletion" + print("✓ Verified habit no longer exists") + + print("\n=== All Integration Tests Passed ✓ ===\n") + + +def test_broken_streak(): + """ + Additional test: Verify broken streak returns 0 (with gap) + """ + print("\n=== Additional Test: Broken Streak (with gap) ===\n") + + # Create habit + status, response = make_request('POST', '/api/habits', { + 'name': 'Sală', + 'frequency': 'daily' + }) + assert status == 201 + habit_id = response['id'] + print(f"✓ Created habit: {habit_id}") + + # Add check from 3 days ago (creating a gap) + three_days_ago = (datetime.utcnow() - timedelta(days=3)).strftime('%Y-%m-%d') + with open(HABITS_FILE, 'r') as f: + data = json.load(f) + + for habit in data['habits']: + if habit['id'] == habit_id: + habit['completions'].append(three_days_ago) + break + + data['lastUpdated'] = datetime.utcnow().isoformat() + 'Z' + with open(HABITS_FILE, 'w') as f: + json.dump(data, f, indent=2) + + # Verify streak is 0 (>1 day gap means broken streak) + status, response = make_request('GET', '/api/habits', None) + habit = next((h for h in response['habits'] if h['id'] == habit_id), None) + assert habit['streak'] == 0, f"Expected streak=0 for broken streak, got {habit['streak']}" + print(f"✓ Broken streak (>1 day gap) correctly returns 0") + + # Cleanup + make_request('DELETE', f'/api/habits/{habit_id}', None) + print("✓ Cleanup complete") + + +def test_weekly_habit_streak(): + """ + Additional test: Weekly habit streak calculation + """ + print("\n=== Additional Test: Weekly Habit Streak ===\n") + + # Create weekly habit + status, response = make_request('POST', '/api/habits', { + 'name': 'Yoga', + 'frequency': 'weekly' + }) + assert status == 201 + habit_id = response['id'] + print(f"✓ Created weekly habit: {habit_id}") + + # Check today (streak = 1 week) + status, response = make_request('POST', f'/api/habits/{habit_id}/check', {}) + assert status == 200 + assert response['streak'] == 1, f"Expected streak=1 week, got {response['streak']}" + print(f"✓ Checked today, weekly streak = {response['streak']}") + + # Add check from 8 days ago (last week) + eight_days_ago = (datetime.utcnow() - timedelta(days=8)).strftime('%Y-%m-%d') + with open(HABITS_FILE, 'r') as f: + data = json.load(f) + + for habit in data['habits']: + if habit['id'] == habit_id: + habit['completions'].append(eight_days_ago) + habit['completions'].sort() + break + + data['lastUpdated'] = datetime.utcnow().isoformat() + 'Z' + with open(HABITS_FILE, 'w') as f: + json.dump(data, f, indent=2) + + # Verify streak is 2 weeks + status, response = make_request('GET', '/api/habits', None) + habit = next((h for h in response['habits'] if h['id'] == habit_id), None) + assert habit['streak'] == 2, f"Expected streak=2 weeks, got {habit['streak']}" + print(f"✓ Weekly streak calculation correct: {habit['streak']} weeks") + + # Cleanup + make_request('DELETE', f'/api/habits/{habit_id}', None) + print("✓ Cleanup complete") + + +if __name__ == '__main__': + try: + # Setup + reset_habits_file() + setup_server() + + # Run tests + test_complete_habit_lifecycle() + test_broken_streak() + test_weekly_habit_streak() + + print("\n🎉 All Integration Tests Passed! 🎉\n") + + except AssertionError as e: + print(f"\n❌ Test Failed: {e}\n") + sys.exit(1) + except Exception as e: + print(f"\n❌ Error: {e}\n") + import traceback + traceback.print_exc() + sys.exit(1) + finally: + # Cleanup + teardown_server() + reset_habits_file()