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,449 +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 is three levels up from this test file (tests/functions/api_v1_00/...)
11
- PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
12
- FUNCTIONS_DIR = os.path.join(PROJECT_ROOT, 'functions')
13
- if FUNCTIONS_DIR not in sys.path:
14
- sys.path.insert(0, FUNCTIONS_DIR)
15
-
16
- # Prepare lightweight fake modules to satisfy imports inside _openai_proxy
17
- # We'll temporarily inject these into sys.modules while importing the module
18
-
19
- # Save any originals so we can restore them after import
20
- _orig_sys_modules = {}
21
- _names_to_fake = [
22
- 'firebase_functions',
23
- 'utils',
24
- 'auth',
25
- 'openai',
26
- 'firebase_admin',
27
- 'billing',
28
- ]
29
- for name in _names_to_fake:
30
- _orig_sys_modules[name] = sys.modules.get(name)
31
-
32
- # Fake firebase_functions.https_fn.Response to capture returned data
33
- firebase_functions = types.ModuleType('firebase_functions')
34
-
35
- class FakeResponse:
36
- def __init__(self, response=None, mimetype=None, status=200, **kwargs):
37
- # Mirror the small subset of the interface tests expect
38
- self.status_code = status
39
- if isinstance(response, (dict, list)):
40
- self._body_text = json.dumps(response)
41
- else:
42
- self._body_text = '' if response is None else response
43
- self.headers = kwargs.get('headers', {})
44
-
45
- def get_data(self, as_text=False):
46
- if as_text:
47
- return self._body_text
48
- return self._body_text.encode('utf-8')
49
-
50
- firebase_functions.https_fn = types.SimpleNamespace(Request=object, Response=FakeResponse)
51
- sys.modules['firebase_functions'] = firebase_functions
52
-
53
- # Fake utils module (provides functions imported by _openai_proxy)
54
- utils_mod = types.ModuleType('utils')
55
-
56
-
57
- def _fake_create_json_response(success: bool, payload: any, status_code: int):
58
- response_body = {"success": success, "payload": payload}
59
- return firebase_functions.https_fn.Response(response=json.dumps(response_body), status=status_code, headers={'Content-Type': 'application/json'})
60
-
61
-
62
- def _fake_get_api_key(llm_family=None):
63
- return 'DUMMY_KEY'
64
-
65
-
66
- def _fake_verify_firebase_token(req):
67
- # Default: no-op (successful)
68
- return {}
69
-
70
-
71
- utils_mod.create_json_response = _fake_create_json_response
72
- utils_mod.get_api_key = _fake_get_api_key
73
- utils_mod.verify_firebase_token = _fake_verify_firebase_token
74
- sys.modules['utils'] = utils_mod
75
-
76
- # Fake auth module with validate_auth_key
77
- auth_mod = types.ModuleType('auth')
78
-
79
-
80
- def _fake_validate_auth_key(val: str) -> bool:
81
- return True
82
-
83
-
84
- auth_mod.validate_auth_key = _fake_validate_auth_key
85
- sys.modules['auth'] = auth_mod
86
-
87
- # Fake openai module with APIError and a default OpenAI class
88
- openai_mod = types.ModuleType('openai')
89
-
90
- class APIError(Exception):
91
- pass
92
-
93
-
94
- class DummyOpenAI:
95
- def __init__(self, api_key=None, timeout=None):
96
- self.api_key = api_key
97
- self.timeout = timeout
98
- # The proxy expects openai_client.responses.create / parse
99
- self.responses = self
100
-
101
- def create(self, **params):
102
- raise NotImplementedError("create not implemented for dummy client")
103
-
104
- def parse(self, **params):
105
- raise NotImplementedError("parse not implemented for dummy client")
106
-
107
-
108
- openai_mod.OpenAI = DummyOpenAI
109
- openai_mod.APIError = APIError
110
- sys.modules['openai'] = openai_mod
111
-
112
- # Fake billing module to satisfy imports in v1_00 _openai_proxy (billing helpers)
113
- billing_mod = types.ModuleType('billing')
114
-
115
- def _fake_ensure_balance_positive(db, user_id):
116
- return (True, 100.0)
117
-
118
- def _fake_calculate_cost(model_id, input_tokens, output_tokens):
119
- return 0.0
120
-
121
- def _fake_bill_with_retry(db, user_id, model_id, usage, cost, reason='usage'):
122
- return ("ok", 100.0)
123
-
124
- billing_mod.ensure_balance_positive = _fake_ensure_balance_positive
125
- billing_mod.calculate_cost = _fake_calculate_cost
126
- billing_mod.bill_with_retry = _fake_bill_with_retry
127
- sys.modules['billing'] = billing_mod
128
-
129
- # Fake firebase_admin module providing a minimal firestore.client to avoid
130
- # attempting to initialize the real Firebase SDK during tests
131
- firebase_admin_mod = types.ModuleType('firebase_admin')
132
- firebase_admin_mod.firestore = types.SimpleNamespace(client=lambda: types.SimpleNamespace(), Client=type('Client', (), {}))
133
- sys.modules['firebase_admin'] = firebase_admin_mod
134
-
135
- # Import the module under test after preparing the fake imports
136
- # Use a package-qualified import to ensure we always import the v1_00 implementation
137
- repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
138
- functions_root = os.path.join(repo_root, 'functions')
139
- module_path = os.path.join(functions_root, 'api_v1_00', '_openai_proxy.py')
140
- spec = importlib.util.spec_from_file_location('api_v1_00._openai_proxy', module_path)
141
- _openai = importlib.util.module_from_spec(spec)
142
- spec.loader.exec_module(_openai)
143
-
144
-
145
- # Restore original sys.modules mappings to avoid side-effects for other tests
146
- for name, orig in _orig_sys_modules.items():
147
- if orig is None:
148
- try:
149
- del sys.modules[name]
150
- except KeyError:
151
- pass
152
- else:
153
- sys.modules[name] = orig
154
-
155
-
156
- # Helper request stub used in tests
157
- class DummyRequest:
158
- def __init__(self, headers=None, method='POST', json_data=None, raise_on_get_json=False):
159
- self.headers = headers or {}
160
- self.method = method
161
- self._json_data = json_data
162
- self._raise = raise_on_get_json
163
-
164
- def get_json(self, silent=False):
165
- if self._raise:
166
- raise Exception('Malformed JSON')
167
- return self._json_data
168
-
169
-
170
- def _parse_response(resp):
171
- data = resp.get_data(as_text=True)
172
- return json.loads(data)
173
-
174
-
175
- # -----------------------------
176
- # Tests
177
- # -----------------------------
178
-
179
-
180
- def test_verify_firebase_token_failure_returns_401():
181
- req = DummyRequest(headers={_openai.AUTH_HEADER_NAME: 'ok'}, method='POST')
182
- with patch.object(_openai, 'verify_firebase_token', side_effect=Exception('invalid token')):
183
- resp = _openai._openai_proxy(req)
184
-
185
- assert resp.status_code == 401
186
- payload = _parse_response(resp)
187
- assert payload['success'] is False
188
- assert 'Unauthorized' in payload['payload']
189
-
190
-
191
- def test_missing_auth_header_returns_401():
192
- # no app auth header
193
- req = DummyRequest(headers={}, method='POST', json_data={'call_type': 'create', 'call_params': {}})
194
- with patch.object(_openai, 'validate_auth_key', return_value=True), patch.object(_openai, 'verify_firebase_token', return_value={}):
195
- resp = _openai._openai_proxy(req)
196
-
197
- assert resp.status_code == 401
198
- payload = _parse_response(resp)
199
- assert payload['success'] is False
200
- assert 'Missing app authentication key' in payload['payload']
201
-
202
-
203
- def test_invalid_auth_key_returns_403():
204
- headers = {_openai.AUTH_HEADER_NAME: 'bad'}
205
- req = DummyRequest(headers=headers, method='POST', json_data={'call_type': 'create', 'call_params': {}})
206
- with patch.object(_openai, 'validate_auth_key', return_value=False), patch.object(_openai, 'verify_firebase_token', return_value={}):
207
- resp = _openai._openai_proxy(req)
208
-
209
- assert resp.status_code == 403
210
- payload = _parse_response(resp)
211
- assert payload['success'] is False
212
- assert 'Invalid app authentication key' in payload['payload']
213
-
214
-
215
- def test_non_post_method_returns_405():
216
- headers = {_openai.AUTH_HEADER_NAME: 'ok'}
217
- req = DummyRequest(headers=headers, method='GET')
218
- with patch.object(_openai, 'validate_auth_key', return_value=True), patch.object(_openai, 'verify_firebase_token', return_value={}):
219
- resp = _openai._openai_proxy(req)
220
-
221
- assert resp.status_code == 405
222
- payload = _parse_response(resp)
223
- assert payload['success'] is False
224
- assert 'POST method required' in payload['payload']
225
-
226
-
227
- def test_malformed_json_returns_400():
228
- headers = {_openai.AUTH_HEADER_NAME: 'ok'}
229
- req = DummyRequest(headers=headers, method='POST', raise_on_get_json=True)
230
- with patch.object(_openai, 'validate_auth_key', return_value=True), patch.object(_openai, 'verify_firebase_token', return_value={}):
231
- resp = _openai._openai_proxy(req)
232
-
233
- assert resp.status_code == 400
234
- payload = _parse_response(resp)
235
- assert payload['success'] is False
236
- assert 'Invalid JSON payload' in payload['payload']
237
-
238
-
239
- def test_missing_or_invalid_call_type_or_call_params_returns_400():
240
- headers = {_openai.AUTH_HEADER_NAME: 'ok'}
241
- req = DummyRequest(headers=headers, method='POST', json_data={'call_type': None, 'call_params': {}})
242
- with patch.object(_openai, 'validate_auth_key', return_value=True), patch.object(_openai, 'verify_firebase_token', return_value={}):
243
- resp = _openai._openai_proxy(req)
244
-
245
- assert resp.status_code == 400
246
- payload = _parse_response(resp)
247
- assert payload['success'] is False
248
- assert 'Missing or invalid required fields' in payload['payload']
249
-
250
-
251
- def test_invalid_call_type_returns_400():
252
- headers = {_openai.AUTH_HEADER_NAME: 'ok'}
253
- req = DummyRequest(headers=headers, method='POST', json_data={'call_type': 'bad', 'call_params': {}})
254
- with patch.object(_openai, 'validate_auth_key', return_value=True), patch.object(_openai, 'verify_firebase_token', return_value={}):
255
- resp = _openai._openai_proxy(req)
256
-
257
- assert resp.status_code == 400
258
- payload = _parse_response(resp)
259
- assert payload['success'] is False
260
- assert "Invalid 'call_type'" in payload['payload']
261
-
262
-
263
- def test_get_api_key_failure_returns_500():
264
- headers = {_openai.AUTH_HEADER_NAME: 'ok'}
265
- req = DummyRequest(headers=headers, method='POST', json_data={'call_type': 'create', 'call_params': {'model': 'm', 'input': 'hi'}})
266
- with patch.object(_openai, 'validate_auth_key', return_value=True), \
267
- patch.object(_openai, 'verify_firebase_token', return_value={}), \
268
- patch.object(_openai, 'get_api_key', side_effect=Exception('boom')):
269
- resp = _openai._openai_proxy(req)
270
-
271
- assert resp.status_code == 500
272
- payload = _parse_response(resp)
273
- assert payload['success'] is False
274
- assert 'Could not retrieve API key' in payload['payload']
275
-
276
-
277
- def test_parse_missing_input_or_text_format_returns_400():
278
- headers = {_openai.AUTH_HEADER_NAME: 'ok'}
279
- # missing 'text_format' to trigger the parse validation
280
- req = DummyRequest(headers=headers, method='POST', json_data={'call_type': 'parse', 'call_params': {'model': 'm', 'input': 'hi'}})
281
-
282
- class NoopOpenAI:
283
- def __init__(self, api_key=None, timeout=None):
284
- self.api_key = api_key
285
- self.timeout = timeout
286
- self.responses = self
287
-
288
- def parse(self, **p):
289
- return None
290
-
291
- def create(self, **p):
292
- return None
293
-
294
- with patch.object(_openai, 'validate_auth_key', return_value=True), \
295
- patch.object(_openai, 'verify_firebase_token', return_value={}), \
296
- patch.object(_openai, 'get_api_key', return_value='KEY'), \
297
- patch.object(_openai.openai, 'OpenAI', NoopOpenAI):
298
- resp = _openai._openai_proxy(req)
299
-
300
- assert resp.status_code == 400
301
- payload = _parse_response(resp)
302
- assert payload['success'] is False
303
- assert "Missing 'input' or 'text_format' for parse call" in payload['payload']
304
-
305
-
306
- def test_create_missing_input_returns_400():
307
- headers = {_openai.AUTH_HEADER_NAME: 'ok'}
308
- req = DummyRequest(headers=headers, method='POST', json_data={'call_type': 'create', 'call_params': {'model': 'm'}})
309
-
310
- class NoopOpenAI:
311
- def __init__(self, api_key=None, timeout=None):
312
- self.api_key = api_key
313
- self.timeout = timeout
314
- self.responses = self
315
-
316
- def parse(self, **p):
317
- return None
318
-
319
- def create(self, **p):
320
- return None
321
-
322
- with patch.object(_openai, 'validate_auth_key', return_value=True), \
323
- patch.object(_openai, 'verify_firebase_token', return_value={}), \
324
- patch.object(_openai, 'get_api_key', return_value='KEY'), \
325
- patch.object(_openai.openai, 'OpenAI', NoopOpenAI):
326
- resp = _openai._openai_proxy(req)
327
-
328
- assert resp.status_code == 400
329
- payload = _parse_response(resp)
330
- assert payload['success'] is False
331
- assert "Missing 'input' for create call" in payload['payload']
332
-
333
-
334
- def test_successful_create_calls_openai_and_returns_payload():
335
- headers = {_openai.AUTH_HEADER_NAME: 'ok'}
336
- req_payload = {'call_type': 'create', 'call_params': {'model': 'gem-model', 'input': 'hello'}}
337
- req = DummyRequest(headers=headers, method='POST', json_data=req_payload)
338
-
339
- class FakeClient:
340
- def __init__(self, api_key, timeout=None):
341
- self.api_key = api_key
342
- self.timeout = timeout
343
- self.responses = self
344
-
345
- def create(self, **params):
346
- class FakeResp:
347
- def model_dump(self_inner, mode='json'):
348
- return {'result': 'ok', 'received_model': params.get('model'), 'usage': {'input_tokens': 1, 'output_tokens': 2}}
349
- return FakeResp()
350
-
351
- with patch.object(_openai, 'validate_auth_key', return_value=True), \
352
- patch.object(_openai, 'verify_firebase_token', return_value={}), \
353
- patch.object(_openai, 'get_api_key', return_value='OPENAI-KEY'), \
354
- patch.object(_openai, 'ensure_balance_positive', return_value=(True, 100.0)), \
355
- patch.object(_openai, 'bill_with_retry', return_value=("ok", 95.0)), \
356
- patch.object(_openai.firestore, 'client', return_value=types.SimpleNamespace()), \
357
- patch.object(_openai.openai, 'OpenAI', FakeClient):
358
- resp = _openai._openai_proxy(req)
359
-
360
- assert resp.status_code == 200
361
- payload = _parse_response(resp)
362
- assert payload['success'] is True
363
- assert isinstance(payload['payload'], dict)
364
- assert payload['payload']['result'] == 'ok'
365
- assert payload['payload']['received_model'] == 'gem-model'
366
-
367
-
368
- def test_successful_parse_calls_openai_and_returns_parsed_and_usage():
369
- headers = {_openai.AUTH_HEADER_NAME: 'ok'}
370
- req_payload = {'call_type': 'parse', 'call_params': {'model': 'gem-model', 'input': 'hello', 'text_format': 'FileSummaryModel'}}
371
- req = DummyRequest(headers=headers, method='POST', json_data=req_payload)
372
-
373
- class FakeClient2:
374
- def __init__(self, api_key, timeout=None):
375
- self.api_key = api_key
376
- self.timeout = timeout
377
- self.responses = self
378
-
379
- def parse(self, **params):
380
- class FakeOutputParsed:
381
- def __init__(self, data):
382
- self._data = data
383
-
384
- def dict(self):
385
- return self._data
386
-
387
- class FakeResp:
388
- def __init__(self, data, usage):
389
- self.output_parsed = FakeOutputParsed(data)
390
- self._usage = usage
391
-
392
- def model_dump(self_inner, mode='json'):
393
- return {'usage': self_inner._usage}
394
-
395
- # detect whether text_format was left as a string
396
- is_text_format_string = isinstance(params.get('text_format'), str)
397
- parsed = {'result': 'ok', 'received_model': params.get('model'), 'is_text_format_string': is_text_format_string}
398
- usage = {'input_tokens': 1, 'output_tokens': 2, 'prompt_tokens': 5}
399
- return FakeResp(parsed, usage)
400
-
401
- with patch.object(_openai, 'validate_auth_key', return_value=True), \
402
- patch.object(_openai, 'verify_firebase_token', return_value={}), \
403
- patch.object(_openai, 'get_api_key', return_value='OPENAI-KEY'), \
404
- patch.object(_openai, 'ensure_balance_positive', return_value=(True, 100.0)), \
405
- patch.object(_openai, 'bill_with_retry', return_value=("ok", 95.0)), \
406
- patch.object(_openai.firestore, 'client', return_value=types.SimpleNamespace()), \
407
- patch.object(_openai.openai, 'OpenAI', FakeClient2):
408
- resp = _openai._openai_proxy(req)
409
-
410
- assert resp.status_code == 200
411
- payload = _parse_response(resp)
412
- assert payload['success'] is True
413
- assert payload['payload']['output_parsed']['result'] == 'ok'
414
- assert payload['payload']['output_parsed']['received_model'] == 'gem-model'
415
- # The proxy should have replaced the text_format string with the model (i.e., not a string)
416
- assert payload['payload']['output_parsed']['is_text_format_string'] is False
417
- assert payload['payload']['usage']['prompt_tokens'] == 5
418
-
419
-
420
- def test_openai_api_error_with_status_code_is_returned_as_error_status():
421
- headers = {_openai.AUTH_HEADER_NAME: 'ok'}
422
- req_payload = {'call_type': 'create', 'call_params': {'model': 'g', 'input': 'hey'}}
423
- req = DummyRequest(headers=headers, method='POST', json_data=req_payload)
424
-
425
- # Use the module's openai.APIError so the proxy's except block catches it
426
- err = _openai.openai.APIError('rate limited')
427
- err.status_code = 502
428
-
429
- class ErrClient:
430
- def __init__(self, api_key=None, timeout=None):
431
- self.api_key = api_key
432
- self.timeout = timeout
433
- self.responses = self
434
-
435
- def create(self, **params):
436
- raise err
437
-
438
- with patch.object(_openai, 'validate_auth_key', return_value=True), \
439
- patch.object(_openai, 'verify_firebase_token', return_value={}), \
440
- patch.object(_openai, 'get_api_key', return_value='OPENAI-KEY'), \
441
- patch.object(_openai, 'ensure_balance_positive', return_value=(True, 100.0)), \
442
- patch.object(_openai.firestore, 'client', return_value=types.SimpleNamespace()), \
443
- patch.object(_openai.openai, 'OpenAI', ErrClient):
444
- resp = _openai._openai_proxy(req)
445
-
446
- assert resp.status_code == 502
447
- payload = _parse_response(resp)
448
- assert payload['success'] is False
449
- assert 'OpenAI API Error' in payload['payload']
@@ -1,167 +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 _redeem_credit_code
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 = ['firebase_functions', 'utils', 'auth', 'firebase_admin']
22
- for name in _names_to_fake:
23
- _orig_sys_modules[name] = sys.modules.get(name)
24
-
25
- # Fake firebase_functions.https_fn.Response to capture returned data
26
- firebase_functions = types.ModuleType('firebase_functions')
27
-
28
- class FakeResponse:
29
- def __init__(self, response=None, mimetype=None, status=200, **kwargs):
30
- # Mirror the small subset of the interface tests expect
31
- self.status_code = status
32
- if isinstance(response, (dict, list)):
33
- self._body_text = json.dumps(response)
34
- else:
35
- self._body_text = '' if response is None else response
36
- self.headers = kwargs.get('headers', {})
37
-
38
- def get_data(self, as_text=False):
39
- if as_text:
40
- return self._body_text
41
- return self._body_text.encode('utf-8')
42
-
43
- firebase_functions.https_fn = types.SimpleNamespace(Request=object, Response=FakeResponse)
44
- sys.modules['firebase_functions'] = firebase_functions
45
-
46
- # Fake utils module (verify_firebase_token, create_json_response)
47
- utils_mod = types.ModuleType('utils')
48
-
49
- def _dummy_verify_firebase_token(req: object) -> dict:
50
- # Default behavior: return a decoded token with a uid
51
- return {'uid': 'test_user'}
52
-
53
- def _create_json_response(success: bool, payload=None, status_code: int = 200):
54
- # Normalize payload to include a message field
55
- if isinstance(payload, dict):
56
- message = payload.get('message', '')
57
- data = {'success': success, 'message': message}
58
- # include rest of payload under 'payload' to mirror production structure
59
- data['payload'] = payload
60
- else:
61
- message = payload if payload is not None else ''
62
- data = {'success': success, 'message': message}
63
- # Use the fake firebase_functions response object
64
- return firebase_functions.https_fn.Response(response=data, status=status_code)
65
-
66
- utils_mod.verify_firebase_token = _dummy_verify_firebase_token
67
- utils_mod.create_json_response = _create_json_response
68
- sys.modules['utils'] = utils_mod
69
-
70
- # Fake auth module (validate_auth_key will be patched per-test as needed)
71
- auth_mod = types.ModuleType('auth')
72
-
73
- def _simple_validate_auth_key(val: str) -> bool:
74
- return bool(val)
75
-
76
- auth_mod.validate_auth_key = _simple_validate_auth_key
77
- sys.modules['auth'] = auth_mod
78
-
79
- # Fake firebase_admin to prevent importing the real package during tests
80
- firebase_admin = types.ModuleType('firebase_admin')
81
- # Minimal fake firestore object with attributes referenced by the function
82
- fake_firestore = types.SimpleNamespace(
83
- client=lambda: None,
84
- transactional=lambda f: f,
85
- SERVER_TIMESTAMP=None
86
- )
87
- firebase_admin.firestore = fake_firestore
88
- sys.modules['firebase_admin'] = firebase_admin
89
-
90
- # -----------------------------
91
- # Import the module under test after preparing the fake imports
92
- # -----------------------------
93
- repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
94
- functions_root = os.path.join(repo_root, 'functions')
95
- module_path = os.path.join(functions_root, 'api_v1_00', '_redeem_credit_code.py')
96
- spec = importlib.util.spec_from_file_location('api_v1_00._redeem_credit_code', module_path)
97
- _redeem = importlib.util.module_from_spec(spec)
98
- spec.loader.exec_module(_redeem)
99
-
100
-
101
- # Restore original sys.modules mappings to avoid side-effects for other tests
102
- for name, orig in _orig_sys_modules.items():
103
- if orig is None:
104
- try:
105
- del sys.modules[name]
106
- except KeyError:
107
- pass
108
- else:
109
- sys.modules[name] = orig
110
-
111
- # Simple helper request stub used in the tests
112
- class DummyRequest:
113
- def __init__(self, headers=None, method='POST', json_data=None, raise_on_get_json=False):
114
- self.headers = headers or {}
115
- self.method = method
116
- self._json_data = json_data
117
- self._raise = raise_on_get_json
118
-
119
- def get_json(self, silent=True):
120
- if self._raise:
121
- raise Exception('Malformed JSON')
122
- return self._json_data
123
-
124
-
125
- def _parse_response(resp):
126
- data = resp.get_data(as_text=True)
127
- return json.loads(data)
128
-
129
-
130
- # -----------------------------
131
- # Tests
132
- # -----------------------------
133
-
134
- def test_missing_auth_header_returns_401():
135
- req = DummyRequest(headers={}, method='POST')
136
- # Ensure verify_firebase_token is not called when header is missing
137
- with patch.object(_redeem, 'verify_firebase_token', side_effect=Exception('Should not be called')):
138
- resp = _redeem._redeem_credit_code(req)
139
-
140
- assert resp.status_code == 401
141
- payload = _parse_response(resp)
142
- assert payload['success'] is False
143
- assert 'Missing authentication key' in payload['message']
144
-
145
-
146
- def test_invalid_auth_key_returns_403():
147
- headers = {_redeem.AUTH_HEADER_NAME: 'bad-token'}
148
- req = DummyRequest(headers=headers, method='POST')
149
- with patch.object(_redeem, 'validate_auth_key', return_value=False):
150
- resp = _redeem._redeem_credit_code(req)
151
- assert resp.status_code == 403
152
- payload = _parse_response(resp)
153
- assert payload['success'] is False
154
- assert 'Invalid authentication key' in payload['message']
155
-
156
-
157
- def test_valid_auth_key_passes_and_proceeds():
158
- headers = {_redeem.AUTH_HEADER_NAME: 'ok'}
159
- # Provide an empty JSON payload to trigger the existing JSON validation path
160
- req = DummyRequest(headers=headers, method='POST', json_data={})
161
- with patch.object(_redeem, 'validate_auth_key', return_value=True):
162
- resp = _redeem._redeem_credit_code(req)
163
-
164
- assert resp.status_code == 400
165
- payload = _parse_response(resp)
166
- assert payload['success'] is False
167
- assert 'Invalid or missing JSON payload' in payload['message']