diff --git a/API_README.md b/API_README.md new file mode 100644 index 0000000..8dd8301 --- /dev/null +++ b/API_README.md @@ -0,0 +1,150 @@ +# Flask Todo API Documentation + +This document describes the REST API endpoints added to the Flask Todo application. + +## API Endpoints + +### Health Check +- **GET** `/api/health` - Check the health status of the API + +**Response:** +```json +{ + "status": "healthy", + "version": "1.0.0" +} +``` + +### Todos + +#### Get All Todos +- **GET** `/api/todos` - Retrieve all todos + +**Response:** +```json +{ + "todos": [ + { + "id": 1, + "title": "Sample Todo", + "completed": false + } + ] +} +``` + +#### Create Todo +- **POST** `/api/todos` - Create a new todo + +**Request Body:** +```json +{ + "title": "New Todo", + "completed": false +} +``` + +**Response:** +```json +{ + "todo": { + "id": 1, + "title": "New Todo", + "completed": false + } +} +``` + +#### Update Todo +- **PUT** `/api/todos/:id` - Update an existing todo + +**Request Body:** +```json +{ + "title": "Updated Todo", + "completed": true +} +``` + +**Response:** +```json +{ + "todo": { + "id": 1, + "title": "Updated Todo", + "completed": true + } +} +``` + +#### Delete Todo +- **DELETE** `/api/todos/:id` - Delete a todo + +**Response:** +```json +{ + "message": "Todo deleted successfully" +} +``` + +## Error Responses + +All API endpoints return appropriate HTTP status codes: + +- **200 OK** - Successful operation +- **201 Created** - Resource created successfully +- **400 Bad Request** - Invalid request data +- **404 Not Found** - Resource not found + +Error responses include a descriptive error message: +```json +{ + "error": "Description of the error" +} +``` + +## Testing + +The API includes comprehensive test coverage: + +- **30 test cases** covering all endpoints +- **CRUD operations** testing +- **Edge cases** and error conditions +- **Integration tests** for complete workflows +- **Data validation** tests + +Run the tests with: +```bash +python3 -m pytest tests/test_api.py tests/test_integration.py -v +``` + +Or use the test runner: +```bash +./run_tests.sh +``` + +## Examples + +### Create a Todo +```bash +curl -X POST -H "Content-Type: application/json" \ + -d '{"title": "Learn API testing", "completed": false}' \ + http://localhost:5000/api/todos +``` + +### Get All Todos +```bash +curl -X GET http://localhost:5000/api/todos +``` + +### Update a Todo +```bash +curl -X PUT -H "Content-Type: application/json" \ + -d '{"title": "Learn API testing", "completed": true}' \ + http://localhost:5000/api/todos/1 +``` + +### Delete a Todo +```bash +curl -X DELETE http://localhost:5000/api/todos/1 +``` \ No newline at end of file diff --git a/FlaskApp/flask_app.py b/FlaskApp/flask_app.py index ae1d414..9b1cdaf 100644 --- a/FlaskApp/flask_app.py +++ b/FlaskApp/flask_app.py @@ -1,8 +1,12 @@ -from flask import Flask, send_from_directory +from flask import Flask, send_from_directory, jsonify, request import os app = Flask(__name__, static_folder=os.path.join(os.path.dirname(__file__), 'static')) +# In-memory storage for todos (in a real app, this would be a database) +todos = [] +todo_id_counter = 1 + @app.route("/") def index(): return send_from_directory(app.static_folder, "index.html") @@ -11,5 +15,66 @@ def index(): def static_files(filename): return send_from_directory(app.static_folder, filename) +# API endpoints for todos +@app.route("/api/todos", methods=["GET"]) +def get_todos(): + """Get all todos""" + return jsonify({"todos": todos}) + +@app.route("/api/todos", methods=["POST"]) +def create_todo(): + """Create a new todo""" + global todo_id_counter + data = request.get_json() + + if not data or not data.get('title'): + return jsonify({"error": "Title is required"}), 400 + + todo = { + "id": todo_id_counter, + "title": data["title"], + "completed": data.get("completed", False) + } + + todos.append(todo) + todo_id_counter += 1 + + return jsonify({"todo": todo}), 201 + +@app.route("/api/todos/", methods=["PUT"]) +def update_todo(todo_id): + """Update a todo""" + data = request.get_json() + + if not data: + return jsonify({"error": "No data provided"}), 400 + + todo = next((t for t in todos if t["id"] == todo_id), None) + if not todo: + return jsonify({"error": "Todo not found"}), 404 + + todo["title"] = data.get("title", todo["title"]) + todo["completed"] = data.get("completed", todo["completed"]) + + return jsonify({"todo": todo}) + +@app.route("/api/todos/", methods=["DELETE"]) +def delete_todo(todo_id): + """Delete a todo""" + global todos + + todo = next((t for t in todos if t["id"] == todo_id), None) + if not todo: + return jsonify({"error": "Todo not found"}), 404 + + todos = [t for t in todos if t["id"] != todo_id] + + return jsonify({"message": "Todo deleted successfully"}) + +@app.route("/api/health", methods=["GET"]) +def health_check(): + """Health check endpoint""" + return jsonify({"status": "healthy", "version": "1.0.0"}) + if __name__ == "__main__": app.run() \ No newline at end of file diff --git a/FunctionApp/function.json b/FunctionApp/function.json index 6f53aa4..8dbc7a2 100644 --- a/FunctionApp/function.json +++ b/FunctionApp/function.json @@ -8,7 +8,9 @@ "name": "req", "methods": [ "get", - "post" + "post", + "put", + "delete" ], "route": "{*route}" }, diff --git a/requirements.txt b/requirements.txt index f8f7d08..3a74130 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ azure-functions flask +pytest diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..6eb8ea9 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Comprehensive test runner script for the Flask Todo API application + +echo "Running Flask Todo API Comprehensive Test Suite..." +echo "=================================================" + +# Run all Python tests +echo "Running all Python tests..." +python3 -m pytest tests/test_*.py -v --tb=short + +# Check if tests passed +if [ $? -eq 0 ]; then + echo "" + echo "✅ All 44 Python tests passed!" + echo "" + echo "Test Coverage Summary:" + echo "- 🔧 Flask Application Tests: 14 tests" + echo "- 🌐 API Endpoint Tests: 14 tests" + echo "- 🔗 Integration Tests: 16 tests" + echo "- 📊 Total Tests: 44 tests" + echo "" + echo "Added Features:" + echo "- 5 new REST API endpoints (CRUD operations + health check)" + echo "- Comprehensive error handling and validation" + echo "- Support for special characters, unicode, and edge cases" + echo "- Full backwards compatibility with existing frontend" + echo "" + echo "API Endpoints:" + echo "- GET /api/health - Health check" + echo "- GET /api/todos - Get all todos" + echo "- POST /api/todos - Create a new todo" + echo "- PUT /api/todos/:id - Update an existing todo" + echo "- DELETE /api/todos/:id - Delete a todo" +else + echo "" + echo "❌ Some tests failed!" + exit 1 +fi \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..90ea774 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,250 @@ +import pytest +import json +import sys +import os + +# Add the parent directory to the path to import the Flask app +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from FlaskApp.flask_app import app + +@pytest.fixture +def client(): + """Create a test client for the Flask app""" + app.config['TESTING'] = True + with app.test_client() as client: + with app.app_context(): + # Clear todos before each test + from FlaskApp.flask_app import todos + todos.clear() + # Reset the counter + import FlaskApp.flask_app as flask_app + flask_app.todo_id_counter = 1 + yield client + +class TestHealthEndpoint: + """Test the health check endpoint""" + + def test_health_check(self, client): + """Test that the health check endpoint returns correct status""" + response = client.get('/api/health') + assert response.status_code == 200 + + data = json.loads(response.data) + assert data['status'] == 'healthy' + assert data['version'] == '1.0.0' + +class TestTodosAPI: + """Test the todos API endpoints""" + + def test_get_empty_todos(self, client): + """Test getting todos when there are none""" + response = client.get('/api/todos') + assert response.status_code == 200 + + data = json.loads(response.data) + assert 'todos' in data + assert data['todos'] == [] + + def test_create_todo(self, client): + """Test creating a new todo""" + todo_data = { + 'title': 'Test Todo', + 'completed': False + } + + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + + assert response.status_code == 201 + + data = json.loads(response.data) + assert 'todo' in data + assert data['todo']['title'] == 'Test Todo' + assert data['todo']['completed'] is False + assert data['todo']['id'] == 1 + + def test_create_todo_with_completed_true(self, client): + """Test creating a todo that is already completed""" + todo_data = { + 'title': 'Completed Todo', + 'completed': True + } + + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + + assert response.status_code == 201 + + data = json.loads(response.data) + assert data['todo']['title'] == 'Completed Todo' + assert data['todo']['completed'] is True + + def test_create_todo_without_title(self, client): + """Test creating a todo without a title should fail""" + todo_data = { + 'completed': False + } + + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + + assert response.status_code == 400 + + data = json.loads(response.data) + assert 'error' in data + assert data['error'] == 'Title is required' + + def test_create_todo_with_empty_title(self, client): + """Test creating a todo with empty title should fail""" + todo_data = { + 'title': '', + 'completed': False + } + + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + + assert response.status_code == 400 + + def test_create_and_get_todos(self, client): + """Test creating todos and then getting them""" + # Create first todo + todo1_data = {'title': 'First Todo', 'completed': False} + response1 = client.post('/api/todos', + data=json.dumps(todo1_data), + content_type='application/json') + assert response1.status_code == 201 + + # Create second todo + todo2_data = {'title': 'Second Todo', 'completed': True} + response2 = client.post('/api/todos', + data=json.dumps(todo2_data), + content_type='application/json') + assert response2.status_code == 201 + + # Get all todos + response = client.get('/api/todos') + assert response.status_code == 200 + + data = json.loads(response.data) + assert len(data['todos']) == 2 + assert data['todos'][0]['title'] == 'First Todo' + assert data['todos'][0]['completed'] is False + assert data['todos'][1]['title'] == 'Second Todo' + assert data['todos'][1]['completed'] is True + + def test_update_todo(self, client): + """Test updating a todo""" + # Create a todo first + todo_data = {'title': 'Original Title', 'completed': False} + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + assert response.status_code == 201 + + created_todo = json.loads(response.data)['todo'] + todo_id = created_todo['id'] + + # Update the todo + update_data = {'title': 'Updated Title', 'completed': True} + response = client.put(f'/api/todos/{todo_id}', + data=json.dumps(update_data), + content_type='application/json') + + assert response.status_code == 200 + + data = json.loads(response.data) + assert data['todo']['title'] == 'Updated Title' + assert data['todo']['completed'] is True + assert data['todo']['id'] == todo_id + + def test_update_todo_partial(self, client): + """Test updating only some fields of a todo""" + # Create a todo first + todo_data = {'title': 'Original Title', 'completed': False} + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + assert response.status_code == 201 + + created_todo = json.loads(response.data)['todo'] + todo_id = created_todo['id'] + + # Update only the completed status + update_data = {'completed': True} + response = client.put(f'/api/todos/{todo_id}', + data=json.dumps(update_data), + content_type='application/json') + + assert response.status_code == 200 + + data = json.loads(response.data) + assert data['todo']['title'] == 'Original Title' # Should remain unchanged + assert data['todo']['completed'] is True + + def test_update_nonexistent_todo(self, client): + """Test updating a todo that doesn't exist""" + update_data = {'title': 'Updated Title', 'completed': True} + response = client.put('/api/todos/999', + data=json.dumps(update_data), + content_type='application/json') + + assert response.status_code == 404 + + data = json.loads(response.data) + assert 'error' in data + assert data['error'] == 'Todo not found' + + def test_delete_todo(self, client): + """Test deleting a todo""" + # Create a todo first + todo_data = {'title': 'To be deleted', 'completed': False} + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + assert response.status_code == 201 + + created_todo = json.loads(response.data)['todo'] + todo_id = created_todo['id'] + + # Delete the todo + response = client.delete(f'/api/todos/{todo_id}') + assert response.status_code == 200 + + data = json.loads(response.data) + assert data['message'] == 'Todo deleted successfully' + + # Verify the todo is deleted + response = client.get('/api/todos') + data = json.loads(response.data) + assert len(data['todos']) == 0 + + def test_delete_nonexistent_todo(self, client): + """Test deleting a todo that doesn't exist""" + response = client.delete('/api/todos/999') + assert response.status_code == 404 + + data = json.loads(response.data) + assert 'error' in data + assert data['error'] == 'Todo not found' + +class TestStaticFilesStillWork: + """Test that the existing static file serving still works""" + + def test_index_page(self, client): + """Test that the index page still loads""" + response = client.get('/') + assert response.status_code == 200 + assert b'todos' in response.data + assert b'TodoMVC' in response.data + + def test_static_js_file(self, client): + """Test that static JavaScript files are served""" + response = client.get('/static/todo-app.js') + assert response.status_code == 200 + assert b'TodoMVC' in response.data or b'todo' in response.data \ No newline at end of file diff --git a/tests/test_flask_app.py b/tests/test_flask_app.py new file mode 100644 index 0000000..ede1631 --- /dev/null +++ b/tests/test_flask_app.py @@ -0,0 +1,139 @@ +import pytest +import sys +import os + +# Add the parent directory to the path to import the Flask app +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from FlaskApp.flask_app import app + +class TestFlaskAppConfiguration: + """Test Flask app configuration and setup""" + + def test_app_exists(self): + """Test that the Flask app exists and can be imported""" + assert app is not None + assert app.name == 'FlaskApp.flask_app' + + def test_app_is_testing_mode(self): + """Test that the app can be configured for testing""" + app.config['TESTING'] = True + assert app.config['TESTING'] is True + + def test_static_folder_configured(self): + """Test that the static folder is configured correctly""" + assert app.static_folder is not None + assert 'static' in app.static_folder + +class TestFlaskAppRoutes: + """Test Flask app routes and basic functionality""" + + def test_app_has_routes(self): + """Test that the app has the expected routes""" + with app.test_client() as client: + # Test that basic routes exist + response = client.get('/') + assert response.status_code == 200 + + # Test that static routes work + response = client.get('/static/todo-app.js') + assert response.status_code == 200 + + # Test that API routes exist + response = client.get('/api/health') + assert response.status_code == 200 + + response = client.get('/api/todos') + assert response.status_code == 200 + +class TestFlaskAppMemoryStorage: + """Test the in-memory storage functionality""" + + def test_todos_list_exists(self): + """Test that the todos list exists in the module""" + from FlaskApp.flask_app import todos + assert todos is not None + assert isinstance(todos, list) + + def test_todo_counter_exists(self): + """Test that the todo counter exists in the module""" + from FlaskApp.flask_app import todo_id_counter + assert todo_id_counter is not None + assert isinstance(todo_id_counter, int) + assert todo_id_counter >= 1 + +class TestFlaskAppUtilities: + """Test utility functions and helper methods""" + + def test_app_can_handle_json_requests(self): + """Test that the app can handle JSON requests""" + with app.test_client() as client: + # Test that the app can handle JSON content type + response = client.post('/api/todos', + data='{"title": "Test"}', + content_type='application/json') + # Should either succeed or fail with a specific error, not crash + assert response.status_code in [200, 201, 400] + + def test_app_handles_missing_routes(self): + """Test that the app handles missing routes gracefully""" + with app.test_client() as client: + response = client.get('/nonexistent-route') + assert response.status_code == 404 + +class TestFlaskAppImports: + """Test that all required imports work""" + + def test_flask_imports(self): + """Test that Flask imports work correctly""" + try: + from flask import Flask, send_from_directory, jsonify, request + assert True + except ImportError: + pytest.fail("Flask imports failed") + + def test_os_imports(self): + """Test that OS imports work correctly""" + try: + import os + assert True + except ImportError: + pytest.fail("OS imports failed") + +class TestFlaskAppInitialization: + """Test Flask app initialization""" + + def test_app_initialization(self): + """Test that the app initializes correctly""" + # Test that we can create a test client + with app.test_client() as client: + assert client is not None + + def test_app_context(self): + """Test that the app context works correctly""" + with app.app_context(): + assert app is not None + # We should be able to access the app within the context + from flask import current_app + assert current_app is not None + +class TestFlaskAppErrorHandling: + """Test error handling in the Flask app""" + + def test_invalid_content_type(self): + """Test handling of invalid content types""" + with app.test_client() as client: + response = client.post('/api/todos', + data='{"title": "Test"}', + content_type='text/plain') + # Should handle gracefully - either process or return error + assert response.status_code in [200, 201, 400, 415] + + def test_malformed_json(self): + """Test handling of malformed JSON""" + with app.test_client() as client: + response = client.post('/api/todos', + data='invalid json', + content_type='application/json') + # Should return 400 Bad Request + assert response.status_code == 400 \ No newline at end of file diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..0c389d2 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,300 @@ +import pytest +import json +import sys +import os + +# Add the parent directory to the path to import the Flask app +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from FlaskApp.flask_app import app + +@pytest.fixture +def client(): + """Create a test client for the Flask app""" + app.config['TESTING'] = True + with app.test_client() as client: + with app.app_context(): + # Clear todos before each test + from FlaskApp.flask_app import todos + todos.clear() + # Reset the counter + import FlaskApp.flask_app as flask_app + flask_app.todo_id_counter = 1 + yield client + +class TestAPIIntegration: + """Integration tests for the API endpoints""" + + def test_full_crud_workflow(self, client): + """Test a complete CRUD workflow""" + # Create a todo + create_data = {'title': 'Integration Test Todo', 'completed': False} + response = client.post('/api/todos', + data=json.dumps(create_data), + content_type='application/json') + assert response.status_code == 201 + + todo = json.loads(response.data)['todo'] + todo_id = todo['id'] + + # Read the todo + response = client.get('/api/todos') + assert response.status_code == 200 + + data = json.loads(response.data) + assert len(data['todos']) == 1 + assert data['todos'][0]['title'] == 'Integration Test Todo' + + # Update the todo + update_data = {'title': 'Updated Integration Test Todo', 'completed': True} + response = client.put(f'/api/todos/{todo_id}', + data=json.dumps(update_data), + content_type='application/json') + assert response.status_code == 200 + + updated_todo = json.loads(response.data)['todo'] + assert updated_todo['title'] == 'Updated Integration Test Todo' + assert updated_todo['completed'] is True + + # Verify the update + response = client.get('/api/todos') + data = json.loads(response.data) + assert data['todos'][0]['title'] == 'Updated Integration Test Todo' + assert data['todos'][0]['completed'] is True + + # Delete the todo + response = client.delete(f'/api/todos/{todo_id}') + assert response.status_code == 200 + + # Verify deletion + response = client.get('/api/todos') + data = json.loads(response.data) + assert len(data['todos']) == 0 + + def test_multiple_todos_operations(self, client): + """Test operations with multiple todos""" + # Create multiple todos + todos_data = [ + {'title': 'Todo 1', 'completed': False}, + {'title': 'Todo 2', 'completed': True}, + {'title': 'Todo 3', 'completed': False} + ] + + todo_ids = [] + for todo_data in todos_data: + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + assert response.status_code == 201 + todo_ids.append(json.loads(response.data)['todo']['id']) + + # Get all todos + response = client.get('/api/todos') + assert response.status_code == 200 + + data = json.loads(response.data) + assert len(data['todos']) == 3 + + # Update the second todo + update_data = {'completed': False} + response = client.put(f'/api/todos/{todo_ids[1]}', + data=json.dumps(update_data), + content_type='application/json') + assert response.status_code == 200 + + # Delete the first todo + response = client.delete(f'/api/todos/{todo_ids[0]}') + assert response.status_code == 200 + + # Verify final state + response = client.get('/api/todos') + data = json.loads(response.data) + assert len(data['todos']) == 2 + assert data['todos'][0]['title'] == 'Todo 2' + assert data['todos'][0]['completed'] is False + assert data['todos'][1]['title'] == 'Todo 3' + +class TestAPIEdgeCases: + """Test edge cases and error conditions""" + + def test_invalid_json_in_create(self, client): + """Test creating a todo with invalid JSON""" + response = client.post('/api/todos', + data='invalid json', + content_type='application/json') + assert response.status_code == 400 + + def test_empty_request_body_in_create(self, client): + """Test creating a todo with empty request body""" + response = client.post('/api/todos', + data='', + content_type='application/json') + assert response.status_code == 400 + + def test_invalid_json_in_update(self, client): + """Test updating a todo with invalid JSON""" + # Create a todo first + todo_data = {'title': 'Test Todo', 'completed': False} + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + assert response.status_code == 201 + + todo_id = json.loads(response.data)['todo']['id'] + + # Try to update with invalid JSON + response = client.put(f'/api/todos/{todo_id}', + data='invalid json', + content_type='application/json') + assert response.status_code == 400 + + def test_empty_request_body_in_update(self, client): + """Test updating a todo with empty request body""" + # Create a todo first + todo_data = {'title': 'Test Todo', 'completed': False} + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + assert response.status_code == 201 + + todo_id = json.loads(response.data)['todo']['id'] + + # Try to update with empty body + response = client.put(f'/api/todos/{todo_id}', + data='', + content_type='application/json') + assert response.status_code == 400 + + def test_special_characters_in_title(self, client): + """Test creating todos with special characters in title""" + special_titles = [ + 'Todo with émojis 🚀', + 'Todo with "quotes" and \'apostrophes\'', + 'Todo with tags', + 'Todo with newlines\nand\ttabs', + 'Todo with unicode: αβγδε' + ] + + for title in special_titles: + todo_data = {'title': title, 'completed': False} + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + assert response.status_code == 201 + + created_todo = json.loads(response.data)['todo'] + assert created_todo['title'] == title + + def test_very_long_title(self, client): + """Test creating a todo with a very long title""" + long_title = 'A' * 1000 # 1000 character title + todo_data = {'title': long_title, 'completed': False} + + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + assert response.status_code == 201 + + created_todo = json.loads(response.data)['todo'] + assert created_todo['title'] == long_title + + def test_non_string_title(self, client): + """Test creating a todo with non-string title""" + todo_data = {'title': 123, 'completed': False} + + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + # Should still work as the title gets converted to string + assert response.status_code == 201 + + created_todo = json.loads(response.data)['todo'] + assert created_todo['title'] == 123 + + def test_non_boolean_completed(self, client): + """Test creating a todo with non-boolean completed value""" + todo_data = {'title': 'Test Todo', 'completed': 'true'} + + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + assert response.status_code == 201 + + created_todo = json.loads(response.data)['todo'] + assert created_todo['completed'] == 'true' # Should preserve the type + +class TestAPIResponseHeaders: + """Test API response headers and content types""" + + def test_health_check_content_type(self, client): + """Test that health check returns correct content type""" + response = client.get('/api/health') + assert response.status_code == 200 + assert response.content_type == 'application/json' + + def test_todos_get_content_type(self, client): + """Test that todos GET returns correct content type""" + response = client.get('/api/todos') + assert response.status_code == 200 + assert response.content_type == 'application/json' + + def test_todos_post_content_type(self, client): + """Test that todos POST returns correct content type""" + todo_data = {'title': 'Test Todo', 'completed': False} + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + assert response.status_code == 201 + assert response.content_type == 'application/json' + +class TestAPIConsistency: + """Test API consistency and behavior""" + + def test_todo_id_incrementing(self, client): + """Test that todo IDs increment correctly""" + # Create multiple todos + for i in range(3): + todo_data = {'title': f'Todo {i+1}', 'completed': False} + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + assert response.status_code == 201 + + created_todo = json.loads(response.data)['todo'] + assert created_todo['id'] == i + 1 + + def test_todo_id_persistence_after_delete(self, client): + """Test that todo IDs don't reuse after deletion""" + # Create and delete a todo + todo_data = {'title': 'First Todo', 'completed': False} + response = client.post('/api/todos', + data=json.dumps(todo_data), + content_type='application/json') + assert response.status_code == 201 + + todo_id = json.loads(response.data)['todo']['id'] + + response = client.delete(f'/api/todos/{todo_id}') + assert response.status_code == 200 + + # Create another todo - should get the next ID, not reuse the deleted one + todo_data2 = {'title': 'Second Todo', 'completed': False} + response = client.post('/api/todos', + data=json.dumps(todo_data2), + content_type='application/json') + assert response.status_code == 201 + + new_todo = json.loads(response.data)['todo'] + assert new_todo['id'] == todo_id + 1 + + def test_api_endpoints_exist(self, client): + """Test that all expected API endpoints exist""" + # Test that endpoints respond (not 404) + endpoints = [ + ('GET', '/api/health'), + ('GET', '/api/todos'), + ] + + for method, endpoint in endpoints: + response = client.open(endpoint, method=method) + assert response.status_code != 404, f"Endpoint {method} {endpoint} should exist" \ No newline at end of file