515 lines
17 KiB
Python
515 lines
17 KiB
Python
|
|
"""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
|