"""Tests for update_delete.py - PUT (update) & DELETE handlers.""" from unittest.mock import MagicMock import pytest from flask import Flask from psycopg2 import IntegrityError class TestMain: """Tests for main() dispatcher function.""" def test_main_put_calls_update(self, mocker): """Test PUT request routes to make_update_request(). Given: A PUT request to the endpoint. When: main() is called. Then: make_update_request() is invoked and returns 200. """ app = Flask(__name__) with app.test_request_context(method="PUT"): mock_update = mocker.patch( "update_delete.make_update_request", return_value=({"id": "123"}, 200, {}), ) import update_delete result = update_delete.main() mock_update.assert_called_once() assert result[1] == 200 def test_main_delete_calls_delete(self, mocker): """Test DELETE request routes to make_delete_request(). Given: A DELETE request to the endpoint. When: main() is called. Then: make_delete_request() is invoked and returns 200. """ app = Flask(__name__) with app.test_request_context(method="DELETE"): mock_delete = mocker.patch( "update_delete.make_delete_request", return_value=({"id": "123"}, 200, {}), ) import update_delete result = update_delete.main() mock_delete.assert_called_once() assert result[1] == 200 def test_main_invalid_method_returns_405(self, mocker): """Test unsupported HTTP method returns 405. Given: A POST request (unsupported method for this endpoint). When: main() is called. Then: Returns 405 Method Not Allowed with error message. """ app = Flask(__name__) with app.test_request_context(method="POST"): import update_delete result = update_delete.main() assert result[1] == 405 assert "error" in result[0] class TestMakeUpdateRequest: """Tests for make_update_request() - user update.""" def test_update_missing_user_id(self, mocker): """Test missing X-Fission-Params-UserID header returns 400. Given: PUT request without X-Fission-Params-UserID header. When: make_update_request() is called. Then: Returns 400 Bad Request. Response contains errorCode 'MISSING_USER_ID'. """ app = Flask(__name__) with app.test_request_context( method="PUT", json={"name": "Updated Name"}, content_type="application/json", ): import update_delete result = update_delete.make_update_request() assert result[1] == 400 response_data = result[0].get_json() assert response_data["errorCode"] == "MISSING_USER_ID" def test_update_user_not_found(self, mocker): """Test update non-existent user returns 404. Given: Valid X-Fission-Params-UserID header. User does not exist in database (SELECT returns None). When: PUT request to update user. Then: Returns 404 Not Found. Response contains errorCode 'USER_NOT_FOUND'. """ app = Flask(__name__) mock_cursor = MagicMock() mock_cursor.fetchone.return_value = None # User not found mock_conn = MagicMock() mock_conn.__enter__ = MagicMock(return_value=mock_conn) mock_conn.__exit__ = MagicMock(return_value=False) mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) mocker.patch("update_delete.init_db_connection", return_value=mock_conn) with app.test_request_context( method="PUT", headers={"X-Fission-Params-UserID": "nonexistent-id"}, json={"name": "Updated Name"}, content_type="application/json", ): import update_delete result = update_delete.make_update_request() assert result[1] == 404 response_data = result[0].get_json() assert response_data["errorCode"] == "USER_NOT_FOUND" def test_update_success(self, mocker, sample_db_row): """Test successful user update returns 200. Given: Valid X-Fission-Params-UserID header. User exists in database. Valid update data in request body. When: PUT request to update user. Then: Returns 200 OK. User data is updated in database. """ app = Flask(__name__) mock_cursor = MagicMock() mock_cursor.description = [ MagicMock(name="id"), MagicMock(name="name"), MagicMock(name="dob"), MagicMock(name="email"), MagicMock(name="gender"), MagicMock(name="created"), MagicMock(name="modified"), ] for i, col_name in enumerate( ["id", "name", "dob", "email", "gender", "created", "modified"] ): mock_cursor.description[i].name = col_name # First fetchone returns existing user, second returns updated user mock_cursor.fetchone.side_effect = [sample_db_row, sample_db_row] mock_conn = MagicMock() mock_conn.__enter__ = MagicMock(return_value=mock_conn) mock_conn.__exit__ = MagicMock(return_value=False) mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) mocker.patch("update_delete.init_db_connection", return_value=mock_conn) with app.test_request_context( method="PUT", headers={"X-Fission-Params-UserID": "550e8400-e29b-41d4-a716-446655440000"}, json={"name": "Updated Name"}, content_type="application/json", ): import update_delete result = update_delete.make_update_request() assert result[1] == 200 def test_update_validation_error_invalid_email(self, mocker): """Test invalid email format raises serialization error. Given: Valid X-Fission-Params-UserID header. Request body with invalid email format 'invalid-email'. When: PUT request to update user. Then: Raises TypeError because ValidationError.errors() contains ValueError which is not JSON serializable. Note: This test documents a bug in the source code where e.errors() is passed directly to jsonify without sanitization. """ app = Flask(__name__) with app.test_request_context( method="PUT", headers={"X-Fission-Params-UserID": "some-id"}, json={"email": "invalid-email"}, content_type="application/json", ): import update_delete with pytest.raises(TypeError, match="not JSON serializable"): update_delete.make_update_request() def test_update_duplicate_email(self, mocker, sample_db_row): """Test duplicate email on update returns 409 Conflict. Given: Valid X-Fission-Params-UserID header. User exists in database. New email already exists for another user. Database raises IntegrityError on UPDATE. When: PUT request to update user email. Then: Returns 409 Conflict. Response contains errorCode 'DUPLICATE_USER'. """ app = Flask(__name__) mock_cursor = MagicMock() mock_cursor.description = [ MagicMock(name="id"), MagicMock(name="name"), MagicMock(name="dob"), MagicMock(name="email"), MagicMock(name="gender"), MagicMock(name="created"), MagicMock(name="modified"), ] for i, col_name in enumerate( ["id", "name", "dob", "email", "gender", "created", "modified"] ): mock_cursor.description[i].name = col_name # First fetchone returns existing user mock_cursor.fetchone.return_value = sample_db_row # Second execute raises IntegrityError mock_cursor.execute.side_effect = [None, IntegrityError("duplicate key")] mock_conn = MagicMock() mock_conn.__enter__ = MagicMock(return_value=mock_conn) mock_conn.__exit__ = MagicMock(return_value=False) mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) mocker.patch("update_delete.init_db_connection", return_value=mock_conn) with app.test_request_context( method="PUT", headers={"X-Fission-Params-UserID": "550e8400-e29b-41d4-a716-446655440000"}, json={"email": "duplicate@example.com"}, content_type="application/json", ): import update_delete result = update_delete.make_update_request() assert result[1] == 409 response_data = result[0].get_json() assert response_data["errorCode"] == "DUPLICATE_USER" class TestMakeDeleteRequest: """Tests for make_delete_request() - user deletion.""" def test_delete_missing_user_id(self, mocker): """Test missing X-Fission-Params-UserID header returns 400. Given: DELETE request without X-Fission-Params-UserID header. When: make_delete_request() is called. Then: Returns 400 Bad Request. Response contains errorCode 'MISSING_USER_ID'. """ app = Flask(__name__) with app.test_request_context(method="DELETE"): import update_delete result = update_delete.make_delete_request() assert result[1] == 400 response_data = result[0].get_json() assert response_data["errorCode"] == "MISSING_USER_ID" def test_delete_user_not_found(self, mocker): """Test delete non-existent user returns 404. Given: Valid X-Fission-Params-UserID header. User does not exist in database (SELECT returns None). When: DELETE request to delete user. Then: Returns 404 Not Found. """ app = Flask(__name__) mock_cursor = MagicMock() mock_cursor.fetchone.return_value = None # User not found mock_conn = MagicMock() mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) mocker.patch("update_delete.init_db_connection", return_value=mock_conn) with app.test_request_context( method="DELETE", headers={"X-Fission-Params-UserID": "nonexistent-id"}, ): import update_delete result = update_delete.make_delete_request() assert result[1] == 404 def test_delete_success(self, mocker, sample_db_row): """Test successful user deletion returns 200. Given: Valid X-Fission-Params-UserID header. User exists in database. When: DELETE request to delete user. Then: Returns 200 OK. User is deleted from database. Response contains deleted user data. """ app = Flask(__name__) mock_cursor = MagicMock() mock_cursor.description = [ MagicMock(name="id"), MagicMock(name="name"), MagicMock(name="dob"), MagicMock(name="email"), MagicMock(name="gender"), MagicMock(name="created"), MagicMock(name="modified"), ] for i, col_name in enumerate( ["id", "name", "dob", "email", "gender", "created", "modified"] ): mock_cursor.description[i].name = col_name # First fetchone checks existence, second returns deleted row mock_cursor.fetchone.side_effect = [(1,), sample_db_row] mock_conn = MagicMock() mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) mocker.patch("update_delete.init_db_connection", return_value=mock_conn) with app.test_request_context( method="DELETE", headers={"X-Fission-Params-UserID": "550e8400-e29b-41d4-a716-446655440000"}, ): import update_delete result = update_delete.make_delete_request() assert result[1] == 200 class TestUpdatePartialFields: """Tests for partial field updates.""" def test_update_only_name(self, mocker, sample_db_row): """Test update only name field. Given: Valid X-Fission-Params-UserID header. User exists in database. Request body contains only 'name' field. When: PUT request to update user. Then: Returns 200 OK. UPDATE SQL only includes name field (plus modified timestamp). """ app = Flask(__name__) mock_cursor = MagicMock() mock_cursor.description = [ MagicMock(name="id"), MagicMock(name="name"), MagicMock(name="dob"), MagicMock(name="email"), MagicMock(name="gender"), MagicMock(name="created"), MagicMock(name="modified"), ] for i, col_name in enumerate( ["id", "name", "dob", "email", "gender", "created", "modified"] ): mock_cursor.description[i].name = col_name mock_cursor.fetchone.side_effect = [sample_db_row, sample_db_row] mock_conn = MagicMock() mock_conn.__enter__ = MagicMock(return_value=mock_conn) mock_conn.__exit__ = MagicMock(return_value=False) mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) mocker.patch("update_delete.init_db_connection", return_value=mock_conn) with app.test_request_context( method="PUT", headers={"X-Fission-Params-UserID": "550e8400-e29b-41d4-a716-446655440000"}, json={"name": "New Name Only"}, content_type="application/json", ): import update_delete result = update_delete.make_update_request() assert result[1] == 200 # Check that UPDATE SQL contains name field call_args = mock_cursor.execute.call_args_list[-1] sql = call_args[0][0] assert "name=" in sql def test_update_multiple_fields(self, mocker, sample_db_row): """Test update multiple fields at once. Given: Valid X-Fission-Params-UserID header. User exists in database. Request body contains name, email, and gender fields. When: PUT request to update user. Then: Returns 200 OK. UPDATE SQL includes all three fields. """ app = Flask(__name__) mock_cursor = MagicMock() mock_cursor.description = [ MagicMock(name="id"), MagicMock(name="name"), MagicMock(name="dob"), MagicMock(name="email"), MagicMock(name="gender"), MagicMock(name="created"), MagicMock(name="modified"), ] for i, col_name in enumerate( ["id", "name", "dob", "email", "gender", "created", "modified"] ): mock_cursor.description[i].name = col_name mock_cursor.fetchone.side_effect = [sample_db_row, sample_db_row] mock_conn = MagicMock() mock_conn.__enter__ = MagicMock(return_value=mock_conn) mock_conn.__exit__ = MagicMock(return_value=False) mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor) mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) mocker.patch("update_delete.init_db_connection", return_value=mock_conn) with app.test_request_context( method="PUT", headers={"X-Fission-Params-UserID": "550e8400-e29b-41d4-a716-446655440000"}, json={"name": "New Name", "email": "new@example.com", "gender": "female"}, content_type="application/json", ): import update_delete result = update_delete.make_update_request() assert result[1] == 200 call_args = mock_cursor.execute.call_args_list[-1] sql = call_args[0][0] assert "name=" in sql assert "email=" in sql assert "gender=" in sql