feat: US-008 - Tests: Update and add tests for all refinements

This commit is contained in:
Echo
2026-02-10 20:52:37 +00:00
parent 033bd63329
commit 1829397195

View File

@@ -39,14 +39,15 @@ def cleanup_test_env(temp_dir):
api.HABITS_FILE = original_habits_file api.HABITS_FILE = original_habits_file
shutil.rmtree(temp_dir) shutil.rmtree(temp_dir)
def start_test_server(port=8765): def start_test_server():
"""Start test server in background thread.""" """Start test server in background thread with random available port."""
server = HTTPServer(('localhost', port), api.TaskBoardHandler) server = HTTPServer(('localhost', 0), api.TaskBoardHandler) # Port 0 = random
port = server.server_address[1] # Get actual assigned port
thread = threading.Thread(target=server.serve_forever) thread = threading.Thread(target=server.serve_forever)
thread.daemon = True thread.daemon = True
thread.start() thread.start()
time.sleep(0.5) # Give server time to start time.sleep(0.3) # Give server time to start
return server return server, port
def http_get(path, port=8765): def http_get(path, port=8765):
"""Make HTTP GET request.""" """Make HTTP GET request."""
@@ -103,10 +104,10 @@ def http_delete(path, port=8765):
# Test 1: GET /api/habits returns empty array when no habits # Test 1: GET /api/habits returns empty array when no habits
def test_get_habits_empty(): def test_get_habits_empty():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
status, data = http_get('/api/habits') status, data = http_get('/api/habits', port)
assert status == 200, f"Expected 200, got {status}" assert status == 200, f"Expected 200, got {status}"
assert data == [], f"Expected empty array, got {data}" assert data == [], f"Expected empty array, got {data}"
print("✓ Test 1: GET /api/habits returns empty array") print("✓ Test 1: GET /api/habits returns empty array")
@@ -117,7 +118,7 @@ def test_get_habits_empty():
# Test 2: POST /api/habits creates new habit with valid input # Test 2: POST /api/habits creates new habit with valid input
def test_post_habit_valid(): def test_post_habit_valid():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
habit_data = { habit_data = {
@@ -133,7 +134,7 @@ def test_post_habit_valid():
} }
} }
status, data = http_post('/api/habits', habit_data) status, data = http_post('/api/habits', habit_data, port)
assert status == 201, f"Expected 201, got {status}" assert status == 201, f"Expected 201, got {status}"
assert 'id' in data, "Response should include habit id" assert 'id' in data, "Response should include habit id"
assert data['name'] == 'Morning Exercise', f"Name mismatch: {data['name']}" assert data['name'] == 'Morning Exercise', f"Name mismatch: {data['name']}"
@@ -149,10 +150,10 @@ def test_post_habit_valid():
# Test 3: POST validates name is required # Test 3: POST validates name is required
def test_post_habit_missing_name(): def test_post_habit_missing_name():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
status, data = http_post('/api/habits', {}) status, data = http_post('/api/habits', {}, port)
assert status == 400, f"Expected 400, got {status}" assert status == 400, f"Expected 400, got {status}"
assert 'error' in data, "Response should include error" assert 'error' in data, "Response should include error"
assert 'name' in data['error'].lower(), f"Error should mention name: {data['error']}" assert 'name' in data['error'].lower(), f"Error should mention name: {data['error']}"
@@ -164,10 +165,10 @@ def test_post_habit_missing_name():
# Test 4: POST validates name max 100 chars # Test 4: POST validates name max 100 chars
def test_post_habit_name_too_long(): def test_post_habit_name_too_long():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
status, data = http_post('/api/habits', {'name': 'x' * 101}) status, data = http_post('/api/habits', {'name': 'x' * 101}, port)
assert status == 400, f"Expected 400, got {status}" assert status == 400, f"Expected 400, got {status}"
assert 'error' in data, "Response should include error" assert 'error' in data, "Response should include error"
assert '100' in data['error'], f"Error should mention max length: {data['error']}" assert '100' in data['error'], f"Error should mention max length: {data['error']}"
@@ -179,13 +180,13 @@ def test_post_habit_name_too_long():
# Test 5: POST validates color hex format # Test 5: POST validates color hex format
def test_post_habit_invalid_color(): def test_post_habit_invalid_color():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
status, data = http_post('/api/habits', { status, data = http_post('/api/habits', {
'name': 'Test', 'name': 'Test',
'color': 'not-a-hex-color' 'color': 'not-a-hex-color'
}) }, port)
assert status == 400, f"Expected 400, got {status}" assert status == 400, f"Expected 400, got {status}"
assert 'error' in data, "Response should include error" assert 'error' in data, "Response should include error"
assert 'color' in data['error'].lower(), f"Error should mention color: {data['error']}" assert 'color' in data['error'].lower(), f"Error should mention color: {data['error']}"
@@ -197,13 +198,13 @@ def test_post_habit_invalid_color():
# Test 6: POST validates frequency type # Test 6: POST validates frequency type
def test_post_habit_invalid_frequency(): def test_post_habit_invalid_frequency():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
status, data = http_post('/api/habits', { status, data = http_post('/api/habits', {
'name': 'Test', 'name': 'Test',
'frequency': {'type': 'invalid_type'} 'frequency': {'type': 'invalid_type'}
}) }, port)
assert status == 400, f"Expected 400, got {status}" assert status == 400, f"Expected 400, got {status}"
assert 'error' in data, "Response should include error" assert 'error' in data, "Response should include error"
assert 'frequency' in data['error'].lower(), f"Error should mention frequency: {data['error']}" assert 'frequency' in data['error'].lower(), f"Error should mention frequency: {data['error']}"
@@ -215,15 +216,15 @@ def test_post_habit_invalid_frequency():
# Test 7: GET /api/habits returns habits with stats enriched # Test 7: GET /api/habits returns habits with stats enriched
def test_get_habits_with_stats(): def test_get_habits_with_stats():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit first # Create a habit first
habit_data = {'name': 'Daily Reading', 'frequency': {'type': 'daily'}} habit_data = {'name': 'Daily Reading', 'frequency': {'type': 'daily'}}
http_post('/api/habits', habit_data) http_post('/api/habits', habit_data, port)
# Get habits # Get habits
status, data = http_get('/api/habits') status, data = http_get('/api/habits', port)
assert status == 200, f"Expected 200, got {status}" assert status == 200, f"Expected 200, got {status}"
assert len(data) == 1, f"Expected 1 habit, got {len(data)}" assert len(data) == 1, f"Expected 1 habit, got {len(data)}"
@@ -240,16 +241,16 @@ def test_get_habits_with_stats():
# Test 8: GET /api/habits sorts by priority ascending # Test 8: GET /api/habits sorts by priority ascending
def test_get_habits_sorted_by_priority(): def test_get_habits_sorted_by_priority():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create habits with different priorities # Create habits with different priorities
http_post('/api/habits', {'name': 'Low Priority', 'priority': 10}) http_post('/api/habits', {'name': 'Low Priority', 'priority': 10}, port)
http_post('/api/habits', {'name': 'High Priority', 'priority': 1}) http_post('/api/habits', {'name': 'High Priority', 'priority': 1}, port)
http_post('/api/habits', {'name': 'Medium Priority', 'priority': 5}) http_post('/api/habits', {'name': 'Medium Priority', 'priority': 5}, port)
# Get habits # Get habits
status, data = http_get('/api/habits') status, data = http_get('/api/habits', port)
assert status == 200, f"Expected 200, got {status}" assert status == 200, f"Expected 200, got {status}"
assert len(data) == 3, f"Expected 3 habits, got {len(data)}" assert len(data) == 3, f"Expected 3 habits, got {len(data)}"
@@ -265,10 +266,10 @@ def test_get_habits_sorted_by_priority():
# Test 9: POST returns 400 for invalid JSON # Test 9: POST returns 400 for invalid JSON
def test_post_habit_invalid_json(): def test_post_habit_invalid_json():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
url = f'http://localhost:8765/api/habits' url = f'http://localhost:{port}/api/habits'
req = urllib.request.Request( req = urllib.request.Request(
url, url,
data=b'invalid json{', data=b'invalid json{',
@@ -287,10 +288,10 @@ def test_post_habit_invalid_json():
# Test 10: POST initializes streak.current=0 # Test 10: POST initializes streak.current=0
def test_post_habit_initial_streak(): def test_post_habit_initial_streak():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
status, data = http_post('/api/habits', {'name': 'Test Habit'}) status, data = http_post('/api/habits', {'name': 'Test Habit'}, port)
assert status == 201, f"Expected 201, got {status}" assert status == 201, f"Expected 201, got {status}"
assert data['streak']['current'] == 0, "Initial streak.current should be 0" assert data['streak']['current'] == 0, "Initial streak.current should be 0"
assert data['streak']['best'] == 0, "Initial streak.best should be 0" assert data['streak']['best'] == 0, "Initial streak.best should be 0"
@@ -303,7 +304,7 @@ def test_post_habit_initial_streak():
# Test 12: PUT /api/habits/{id} updates habit successfully # Test 12: PUT /api/habits/{id} updates habit successfully
def test_put_habit_valid(): def test_put_habit_valid():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit first # Create a habit first
@@ -313,7 +314,7 @@ def test_put_habit_valid():
'color': '#10b981', 'color': '#10b981',
'priority': 3 'priority': 3
} }
status, created_habit = http_post('/api/habits', habit_data) status, created_habit = http_post('/api/habits', habit_data, port)
habit_id = created_habit['id'] habit_id = created_habit['id']
# Update the habit # Update the habit
@@ -324,7 +325,7 @@ def test_put_habit_valid():
'priority': 1, 'priority': 1,
'notes': 'New notes' 'notes': 'New notes'
} }
status, updated_habit = http_put(f'/api/habits/{habit_id}', update_data) status, updated_habit = http_put(f'/api/habits/{habit_id}', update_data, port)
assert status == 200, f"Expected 200, got {status}" assert status == 200, f"Expected 200, got {status}"
assert updated_habit['name'] == 'Updated Name', "Name not updated" assert updated_habit['name'] == 'Updated Name', "Name not updated"
@@ -341,12 +342,12 @@ def test_put_habit_valid():
# Test 13: PUT /api/habits/{id} does not allow editing protected fields # Test 13: PUT /api/habits/{id} does not allow editing protected fields
def test_put_habit_protected_fields(): def test_put_habit_protected_fields():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit first # Create a habit first
habit_data = {'name': 'Test Habit'} habit_data = {'name': 'Test Habit'}
status, created_habit = http_post('/api/habits', habit_data) status, created_habit = http_post('/api/habits', habit_data, port)
habit_id = created_habit['id'] habit_id = created_habit['id']
original_created_at = created_habit['createdAt'] original_created_at = created_habit['createdAt']
@@ -359,7 +360,7 @@ def test_put_habit_protected_fields():
'lives': 10, 'lives': 10,
'completions': [{'date': '2025-01-01'}] 'completions': [{'date': '2025-01-01'}]
} }
status, updated_habit = http_put(f'/api/habits/{habit_id}', update_data) status, updated_habit = http_put(f'/api/habits/{habit_id}', update_data, port)
assert status == 200, f"Expected 200, got {status}" assert status == 200, f"Expected 200, got {status}"
assert updated_habit['name'] == 'Updated Name', "Name should be updated" assert updated_habit['name'] == 'Updated Name', "Name should be updated"
@@ -376,11 +377,11 @@ def test_put_habit_protected_fields():
# Test 14: PUT /api/habits/{id} returns 404 for non-existent habit # Test 14: PUT /api/habits/{id} returns 404 for non-existent habit
def test_put_habit_not_found(): def test_put_habit_not_found():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
update_data = {'name': 'Updated Name'} update_data = {'name': 'Updated Name'}
status, response = http_put('/api/habits/non-existent-id', update_data) status, response = http_put('/api/habits/non-existent-id', update_data, port)
assert status == 404, f"Expected 404, got {status}" assert status == 404, f"Expected 404, got {status}"
assert 'error' in response, "Expected error message" assert 'error' in response, "Expected error message"
@@ -392,27 +393,27 @@ def test_put_habit_not_found():
# Test 15: PUT /api/habits/{id} validates input # Test 15: PUT /api/habits/{id} validates input
def test_put_habit_invalid_input(): def test_put_habit_invalid_input():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit first # Create a habit first
habit_data = {'name': 'Test Habit'} habit_data = {'name': 'Test Habit'}
status, created_habit = http_post('/api/habits', habit_data) status, created_habit = http_post('/api/habits', habit_data, port)
habit_id = created_habit['id'] habit_id = created_habit['id']
# Test invalid color # Test invalid color
update_data = {'color': 'not-a-hex-color'} update_data = {'color': 'not-a-hex-color'}
status, response = http_put(f'/api/habits/{habit_id}', update_data) status, response = http_put(f'/api/habits/{habit_id}', update_data, port)
assert status == 400, f"Expected 400 for invalid color, got {status}" assert status == 400, f"Expected 400 for invalid color, got {status}"
# Test empty name # Test empty name
update_data = {'name': ''} update_data = {'name': ''}
status, response = http_put(f'/api/habits/{habit_id}', update_data) status, response = http_put(f'/api/habits/{habit_id}', update_data, port)
assert status == 400, f"Expected 400 for empty name, got {status}" assert status == 400, f"Expected 400 for empty name, got {status}"
# Test name too long # Test name too long
update_data = {'name': 'x' * 101} update_data = {'name': 'x' * 101}
status, response = http_put(f'/api/habits/{habit_id}', update_data) status, response = http_put(f'/api/habits/{habit_id}', update_data, port)
assert status == 400, f"Expected 400 for long name, got {status}" assert status == 400, f"Expected 400 for long name, got {status}"
print("✓ Test 15: PUT /api/habits/{id} validates input") print("✓ Test 15: PUT /api/habits/{id} validates input")
@@ -423,24 +424,24 @@ def test_put_habit_invalid_input():
# Test 16: DELETE /api/habits/{id} removes habit successfully # Test 16: DELETE /api/habits/{id} removes habit successfully
def test_delete_habit_success(): def test_delete_habit_success():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit first # Create a habit first
habit_data = {'name': 'Habit to Delete'} habit_data = {'name': 'Habit to Delete'}
status, created_habit = http_post('/api/habits', habit_data) status, created_habit = http_post('/api/habits', habit_data, port)
habit_id = created_habit['id'] habit_id = created_habit['id']
# Verify habit exists # Verify habit exists
status, habits = http_get('/api/habits') status, habits = http_get('/api/habits', port)
assert len(habits) == 1, "Should have 1 habit" assert len(habits) == 1, "Should have 1 habit"
# Delete the habit # Delete the habit
status, _ = http_delete(f'/api/habits/{habit_id}') status, _ = http_delete(f'/api/habits/{habit_id}', port)
assert status == 204, f"Expected 204, got {status}" assert status == 204, f"Expected 204, got {status}"
# Verify habit is deleted # Verify habit is deleted
status, habits = http_get('/api/habits') status, habits = http_get('/api/habits', port)
assert len(habits) == 0, "Should have 0 habits after deletion" assert len(habits) == 0, "Should have 0 habits after deletion"
print("✓ Test 16: DELETE /api/habits/{id} removes habit successfully") print("✓ Test 16: DELETE /api/habits/{id} removes habit successfully")
finally: finally:
@@ -450,10 +451,10 @@ def test_delete_habit_success():
# Test 17: DELETE /api/habits/{id} returns 404 for non-existent habit # Test 17: DELETE /api/habits/{id} returns 404 for non-existent habit
def test_delete_habit_not_found(): def test_delete_habit_not_found():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
status, response = http_delete('/api/habits/non-existent-id') status, response = http_delete('/api/habits/non-existent-id', port)
assert status == 404, f"Expected 404, got {status}" assert status == 404, f"Expected 404, got {status}"
assert 'error' in response, "Expected error message" assert 'error' in response, "Expected error message"
@@ -465,11 +466,11 @@ def test_delete_habit_not_found():
# Test 18: do_OPTIONS includes PUT and DELETE methods # Test 18: do_OPTIONS includes PUT and DELETE methods
def test_options_includes_put_delete(): def test_options_includes_put_delete():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Make OPTIONS request # Make OPTIONS request
url = 'http://localhost:8765/api/habits' url = f'http://localhost:{port}/api/habits'
req = urllib.request.Request(url, method='OPTIONS') req = urllib.request.Request(url, method='OPTIONS')
with urllib.request.urlopen(req) as response: with urllib.request.urlopen(req) as response:
allowed_methods = response.headers.get('Access-Control-Allow-Methods', '') allowed_methods = response.headers.get('Access-Control-Allow-Methods', '')
@@ -483,19 +484,19 @@ def test_options_includes_put_delete():
# Test 20: POST /api/habits/{id}/check adds completion entry # Test 20: POST /api/habits/{id}/check adds completion entry
def test_check_in_basic(): def test_check_in_basic():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a daily habit # Create a daily habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Morning Exercise', 'name': 'Morning Exercise',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
assert status == 201, f"Failed to create habit: {status}" assert status == 201, f"Failed to create habit: {status}"
habit_id = habit['id'] habit_id = habit['id']
# Check in on the habit # Check in on the habit
status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {}) status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {}, port)
assert status == 200, f"Expected 200, got {status}" assert status == 200, f"Expected 200, got {status}"
assert len(updated_habit['completions']) == 1, "Expected 1 completion" assert len(updated_habit['completions']) == 1, "Expected 1 completion"
@@ -509,14 +510,14 @@ def test_check_in_basic():
# Test 21: Check-in accepts optional note, rating, mood # Test 21: Check-in accepts optional note, rating, mood
def test_check_in_with_details(): def test_check_in_with_details():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a daily habit # Create a daily habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Meditation', 'name': 'Meditation',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
habit_id = habit['id'] habit_id = habit['id']
# Check in with details # Check in with details
@@ -524,7 +525,7 @@ def test_check_in_with_details():
'note': 'Felt very relaxed today', 'note': 'Felt very relaxed today',
'rating': 5, 'rating': 5,
'mood': 'happy' 'mood': 'happy'
}) }, port)
assert status == 200, f"Expected 200, got {status}" assert status == 200, f"Expected 200, got {status}"
completion = updated_habit['completions'][0] completion = updated_habit['completions'][0]
@@ -539,10 +540,10 @@ def test_check_in_with_details():
# Test 22: Check-in returns 404 if habit not found # Test 22: Check-in returns 404 if habit not found
def test_check_in_not_found(): def test_check_in_not_found():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
status, response = http_post('/api/habits/non-existent-id/check', {}) status, response = http_post('/api/habits/non-existent-id/check', {}, port)
assert status == 404, f"Expected 404, got {status}" assert status == 404, f"Expected 404, got {status}"
assert 'error' in response assert 'error' in response
@@ -554,7 +555,7 @@ def test_check_in_not_found():
# Test 23: Check-in returns 400 if habit not relevant for today # Test 23: Check-in returns 400 if habit not relevant for today
def test_check_in_not_relevant(): def test_check_in_not_relevant():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit for specific days (e.g., Monday only) # Create a habit for specific days (e.g., Monday only)
@@ -568,11 +569,11 @@ def test_check_in_not_relevant():
'type': 'specific_days', 'type': 'specific_days',
'days': [different_day] 'days': [different_day]
} }
}) }, port)
habit_id = habit['id'] habit_id = habit['id']
# Try to check in # Try to check in
status, response = http_post(f'/api/habits/{habit_id}/check', {}) status, response = http_post(f'/api/habits/{habit_id}/check', {}, port)
assert status == 400, f"Expected 400, got {status}" assert status == 400, f"Expected 400, got {status}"
assert 'not relevant' in response.get('error', '').lower() assert 'not relevant' in response.get('error', '').lower()
@@ -584,22 +585,22 @@ def test_check_in_not_relevant():
# Test 24: Check-in returns 409 if already checked today # Test 24: Check-in returns 409 if already checked today
def test_check_in_already_checked(): def test_check_in_already_checked():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a daily habit # Create a daily habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Water Plants', 'name': 'Water Plants',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
habit_id = habit['id'] habit_id = habit['id']
# Check in once # Check in once
status, _ = http_post(f'/api/habits/{habit_id}/check', {}) status, _ = http_post(f'/api/habits/{habit_id}/check', {}, port)
assert status == 200, "First check-in should succeed" assert status == 200, "First check-in should succeed"
# Try to check in again # Try to check in again
status, response = http_post(f'/api/habits/{habit_id}/check', {}) status, response = http_post(f'/api/habits/{habit_id}/check', {}, port)
assert status == 409, f"Expected 409, got {status}" assert status == 409, f"Expected 409, got {status}"
assert 'already checked' in response.get('error', '').lower() assert 'already checked' in response.get('error', '').lower()
@@ -611,18 +612,18 @@ def test_check_in_already_checked():
# Test 25: Streak is recalculated after check-in # Test 25: Streak is recalculated after check-in
def test_check_in_updates_streak(): def test_check_in_updates_streak():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a daily habit # Create a daily habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Read', 'name': 'Read',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
habit_id = habit['id'] habit_id = habit['id']
# Check in # Check in
status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {}) status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {}, port)
assert status == 200, f"Expected 200, got {status}" assert status == 200, f"Expected 200, got {status}"
assert updated_habit['streak']['current'] == 1, f"Expected streak 1, got {updated_habit['streak']['current']}" assert updated_habit['streak']['current'] == 1, f"Expected streak 1, got {updated_habit['streak']['current']}"
@@ -635,21 +636,21 @@ def test_check_in_updates_streak():
# Test 26: lastCheckIn is updated after check-in # Test 26: lastCheckIn is updated after check-in
def test_check_in_updates_last_check_in(): def test_check_in_updates_last_check_in():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a daily habit # Create a daily habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Floss', 'name': 'Floss',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
habit_id = habit['id'] habit_id = habit['id']
# Initially lastCheckIn should be None # Initially lastCheckIn should be None
assert habit['streak']['lastCheckIn'] is None assert habit['streak']['lastCheckIn'] is None
# Check in # Check in
status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {}) status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {}, port)
today = datetime.now().date().isoformat() today = datetime.now().date().isoformat()
assert updated_habit['streak']['lastCheckIn'] == today assert updated_habit['streak']['lastCheckIn'] == today
@@ -661,14 +662,14 @@ def test_check_in_updates_last_check_in():
# Test 27: Lives are restored after 7 consecutive check-ins # Test 27: Lives are restored after 7 consecutive check-ins
def test_check_in_life_restore(): def test_check_in_life_restore():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a daily habit and manually set up 6 previous check-ins # Create a daily habit and manually set up 6 previous check-ins
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Yoga', 'name': 'Yoga',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
habit_id = habit['id'] habit_id = habit['id']
# Manually add 6 previous check-ins and reduce lives to 2 # Manually add 6 previous check-ins and reduce lives to 2
@@ -687,7 +688,7 @@ def test_check_in_life_restore():
api.HABITS_FILE.write_text(json.dumps(habits_data, indent=2)) api.HABITS_FILE.write_text(json.dumps(habits_data, indent=2))
# Check in for today (7th consecutive) # Check in for today (7th consecutive)
status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {}) status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {}, port)
assert status == 200, f"Expected 200, got {status}" assert status == 200, f"Expected 200, got {status}"
assert updated_habit['lives'] == 3, f"Expected 3 lives restored, got {updated_habit['lives']}" assert updated_habit['lives'] == 3, f"Expected 3 lives restored, got {updated_habit['lives']}"
@@ -699,20 +700,20 @@ def test_check_in_life_restore():
# Test 28: Check-in validates rating range # Test 28: Check-in validates rating range
def test_check_in_invalid_rating(): def test_check_in_invalid_rating():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a daily habit # Create a daily habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Journal', 'name': 'Journal',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
habit_id = habit['id'] habit_id = habit['id']
# Try to check in with invalid rating # Try to check in with invalid rating
status, response = http_post(f'/api/habits/{habit_id}/check', { status, response = http_post(f'/api/habits/{habit_id}/check', {
'rating': 10 # Invalid, should be 1-5 'rating': 10 # Invalid, should be 1-5
}) }, port)
assert status == 400, f"Expected 400, got {status}" assert status == 400, f"Expected 400, got {status}"
assert 'rating' in response.get('error', '').lower() assert 'rating' in response.get('error', '').lower()
@@ -724,20 +725,20 @@ def test_check_in_invalid_rating():
# Test 29: Check-in validates mood values # Test 29: Check-in validates mood values
def test_check_in_invalid_mood(): def test_check_in_invalid_mood():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a daily habit # Create a daily habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Gratitude', 'name': 'Gratitude',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
habit_id = habit['id'] habit_id = habit['id']
# Try to check in with invalid mood # Try to check in with invalid mood
status, response = http_post(f'/api/habits/{habit_id}/check', { status, response = http_post(f'/api/habits/{habit_id}/check', {
'mood': 'excited' # Invalid, should be happy/neutral/sad 'mood': 'excited' # Invalid, should be happy/neutral/sad
}) }, port)
assert status == 400, f"Expected 400, got {status}" assert status == 400, f"Expected 400, got {status}"
assert 'mood' in response.get('error', '').lower() assert 'mood' in response.get('error', '').lower()
@@ -749,19 +750,19 @@ def test_check_in_invalid_mood():
# Test 30: Skip basic - decrements lives # Test 30: Skip basic - decrements lives
def test_skip_basic(): def test_skip_basic():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit # Create a habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Daily Exercise', 'name': 'Daily Exercise',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
assert status == 201 assert status == 201
habit_id = habit['id'] habit_id = habit['id']
# Skip a day # Skip a day
status, response = http_post(f'/api/habits/{habit_id}/skip', {}) status, response = http_post(f'/api/habits/{habit_id}/skip', {}, port)
assert status == 200, f"Expected 200, got {status}" assert status == 200, f"Expected 200, got {status}"
assert response['lives'] == 2, f"Expected 2 lives, got {response['lives']}" assert response['lives'] == 2, f"Expected 2 lives, got {response['lives']}"
@@ -780,28 +781,28 @@ def test_skip_basic():
# Test 31: Skip preserves streak # Test 31: Skip preserves streak
def test_skip_preserves_streak(): def test_skip_preserves_streak():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit # Create a habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Daily Exercise', 'name': 'Daily Exercise',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
assert status == 201 assert status == 201
habit_id = habit['id'] habit_id = habit['id']
# Check in to build a streak # Check in to build a streak
http_post(f'/api/habits/{habit_id}/check', {}) http_post(f'/api/habits/{habit_id}/check', {}, port)
# Get current streak # Get current streak
status, habits = http_get('/api/habits') status, habits = http_get('/api/habits', port)
current_streak = habits[0]['current_streak'] current_streak = habits[0]['current_streak']
assert current_streak > 0 assert current_streak > 0
# Skip the next day (simulate by adding skip manually and checking streak doesn't break) # Skip the next day (simulate by adding skip manually and checking streak doesn't break)
# Since we can't time travel, we'll verify that skip doesn't recalculate streak # Since we can't time travel, we'll verify that skip doesn't recalculate streak
status, response = http_post(f'/api/habits/{habit_id}/skip', {}) status, response = http_post(f'/api/habits/{habit_id}/skip', {}, port)
assert status == 200, f"Expected 200, got {status}" assert status == 200, f"Expected 200, got {status}"
# Verify lives decremented # Verify lives decremented
@@ -821,10 +822,10 @@ def test_skip_preserves_streak():
# Test 32: Skip returns 404 for non-existent habit # Test 32: Skip returns 404 for non-existent habit
def test_skip_not_found(): def test_skip_not_found():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
status, response = http_post('/api/habits/nonexistent-id/skip', {}) status, response = http_post('/api/habits/nonexistent-id/skip', {}, port)
assert status == 404, f"Expected 404, got {status}" assert status == 404, f"Expected 404, got {status}"
assert 'not found' in response.get('error', '').lower() assert 'not found' in response.get('error', '').lower()
@@ -837,25 +838,25 @@ def test_skip_not_found():
# Test 33: Skip returns 400 when no lives remaining # Test 33: Skip returns 400 when no lives remaining
def test_skip_no_lives(): def test_skip_no_lives():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit # Create a habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Daily Exercise', 'name': 'Daily Exercise',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
assert status == 201 assert status == 201
habit_id = habit['id'] habit_id = habit['id']
# Use all 3 lives # Use all 3 lives
for i in range(3): for i in range(3):
status, response = http_post(f'/api/habits/{habit_id}/skip', {}) status, response = http_post(f'/api/habits/{habit_id}/skip', {}, port)
assert status == 200, f"Skip {i+1} failed with status {status}" assert status == 200, f"Skip {i+1} failed with status {status}"
assert response['lives'] == 2 - i, f"Expected {2-i} lives, got {response['lives']}" assert response['lives'] == 2 - i, f"Expected {2-i} lives, got {response['lives']}"
# Try to skip again with no lives # Try to skip again with no lives
status, response = http_post(f'/api/habits/{habit_id}/skip', {}) status, response = http_post(f'/api/habits/{habit_id}/skip', {}, port)
assert status == 400, f"Expected 400, got {status}" assert status == 400, f"Expected 400, got {status}"
assert 'no lives remaining' in response.get('error', '').lower() assert 'no lives remaining' in response.get('error', '').lower()
@@ -868,20 +869,20 @@ def test_skip_no_lives():
# Test 34: Skip returns updated habit with new lives count # Test 34: Skip returns updated habit with new lives count
def test_skip_returns_updated_habit(): def test_skip_returns_updated_habit():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit # Create a habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Daily Exercise', 'name': 'Daily Exercise',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
assert status == 201 assert status == 201
habit_id = habit['id'] habit_id = habit['id']
original_updated_at = habit['updatedAt'] original_updated_at = habit['updatedAt']
# Skip a day # Skip a day
status, response = http_post(f'/api/habits/{habit_id}/skip', {}) status, response = http_post(f'/api/habits/{habit_id}/skip', {}, port)
assert status == 200 assert status == 200
assert response['id'] == habit_id assert response['id'] == habit_id
@@ -899,26 +900,26 @@ def test_skip_returns_updated_habit():
# Test 35: DELETE uncheck - removes completion for specified date # Test 35: DELETE uncheck - removes completion for specified date
def test_uncheck_removes_completion(): def test_uncheck_removes_completion():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit # Create a habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Daily Exercise', 'name': 'Daily Exercise',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
assert status == 201 assert status == 201
habit_id = habit['id'] habit_id = habit['id']
# Check in on a specific date # Check in on a specific date
today = datetime.now().date().isoformat() today = datetime.now().date().isoformat()
status, response = http_post(f'/api/habits/{habit_id}/check', {}) status, response = http_post(f'/api/habits/{habit_id}/check', {}, port)
assert status == 200 assert status == 200
assert len(response['completions']) == 1 assert len(response['completions']) == 1
assert response['completions'][0]['date'] == today assert response['completions'][0]['date'] == today
# Uncheck the habit for today # Uncheck the habit for today
status, response = http_delete(f'/api/habits/{habit_id}/check?date={today}') status, response = http_delete(f'/api/habits/{habit_id}/check?date={today}', port)
assert status == 200 assert status == 200
assert len(response['completions']) == 0, "Completion should be removed" assert len(response['completions']) == 0, "Completion should be removed"
assert response['id'] == habit_id assert response['id'] == habit_id
@@ -931,20 +932,20 @@ def test_uncheck_removes_completion():
# Test 36: DELETE uncheck - returns 404 if no completion for date # Test 36: DELETE uncheck - returns 404 if no completion for date
def test_uncheck_no_completion_for_date(): def test_uncheck_no_completion_for_date():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit (but don't check in) # Create a habit (but don't check in)
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Daily Exercise', 'name': 'Daily Exercise',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
assert status == 201 assert status == 201
habit_id = habit['id'] habit_id = habit['id']
# Try to uncheck a date with no completion # Try to uncheck a date with no completion
today = datetime.now().date().isoformat() today = datetime.now().date().isoformat()
status, response = http_delete(f'/api/habits/{habit_id}/check?date={today}') status, response = http_delete(f'/api/habits/{habit_id}/check?date={today}', port)
assert status == 404 assert status == 404
assert 'error' in response assert 'error' in response
assert 'No completion found' in response['error'] assert 'No completion found' in response['error']
@@ -957,11 +958,11 @@ def test_uncheck_no_completion_for_date():
# Test 37: DELETE uncheck - returns 404 if habit not found # Test 37: DELETE uncheck - returns 404 if habit not found
def test_uncheck_habit_not_found(): def test_uncheck_habit_not_found():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
today = datetime.now().date().isoformat() today = datetime.now().date().isoformat()
status, response = http_delete(f'/api/habits/nonexistent-id/check?date={today}') status, response = http_delete(f'/api/habits/nonexistent-id/check?date={today}', port)
assert status == 404 assert status == 404
assert 'error' in response assert 'error' in response
assert 'Habit not found' in response['error'] assert 'Habit not found' in response['error']
@@ -974,14 +975,14 @@ def test_uncheck_habit_not_found():
# Test 38: DELETE uncheck - recalculates streak correctly # Test 38: DELETE uncheck - recalculates streak correctly
def test_uncheck_recalculates_streak(): def test_uncheck_recalculates_streak():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit # Create a habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Daily Exercise', 'name': 'Daily Exercise',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
assert status == 201 assert status == 201
habit_id = habit['id'] habit_id = habit['id']
@@ -999,14 +1000,14 @@ def test_uncheck_recalculates_streak():
json.dump(data, f) json.dump(data, f)
# Get habit to verify streak is 3 # Get habit to verify streak is 3
status, habit = http_get('/api/habits') status, habit = http_get('/api/habits', port)
assert status == 200 assert status == 200
habit = [h for h in habit if h['id'] == habit_id][0] habit = [h for h in habit if h['id'] == habit_id][0]
assert habit['current_streak'] == 3 assert habit['current_streak'] == 3
# Uncheck the middle day # Uncheck the middle day
middle_date = (today - timedelta(days=1)).isoformat() middle_date = (today - timedelta(days=1)).isoformat()
status, response = http_delete(f'/api/habits/{habit_id}/check?date={middle_date}') status, response = http_delete(f'/api/habits/{habit_id}/check?date={middle_date}', port)
assert status == 200 assert status == 200
# Streak should now be 1 (only today counts) # Streak should now be 1 (only today counts)
@@ -1020,21 +1021,21 @@ def test_uncheck_recalculates_streak():
# Test 39: DELETE uncheck - returns updated habit object # Test 39: DELETE uncheck - returns updated habit object
def test_uncheck_returns_updated_habit(): def test_uncheck_returns_updated_habit():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create and check in # Create and check in
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Daily Exercise', 'name': 'Daily Exercise',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
habit_id = habit['id'] habit_id = habit['id']
today = datetime.now().date().isoformat() today = datetime.now().date().isoformat()
status, _ = http_post(f'/api/habits/{habit_id}/check', {}) status, _ = http_post(f'/api/habits/{habit_id}/check', {}, port)
# Uncheck and verify response structure # Uncheck and verify response structure
status, response = http_delete(f'/api/habits/{habit_id}/check?date={today}') status, response = http_delete(f'/api/habits/{habit_id}/check?date={today}', port)
assert status == 200 assert status == 200
assert 'id' in response assert 'id' in response
assert 'name' in response assert 'name' in response
@@ -1050,18 +1051,18 @@ def test_uncheck_returns_updated_habit():
# Test 40: DELETE uncheck - requires date parameter # Test 40: DELETE uncheck - requires date parameter
def test_uncheck_requires_date(): def test_uncheck_requires_date():
temp_dir = setup_test_env() temp_dir = setup_test_env()
server = start_test_server() server, port = start_test_server()
try: try:
# Create a habit # Create a habit
status, habit = http_post('/api/habits', { status, habit = http_post('/api/habits', {
'name': 'Daily Exercise', 'name': 'Daily Exercise',
'frequency': {'type': 'daily'} 'frequency': {'type': 'daily'}
}) }, port)
habit_id = habit['id'] habit_id = habit['id']
# Try to uncheck without date parameter # Try to uncheck without date parameter
status, response = http_delete(f'/api/habits/{habit_id}/check') status, response = http_delete(f'/api/habits/{habit_id}/check', port)
assert status == 400 assert status == 400
assert 'error' in response assert 'error' in response
assert 'date parameter is required' in response['error'] assert 'date parameter is required' in response['error']