feat: US-004 - Backend API - Check-in endpoint with streak logic

This commit is contained in:
Echo
2026-02-10 16:06:34 +00:00
parent 648185abe6
commit 71bcc5f6f6
2 changed files with 398 additions and 2 deletions

View File

@@ -475,7 +475,273 @@ def test_options_includes_put_delete():
server.shutdown()
cleanup_test_env(temp_dir)
# Test 19: Typecheck passes
# Test 20: POST /api/habits/{id}/check adds completion entry
def test_check_in_basic():
temp_dir = setup_test_env()
server = start_test_server()
try:
# Create a daily habit
status, habit = http_post('/api/habits', {
'name': 'Morning Exercise',
'frequency': {'type': 'daily'}
})
assert status == 201, f"Failed to create habit: {status}"
habit_id = habit['id']
# Check in on the habit
status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {})
assert status == 200, f"Expected 200, got {status}"
assert len(updated_habit['completions']) == 1, "Expected 1 completion"
assert updated_habit['completions'][0]['date'] == datetime.now().date().isoformat()
assert updated_habit['completions'][0]['type'] == 'check'
print("✓ Test 20: POST /api/habits/{id}/check adds completion entry")
finally:
server.shutdown()
cleanup_test_env(temp_dir)
# Test 21: Check-in accepts optional note, rating, mood
def test_check_in_with_details():
temp_dir = setup_test_env()
server = start_test_server()
try:
# Create a daily habit
status, habit = http_post('/api/habits', {
'name': 'Meditation',
'frequency': {'type': 'daily'}
})
habit_id = habit['id']
# Check in with details
status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {
'note': 'Felt very relaxed today',
'rating': 5,
'mood': 'happy'
})
assert status == 200, f"Expected 200, got {status}"
completion = updated_habit['completions'][0]
assert completion['note'] == 'Felt very relaxed today'
assert completion['rating'] == 5
assert completion['mood'] == 'happy'
print("✓ Test 21: Check-in accepts optional note, rating (1-5), and mood")
finally:
server.shutdown()
cleanup_test_env(temp_dir)
# Test 22: Check-in returns 404 if habit not found
def test_check_in_not_found():
temp_dir = setup_test_env()
server = start_test_server()
try:
status, response = http_post('/api/habits/non-existent-id/check', {})
assert status == 404, f"Expected 404, got {status}"
assert 'error' in response
print("✓ Test 22: Check-in returns 404 if habit not found")
finally:
server.shutdown()
cleanup_test_env(temp_dir)
# Test 23: Check-in returns 400 if habit not relevant for today
def test_check_in_not_relevant():
temp_dir = setup_test_env()
server = start_test_server()
try:
# Create a habit for specific days (e.g., Monday only)
# If today is not Monday, it should fail
today_weekday = datetime.now().date().weekday()
different_day = (today_weekday + 1) % 7 # Pick a different day
status, habit = http_post('/api/habits', {
'name': 'Monday Only Habit',
'frequency': {
'type': 'specific_days',
'days': [different_day]
}
})
habit_id = habit['id']
# Try to check in
status, response = http_post(f'/api/habits/{habit_id}/check', {})
assert status == 400, f"Expected 400, got {status}"
assert 'not relevant' in response.get('error', '').lower()
print("✓ Test 23: Check-in returns 400 if habit not relevant for today")
finally:
server.shutdown()
cleanup_test_env(temp_dir)
# Test 24: Check-in returns 409 if already checked today
def test_check_in_already_checked():
temp_dir = setup_test_env()
server = start_test_server()
try:
# Create a daily habit
status, habit = http_post('/api/habits', {
'name': 'Water Plants',
'frequency': {'type': 'daily'}
})
habit_id = habit['id']
# Check in once
status, _ = http_post(f'/api/habits/{habit_id}/check', {})
assert status == 200, "First check-in should succeed"
# Try to check in again
status, response = http_post(f'/api/habits/{habit_id}/check', {})
assert status == 409, f"Expected 409, got {status}"
assert 'already checked' in response.get('error', '').lower()
print("✓ Test 24: Check-in returns 409 if already checked today")
finally:
server.shutdown()
cleanup_test_env(temp_dir)
# Test 25: Streak is recalculated after check-in
def test_check_in_updates_streak():
temp_dir = setup_test_env()
server = start_test_server()
try:
# Create a daily habit
status, habit = http_post('/api/habits', {
'name': 'Read',
'frequency': {'type': 'daily'}
})
habit_id = habit['id']
# Check in
status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {})
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']['best'] == 1, f"Expected best streak 1, got {updated_habit['streak']['best']}"
print("✓ Test 25: Streak current and best are recalculated after check-in")
finally:
server.shutdown()
cleanup_test_env(temp_dir)
# Test 26: lastCheckIn is updated after check-in
def test_check_in_updates_last_check_in():
temp_dir = setup_test_env()
server = start_test_server()
try:
# Create a daily habit
status, habit = http_post('/api/habits', {
'name': 'Floss',
'frequency': {'type': 'daily'}
})
habit_id = habit['id']
# Initially lastCheckIn should be None
assert habit['streak']['lastCheckIn'] is None
# Check in
status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {})
today = datetime.now().date().isoformat()
assert updated_habit['streak']['lastCheckIn'] == today
print("✓ Test 26: lastCheckIn is updated to today's date")
finally:
server.shutdown()
cleanup_test_env(temp_dir)
# Test 27: Lives are restored after 7 consecutive check-ins
def test_check_in_life_restore():
temp_dir = setup_test_env()
server = start_test_server()
try:
# Create a daily habit and manually set up 6 previous check-ins
status, habit = http_post('/api/habits', {
'name': 'Yoga',
'frequency': {'type': 'daily'}
})
habit_id = habit['id']
# Manually add 6 previous check-ins and reduce lives to 2
habits_data = json.loads(api.HABITS_FILE.read_text())
for h in habits_data['habits']:
if h['id'] == habit_id:
h['lives'] = 2
# Add 6 check-ins from previous days
for i in range(6, 0, -1):
past_date = (datetime.now().date() - timedelta(days=i)).isoformat()
h['completions'].append({
'date': past_date,
'type': 'check'
})
break
api.HABITS_FILE.write_text(json.dumps(habits_data, indent=2))
# Check in for today (7th consecutive)
status, updated_habit = http_post(f'/api/habits/{habit_id}/check', {})
assert status == 200, f"Expected 200, got {status}"
assert updated_habit['lives'] == 3, f"Expected 3 lives restored, got {updated_habit['lives']}"
print("✓ Test 27: Lives are restored by 1 (max 3) after 7 consecutive check-ins")
finally:
server.shutdown()
cleanup_test_env(temp_dir)
# Test 28: Check-in validates rating range
def test_check_in_invalid_rating():
temp_dir = setup_test_env()
server = start_test_server()
try:
# Create a daily habit
status, habit = http_post('/api/habits', {
'name': 'Journal',
'frequency': {'type': 'daily'}
})
habit_id = habit['id']
# Try to check in with invalid rating
status, response = http_post(f'/api/habits/{habit_id}/check', {
'rating': 10 # Invalid, should be 1-5
})
assert status == 400, f"Expected 400, got {status}"
assert 'rating' in response.get('error', '').lower()
print("✓ Test 28: Check-in validates rating is between 1 and 5")
finally:
server.shutdown()
cleanup_test_env(temp_dir)
# Test 29: Check-in validates mood values
def test_check_in_invalid_mood():
temp_dir = setup_test_env()
server = start_test_server()
try:
# Create a daily habit
status, habit = http_post('/api/habits', {
'name': 'Gratitude',
'frequency': {'type': 'daily'}
})
habit_id = habit['id']
# Try to check in with invalid mood
status, response = http_post(f'/api/habits/{habit_id}/check', {
'mood': 'excited' # Invalid, should be happy/neutral/sad
})
assert status == 400, f"Expected 400, got {status}"
assert 'mood' in response.get('error', '').lower()
print("✓ Test 29: Check-in validates mood is one of: happy, neutral, sad")
finally:
server.shutdown()
cleanup_test_env(temp_dir)
# Test 30: Typecheck passes
def test_typecheck():
result = subprocess.run(
['python3', '-m', 'py_compile', str(Path(__file__).parent.parent / 'api.py')],
@@ -506,6 +772,16 @@ if __name__ == '__main__':
test_delete_habit_success()
test_delete_habit_not_found()
test_options_includes_put_delete()
test_check_in_basic()
test_check_in_with_details()
test_check_in_not_found()
test_check_in_not_relevant()
test_check_in_already_checked()
test_check_in_updates_streak()
test_check_in_updates_last_check_in()
test_check_in_life_restore()
test_check_in_invalid_rating()
test_check_in_invalid_mood()
test_typecheck()
print("\n✅ All 19 tests passed!\n")
print("\n✅ All 30 tests passed!\n")