add new
Some checks failed
K8S Fission Deployment / Deployment fission functions (push) Failing after 12s
Some checks failed
K8S Fission Deployment / Deployment fission functions (push) Failing after 12s
This commit is contained in:
@@ -44,10 +44,27 @@ def main():
|
||||
|
||||
|
||||
def make_insert_request():
|
||||
"""
|
||||
Handle POST request to create a new AI user.
|
||||
|
||||
Validates the request body using AiUserCreate schema, inserts a new record
|
||||
into the public.ai_user table, and returns the created user data.
|
||||
|
||||
Returns:
|
||||
tuple: (json_response, status_code, headers)
|
||||
- 201: User created successfully
|
||||
- 400: Validation error in request body
|
||||
- 409: Duplicate entry violation
|
||||
- 500: Internal server error
|
||||
"""
|
||||
try:
|
||||
body = AiUserCreate(**(request.get_json(silent=True) or {}))
|
||||
except ValidationError as e:
|
||||
return jsonify({"errorCode": "VALIDATION_ERROR", "details": e.errors()}), 400, CORS_HEADERS
|
||||
return (
|
||||
jsonify({"errorCode": "VALIDATION_ERROR", "details": e.errors()}),
|
||||
400,
|
||||
CORS_HEADERS,
|
||||
)
|
||||
|
||||
sql = """
|
||||
INSERT INTO public.ai_user (id, name, dob, email, gender)
|
||||
@@ -59,13 +76,19 @@ def make_insert_request():
|
||||
conn = init_db_connection()
|
||||
with conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, (str(uuid.uuid4()), body.name,
|
||||
body.dob, body.email, body.gender))
|
||||
cur.execute(
|
||||
sql,
|
||||
(str(uuid.uuid4()), body.name, body.dob, body.email, body.gender),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return jsonify(db_row_to_dict(cur, row)), 201, CORS_HEADERS
|
||||
except IntegrityError as e:
|
||||
# vi phạm unique(tag,kind,ref)
|
||||
return jsonify({"errorCode": "DUPLICATE_TAG", "details": str(e)}), 409, CORS_HEADERS
|
||||
return (
|
||||
jsonify({"errorCode": "DUPLICATE_TAG", "details": str(e)}),
|
||||
409,
|
||||
CORS_HEADERS,
|
||||
)
|
||||
except Exception as err:
|
||||
return jsonify({"error": str(err)}), 500, CORS_HEADERS
|
||||
finally:
|
||||
@@ -74,6 +97,17 @@ def make_insert_request():
|
||||
|
||||
|
||||
def make_filter_request():
|
||||
"""
|
||||
Handle GET request to filter and list AI users.
|
||||
|
||||
Retrieves pagination parameters from request queries, executes the filter
|
||||
query against the database, and returns a list of matching users.
|
||||
|
||||
Returns:
|
||||
tuple: (json_response, status_code, headers)
|
||||
- 200: Successfully retrieved users list
|
||||
- 500: Internal server error
|
||||
"""
|
||||
paging = UserPage.from_request_queries()
|
||||
|
||||
conn = None
|
||||
@@ -89,6 +123,19 @@ def make_filter_request():
|
||||
|
||||
|
||||
def __filter_users(cursor, paging: "UserPage"):
|
||||
"""
|
||||
Build and execute SQL query to filter users based on pagination and filter criteria.
|
||||
|
||||
Constructs dynamic WHERE clause from UserFilter attributes, applies sorting
|
||||
and pagination, and returns matching database records.
|
||||
|
||||
Args:
|
||||
cursor: Database cursor for executing queries.
|
||||
paging: UserPage object containing pagination and filter parameters.
|
||||
|
||||
Returns:
|
||||
list: List of user records matching the filter criteria.
|
||||
"""
|
||||
conditions = []
|
||||
values = {}
|
||||
|
||||
@@ -170,6 +217,15 @@ class Page:
|
||||
|
||||
@classmethod
|
||||
def from_request_queries(cls) -> "Page":
|
||||
"""
|
||||
Create Page instance from HTTP request query parameters.
|
||||
|
||||
Extracts 'page', 'size', and 'asc' parameters from the request URL.
|
||||
Defaults: page=0, size=8, asc=None.
|
||||
|
||||
Returns:
|
||||
Page: A new Page instance with values from query parameters.
|
||||
"""
|
||||
paging = Page()
|
||||
paging.page = int(request.args.get("page", 0))
|
||||
paging.size = int(request.args.get("size", 8))
|
||||
@@ -193,6 +249,15 @@ class UserFilter:
|
||||
|
||||
@classmethod
|
||||
def from_request_queries(cls) -> "UserFilter":
|
||||
"""
|
||||
Create UserFilter instance from HTTP request query parameters.
|
||||
|
||||
Extracts filter parameters with 'filter[...]' prefix from the request URL.
|
||||
Supports filtering by ids, keyword, name, email, gender, date ranges.
|
||||
|
||||
Returns:
|
||||
UserFilter: A new UserFilter instance with values from query parameters.
|
||||
"""
|
||||
filter = UserFilter()
|
||||
filter.ids = request.args.getlist("filter[ids]")
|
||||
filter.keyword = request.args.get("filter[keyword]")
|
||||
@@ -222,6 +287,15 @@ class UserPage(Page):
|
||||
|
||||
@classmethod
|
||||
def from_request_queries(cls) -> "UserPage":
|
||||
"""
|
||||
Create UserPage instance from HTTP request query parameters.
|
||||
|
||||
Combines pagination (page, size, asc) and filter parameters from the request URL.
|
||||
Also parses 'sortby' parameter to UserSortField enum.
|
||||
|
||||
Returns:
|
||||
UserPage: A new UserPage instance with all query parameters.
|
||||
"""
|
||||
base = super(UserPage, cls).from_request_queries()
|
||||
paging = UserPage(**dataclasses.asdict(base))
|
||||
|
||||
|
||||
@@ -2,3 +2,5 @@ psycopg2-binary==2.9.10
|
||||
pydantic==2.11.7
|
||||
PyNaCl==1.6.0
|
||||
Flask==3.1.0
|
||||
pytest==8.3.4
|
||||
pytest-mock==3.14.0
|
||||
|
||||
0
apps/tests/__init__.py
Normal file
0
apps/tests/__init__.py
Normal file
56
apps/tests/conftest.py
Normal file
56
apps/tests/conftest.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from flask import Flask
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
"""Create Flask test app"""
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
"""Create Flask test client"""
|
||||
return app.test_client()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_connection():
|
||||
"""Mock database connection"""
|
||||
mock_conn = MagicMock()
|
||||
mock_cursor = 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)
|
||||
|
||||
return mock_conn, mock_cursor
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_user_data():
|
||||
"""Sample user data for testing"""
|
||||
return {
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"dob": "1990-01-01",
|
||||
"gender": "male"
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_filter_params():
|
||||
"""Sample filter parameters for testing"""
|
||||
return {
|
||||
"page": 0,
|
||||
"size": 8,
|
||||
"asc": "false",
|
||||
"filter[keyword]": "test",
|
||||
"filter[name]": "John",
|
||||
"filter[email]": "john@example.com",
|
||||
"filter[gender]": "male",
|
||||
}
|
||||
357
apps/tests/test_filter_insert.py
Normal file
357
apps/tests/test_filter_insert.py
Normal file
@@ -0,0 +1,357 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
class TestMain:
|
||||
"""Test cases for the main() function"""
|
||||
|
||||
@patch("filter_insert.make_filter_request")
|
||||
def test_main_get_method(self, mock_filter_request):
|
||||
"""Test main() with GET method calls make_filter_request()"""
|
||||
from flask import Flask
|
||||
from filter_insert import main
|
||||
|
||||
# Create a test app context
|
||||
app = Flask(__name__)
|
||||
with app.test_request_context("/ai/admin/users", method="GET"):
|
||||
mock_filter_request.return_value = ({"data": "test"}, 200, {})
|
||||
result = main()
|
||||
|
||||
mock_filter_request.assert_called_once()
|
||||
assert result == ({"data": "test"}, 200, {})
|
||||
|
||||
@patch("filter_insert.make_insert_request")
|
||||
def test_main_post_method(self, mock_insert_request):
|
||||
"""Test main() with POST method calls make_insert_request()"""
|
||||
from flask import Flask
|
||||
from filter_insert import main
|
||||
|
||||
app = Flask(__name__)
|
||||
with app.test_request_context("/ai/admin/users", method="POST", json={"name": "Test", "email": "test@example.com"}):
|
||||
mock_insert_request.return_value = ({"id": "123"}, 201, {})
|
||||
result = main()
|
||||
|
||||
mock_insert_request.assert_called_once()
|
||||
assert result == ({"id": "123"}, 201, {})
|
||||
|
||||
def test_main_method_not_allowed(self):
|
||||
"""Test main() with unsupported HTTP method returns 405"""
|
||||
from flask import Flask
|
||||
from filter_insert import main, CORS_HEADERS
|
||||
|
||||
app = Flask(__name__)
|
||||
with app.test_request_context("/ai/admin/users", method="PUT"):
|
||||
result = main()
|
||||
|
||||
expected = ({"error": "Method not allow"}, 405, CORS_HEADERS)
|
||||
assert result == expected
|
||||
|
||||
def test_main_exception_handling(self):
|
||||
"""Test main() catches exceptions and returns 500"""
|
||||
from flask import Flask
|
||||
from filter_insert import main
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
with patch("filter_insert.make_filter_request") as mock_filter:
|
||||
mock_filter.side_effect = Exception("Database connection failed")
|
||||
with app.test_request_context("/ai/admin/users", method="GET"):
|
||||
result = main()
|
||||
|
||||
assert result[1] == 500
|
||||
assert "error" in result[0]
|
||||
|
||||
|
||||
class TestMakeInsertRequest:
|
||||
"""Test cases for make_insert_request() function"""
|
||||
|
||||
@patch("filter_insert.init_db_connection")
|
||||
@patch("filter_insert.request")
|
||||
@patch("filter_insert.db_row_to_dict")
|
||||
def test_make_insert_request_success(self, mock_db_row, mock_request, mock_init_db):
|
||||
"""Test successful user insertion"""
|
||||
from flask import Flask
|
||||
from filter_insert import make_insert_request, CORS_HEADERS
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
mock_request.get_json.return_value = {
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"dob": "1990-01-01",
|
||||
"gender": "male"
|
||||
}
|
||||
|
||||
mock_conn = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.fetchone.return_value = ("uuid-123", "John Doe", "1990-01-01", "john@example.com", "male", "2024-01-01", "2024-01-01")
|
||||
mock_cursor.description = [
|
||||
MagicMock(name="id"),
|
||||
MagicMock(name="name"),
|
||||
MagicMock(name="dob"),
|
||||
MagicMock(name="email"),
|
||||
MagicMock(name="gender"),
|
||||
MagicMock(name="created"),
|
||||
MagicMock(name="modified"),
|
||||
]
|
||||
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)
|
||||
mock_init_db.return_value = mock_conn
|
||||
mock_db_row.return_value = {"id": "uuid-123", "name": "John Doe"}
|
||||
|
||||
with app.test_request_context("/ai/admin/users", method="POST", json=mock_request.get_json.return_value):
|
||||
result = make_insert_request()
|
||||
|
||||
assert result[1] == 201
|
||||
assert result[2] == CORS_HEADERS
|
||||
|
||||
@patch("filter_insert.request")
|
||||
def test_make_insert_request_validation_error(self, mock_request):
|
||||
"""Test make_insert_request() with invalid data returns 400"""
|
||||
from flask import Flask
|
||||
from filter_insert import make_insert_request, CORS_HEADERS
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Invalid email format
|
||||
mock_request.get_json.return_value = {
|
||||
"name": "John Doe",
|
||||
"email": "invalid-email"
|
||||
}
|
||||
|
||||
with app.test_request_context("/ai/admin/users", method="POST", json=mock_request.get_json.return_value):
|
||||
result = make_insert_request()
|
||||
|
||||
assert result[1] == 400
|
||||
assert result[0].json["errorCode"] == "VALIDATION_ERROR"
|
||||
assert result[2] == CORS_HEADERS
|
||||
|
||||
@patch("filter_insert.init_db_connection")
|
||||
@patch("filter_insert.request")
|
||||
def test_make_insert_request_duplicate_error(self, mock_request, mock_init_db):
|
||||
"""Test make_insert_request() with duplicate email returns 409"""
|
||||
from flask import Flask
|
||||
from filter_insert import make_insert_request, CORS_HEADERS
|
||||
from psycopg2 import IntegrityError
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
mock_request.get_json.return_value = {
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com"
|
||||
}
|
||||
|
||||
mock_conn = MagicMock()
|
||||
mock_cursor = 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)
|
||||
mock_init_db.return_value = mock_conn
|
||||
|
||||
# Simulate IntegrityError
|
||||
mock_cursor.execute.side_effect = IntegrityError("duplicate key value violates unique constraint")
|
||||
|
||||
with app.test_request_context("/ai/admin/users", method="POST", json=mock_request.get_json.return_value):
|
||||
result = make_insert_request()
|
||||
|
||||
assert result[1] == 409
|
||||
assert result[0].json["errorCode"] == "DUPLICATE_TAG"
|
||||
assert result[2] == CORS_HEADERS
|
||||
|
||||
|
||||
class TestMakeFilterRequest:
|
||||
"""Test cases for make_filter_request() function"""
|
||||
|
||||
@patch("filter_insert.init_db_connection")
|
||||
@patch("filter_insert.request")
|
||||
@patch("filter_insert.UserPage")
|
||||
@patch("filter_insert.db_rows_to_array")
|
||||
def test_make_filter_request_success(self, mock_db_rows, mock_page_class, mock_request, mock_init_db):
|
||||
"""Test successful filter request"""
|
||||
from flask import Flask
|
||||
from filter_insert import make_filter_request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
mock_paging = MagicMock()
|
||||
mock_paging.size = 8
|
||||
mock_paging.page = 0
|
||||
mock_paging.filter = MagicMock()
|
||||
mock_paging.filter.ids = None
|
||||
mock_paging.filter.keyword = None
|
||||
mock_paging.filter.name = None
|
||||
mock_paging.filter.email = None
|
||||
mock_paging.filter.created_from = None
|
||||
mock_paging.filter.created_to = None
|
||||
mock_paging.filter.modified_from = None
|
||||
mock_paging.filter.modified_to = None
|
||||
mock_paging.filter.dob_from = None
|
||||
mock_paging.filter.dob_to = None
|
||||
mock_paging.sortby = None
|
||||
mock_paging.asc = None
|
||||
mock_page_class.from_request_queries.return_value = mock_paging
|
||||
|
||||
mock_request.args.get.return_value = None
|
||||
|
||||
mock_conn = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.fetchall.return_value = []
|
||||
mock_conn.cursor.return_value = mock_cursor
|
||||
mock_init_db.return_value = mock_conn
|
||||
|
||||
mock_db_rows.return_value = []
|
||||
|
||||
with app.test_request_context("/ai/admin/users?page=0&size=8"):
|
||||
result = make_filter_request()
|
||||
|
||||
assert result[1] == 200
|
||||
mock_db_rows.assert_called_once()
|
||||
|
||||
|
||||
class TestUserFilter:
|
||||
"""Test cases for UserFilter.from_request_queries()"""
|
||||
|
||||
@patch("filter_insert.request")
|
||||
def test_user_filter_from_queries(self, mock_request):
|
||||
"""Test UserFilter parses query parameters correctly"""
|
||||
from filter_insert import UserFilter
|
||||
|
||||
mock_request.args.get.side_effect = lambda key, default=None: {
|
||||
"filter[ids]": None,
|
||||
"filter[keyword]": "test",
|
||||
"filter[name]": "John",
|
||||
"filter[email]": "john@example.com",
|
||||
"filter[gender]": "male",
|
||||
"filter[created_from]": "2024-01-01",
|
||||
"filter[created_to]": "2024-12-31",
|
||||
"filter[modified_from]": None,
|
||||
"filter[modified_to]": None,
|
||||
"filter[dob_from]": None,
|
||||
"filter[dob_to]": None,
|
||||
}.get(key, default)
|
||||
|
||||
mock_request.args.getlist.return_value = []
|
||||
|
||||
result = UserFilter.from_request_queries()
|
||||
|
||||
assert result.keyword == "test"
|
||||
assert result.name == "John"
|
||||
assert result.email == "john@example.com"
|
||||
assert result.gender == "male"
|
||||
assert result.created_from == "2024-01-01"
|
||||
assert result.created_to == "2024-12-31"
|
||||
|
||||
|
||||
class TestPage:
|
||||
"""Test cases for Page.from_request_queries()"""
|
||||
|
||||
@patch("filter_insert.request")
|
||||
def test_page_default_values(self, mock_request):
|
||||
"""Test Page uses default values when params not provided"""
|
||||
from filter_insert import Page
|
||||
|
||||
mock_request.args.get.side_effect = lambda key, default=None, type=None: {
|
||||
"page": 0,
|
||||
"size": 8,
|
||||
"asc": "false",
|
||||
}.get(key, default)
|
||||
|
||||
result = Page.from_request_queries()
|
||||
|
||||
assert result.page == 0
|
||||
assert result.size == 8
|
||||
assert result.asc is False
|
||||
|
||||
|
||||
class TestUserPage:
|
||||
"""Test cases for UserPage.from_request_queries()"""
|
||||
|
||||
@patch("filter_insert.request")
|
||||
def test_user_page_with_sortby(self, mock_request):
|
||||
"""Test UserPage parses sortby parameter correctly"""
|
||||
from filter_insert import UserPage, UserSortField
|
||||
|
||||
# Simulate request.args.get behavior
|
||||
def mock_get(key, default=None, type=None):
|
||||
values = {
|
||||
"page": 0,
|
||||
"size": 8,
|
||||
"asc": "true",
|
||||
"sortby": "created",
|
||||
}
|
||||
return values.get(key, default)
|
||||
|
||||
mock_request.args.get = mock_get
|
||||
mock_request.args.getlist.return_value = []
|
||||
|
||||
result = UserPage.from_request_queries()
|
||||
|
||||
assert result.sortby == UserSortField.CREATED
|
||||
assert result.asc is True
|
||||
|
||||
@patch("filter_insert.request")
|
||||
def test_user_page_invalid_sortby(self, mock_request):
|
||||
"""Test UserPage handles invalid sortby gracefully"""
|
||||
from filter_insert import UserPage
|
||||
|
||||
def mock_get(key, default=None, type=None):
|
||||
values = {
|
||||
"page": 0,
|
||||
"size": 8,
|
||||
"asc": "false",
|
||||
"sortby": "invalid_field",
|
||||
}
|
||||
return values.get(key, default)
|
||||
|
||||
mock_request.args.get = mock_get
|
||||
mock_request.args.getlist.return_value = []
|
||||
|
||||
result = UserPage.from_request_queries()
|
||||
|
||||
assert result.sortby is None
|
||||
|
||||
|
||||
class TestUserSortField:
|
||||
"""Test cases for UserSortField enum"""
|
||||
|
||||
def test_user_sort_field_values(self):
|
||||
"""Test UserSortField has correct values"""
|
||||
from filter_insert import UserSortField
|
||||
|
||||
assert UserSortField.CREATED.value == "created"
|
||||
assert UserSortField.MODIFIED.value == "modified"
|
||||
|
||||
|
||||
class TestHelpers:
|
||||
"""Test cases for helper functions"""
|
||||
|
||||
def test_str_to_bool_true(self):
|
||||
"""Test str_to_bool with true values"""
|
||||
from helpers import str_to_bool
|
||||
|
||||
assert str_to_bool("true") is True
|
||||
assert str_to_bool("True") is True
|
||||
assert str_to_bool("TRUE") is True
|
||||
|
||||
def test_str_to_bool_false(self):
|
||||
"""Test str_to_bool with false values"""
|
||||
from helpers import str_to_bool
|
||||
|
||||
assert str_to_bool("false") is False
|
||||
assert str_to_bool("False") is False
|
||||
assert str_to_bool("FALSE") is False
|
||||
|
||||
def test_str_to_bool_none(self):
|
||||
"""Test str_to_bool with invalid values returns None"""
|
||||
from helpers import str_to_bool
|
||||
|
||||
assert str_to_bool("invalid") is None
|
||||
assert str_to_bool("") is None
|
||||
assert str_to_bool(None) is None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user