feat: US-003 - Backend API - PUT and DELETE habits
This commit is contained in:
@@ -70,6 +70,31 @@ def http_post(path, data, port=8765):
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, json.loads(e.read().decode()) if e.headers.get('Content-Type') == 'application/json' else {}
|
||||
|
||||
def http_put(path, data, port=8765):
|
||||
"""Make HTTP PUT request."""
|
||||
url = f'http://localhost:{port}{path}'
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps(data).encode(),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
method='PUT'
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
return response.status, json.loads(response.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, json.loads(e.read().decode()) if e.headers.get('Content-Type') == 'application/json' else {}
|
||||
|
||||
def http_delete(path, port=8765):
|
||||
"""Make HTTP DELETE request."""
|
||||
url = f'http://localhost:{port}{path}'
|
||||
req = urllib.request.Request(url, method='DELETE')
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
return response.status, None
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, json.loads(e.read().decode()) if e.headers.get('Content-Type') == 'application/json' else {}
|
||||
|
||||
# Test 1: GET /api/habits returns empty array when no habits
|
||||
def test_get_habits_empty():
|
||||
temp_dir = setup_test_env()
|
||||
@@ -270,7 +295,187 @@ def test_post_habit_initial_streak():
|
||||
server.shutdown()
|
||||
cleanup_test_env(temp_dir)
|
||||
|
||||
# Test 11: Typecheck passes
|
||||
# Test 12: PUT /api/habits/{id} updates habit successfully
|
||||
def test_put_habit_valid():
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
|
||||
try:
|
||||
# Create a habit first
|
||||
habit_data = {
|
||||
'name': 'Original Name',
|
||||
'category': 'health',
|
||||
'color': '#10b981',
|
||||
'priority': 3
|
||||
}
|
||||
status, created_habit = http_post('/api/habits', habit_data)
|
||||
habit_id = created_habit['id']
|
||||
|
||||
# Update the habit
|
||||
update_data = {
|
||||
'name': 'Updated Name',
|
||||
'category': 'productivity',
|
||||
'color': '#ef4444',
|
||||
'priority': 1,
|
||||
'notes': 'New notes'
|
||||
}
|
||||
status, updated_habit = http_put(f'/api/habits/{habit_id}', update_data)
|
||||
|
||||
assert status == 200, f"Expected 200, got {status}"
|
||||
assert updated_habit['name'] == 'Updated Name', "Name not updated"
|
||||
assert updated_habit['category'] == 'productivity', "Category not updated"
|
||||
assert updated_habit['color'] == '#ef4444', "Color not updated"
|
||||
assert updated_habit['priority'] == 1, "Priority not updated"
|
||||
assert updated_habit['notes'] == 'New notes', "Notes not updated"
|
||||
assert updated_habit['id'] == habit_id, "ID should not change"
|
||||
print("✓ Test 12: PUT /api/habits/{id} updates habit successfully")
|
||||
finally:
|
||||
server.shutdown()
|
||||
cleanup_test_env(temp_dir)
|
||||
|
||||
# Test 13: PUT /api/habits/{id} does not allow editing protected fields
|
||||
def test_put_habit_protected_fields():
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
|
||||
try:
|
||||
# Create a habit first
|
||||
habit_data = {'name': 'Test Habit'}
|
||||
status, created_habit = http_post('/api/habits', habit_data)
|
||||
habit_id = created_habit['id']
|
||||
original_created_at = created_habit['createdAt']
|
||||
|
||||
# Try to update protected fields
|
||||
update_data = {
|
||||
'name': 'Updated Name',
|
||||
'id': 'new-id',
|
||||
'createdAt': '2020-01-01T00:00:00',
|
||||
'streak': {'current': 100, 'best': 200},
|
||||
'lives': 10,
|
||||
'completions': [{'date': '2025-01-01'}]
|
||||
}
|
||||
status, updated_habit = http_put(f'/api/habits/{habit_id}', update_data)
|
||||
|
||||
assert status == 200, f"Expected 200, got {status}"
|
||||
assert updated_habit['name'] == 'Updated Name', "Name should be updated"
|
||||
assert updated_habit['id'] == habit_id, "ID should not change"
|
||||
assert updated_habit['createdAt'] == original_created_at, "createdAt should not change"
|
||||
assert updated_habit['streak']['current'] == 0, "streak should not change"
|
||||
assert updated_habit['lives'] == 3, "lives should not change"
|
||||
assert updated_habit['completions'] == [], "completions should not change"
|
||||
print("✓ Test 13: PUT /api/habits/{id} does not allow editing protected fields")
|
||||
finally:
|
||||
server.shutdown()
|
||||
cleanup_test_env(temp_dir)
|
||||
|
||||
# Test 14: PUT /api/habits/{id} returns 404 for non-existent habit
|
||||
def test_put_habit_not_found():
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
|
||||
try:
|
||||
update_data = {'name': 'Updated Name'}
|
||||
status, response = http_put('/api/habits/non-existent-id', update_data)
|
||||
|
||||
assert status == 404, f"Expected 404, got {status}"
|
||||
assert 'error' in response, "Expected error message"
|
||||
print("✓ Test 14: PUT /api/habits/{id} returns 404 for non-existent habit")
|
||||
finally:
|
||||
server.shutdown()
|
||||
cleanup_test_env(temp_dir)
|
||||
|
||||
# Test 15: PUT /api/habits/{id} validates input
|
||||
def test_put_habit_invalid_input():
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
|
||||
try:
|
||||
# Create a habit first
|
||||
habit_data = {'name': 'Test Habit'}
|
||||
status, created_habit = http_post('/api/habits', habit_data)
|
||||
habit_id = created_habit['id']
|
||||
|
||||
# Test invalid color
|
||||
update_data = {'color': 'not-a-hex-color'}
|
||||
status, response = http_put(f'/api/habits/{habit_id}', update_data)
|
||||
assert status == 400, f"Expected 400 for invalid color, got {status}"
|
||||
|
||||
# Test empty name
|
||||
update_data = {'name': ''}
|
||||
status, response = http_put(f'/api/habits/{habit_id}', update_data)
|
||||
assert status == 400, f"Expected 400 for empty name, got {status}"
|
||||
|
||||
# Test name too long
|
||||
update_data = {'name': 'x' * 101}
|
||||
status, response = http_put(f'/api/habits/{habit_id}', update_data)
|
||||
assert status == 400, f"Expected 400 for long name, got {status}"
|
||||
|
||||
print("✓ Test 15: PUT /api/habits/{id} validates input")
|
||||
finally:
|
||||
server.shutdown()
|
||||
cleanup_test_env(temp_dir)
|
||||
|
||||
# Test 16: DELETE /api/habits/{id} removes habit successfully
|
||||
def test_delete_habit_success():
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
|
||||
try:
|
||||
# Create a habit first
|
||||
habit_data = {'name': 'Habit to Delete'}
|
||||
status, created_habit = http_post('/api/habits', habit_data)
|
||||
habit_id = created_habit['id']
|
||||
|
||||
# Verify habit exists
|
||||
status, habits = http_get('/api/habits')
|
||||
assert len(habits) == 1, "Should have 1 habit"
|
||||
|
||||
# Delete the habit
|
||||
status, _ = http_delete(f'/api/habits/{habit_id}')
|
||||
assert status == 204, f"Expected 204, got {status}"
|
||||
|
||||
# Verify habit is deleted
|
||||
status, habits = http_get('/api/habits')
|
||||
assert len(habits) == 0, "Should have 0 habits after deletion"
|
||||
print("✓ Test 16: DELETE /api/habits/{id} removes habit successfully")
|
||||
finally:
|
||||
server.shutdown()
|
||||
cleanup_test_env(temp_dir)
|
||||
|
||||
# Test 17: DELETE /api/habits/{id} returns 404 for non-existent habit
|
||||
def test_delete_habit_not_found():
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
|
||||
try:
|
||||
status, response = http_delete('/api/habits/non-existent-id')
|
||||
|
||||
assert status == 404, f"Expected 404, got {status}"
|
||||
assert 'error' in response, "Expected error message"
|
||||
print("✓ Test 17: DELETE /api/habits/{id} returns 404 for non-existent habit")
|
||||
finally:
|
||||
server.shutdown()
|
||||
cleanup_test_env(temp_dir)
|
||||
|
||||
# Test 18: do_OPTIONS includes PUT and DELETE methods
|
||||
def test_options_includes_put_delete():
|
||||
temp_dir = setup_test_env()
|
||||
server = start_test_server()
|
||||
|
||||
try:
|
||||
# Make OPTIONS request
|
||||
url = 'http://localhost:8765/api/habits'
|
||||
req = urllib.request.Request(url, method='OPTIONS')
|
||||
with urllib.request.urlopen(req) as response:
|
||||
allowed_methods = response.headers.get('Access-Control-Allow-Methods', '')
|
||||
assert 'PUT' in allowed_methods, f"PUT not in allowed methods: {allowed_methods}"
|
||||
assert 'DELETE' in allowed_methods, f"DELETE not in allowed methods: {allowed_methods}"
|
||||
print("✓ Test 18: do_OPTIONS includes PUT and DELETE methods")
|
||||
finally:
|
||||
server.shutdown()
|
||||
cleanup_test_env(temp_dir)
|
||||
|
||||
# Test 19: Typecheck passes
|
||||
def test_typecheck():
|
||||
result = subprocess.run(
|
||||
['python3', '-m', 'py_compile', str(Path(__file__).parent.parent / 'api.py')],
|
||||
@@ -294,6 +499,13 @@ if __name__ == '__main__':
|
||||
test_get_habits_sorted_by_priority()
|
||||
test_post_habit_invalid_json()
|
||||
test_post_habit_initial_streak()
|
||||
test_put_habit_valid()
|
||||
test_put_habit_protected_fields()
|
||||
test_put_habit_not_found()
|
||||
test_put_habit_invalid_input()
|
||||
test_delete_habit_success()
|
||||
test_delete_habit_not_found()
|
||||
test_options_includes_put_delete()
|
||||
test_typecheck()
|
||||
|
||||
print("\n✅ All 11 tests passed!\n")
|
||||
print("\n✅ All 19 tests passed!\n")
|
||||
|
||||
Reference in New Issue
Block a user