280 lines
9.5 KiB
Python
280 lines
9.5 KiB
Python
#!/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()
|