velocity-python 0.0.132__py3-none-any.whl → 0.0.135__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of velocity-python might be problematic. Click here for more details.

Files changed (67) hide show
  1. velocity/__init__.py +1 -1
  2. velocity/app/tests/__init__.py +1 -0
  3. velocity/app/tests/test_email_processing.py +112 -0
  4. velocity/app/tests/test_payment_profile_sorting.py +191 -0
  5. velocity/app/tests/test_spreadsheet_functions.py +124 -0
  6. velocity/aws/tests/__init__.py +1 -0
  7. velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
  8. velocity/aws/tests/test_response.py +163 -0
  9. velocity/db/core/decorators.py +20 -3
  10. velocity/db/core/engine.py +33 -7
  11. velocity/db/exceptions.py +7 -0
  12. velocity/db/servers/base/initializer.py +2 -1
  13. velocity/db/servers/mysql/__init__.py +13 -4
  14. velocity/db/servers/postgres/__init__.py +14 -4
  15. velocity/db/servers/sqlite/__init__.py +13 -4
  16. velocity/db/servers/sqlserver/__init__.py +13 -4
  17. velocity/db/tests/__init__.py +1 -0
  18. velocity/db/tests/common_db_test.py +0 -0
  19. velocity/db/tests/postgres/__init__.py +1 -0
  20. velocity/db/tests/postgres/common.py +49 -0
  21. velocity/db/tests/postgres/test_column.py +29 -0
  22. velocity/db/tests/postgres/test_connections.py +25 -0
  23. velocity/db/tests/postgres/test_database.py +21 -0
  24. velocity/db/tests/postgres/test_engine.py +205 -0
  25. velocity/db/tests/postgres/test_general_usage.py +88 -0
  26. velocity/db/tests/postgres/test_imports.py +8 -0
  27. velocity/db/tests/postgres/test_result.py +19 -0
  28. velocity/db/tests/postgres/test_row.py +137 -0
  29. velocity/db/tests/postgres/test_row_comprehensive.py +707 -0
  30. velocity/db/tests/postgres/test_schema_locking.py +335 -0
  31. velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
  32. velocity/db/tests/postgres/test_sequence.py +34 -0
  33. velocity/db/tests/postgres/test_sql_comprehensive.py +471 -0
  34. velocity/db/tests/postgres/test_table.py +101 -0
  35. velocity/db/tests/postgres/test_table_comprehensive.py +644 -0
  36. velocity/db/tests/postgres/test_transaction.py +106 -0
  37. velocity/db/tests/sql/__init__.py +1 -0
  38. velocity/db/tests/sql/common.py +177 -0
  39. velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
  40. velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
  41. velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
  42. velocity/db/tests/test_db_utils.py +221 -0
  43. velocity/db/tests/test_postgres.py +212 -0
  44. velocity/db/tests/test_postgres_unchanged.py +81 -0
  45. velocity/db/tests/test_process_error_robustness.py +292 -0
  46. velocity/db/tests/test_result_caching.py +279 -0
  47. velocity/db/tests/test_result_sql_aware.py +117 -0
  48. velocity/db/tests/test_row_get_missing_column.py +72 -0
  49. velocity/db/tests/test_schema_locking_initializers.py +226 -0
  50. velocity/db/tests/test_schema_locking_simple.py +97 -0
  51. velocity/db/tests/test_sql_builder.py +165 -0
  52. velocity/db/tests/test_tablehelper.py +486 -0
  53. velocity/misc/tests/__init__.py +1 -0
  54. velocity/misc/tests/test_db.py +90 -0
  55. velocity/misc/tests/test_fix.py +78 -0
  56. velocity/misc/tests/test_format.py +64 -0
  57. velocity/misc/tests/test_iconv.py +203 -0
  58. velocity/misc/tests/test_merge.py +82 -0
  59. velocity/misc/tests/test_oconv.py +144 -0
  60. velocity/misc/tests/test_original_error.py +52 -0
  61. velocity/misc/tests/test_timer.py +74 -0
  62. {velocity_python-0.0.132.dist-info → velocity_python-0.0.135.dist-info}/METADATA +1 -1
  63. velocity_python-0.0.135.dist-info/RECORD +128 -0
  64. velocity_python-0.0.132.dist-info/RECORD +0 -76
  65. {velocity_python-0.0.132.dist-info → velocity_python-0.0.135.dist-info}/WHEEL +0 -0
  66. {velocity_python-0.0.132.dist-info → velocity_python-0.0.135.dist-info}/licenses/LICENSE +0 -0
  67. {velocity_python-0.0.132.dist-info → velocity_python-0.0.135.dist-info}/top_level.txt +0 -0
