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.
- roksta/__init__.cpython-311-darwin.so +0 -0
- roksta/ai/__init__.cpython-311-darwin.so +0 -0
- roksta/ai/call_ai.cpython-311-darwin.so +0 -0
- roksta/ai/gemini.cpython-311-darwin.so +0 -0
- roksta/ai/generic.cpython-311-darwin.so +0 -0
- roksta/ai/llm.cpython-311-darwin.so +0 -0
- roksta/ai/openai.cpython-311-darwin.so +0 -0
- roksta/ai/tools/__init__.cpython-311-darwin.so +0 -0
- roksta/ai/tools/delete_file.cpython-311-darwin.so +0 -0
- roksta/ai/tools/edit_file.cpython-311-darwin.so +0 -0
- roksta/ai/tools/final_response.cpython-311-darwin.so +0 -0
- roksta/ai/tools/get_file_summaries.cpython-311-darwin.so +0 -0
- roksta/ai/tools/read_file.cpython-311-darwin.so +0 -0
- roksta/ai/tools/regex_replace.cpython-311-darwin.so +0 -0
- roksta/ai/tools/shell_any.cpython-311-darwin.so +0 -0
- roksta/ai/tools/shell_limited.cpython-311-darwin.so +0 -0
- roksta/ai/tools/tool_defs.cpython-311-darwin.so +0 -0
- roksta/ai/tools/tool_utils.cpython-311-darwin.so +0 -0
- roksta/ai/tools/web_fetch.cpython-311-darwin.so +0 -0
- roksta/ai/tools/write_file.cpython-311-darwin.so +0 -0
- roksta/analytics.cpython-311-darwin.so +0 -0
- roksta/balance.cpython-311-darwin.so +0 -0
- roksta/build_project.cpython-311-darwin.so +0 -0
- roksta/chat_workflow.cpython-311-darwin.so +0 -0
- roksta/check_for_updates.cpython-311-darwin.so +0 -0
- roksta/check_subtask_sequence.cpython-311-darwin.so +0 -0
- roksta/checkpoints.cpython-311-darwin.so +0 -0
- roksta/clarify_goal.cpython-311-darwin.so +0 -0
- roksta/codebase_listing.cpython-311-darwin.so +0 -0
- roksta/command_handlers/__init__.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_activate_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_add_funds_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_auto_charge_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_auto_commit_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_building_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_chat_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_dev_rate_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_feedback_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_goal_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_help_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_init_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_linting_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_login_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_logout_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_payment_details_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_quit_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_redeem_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_request_activation_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_testing_command.cpython-311-darwin.so +0 -0
- roksta/command_handlers/handle_usage_command.cpython-311-darwin.so +0 -0
- roksta/create_default_config.cpython-311-darwin.so +0 -0
- roksta/create_default_ignore_file.cpython-311-darwin.so +0 -0
- roksta/default_config.cpython-311-darwin.so +0 -0
- roksta/default_ignores.cpython-311-darwin.so +0 -0
- roksta/discover_test_command.cpython-311-darwin.so +0 -0
- roksta/enums.cpython-311-darwin.so +0 -0
- roksta/env.cpython-311-darwin.so +0 -0
- roksta/extended_text_area.cpython-311-darwin.so +0 -0
- roksta/firebase.cpython-311-darwin.so +0 -0
- roksta/firebase_auth_web.cpython-311-darwin.so +0 -0
- roksta/firebase_config.cpython-311-darwin.so +0 -0
- roksta/fix_tests.cpython-311-darwin.so +0 -0
- roksta/gen_codebase_summaries.cpython-311-darwin.so +0 -0
- roksta/gen_one_line_goal.cpython-311-darwin.so +0 -0
- roksta/gen_subtasks.cpython-311-darwin.so +0 -0
- roksta/get_codebase_structure.cpython-311-darwin.so +0 -0
- roksta/get_failing_tests.cpython-311-darwin.so +0 -0
- roksta/goal_workflow.cpython-311-darwin.so +0 -0
- roksta/init_codebase.cpython-311-darwin.so +0 -0
- roksta/lint_code.cpython-311-darwin.so +0 -0
- roksta/logger.cpython-311-darwin.so +0 -0
- roksta/main.cpython-311-darwin.so +0 -0
- roksta/make_issue.cpython-311-darwin.so +0 -0
- roksta/new_features.cpython-311-darwin.so +0 -0
- roksta/parse_directive_cli_tokens.cpython-311-darwin.so +0 -0
- roksta/parse_readme.cpython-311-darwin.so +0 -0
- roksta/propose_solution.cpython-311-darwin.so +0 -0
- roksta/response_formats.cpython-311-darwin.so +0 -0
- roksta/rewrite_goal.cpython-311-darwin.so +0 -0
- roksta/roksta.cpython-311-darwin.so +0 -0
- roksta/run_cli_goal.cpython-311-darwin.so +0 -0
- roksta/save_chat_transcript.cpython-311-darwin.so +0 -0
- roksta/select_files.cpython-311-darwin.so +0 -0
- roksta/tips.cpython-311-darwin.so +0 -0
- roksta/utils.cpython-311-darwin.so +0 -0
- roksta/write_code.cpython-311-darwin.so +0 -0
- roksta-0.3.8.dist-info/METADATA +471 -0
- roksta-0.3.8.dist-info/RECORD +91 -0
- {roksta-0.3.2.dist-info → roksta-0.3.8.dist-info}/top_level.txt +0 -1
- roksta-0.3.2.dist-info/METADATA +0 -40
- roksta-0.3.2.dist-info/RECORD +0 -121
- tests/__init__.py +0 -2
- tests/conftest.py +0 -211
- tests/functions/__init__.py +0 -2
- tests/functions/api_v1_00/__init__.py +0 -2
- tests/functions/api_v1_00/test__analytics.py +0 -416
- tests/functions/api_v1_00/test__gemini_proxy.py +0 -352
- tests/functions/api_v1_00/test__generic_proxy.py +0 -428
- tests/functions/api_v1_00/test__get_payment_details.py +0 -356
- tests/functions/api_v1_00/test__openai_proxy.py +0 -449
- tests/functions/api_v1_00/test__redeem_credit_code.py +0 -167
- tests/functions/api_v1_00/test__sync_emails.py +0 -325
- tests/functions/api_v1_00/test__take_payment.py +0 -491
- tests/functions/api_v1_00/test__use_activation_code.py +0 -438
- tests/functions/api_v1_01/__init__.py +0 -2
- tests/functions/api_v1_01/test__analytics.py +0 -416
- tests/functions/api_v1_01/test__gemini_proxy.py +0 -352
- tests/functions/api_v1_01/test__generic_proxy.py +0 -428
- tests/functions/api_v1_01/test__get_payment_details.py +0 -356
- tests/functions/api_v1_01/test__openai_proxy.py +0 -449
- tests/functions/api_v1_01/test__redeem_credit_code.py +0 -167
- tests/functions/api_v1_01/test__sync_emails.py +0 -325
- tests/functions/api_v1_01/test__take_payment.py +0 -491
- tests/functions/api_v1_01/test__use_activation_code.py +0 -438
- tests/functions/api_v1_02/__init__.py +0 -2
- tests/functions/api_v1_02/test__analytics.py +0 -416
- tests/functions/api_v1_02/test__gemini_proxy.py +0 -352
- tests/functions/api_v1_02/test__generic_proxy.py +0 -428
- tests/functions/api_v1_02/test__get_payment_details.py +0 -356
- tests/functions/api_v1_02/test__openai_proxy.py +0 -449
- tests/functions/api_v1_02/test__redeem_credit_code.py +0 -167
- tests/functions/api_v1_02/test__sync_emails.py +0 -325
- tests/functions/api_v1_02/test__take_payment.py +0 -491
- tests/functions/api_v1_02/test__use_activation_code.py +0 -438
- tests/functions/api_v1_02/test_proxy_keyword_replacement.py +0 -557
- tests/functions/api_v1_02/test_replace_keywords.py +0 -74
- tests/functions/test_auth.py +0 -24
- tests/functions/test_main.py +0 -73
- tests/functions/test_utils.py +0 -484
- {roksta-0.3.2.dist-info → roksta-0.3.8.dist-info}/WHEEL +0 -0
- {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']
|