roksta 0.3.2__cp311-cp311-macosx_10_9_universal2.whl → 0.3.8__cp311-cp311-macosx_10_9_universal2.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.
Files changed (131) hide show
  1. roksta/__init__.cpython-311-darwin.so +0 -0
  2. roksta/ai/__init__.cpython-311-darwin.so +0 -0
  3. roksta/ai/call_ai.cpython-311-darwin.so +0 -0
  4. roksta/ai/gemini.cpython-311-darwin.so +0 -0
  5. roksta/ai/generic.cpython-311-darwin.so +0 -0
  6. roksta/ai/llm.cpython-311-darwin.so +0 -0
  7. roksta/ai/openai.cpython-311-darwin.so +0 -0
  8. roksta/ai/tools/__init__.cpython-311-darwin.so +0 -0
  9. roksta/ai/tools/delete_file.cpython-311-darwin.so +0 -0
  10. roksta/ai/tools/edit_file.cpython-311-darwin.so +0 -0
  11. roksta/ai/tools/final_response.cpython-311-darwin.so +0 -0
  12. roksta/ai/tools/get_file_summaries.cpython-311-darwin.so +0 -0
  13. roksta/ai/tools/read_file.cpython-311-darwin.so +0 -0
  14. roksta/ai/tools/regex_replace.cpython-311-darwin.so +0 -0
  15. roksta/ai/tools/shell_any.cpython-311-darwin.so +0 -0
  16. roksta/ai/tools/shell_limited.cpython-311-darwin.so +0 -0
  17. roksta/ai/tools/tool_defs.cpython-311-darwin.so +0 -0
  18. roksta/ai/tools/tool_utils.cpython-311-darwin.so +0 -0
  19. roksta/ai/tools/web_fetch.cpython-311-darwin.so +0 -0
  20. roksta/ai/tools/write_file.cpython-311-darwin.so +0 -0
  21. roksta/analytics.cpython-311-darwin.so +0 -0
  22. roksta/balance.cpython-311-darwin.so +0 -0
  23. roksta/build_project.cpython-311-darwin.so +0 -0
  24. roksta/chat_workflow.cpython-311-darwin.so +0 -0
  25. roksta/check_for_updates.cpython-311-darwin.so +0 -0
  26. roksta/check_subtask_sequence.cpython-311-darwin.so +0 -0
  27. roksta/checkpoints.cpython-311-darwin.so +0 -0
  28. roksta/clarify_goal.cpython-311-darwin.so +0 -0
  29. roksta/codebase_listing.cpython-311-darwin.so +0 -0
  30. roksta/command_handlers/__init__.cpython-311-darwin.so +0 -0
  31. roksta/command_handlers/handle_activate_command.cpython-311-darwin.so +0 -0
  32. roksta/command_handlers/handle_add_funds_command.cpython-311-darwin.so +0 -0
  33. roksta/command_handlers/handle_auto_charge_command.cpython-311-darwin.so +0 -0
  34. roksta/command_handlers/handle_auto_commit_command.cpython-311-darwin.so +0 -0
  35. roksta/command_handlers/handle_building_command.cpython-311-darwin.so +0 -0
  36. roksta/command_handlers/handle_chat_command.cpython-311-darwin.so +0 -0
  37. roksta/command_handlers/handle_dev_rate_command.cpython-311-darwin.so +0 -0
  38. roksta/command_handlers/handle_feedback_command.cpython-311-darwin.so +0 -0
  39. roksta/command_handlers/handle_goal_command.cpython-311-darwin.so +0 -0
  40. roksta/command_handlers/handle_help_command.cpython-311-darwin.so +0 -0
  41. roksta/command_handlers/handle_init_command.cpython-311-darwin.so +0 -0
  42. roksta/command_handlers/handle_linting_command.cpython-311-darwin.so +0 -0
  43. roksta/command_handlers/handle_login_command.cpython-311-darwin.so +0 -0
  44. roksta/command_handlers/handle_logout_command.cpython-311-darwin.so +0 -0
  45. roksta/command_handlers/handle_payment_details_command.cpython-311-darwin.so +0 -0
  46. roksta/command_handlers/handle_quit_command.cpython-311-darwin.so +0 -0
  47. roksta/command_handlers/handle_redeem_command.cpython-311-darwin.so +0 -0
  48. roksta/command_handlers/handle_request_activation_command.cpython-311-darwin.so +0 -0
  49. roksta/command_handlers/handle_testing_command.cpython-311-darwin.so +0 -0
  50. roksta/command_handlers/handle_usage_command.cpython-311-darwin.so +0 -0
  51. roksta/create_default_config.cpython-311-darwin.so +0 -0
  52. roksta/create_default_ignore_file.cpython-311-darwin.so +0 -0
  53. roksta/default_config.cpython-311-darwin.so +0 -0
  54. roksta/default_ignores.cpython-311-darwin.so +0 -0
  55. roksta/discover_test_command.cpython-311-darwin.so +0 -0
  56. roksta/enums.cpython-311-darwin.so +0 -0
  57. roksta/env.cpython-311-darwin.so +0 -0
  58. roksta/extended_text_area.cpython-311-darwin.so +0 -0
  59. roksta/firebase.cpython-311-darwin.so +0 -0
  60. roksta/firebase_auth_web.cpython-311-darwin.so +0 -0
  61. roksta/firebase_config.cpython-311-darwin.so +0 -0
  62. roksta/fix_tests.cpython-311-darwin.so +0 -0
  63. roksta/gen_codebase_summaries.cpython-311-darwin.so +0 -0
  64. roksta/gen_one_line_goal.cpython-311-darwin.so +0 -0
  65. roksta/gen_subtasks.cpython-311-darwin.so +0 -0
  66. roksta/get_codebase_structure.cpython-311-darwin.so +0 -0
  67. roksta/get_failing_tests.cpython-311-darwin.so +0 -0
  68. roksta/goal_workflow.cpython-311-darwin.so +0 -0
  69. roksta/init_codebase.cpython-311-darwin.so +0 -0
  70. roksta/lint_code.cpython-311-darwin.so +0 -0
  71. roksta/logger.cpython-311-darwin.so +0 -0
  72. roksta/main.cpython-311-darwin.so +0 -0
  73. roksta/make_issue.cpython-311-darwin.so +0 -0
  74. roksta/new_features.cpython-311-darwin.so +0 -0
  75. roksta/parse_directive_cli_tokens.cpython-311-darwin.so +0 -0
  76. roksta/parse_readme.cpython-311-darwin.so +0 -0
  77. roksta/propose_solution.cpython-311-darwin.so +0 -0
  78. roksta/response_formats.cpython-311-darwin.so +0 -0
  79. roksta/rewrite_goal.cpython-311-darwin.so +0 -0
  80. roksta/roksta.cpython-311-darwin.so +0 -0
  81. roksta/run_cli_goal.cpython-311-darwin.so +0 -0
  82. roksta/save_chat_transcript.cpython-311-darwin.so +0 -0
  83. roksta/select_files.cpython-311-darwin.so +0 -0
  84. roksta/tips.cpython-311-darwin.so +0 -0
  85. roksta/utils.cpython-311-darwin.so +0 -0
  86. roksta/write_code.cpython-311-darwin.so +0 -0
  87. roksta-0.3.8.dist-info/METADATA +471 -0
  88. roksta-0.3.8.dist-info/RECORD +91 -0
  89. {roksta-0.3.2.dist-info → roksta-0.3.8.dist-info}/top_level.txt +0 -1
  90. roksta-0.3.2.dist-info/METADATA +0 -40
  91. roksta-0.3.2.dist-info/RECORD +0 -121
  92. tests/__init__.py +0 -2
  93. tests/conftest.py +0 -211
  94. tests/functions/__init__.py +0 -2
  95. tests/functions/api_v1_00/__init__.py +0 -2
  96. tests/functions/api_v1_00/test__analytics.py +0 -416
  97. tests/functions/api_v1_00/test__gemini_proxy.py +0 -352
  98. tests/functions/api_v1_00/test__generic_proxy.py +0 -428
  99. tests/functions/api_v1_00/test__get_payment_details.py +0 -356
  100. tests/functions/api_v1_00/test__openai_proxy.py +0 -449
  101. tests/functions/api_v1_00/test__redeem_credit_code.py +0 -167
  102. tests/functions/api_v1_00/test__sync_emails.py +0 -325
  103. tests/functions/api_v1_00/test__take_payment.py +0 -491
  104. tests/functions/api_v1_00/test__use_activation_code.py +0 -438
  105. tests/functions/api_v1_01/__init__.py +0 -2
  106. tests/functions/api_v1_01/test__analytics.py +0 -416
  107. tests/functions/api_v1_01/test__gemini_proxy.py +0 -352
  108. tests/functions/api_v1_01/test__generic_proxy.py +0 -428
  109. tests/functions/api_v1_01/test__get_payment_details.py +0 -356
  110. tests/functions/api_v1_01/test__openai_proxy.py +0 -449
  111. tests/functions/api_v1_01/test__redeem_credit_code.py +0 -167
  112. tests/functions/api_v1_01/test__sync_emails.py +0 -325
  113. tests/functions/api_v1_01/test__take_payment.py +0 -491
  114. tests/functions/api_v1_01/test__use_activation_code.py +0 -438
  115. tests/functions/api_v1_02/__init__.py +0 -2
  116. tests/functions/api_v1_02/test__analytics.py +0 -416
  117. tests/functions/api_v1_02/test__gemini_proxy.py +0 -352
  118. tests/functions/api_v1_02/test__generic_proxy.py +0 -428
  119. tests/functions/api_v1_02/test__get_payment_details.py +0 -356
  120. tests/functions/api_v1_02/test__openai_proxy.py +0 -449
  121. tests/functions/api_v1_02/test__redeem_credit_code.py +0 -167
  122. tests/functions/api_v1_02/test__sync_emails.py +0 -325
  123. tests/functions/api_v1_02/test__take_payment.py +0 -491
  124. tests/functions/api_v1_02/test__use_activation_code.py +0 -438
  125. tests/functions/api_v1_02/test_proxy_keyword_replacement.py +0 -557
  126. tests/functions/api_v1_02/test_replace_keywords.py +0 -74
  127. tests/functions/test_auth.py +0 -24
  128. tests/functions/test_main.py +0 -73
  129. tests/functions/test_utils.py +0 -484
  130. {roksta-0.3.2.dist-info → roksta-0.3.8.dist-info}/WHEEL +0 -0
  131. {roksta-0.3.2.dist-info → roksta-0.3.8.dist-info}/entry_points.txt +0 -0
