#!/usr/bin/env python3 """ Tests for POST /api/habits/{id}/check endpoint """ import json import sys import time from datetime import datetime, timedelta from pathlib import Path from http.client import HTTPConnection # Test against local server HOST = 'localhost' PORT = 8088 HABITS_FILE = Path(__file__).parent / 'habits.json' def cleanup_test_habits(): """Reset habits.json to empty state for testing.""" data = { 'lastUpdated': datetime.now().isoformat(), 'habits': [] } HABITS_FILE.write_text(json.dumps(data, indent=2)) def create_test_habit(name='Test Habit', frequency='daily'): """Helper to create a test habit and return its ID.""" conn = HTTPConnection(HOST, PORT) payload = json.dumps({'name': name, 'frequency': frequency}) headers = {'Content-Type': 'application/json'} conn.request('POST', '/api/habits', payload, headers) response = conn.getresponse() data = json.loads(response.read().decode()) conn.close() return data['id'] def test_check_habit_success(): """Test successfully checking a habit for today.""" cleanup_test_habits() habit_id = create_test_habit('Morning Run', 'daily') conn = HTTPConnection(HOST, PORT) conn.request('POST', f'/api/habits/{habit_id}/check') response = conn.getresponse() data = json.loads(response.read().decode()) conn.close() assert response.status == 200, f"Expected 200, got {response.status}" assert data['id'] == habit_id, "Habit ID should match" assert 'completions' in data, "Response should include completions" assert len(data['completions']) == 1, "Should have exactly 1 completion" # Check that completion date is today (YYYY-MM-DD format) today = datetime.now().date().isoformat() assert data['completions'][0] == today, f"Completion should be today's date: {today}" # Check that streak is calculated and included assert 'streak' in data, "Response should include streak" assert data['streak'] == 1, "Streak should be 1 after first check" print("✓ test_check_habit_success") def test_check_habit_already_checked(): """Test checking a habit that was already checked today.""" cleanup_test_habits() habit_id = create_test_habit('Reading', 'daily') # Check it once conn = HTTPConnection(HOST, PORT) conn.request('POST', f'/api/habits/{habit_id}/check') conn.getresponse().read() conn.close() # Try to check again conn = HTTPConnection(HOST, PORT) conn.request('POST', f'/api/habits/{habit_id}/check') response = conn.getresponse() data = json.loads(response.read().decode()) conn.close() assert response.status == 400, f"Expected 400, got {response.status}" assert 'error' in data, "Response should include error" assert 'already checked' in data['error'].lower(), "Error should mention already checked" print("✓ test_check_habit_already_checked") def test_check_habit_not_found(): """Test checking a non-existent habit.""" cleanup_test_habits() conn = HTTPConnection(HOST, PORT) conn.request('POST', '/api/habits/nonexistent-id/check') response = conn.getresponse() data = json.loads(response.read().decode()) conn.close() assert response.status == 404, f"Expected 404, got {response.status}" assert 'error' in data, "Response should include error" print("✓ test_check_habit_not_found") def test_check_habit_persistence(): """Test that completions are persisted to habits.json.""" cleanup_test_habits() habit_id = create_test_habit('Meditation', 'daily') # Check the habit conn = HTTPConnection(HOST, PORT) conn.request('POST', f'/api/habits/{habit_id}/check') conn.getresponse().read() conn.close() # Read habits.json directly habits_data = json.loads(HABITS_FILE.read_text()) habit = next((h for h in habits_data['habits'] if h['id'] == habit_id), None) assert habit is not None, "Habit should exist in file" assert len(habit['completions']) == 1, "Should have 1 completion in file" today = datetime.now().date().isoformat() assert habit['completions'][0] == today, "Completion date should be today" print("✓ test_check_habit_persistence") def test_check_habit_sorted_completions(): """Test that completions array is sorted chronologically.""" cleanup_test_habits() # Create a habit and manually add out-of-order completions habit_id = create_test_habit('Workout', 'daily') # Manually add past completions in reverse order habits_data = json.loads(HABITS_FILE.read_text()) habit = next((h for h in habits_data['habits'] if h['id'] == habit_id), None) today = datetime.now().date() habit['completions'] = [ (today - timedelta(days=2)).isoformat(), # 2 days ago (today - timedelta(days=4)).isoformat(), # 4 days ago (today - timedelta(days=1)).isoformat(), # yesterday ] HABITS_FILE.write_text(json.dumps(habits_data, indent=2)) # Check today conn = HTTPConnection(HOST, PORT) conn.request('POST', f'/api/habits/{habit_id}/check') response = conn.getresponse() data = json.loads(response.read().decode()) conn.close() # Verify completions are sorted oldest first expected_order = [ (today - timedelta(days=4)).isoformat(), (today - timedelta(days=2)).isoformat(), (today - timedelta(days=1)).isoformat(), today.isoformat() ] assert data['completions'] == expected_order, f"Completions should be sorted. Got: {data['completions']}" print("✓ test_check_habit_sorted_completions") def test_check_habit_streak_calculation(): """Test that streak is calculated correctly after checking.""" cleanup_test_habits() habit_id = create_test_habit('Journaling', 'daily') # Add consecutive past completions today = datetime.now().date() habits_data = json.loads(HABITS_FILE.read_text()) habit = next((h for h in habits_data['habits'] if h['id'] == habit_id), None) habit['completions'] = [ (today - timedelta(days=2)).isoformat(), (today - timedelta(days=1)).isoformat(), ] HABITS_FILE.write_text(json.dumps(habits_data, indent=2)) # Check today conn = HTTPConnection(HOST, PORT) conn.request('POST', f'/api/habits/{habit_id}/check') response = conn.getresponse() data = json.loads(response.read().decode()) conn.close() # Streak should be 3 (today + yesterday + day before) assert data['streak'] == 3, f"Expected streak 3, got {data['streak']}" print("✓ test_check_habit_streak_calculation") def test_check_weekly_habit(): """Test checking a weekly habit.""" cleanup_test_habits() habit_id = create_test_habit('Team Meeting', 'weekly') conn = HTTPConnection(HOST, PORT) conn.request('POST', f'/api/habits/{habit_id}/check') response = conn.getresponse() data = json.loads(response.read().decode()) conn.close() assert response.status == 200, f"Expected 200, got {response.status}" assert len(data['completions']) == 1, "Should have 1 completion" assert data['streak'] == 1, "Weekly habit should have streak of 1" print("✓ test_check_weekly_habit") def test_check_habit_iso_date_format(): """Test that completion dates use ISO YYYY-MM-DD format (not timestamps).""" cleanup_test_habits() habit_id = create_test_habit('Water Plants', 'daily') conn = HTTPConnection(HOST, PORT) conn.request('POST', f'/api/habits/{habit_id}/check') response = conn.getresponse() data = json.loads(response.read().decode()) conn.close() completion = data['completions'][0] # Verify format is YYYY-MM-DD (exactly 10 chars, 2 dashes) assert len(completion) == 10, f"Date should be 10 chars, got {len(completion)}" assert completion.count('-') == 2, "Date should have 2 dashes" assert 'T' not in completion, "Date should not include time (no T)" # Verify it parses as a valid date try: datetime.fromisoformat(completion) except ValueError: assert False, f"Completion date should be valid ISO date: {completion}" print("✓ test_check_habit_iso_date_format") if __name__ == '__main__': print("Running tests for POST /api/habits/{id}/check...") print() try: test_check_habit_success() test_check_habit_already_checked() test_check_habit_not_found() test_check_habit_persistence() test_check_habit_sorted_completions() test_check_habit_streak_calculation() test_check_weekly_habit() test_check_habit_iso_date_format() print() print("✅ All tests passed!") sys.exit(0) except AssertionError as e: print() print(f"❌ Test failed: {e}") sys.exit(1) except Exception as e: print() print(f"❌ Error running tests: {e}") import traceback traceback.print_exc() sys.exit(1)