add new
Some checks failed
K8S Fission Deployment / Deployment fission functions (push) Failing after 12s

This commit is contained in:
Nguyen Duc Thao
2026-01-26 23:07:28 +07:00
parent 018f267fab
commit 3861b027b2
16 changed files with 3164 additions and 60 deletions

View File

@@ -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))

View File

@@ -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
View File

56
apps/tests/conftest.py Normal file
View 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",
}

View 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"])