@@ -1,491 +0,0 @@
1
- import os
2
- import sys
3
- import json
4
- import types
5
- import importlib
6
- from unittest.mock import patch
7
-
8
-
9
- # Ensure the functions/ directory is importable as a top-level module location
10
- PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
11
- FUNCTIONS_DIR = os.path.join(PROJECT_ROOT, 'functions')
12
- if FUNCTIONS_DIR not in sys.path:
13
- sys.path.insert(0, FUNCTIONS_DIR)
14
-
15
- # -----------------------------
16
- # Provide lightweight fake modules to satisfy imports inside _take_payment
17
- # These are minimal and will be patched in individual tests as needed.
18
- # -----------------------------
19
- # Save original sys.modules entries so we can restore them after importing the module under test
20
- _orig_sys_modules = {}
21
- _names_to_fake = [
22
- 'firebase_functions',
23
- 'utils',
24
- 'auth',
25
- 'firebase_admin',
26
- 'firebase_admin.auth',
27
- 'firebase_admin.firestore',
28
- 'stripe',
29
- 'ulid',
30
- ]
31
- for name in _names_to_fake:
32
- _orig_sys_modules[name] = sys.modules.get(name)
33
-
34
- # Fake firebase_functions.https_fn.Response to capture returned data
35
- firebase_functions = types.ModuleType('firebase_functions')
36
-
37
- class FakeResponse:
38
- def __init__(self, response=None, mimetype=None, status=200, **kwargs):
39
- # Mirror the small subset of the interface tests expect
40
- self.status_code = status
41
- if isinstance(response, (dict, list)):
42
- self._body_text = json.dumps(response)
43
- else:
44
- self._body_text = '' if response is None else response
45
- self.headers = kwargs.get('headers', {})
46
-
47
- def get_data(self, as_text=False):
48
- if as_text:
49
- return self._body_text
50
- return self._body_text.encode('utf-8')
51
-
52
- firebase_functions.https_fn = types.SimpleNamespace(Request=object, Response=FakeResponse)
53
- sys.modules['firebase_functions'] = firebase_functions
54
-
55
- # Fake utils module (create_json_response, get_secret_key, verify_firebase_token)
56
- utils_mod = types.ModuleType('utils')
57
-
58
-
59
- def _fake_create_json_response(success: bool, payload: any, status_code: int):
60
- response_body = {"success": success, "payload": payload}
61
- return firebase_functions.https_fn.Response(response=response_body, status=status_code, headers={'Content-Type': 'application/json'})
62
-
63
-
64
- def _fake_get_secret_key(name: str) -> str:
65
- return 'DUMMY_STRIPE_KEY'
66
-
67
-
68
- def _fake_verify_firebase_token(req):
69
- # Default: successful verification returning a uid
70
- return {'uid': 'user_1'}
71
-
72
-
73
- utils_mod.create_json_response = _fake_create_json_response
74
- utils_mod.get_secret_key = _fake_get_secret_key
75
- utils_mod.verify_firebase_token = _fake_verify_firebase_token
76
- sys.modules['utils'] = utils_mod
77
-
78
- # Fake top-level auth module (validate_auth_key will be patched per-test as needed)
79
- auth_mod = types.ModuleType('auth')
80
-
81
- def _simple_validate_auth_key(val: str) -> bool:
82
- return True
83
-
84
-
85
- auth_mod.validate_auth_key = _simple_validate_auth_key
86
- sys.modules['auth'] = auth_mod
87
-
88
- # Fake firebase_admin.auth and firebase_admin.firestore
89
- firebase_admin_mod = types.ModuleType('firebase_admin')
90
-
91
- # firebase_admin.auth fake
92
- fauth_mod = types.ModuleType('firebase_admin.auth')
93
-
94
- def _fake_get_user(uid):
95
- return types.SimpleNamespace(email='user@example.com')
96
-
97
- fauth_mod.get_user = _fake_get_user
98
- sys.modules['firebase_admin.auth'] = fauth_mod
99
- firebase_admin_mod.auth = fauth_mod
100
-
101
- # In-memory fake DB used by the fake Firestore implementation
102
- fake_db_data = {}
103
-
104
- # Fake Firestore implementation
105
- class DocumentSnapshot:
106
- def __init__(self, exists, data):
107
- self.exists = exists
108
- self._data = data
109
-
110
- def to_dict(self):
111
- return self._data
112
-
113
- def get(self, key):
114
- if not self._data:
115
- return None
116
- return self._data.get(key)
117
-
118
-
119
- class DocumentRef:
120
- def __init__(self, collection_name, doc_id):
121
- self.collection = collection_name
122
- self.doc_id = doc_id
123
-
124
- def get(self, transaction=None):
125
- coll = fake_db_data.get(self.collection, {})
126
- if self.doc_id in coll:
127
- return DocumentSnapshot(True, coll[self.doc_id])
128
- return DocumentSnapshot(False, None)
129
-
130
- def update(self, update_dict):
131
- coll = fake_db_data.setdefault(self.collection, {})
132
- doc = coll.setdefault(self.doc_id, {})
133
- if isinstance(doc, dict):
134
- doc.update(update_dict)
135
- else:
136
- coll[self.doc_id] = update_dict
137
-
138
- def set(self, data):
139
- coll = fake_db_data.setdefault(self.collection, {})
140
- coll[self.doc_id] = data
141
-
142
-
143
- class CollectionRef:
144
- def __init__(self, name):
145
- self.name = name
146
-
147
- def document(self, doc_id):
148
- return DocumentRef(self.name, doc_id)
149
-
150
-
151
- class FakeTransaction:
152
- def __init__(self):
153
- pass
154
-
155
- def update(self, doc_ref, update_dict):
156
- coll = fake_db_data.setdefault(doc_ref.collection, {})
157
- doc = coll.setdefault(doc_ref.doc_id, {})
158
- if isinstance(doc, dict):
159
- doc.update(update_dict)
160
- else:
161
- coll[doc_ref.doc_id] = update_dict
162
-
163
- def set(self, doc_ref, data):
164
- coll = fake_db_data.setdefault(doc_ref.collection, {})
165
- coll[doc_ref.doc_id] = data
166
-
167
-
168
- class FakeFirestoreClient:
169
- def __init__(self):
170
- pass
171
-
172
- def collection(self, name):
173
- return CollectionRef(name)
174
-
175
- def transaction(self):
176
- return FakeTransaction()
177
-
178
-
179
- fake_firestore_mod = types.ModuleType('firebase_admin.firestore')
180
- fake_firestore_mod.client = lambda: FakeFirestoreClient()
181
- fake_firestore_mod.transactional = lambda f: f
182
- fake_firestore_mod.SERVER_TIMESTAMP = object()
183
- sys.modules['firebase_admin.firestore'] = fake_firestore_mod
184
- firebase_admin_mod.firestore = fake_firestore_mod
185
- sys.modules['firebase_admin'] = firebase_admin_mod
186
-
187
- # Fake stripe module
188
- stripe_mod = types.ModuleType('stripe')
189
-
190
- class StripeError(Exception):
191
- pass
192
-
193
- stripe_mod.error = types.SimpleNamespace(StripeError=StripeError)
194
- stripe_mod.api_key = None
195
-
196
- class _PaymentIntent:
197
- @staticmethod
198
- def create(**kwargs):
199
- raise NotImplementedError("PaymentIntent.create not implemented for test stub")
200
-
201
- stripe_mod.PaymentIntent = _PaymentIntent
202
- sys.modules['stripe'] = stripe_mod
203
-
204
- # Fake ulid module
205
- ulid_mod = types.ModuleType('ulid')
206
- ulid_mod.new = lambda: 'ULID123'
207
- sys.modules['ulid'] = ulid_mod
208
-
209
- # -----------------------------
210
- # Import the module under test after preparing the fake imports
211
- repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
212
- functions_root = os.path.join(repo_root, 'functions')
213
- module_path = os.path.join(functions_root, 'api_v1_00', '_take_payment.py')
214
- spec = importlib.util.spec_from_file_location('api_v1_00._take_payment', module_path)
215
- _take_payment = importlib.util.module_from_spec(spec)
216
- spec.loader.exec_module(_take_payment)
217
-
218
-
219
- # Restore original sys.modules mappings to avoid side-effects for other tests
220
- for name, orig in _orig_sys_modules.items():
221
- if orig is None:
222
- try:
223
- del sys.modules[name]
224
- except KeyError:
225
- pass
226
- else:
227
- sys.modules[name] = orig
228
-
229
- # Import env constants for auth header usage in tests
230
- import env as functions_env # noqa: E402
231
- AUTH_HEADER_NAME = functions_env.AUTH_HEADER_NAME
232
-
233
-
234
- # Simple helper request stub used in the tests
235
- class DummyRequest:
236
- def __init__(self, headers=None, method='POST', json_data=None, raise_on_get_json=False):
237
- self.headers = headers or {}
238
- self.method = method
239
- self._json_data = json_data
240
- self._raise = raise_on_get_json
241
-
242
- def get_json(self, silent=True):
243
- if self._raise:
244
- raise Exception('Malformed JSON')
245
- return self._json_data
246
-
247
-
248
- def _parse_response(resp):
249
- data = resp.get_data(as_text=True)
250
- return json.loads(data)
251
-
252
-
253
- # -----------------------------
254
- # Tests
255
- # -----------------------------
256
-
257
-
258
- def test_missing_auth_header_returns_401():
259
- req = DummyRequest(headers={}, method='POST')
260
- resp = _take_payment._take_payment(req)
261
-
262
- assert resp.status_code == 401
263
- payload = _parse_response(resp)
264
- assert payload['success'] is False
265
- assert 'Missing app authentication key' in payload['payload']
266
-
267
-
268
- def test_invalid_auth_key_returns_403():
269
- headers = {AUTH_HEADER_NAME: 'bad-token'}
270
- req = DummyRequest(headers=headers, method='POST')
271
- with patch.object(_take_payment, 'validate_auth_key', return_value=False):
272
- resp = _take_payment._take_payment(req)
273
-
274
- assert resp.status_code == 403
275
- payload = _parse_response(resp)
276
- assert payload['success'] is False
277
- assert 'Invalid app authentication key' in payload['payload']
278
-
279
-
280
- def test_verify_firebase_token_failure_returns_403():
281
- headers = {AUTH_HEADER_NAME: 'ok'}
282
- req = DummyRequest(headers=headers, method='POST')
283
- with patch.object(_take_payment, 'validate_auth_key', return_value=True), patch.object(_take_payment, 'verify_firebase_token', side_effect=Exception('boom')):
284
- resp = _take_payment._take_payment(req)
285
-
286
- assert resp.status_code == 403
287
- payload = _parse_response(resp)
288
- assert payload['success'] is False
289
- assert 'Authentication failed' in payload['payload']
290
-
291
-
292
- def test_missing_json_payload_returns_400():
293
- headers = {AUTH_HEADER_NAME: 'ok'}
294
- req = DummyRequest(headers=headers, method='POST', json_data=None)
295
- with patch.object(_take_payment, 'validate_auth_key', return_value=True), patch.object(_take_payment, 'verify_firebase_token', return_value={'uid': 'user_1'}):
296
- resp = _take_payment._take_payment(req)
297
-
298
- assert resp.status_code == 400
299
- payload = _parse_response(resp)
300
- assert payload['success'] is False
301
- assert 'No JSON payload provided' in payload['payload']
302
-
303
-
304
- def test_missing_amount_returns_400():
305
- headers = {AUTH_HEADER_NAME: 'ok'}
306
- # Provide a non-empty JSON object so get_json() returns a truthy value but without 'amount'
307
- req = DummyRequest(headers=headers, method='POST', json_data={'foo': 'bar'})
308
- with patch.object(_take_payment, 'validate_auth_key', return_value=True), patch.object(_take_payment, 'verify_firebase_token', return_value={'uid': 'user_1'}):
309
- resp = _take_payment._take_payment(req)
310
-
311
- assert resp.status_code == 400
312
- payload = _parse_response(resp)
313
- assert payload['success'] is False
314
- assert 'Missing amount' in payload['payload']
315
-
316
-
317
- def test_invalid_amount_returns_400():
318
- headers = {AUTH_HEADER_NAME: 'ok'}
319
- req = DummyRequest(headers=headers, method='POST', json_data={'amount': 'not-a-number'})
320
- with patch.object(_take_payment, 'validate_auth_key', return_value=True), patch.object(_take_payment, 'verify_firebase_token', return_value={'uid': 'user_1'}):
321
- resp = _take_payment._take_payment(req)
322
-
323
- assert resp.status_code == 400
324
- payload = _parse_response(resp)
325
- assert payload['success'] is False
326
- assert 'Invalid amount value provided' in payload['payload']
327
-
328
-
329
- def test_billing_doc_missing_returns_404():
330
- fake_db_data.clear()
331
- headers = {AUTH_HEADER_NAME: 'ok'}
332
- req = DummyRequest(headers=headers, method='POST', json_data={'amount': '10'})
333
- with patch.object(_take_payment, 'validate_auth_key', return_value=True), patch.object(_take_payment, 'verify_firebase_token', return_value={'uid': 'user_1'}):
334
- resp = _take_payment._take_payment(req)
335
-
336
- assert resp.status_code == 404
337
- payload = _parse_response(resp)
338
- assert payload['success'] is False
339
- assert 'Billing document does not exist' in payload['payload']
340
-
341
-
342
- def test_missing_billing_fields_returns_400():
343
- fake_db_data.clear()
344
- fake_db_data.setdefault('billing', {})['user_1'] = {}
345
-
346
- headers = {AUTH_HEADER_NAME: 'ok'}
347
- req = DummyRequest(headers=headers, method='POST', json_data={'amount': '10'})
348
-
349
- with patch.object(_take_payment, 'validate_auth_key', return_value=True), patch.object(_take_payment, 'verify_firebase_token', return_value={'uid': 'user_1'}):
350
- resp = _take_payment._take_payment(req)
351
-
352
- assert resp.status_code == 400
353
- payload = _parse_response(resp)
354
- assert payload['success'] is False
355
- assert 'Missing field(s) in billing document' in payload['payload']
356
-
357
-
358
- def test_get_user_failure_returns_400():
359
- fake_db_data.clear()
360
- fake_db_data.setdefault('billing', {})['user_1'] = {
361
- 'stripe_customer_id': 'cus_x',
362
- 'stripe_payment_method_id': 'pm_x',
363
- }
364
-
365
- headers = {AUTH_HEADER_NAME: 'ok'}
366
- req = DummyRequest(headers=headers, method='POST', json_data={'amount': '10'})
367
-
368
- with patch.object(_take_payment, 'validate_auth_key', return_value=True), \
369
- patch.object(_take_payment, 'verify_firebase_token', return_value={'uid': 'user_1'}), \
370
- patch.object(_take_payment.auth, 'get_user', side_effect=Exception('nope')):
371
- resp = _take_payment._take_payment(req)
372
-
373
- assert resp.status_code == 400
374
- payload = _parse_response(resp)
375
- assert payload['success'] is False
376
- assert 'Failed to retrieve user auth record' in payload['payload']
377
-
378
-
379
- def test_get_secret_key_failure_returns_500():
380
- fake_db_data.clear()
381
- fake_db_data.setdefault('billing', {})['user_1'] = {
382
- 'stripe_customer_id': 'cus_x',
383
- 'stripe_payment_method_id': 'pm_x',
384
- }
385
-
386
- headers = {AUTH_HEADER_NAME: 'ok'}
387
- req = DummyRequest(headers=headers, method='POST', json_data={'amount': '10'})
388
-
389
- with patch.object(_take_payment, 'validate_auth_key', return_value=True), \
390
- patch.object(_take_payment, 'verify_firebase_token', return_value={'uid': 'user_1'}), \
391
- patch.object(_take_payment.auth, 'get_user', return_value=types.SimpleNamespace(email='user@example.com')), \
392
- patch.object(_take_payment, 'get_secret_key', side_effect=Exception('boom')):
393
- resp = _take_payment._take_payment(req)
394
-
395
- assert resp.status_code == 500
396
- payload = _parse_response(resp)
397
- assert payload['success'] is False
398
- assert 'Failed to retrieve Stripe configuration' in payload['payload']
399
-
400
-
401
- def test_stripe_raises_StripeError_returns_400():
402
- fake_db_data.clear()
403
- fake_db_data.setdefault('billing', {})['user_1'] = {
404
- 'stripe_customer_id': 'cus_x',
405
- 'stripe_payment_method_id': 'pm_x',
406
- }
407
-
408
- headers = {AUTH_HEADER_NAME: 'ok'}
409
- req = DummyRequest(headers=headers, method='POST', json_data={'amount': '10'})
410
-
411
- with patch.object(_take_payment, 'validate_auth_key', return_value=True), \
412
- patch.object(_take_payment, 'verify_firebase_token', return_value={'uid': 'user_1'}), \
413
- patch.object(_take_payment.auth, 'get_user', return_value=types.SimpleNamespace(email='user@example.com')), \
414
- patch.object(_take_payment, 'get_secret_key', return_value='DUMMY_KEY'), \
415
- patch.object(_take_payment.stripe.PaymentIntent, 'create', side_effect=_take_payment.stripe.error.StripeError('fail')):
416
- resp = _take_payment._take_payment(req)
417
-
418
- assert resp.status_code == 400
419
- payload = _parse_response(resp)
420
- assert payload['success'] is False
421
- assert 'Stripe error during payment processing' in payload['payload']
422
-
423
-
424
- def test_payment_intent_non_succeeded_returns_402():
425
- fake_db_data.clear()
426
- fake_db_data.setdefault('billing', {})['user_1'] = {
427
- 'stripe_customer_id': 'cus_x',
428
- 'stripe_payment_method_id': 'pm_x',
429
- }
430
-
431
- headers = {AUTH_HEADER_NAME: 'ok'}
432
- req = DummyRequest(headers=headers, method='POST', json_data={'amount': '10'})
433
-
434
- fake_resp = types.SimpleNamespace(status='requires_payment_method')
435
-
436
- with patch.object(_take_payment, 'validate_auth_key', return_value=True), \
437
- patch.object(_take_payment, 'verify_firebase_token', return_value={'uid': 'user_1'}), \
438
- patch.object(_take_payment.auth, 'get_user', return_value=types.SimpleNamespace(email='user@example.com')), \
439
- patch.object(_take_payment, 'get_secret_key', return_value='DUMMY_KEY'), \
440
- patch.object(_take_payment.stripe.PaymentIntent, 'create', return_value=fake_resp):
441
- resp = _take_payment._take_payment(req)
442
-
443
- assert resp.status_code == 402
444
- payload = _parse_response(resp)
445
- assert payload['success'] is False
446
- assert 'Payment failed' in payload['payload']
447
-
448
-
449
- def test_successful_payment_updates_balance_and_records_transaction_returns_200():
450
- fake_db_data.clear()
451
- fake_db_data.setdefault('billing', {})['user_1'] = {
452
- 'stripe_customer_id': 'cus_x',
453
- 'stripe_payment_method_id': 'pm_x',
454
- 'balance': 5.0,
455
- }
456
-
457
- headers = {AUTH_HEADER_NAME: 'ok'}
458
- req = DummyRequest(headers=headers, method='POST', json_data={'amount': '10'})
459
-
460
- fake_payment_intent = types.SimpleNamespace(status='succeeded')
461
-
462
- with patch.object(_take_payment, 'validate_auth_key', return_value=True), \
463
- patch.object(_take_payment, 'verify_firebase_token', return_value={'uid': 'user_1'}), \
464
- patch.object(_take_payment.auth, 'get_user', return_value=types.SimpleNamespace(email='user@example.com')), \
465
- patch.object(_take_payment, 'get_secret_key', return_value='DUMMY_KEY'), \
466
- patch.object(_take_payment.stripe.PaymentIntent, 'create', return_value=fake_payment_intent), \
467
- patch.object(_take_payment.ulid, 'new', return_value='ULID123'):
468
- resp = _take_payment._take_payment(req)
469
-
470
- assert resp.status_code == 200
471
- payload = _parse_response(resp)
472
- assert payload['success'] is True
473
- assert 'Payment processed and balance updated successfully' in payload['payload']
474
-
475
- # Check that billing balance was updated
476
- billing_doc = fake_db_data.get('billing', {}).get('user_1')
477
- assert billing_doc is not None
478
- assert billing_doc.get('balance') == 15.0
479
-
480
- # Check that transaction was recorded
481
- transactions = fake_db_data.get('transactions', {})
482
- # Expect a key like 'credit_user_1_ULID123'
483
- matching = [k for k in transactions.keys() if k.startswith('credit_user_1_')]
484
- assert matching, f"No transaction created, transactions keys: {list(transactions.keys())}"
485
- tx = transactions[matching[0]]
486
- assert tx['user_id'] == 'user_1'
487
- assert tx['value'] == 10.0
488
- assert tx['balance'] == 15.0
489
- assert tx['type'] == 'credit'
490
- assert tx['reason'] == 'payment'
491
- assert 'timestamp' in tx