velocity/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.132"
1
+ __version__ = version = "0.0.135"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -0,0 +1 @@
1
+ # App module tests
@@ -0,0 +1,112 @@
1
+ import unittest
2
+ from email.mime.text import MIMEText
3
+ from email.mime.multipart import MIMEMultipart
4
+ from email.mime.application import MIMEApplication
5
+ from velocity.misc.mail import (
6
+ Attachment,
7
+ get_full_emails,
8
+ get_address_only,
9
+ parse_attachment,
10
+ parse,
11
+ )
12
+ import hashlib
13
+
14
+
15
+ class TestEmailProcessing(unittest.TestCase):
16
+
17
+ def test_get_full_emails(self):
18
+ """Test that email addresses with names are formatted correctly."""
19
+ mock_address = [
20
+ type(
21
+ "Address",
22
+ (),
23
+ {"mailbox": b"john", "host": b"example.com", "name": b"John Doe"},
24
+ ),
25
+ type(
26
+ "Address",
27
+ (),
28
+ {"mailbox": b"jane", "host": b"example.com", "name": None},
29
+ ),
30
+ ]
31
+ result = get_full_emails(mock_address)
32
+ self.assertEqual(result, ["John Doe <john@example.com>", "jane@example.com"])
33
+
34
+ def test_get_address_only(self):
35
+ """Test that only email addresses without names are returned."""
36
+ mock_address = [
37
+ type("Address", (), {"mailbox": b"john", "host": b"example.com"}),
38
+ type("Address", (), {"mailbox": b"jane", "host": b"example.com"}),
39
+ ]
40
+ result = get_address_only(mock_address)
41
+ self.assertEqual(result, ["john@example.com", "jane@example.com"])
42
+
43
+ def test_attachment_initialization(self):
44
+ """Test the initialization of the Attachment class."""
45
+ data = b"file data"
46
+ attachment = Attachment(name="file.txt", data=data)
47
+ self.assertEqual(attachment.name, "file.txt")
48
+ self.assertEqual(attachment.data, data)
49
+ self.assertEqual(attachment.size, len(data))
50
+ self.assertEqual(attachment.ctype, "text/plain")
51
+ self.assertEqual(attachment.hash, hashlib.sha1(data).hexdigest())
52
+
53
+ def test_parse_attachment(self):
54
+ """Test parsing a valid attachment from a message part."""
55
+ part = MIMEApplication(b"Test file data", Name="test.txt")
56
+ part["Content-Disposition"] = 'attachment; filename="test.txt"'
57
+ attachment = parse_attachment(part)
58
+ self.assertIsInstance(attachment, Attachment)
59
+ self.assertEqual(attachment.name, "test.txt")
60
+ self.assertEqual(attachment.ctype, "text/plain")
61
+ self.assertEqual(attachment.data, b"Test file data")
62
+
63
+ def test_parse_attachment_none(self):
64
+ """Test that parse_attachment returns None if there's no attachment."""
65
+ part = MIMEText("This is a plain text email part")
66
+ self.assertIsNone(parse_attachment(part))
67
+
68
+ def test_parse_plain_text_email(self):
69
+ """Test parsing a plain text email."""
70
+ msg = MIMEText("This is a plain text email")
71
+ msg["Content-Type"] = "text/plain"
72
+ result = parse(msg.as_string())
73
+ self.assertEqual(result["body"], "This is a plain text email")
74
+ self.assertIsNone(result["html"])
75
+ self.assertEqual(result["attachments"], [])
76
+
77
+ def test_parse_html_email(self):
78
+ """Test parsing an HTML email."""
79
+ msg = MIMEText("<p>This is an HTML email</p>", "html")
80
+ msg["Content-Type"] = "text/html"
81
+ result = parse(msg.as_string())
82
+ self.assertEqual(result["html"], "<p>This is an HTML email</p>")
83
+ self.assertIsNone(result["body"])
84
+ self.assertEqual(result["attachments"], [])
85
+
86
+ def test_parse_multipart_email_with_attachments(self):
87
+ """Test parsing a multipart email with attachments."""
88
+ msg = MIMEMultipart()
89
+ msg.attach(MIMEText("This is a plain text part", "plain"))
90
+ msg.attach(MIMEText("<p>This is an HTML part</p>", "html"))
91
+
92
+ attachment_part = MIMEApplication(b"Attachment data", Name="attachment.txt")
93
+ attachment_part["Content-Disposition"] = 'attachment; filename="attachment.txt"'
94
+ msg.attach(attachment_part)
95
+
96
+ result = parse(msg.as_string())
97
+ self.assertEqual(result["body"], "This is a plain text part")
98
+ self.assertEqual(result["html"], "<p>This is an HTML part</p>")
99
+ self.assertEqual(len(result["attachments"]), 1)
100
+ self.assertEqual(result["attachments"][0].name, "attachment.txt")
101
+ self.assertEqual(result["attachments"][0].data, b"Attachment data")
102
+
103
+ def test_parse_empty_email(self):
104
+ """Test parsing an empty email string."""
105
+ result = parse("")
106
+ self.assertIsNone(result["body"])
107
+ self.assertIsNone(result["html"])
108
+ self.assertEqual(result["attachments"], [])
109
+
110
+
111
+ if __name__ == "__main__":
112
+ unittest.main()
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test for payment profile sorting with None expiration dates.
4
+
5
+ This addresses the production error:
6
+ TypeError: '<' not supported between instances of 'NoneType' and 'NoneType'
7
+
8
+ The error occurs when sorting payment profiles by expiration_date when some values are None.
9
+ """
10
+
11
+ import unittest
12
+ from datetime import datetime
13
+
14
+
15
+ class TestPaymentProfileSorting(unittest.TestCase):
16
+ """Test sorting payment profiles with various expiration date scenarios."""
17
+
18
+ def setUp(self):
19
+ """Set up test data with various expiration date scenarios."""
20
+ self.payment_profiles = [
21
+ {
22
+ "sys_id": 1,
23
+ "email_address": "test1@example.com",
24
+ "card_number": "1234",
25
+ "expiration_date": "2024-12",
26
+ "status": "active",
27
+ },
28
+ {
29
+ "sys_id": 2,
30
+ "email_address": "test1@example.com",
31
+ "card_number": "1234",
32
+ "expiration_date": "2024-06",
33
+ "status": "active",
34
+ },
35
+ {
36
+ "sys_id": 3,
37
+ "email_address": "test1@example.com",
38
+ "card_number": "1234",
39
+ "expiration_date": None,
40
+ "status": "active",
41
+ },
42
+ {
43
+ "sys_id": 4,
44
+ "email_address": "test1@example.com",
45
+ "card_number": "1234",
46
+ "expiration_date": "2025-01",
47
+ "status": "active",
48
+ },
49
+ {
50
+ "sys_id": 5,
51
+ "email_address": "test1@example.com",
52
+ "card_number": "1234",
53
+ "expiration_date": None,
54
+ "status": "active",
55
+ },
56
+ ]
57
+
58
+ def test_original_error_reproduction(self):
59
+ """Reproduce the original error to confirm it happens."""
60
+ with self.assertRaises(TypeError) as context:
61
+ # This should fail with the original error
62
+ sorted(self.payment_profiles, key=lambda x: x["expiration_date"])
63
+
64
+ self.assertIn(
65
+ "'<' not supported between instances of 'NoneType'", str(context.exception)
66
+ )
67
+
68
+ def test_safe_sorting_with_none_last(self):
69
+ """Test sorting with None values placed at the end."""
70
+
71
+ def safe_sort_key_none_last(row):
72
+ """Sort key that places None values at the end."""
73
+ exp_date = row["expiration_date"]
74
+ if exp_date is None:
75
+ return (1, "") # (1, "") sorts after (0, any_date)
76
+ return (0, exp_date)
77
+
78
+ sorted_profiles = sorted(self.payment_profiles, key=safe_sort_key_none_last)
79
+
80
+ # Check that non-None dates come first and are in order
81
+ non_none_dates = [
82
+ p["expiration_date"]
83
+ for p in sorted_profiles
84
+ if p["expiration_date"] is not None
85
+ ]
86
+ self.assertEqual(non_none_dates, ["2024-06", "2024-12", "2025-01"])
87
+
88
+ # Check that None values come last
89
+ none_dates = [
90
+ p["expiration_date"]
91
+ for p in sorted_profiles
92
+ if p["expiration_date"] is None
93
+ ]
94
+ self.assertEqual(len(none_dates), 2)
95
+
96
+ # Verify the full sort order
97
+ expected_order = ["2024-06", "2024-12", "2025-01", None, None]
98
+ actual_order = [p["expiration_date"] for p in sorted_profiles]
99
+ self.assertEqual(actual_order, expected_order)
100
+
101
+ def test_safe_sorting_with_none_first(self):
102
+ """Test sorting with None values placed at the beginning."""
103
+
104
+ def safe_sort_key_none_first(row):
105
+ """Sort key that places None values at the beginning."""
106
+ exp_date = row["expiration_date"]
107
+ if exp_date is None:
108
+ return (0, "") # (0, "") sorts before (1, any_date)
109
+ return (1, exp_date)
110
+
111
+ sorted_profiles = sorted(self.payment_profiles, key=safe_sort_key_none_first)
112
+
113
+ # Check that None values come first
114
+ none_count = sum(1 for p in sorted_profiles[:2] if p["expiration_date"] is None)
115
+ self.assertEqual(none_count, 2)
116
+
117
+ # Check that non-None dates come after and are in order
118
+ non_none_dates = [
119
+ p["expiration_date"]
120
+ for p in sorted_profiles
121
+ if p["expiration_date"] is not None
122
+ ]
123
+ self.assertEqual(non_none_dates, ["2024-06", "2024-12", "2025-01"])
124
+
125
+ def test_safe_sorting_with_default_date(self):
126
+ """Test sorting with None values replaced by a default date."""
127
+
128
+ def safe_sort_key_with_default(row):
129
+ """Sort key that replaces None with a default date."""
130
+ exp_date = row["expiration_date"]
131
+ if exp_date is None:
132
+ return "1900-01" # Very old date so None values sort first
133
+ return exp_date
134
+
135
+ sorted_profiles = sorted(self.payment_profiles, key=safe_sort_key_with_default)
136
+
137
+ # Check that None values (now "1900-01") come first
138
+ none_count = sum(1 for p in sorted_profiles[:2] if p["expiration_date"] is None)
139
+ self.assertEqual(none_count, 2)
140
+
141
+ # Check the overall order
142
+ actual_order = [p["expiration_date"] for p in sorted_profiles]
143
+ expected_order = [None, None, "2024-06", "2024-12", "2025-01"]
144
+ self.assertEqual(actual_order, expected_order)
145
+
146
+ def test_grouped_sorting_scenario(self):
147
+ """Test the specific scenario from the billing handler."""
148
+ # Group profiles by email and card number (as in the original code)
149
+ groups = {}
150
+ for row in self.payment_profiles:
151
+ key = (row["email_address"], row["card_number"])
152
+ if key not in groups:
153
+ groups[key] = []
154
+ groups[key].append(row)
155
+
156
+ # Process each group with safe sorting
157
+ for group in groups.values():
158
+ # This should not raise an error
159
+ def safe_sort_key(row):
160
+ exp_date = row["expiration_date"]
161
+ return (exp_date is None, exp_date or "")
162
+
163
+ sorted_group = sorted(group, key=safe_sort_key)
164
+
165
+ # Verify we can enumerate through the sorted group
166
+ for idx, row in enumerate(sorted_group):
167
+ self.assertIsInstance(idx, int)
168
+ self.assertIn("sys_id", row)
169
+ self.assertIn("expiration_date", row)
170
+
171
+
172
+ def safe_expiration_date_sort_key(row):
173
+ """
174
+ Safe sorting key for payment profiles by expiration date.
175
+
176
+ Handles None values by placing them at the end of the sort order.
177
+
178
+ Args:
179
+ row: Dictionary representing a payment profile with 'expiration_date' key
180
+
181
+ Returns:
182
+ Tuple that can be safely sorted, with None values last
183
+ """
184
+ exp_date = row["expiration_date"]
185
+ if exp_date is None:
186
+ return (1, "") # (1, "") sorts after (0, any_date)
187
+ return (0, exp_date)
188
+
189
+
190
+ if __name__ == "__main__":
191
+ unittest.main()
@@ -0,0 +1,124 @@
1
+ import unittest
2
+ import base64
3
+ from io import BytesIO
4
+ from openpyxl import load_workbook
5
+ from velocity.misc.export import (
6
+ extract,
7
+ autosize_columns,
8
+ create_spreadsheet,
9
+ get_downloadable_spreadsheet,
10
+ )
11
+
12
+
13
+ class TestSpreadsheetFunctions(unittest.TestCase):
14
+
15
+ def test_extract(self):
16
+ """Test extracting values from a dictionary based on a list of keys."""
17
+ data = {"name": "Alice", "age": 30, "city": "Wonderland"}
18
+ keys = ["name", "city", "nonexistent_key"]
19
+ result = extract(data, keys)
20
+ self.assertEqual(result, ["Alice", "Wonderland", None])
21
+
22
+ def test_autosize_columns(self):
23
+ """Test that the columns are autosized based on the content."""
24
+ buffer = BytesIO()
25
+ headers = ["Column1", "Column2"]
26
+ rows = [["Short", "This is a longer text that should set the column width"]]
27
+
28
+ # Create a workbook and worksheet
29
+ create_spreadsheet(headers, rows, buffer)
30
+ buffer.seek(0)
31
+ workbook = load_workbook(buffer)
32
+ worksheet = workbook.active
33
+
34
+ # Run autosize_columns on the loaded worksheet
35
+ autosize_columns(worksheet)
36
+
37
+ # Check if the column widths were adjusted properly
38
+ col1_width = worksheet.column_dimensions["A"].width
39
+ col2_width = worksheet.column_dimensions["B"].width
40
+ self.assertGreater(col2_width, col1_width)
41
+
42
+ def test_create_spreadsheet_basic(self):
43
+ """Test creating a spreadsheet with headers and rows."""
44
+ buffer = BytesIO()
45
+ headers = ["Header1", "Header2"]
46
+ rows = [["Row1-Col1", "Row1-Col2"], ["Row2-Col1", "Row2-Col2"]]
47
+
48
+ create_spreadsheet(headers, rows, buffer)
49
+ buffer.seek(0)
50
+ workbook = load_workbook(buffer)
51
+ worksheet = workbook.active
52
+
53
+ # Verify headers and rows
54
+ self.assertEqual(worksheet["A1"].value, "Header1")
55
+ self.assertEqual(worksheet["B1"].value, "Header2")
56
+ self.assertEqual(worksheet["A2"].value, "Row1-Col1")
57
+ self.assertEqual(worksheet["B2"].value, "Row1-Col2")
58
+ self.assertEqual(worksheet["A3"].value, "Row2-Col1")
59
+ self.assertEqual(worksheet["B3"].value, "Row2-Col2")
60
+
61
+ def test_create_spreadsheet_with_styles_and_merge(self):
62
+ """Test creating a spreadsheet with custom styles and merged cells."""
63
+ buffer = BytesIO()
64
+ headers = ["Header1", "Header2"]
65
+ rows = [["Row1-Col1", "Row1-Col2"]]
66
+ styles = {"A1": "col_header", "B1": "col_header"}
67
+ merge = ["A1:B1"]
68
+
69
+ create_spreadsheet(headers, rows, buffer, styles=styles, merge=merge)
70
+ buffer.seek(0)
71
+ workbook = load_workbook(buffer)
72
+ worksheet = workbook.active
73
+
74
+ # Verify merged cells and styles
75
+ self.assertTrue(worksheet.merged_cells.ranges)
76
+ self.assertEqual(str(worksheet.merged_cells.ranges[0]), "A1:B1")
77
+ self.assertEqual(worksheet["A1"].style, "col_header")
78
+ self.assertEqual(worksheet["B1"].style, "col_header")
79
+
80
+ def test_create_spreadsheet_with_freeze_panes_and_dimensions(self):
81
+ """Test creating a spreadsheet with freeze panes and custom row/column dimensions."""
82
+ buffer = BytesIO()
83
+ headers = ["Header1", "Header2"]
84
+ rows = [["Row1-Col1", "Row1-Col2"]]
85
+ dimensions = {"rows": {1: 25}, "columns": {"A": 20, "B": 30}}
86
+
87
+ create_spreadsheet(
88
+ headers, rows, buffer, freeze_panes="A2", dimensions=dimensions
89
+ )
90
+ buffer.seek(0)
91
+ workbook = load_workbook(buffer)
92
+ worksheet = workbook.active
93
+
94
+ # Verify freeze panes and custom dimensions
95
+ self.assertEqual(worksheet.freeze_panes, "A2")
96
+ self.assertEqual(worksheet.row_dimensions[1].height, 25)
97
+ self.assertEqual(worksheet.column_dimensions["A"].width, 20)
98
+ self.assertEqual(worksheet.column_dimensions["B"].width, 30)
99
+
100
+ def test_get_downloadable_spreadsheet(self):
101
+ """Test generating a downloadable spreadsheet encoded in base64."""
102
+ headers = ["Header1", "Header2"]
103
+ rows = [["Row1-Col1", "Row1-Col2"], ["Row2-Col1", "Row2-Col2"]]
104
+
105
+ # Generate the base64-encoded spreadsheet
106
+ encoded_spreadsheet = get_downloadable_spreadsheet(headers, rows)
107
+ decoded_data = base64.b64decode(encoded_spreadsheet)
108
+
109
+ # Load the spreadsheet from the decoded data and verify its content
110
+ buffer = BytesIO(decoded_data)
111
+ workbook = load_workbook(buffer)
112
+ worksheet = workbook.active
113
+
114
+ # Verify headers and rows
115
+ self.assertEqual(worksheet["A1"].value, "Header1")
116
+ self.assertEqual(worksheet["B1"].value, "Header2")
117
+ self.assertEqual(worksheet["A2"].value, "Row1-Col1")
118
+ self.assertEqual(worksheet["B2"].value, "Row1-Col2")
119
+ self.assertEqual(worksheet["A3"].value, "Row2-Col1")
120
+ self.assertEqual(worksheet["B3"].value, "Row2-Col2")
121
+
122
+
123
+ if __name__ == "__main__":
124
+ unittest.main()
@@ -0,0 +1 @@
1
+ # AWS module tests
@@ -0,0 +1,120 @@
1
+ """
2
+ Test Lambda Handler JSON Serialization Fix
3
+
4
+ This test verifies that LambdaHandler.serve() returns a JSON-serializable
5
+ dictionary instead of a Response object, preventing the
6
+ "Object of type method is not JSON serializable" error.
7
+ """
8
+
9
+ import json
10
+ import unittest
11
+ from unittest.mock import MagicMock, patch
12
+ import sys
13
+ import os
14
+
15
+ # Add src to path for testing
16
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
17
+
18
+ from velocity.aws.handlers.lambda_handler import LambdaHandler
19
+ from velocity.aws.handlers.response import Response
20
+
21
+
22
+ class TestLambdaHandlerJSONSerialization(unittest.TestCase):
23
+ """Test cases for Lambda Handler JSON serialization."""
24
+
25
+ def setUp(self):
26
+ """Set up test fixtures."""
27
+ self.test_event = {
28
+ "body": '{"action": "test", "payload": {}}',
29
+ "httpMethod": "POST",
30
+ "headers": {"Content-Type": "application/json"},
31
+ "requestContext": {
32
+ "identity": {
33
+ "sourceIp": "127.0.0.1", # localhost test IP for unit testing
34
+ "userAgent": "test-agent"
35
+ }
36
+ },
37
+ "queryStringParameters": {}
38
+ }
39
+
40
+ self.test_context = MagicMock()
41
+ self.test_context.function_name = "test-function"
42
+
43
+ def test_serve_returns_json_serializable_dict(self):
44
+ """Test that serve() returns a JSON-serializable dictionary."""
45
+
46
+ # Create handler
47
+ handler = LambdaHandler(self.test_event, self.test_context)
48
+
49
+ # Mock the transaction decorator to pass through tx
50
+ with patch('velocity.aws.handlers.lambda_handler.engine') as mock_engine:
51
+ def mock_transaction(func):
52
+ def wrapper(*args, **kwargs):
53
+ mock_tx = MagicMock()
54
+ return func(mock_tx, *args, **kwargs)
55
+ return wrapper
56
+
57
+ mock_engine.transaction = mock_transaction
58
+
59
+ # Call serve method
60
+ result = handler.serve(MagicMock())
61
+
62
+ # Verify result is a dictionary (JSON-serializable)
63
+ self.assertIsInstance(result, dict)
64
+
65
+ # Verify it has the expected Lambda response structure
66
+ self.assertIn("statusCode", result)
67
+ self.assertIn("headers", result)
68
+ self.assertIn("body", result)
69
+
70
+ # Verify the body is a JSON string
71
+ self.assertIsInstance(result["body"], str)
72
+
73
+ # Verify the entire result can be JSON serialized
74
+ try:
75
+ json.dumps(result)
76
+ except (TypeError, ValueError) as e:
77
+ self.fail(f"Result is not JSON serializable: {e}")
78
+
79
+ def test_response_object_has_render_method(self):
80
+ """Test that Response object has a proper render method."""
81
+ response = Response()
82
+
83
+ # Verify render method exists
84
+ self.assertTrue(hasattr(response, 'render'))
85
+ self.assertTrue(callable(response.render))
86
+
87
+ # Verify render returns a dictionary
88
+ rendered = response.render()
89
+ self.assertIsInstance(rendered, dict)
90
+
91
+ # Verify structure
92
+ self.assertIn("statusCode", rendered)
93
+ self.assertIn("headers", rendered)
94
+ self.assertIn("body", rendered)
95
+
96
+ # Verify JSON serializable
97
+ try:
98
+ json.dumps(rendered)
99
+ except (TypeError, ValueError) as e:
100
+ self.fail(f"Rendered response is not JSON serializable: {e}")
101
+
102
+ def test_response_render_vs_raw_object(self):
103
+ """Test the difference between Response object and rendered response."""
104
+ response = Response()
105
+
106
+ # Raw response object should not be directly JSON serializable
107
+ # (it contains method references)
108
+ with self.assertRaises((TypeError, ValueError)):
109
+ json.dumps(response)
110
+
111
+ # But rendered response should be JSON serializable
112
+ rendered = response.render()
113
+ try:
114
+ json.dumps(rendered)
115
+ except (TypeError, ValueError) as e:
116
+ self.fail(f"Rendered response should be JSON serializable: {e}")
117
+
118
+
119
+ if __name__ == '__main__':
120
+ unittest.main()