roksta 0.2.7__cp314-cp314t-macosx_10_13_universal2.whl → 0.3.1__cp314-cp314t-macosx_10_13_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.
Potentially problematic release.
This version of roksta might be problematic. Click here for more details.
- roksta/ai/call_ai.cpython-314t-darwin.so +0 -0
- roksta/ai/gemini.cpython-314t-darwin.so +0 -0
- roksta/ai/generic.cpython-314t-darwin.so +0 -0
- roksta/ai/llm.cpython-314t-darwin.so +0 -0
- roksta/ai/openai.cpython-314t-darwin.so +0 -0
- roksta/ai/tools/__init__.cpython-314t-darwin.so +0 -0
- roksta/ai/tools/delete_file.cpython-314t-darwin.so +0 -0
- roksta/ai/tools/edit_file.cpython-314t-darwin.so +0 -0
- roksta/ai/tools/final_response.cpython-314t-darwin.so +0 -0
- roksta/ai/tools/get_file_summaries.cpython-314t-darwin.so +0 -0
- roksta/ai/tools/read_file.cpython-314t-darwin.so +0 -0
- roksta/ai/tools/regex_replace.cpython-314t-darwin.so +0 -0
- roksta/ai/tools/shell_any.cpython-314t-darwin.so +0 -0
- roksta/ai/tools/shell_limited.cpython-314t-darwin.so +0 -0
- roksta/ai/tools/tool_defs.cpython-314t-darwin.so +0 -0
- roksta/ai/tools/tool_utils.cpython-314t-darwin.so +0 -0
- roksta/ai/tools/write_file.cpython-314t-darwin.so +0 -0
- roksta/balance.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/__init__.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_activate_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_add_funds_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_auto_charge_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_auto_commit_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_building_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_chat_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_dev_rate_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_feedback_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_goal_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_help_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_init_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_linting_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_login_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_logout_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_payment_details_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_quit_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_redeem_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_request_activation_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_testing_command.cpython-314t-darwin.so +0 -0
- roksta/command_handlers/handle_usage_command.cpython-314t-darwin.so +0 -0
- roksta/env.cpython-314t-darwin.so +0 -0
- roksta/extended_text_area.cpython-314t-darwin.so +0 -0
- roksta/firebase.cpython-314t-darwin.so +0 -0
- roksta/fix_tests.cpython-314t-darwin.so +0 -0
- roksta/goal_workflow.cpython-314t-darwin.so +0 -0
- roksta/lint_code.cpython-314t-darwin.so +0 -0
- roksta/main.cpython-314t-darwin.so +0 -0
- roksta/new_features.cpython-314t-darwin.so +0 -0
- roksta/roksta.cpython-314t-darwin.so +0 -0
- roksta/tips.cpython-314t-darwin.so +0 -0
- roksta/utils.cpython-314t-darwin.so +0 -0
- {roksta-0.2.7.dist-info → roksta-0.3.1.dist-info}/METADATA +2 -1
- roksta-0.3.1.dist-info/RECORD +109 -0
- tests/conftest.py +42 -0
- tests/functions/{api_v0_01 → api_v1_01}/__init__.py +1 -1
- tests/functions/{api_v0_01 → api_v1_01}/test__analytics.py +2 -3
- tests/functions/{api_v0_01 → api_v1_01}/test__gemini_proxy.py +51 -6
- tests/functions/{api_v0_01 → api_v1_01}/test__generic_proxy.py +31 -2
- tests/functions/{api_v0_01 → api_v1_01}/test__get_payment_details.py +2 -2
- tests/functions/{api_v0_01 → api_v1_01}/test__openai_proxy.py +50 -14
- tests/functions/{api_v0_01 → api_v1_01}/test__redeem_credit_code.py +2 -2
- tests/functions/{api_v0_01 → api_v1_01}/test__sync_emails.py +3 -2
- tests/functions/{api_v0_01 → api_v1_01}/test__take_payment.py +2 -2
- tests/functions/{api_v0_01 → api_v1_01}/test__use_activation_code.py +3 -2
- tests/functions/test_utils.py +484 -0
- roksta/ai/tools.cpython-314t-darwin.so +0 -0
- roksta/command_handlers.cpython-314t-darwin.so +0 -0
- roksta-0.2.7.dist-info/RECORD +0 -78
- tests/functions/test_utils_functions.py +0 -222
- {roksta-0.2.7.dist-info → roksta-0.3.1.dist-info}/WHEEL +0 -0
- {roksta-0.2.7.dist-info → roksta-0.3.1.dist-info}/entry_points.txt +0 -0
- {roksta-0.2.7.dist-info → roksta-0.3.1.dist-info}/top_level.txt +0 -0
- /tests/functions/{test_main_functions.py → test_main.py} +0 -0
|
@@ -7,7 +7,8 @@ from unittest.mock import patch
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
# Ensure the functions/ directory is importable as a top-level module location
|
|
10
|
-
|
|
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__), '..', '..', '..'))
|
|
11
12
|
FUNCTIONS_DIR = os.path.join(PROJECT_ROOT, 'functions')
|
|
12
13
|
if FUNCTIONS_DIR not in sys.path:
|
|
13
14
|
sys.path.insert(0, FUNCTIONS_DIR)
|
|
@@ -22,6 +23,8 @@ _names_to_fake = [
|
|
|
22
23
|
'utils',
|
|
23
24
|
'auth',
|
|
24
25
|
'openai',
|
|
26
|
+
'firebase_admin',
|
|
27
|
+
'billing',
|
|
25
28
|
]
|
|
26
29
|
for name in _names_to_fake:
|
|
27
30
|
_orig_sys_modules[name] = sys.modules.get(name)
|
|
@@ -82,12 +85,7 @@ auth_mod.validate_auth_key = _fake_validate_auth_key
|
|
|
82
85
|
sys.modules['auth'] = auth_mod
|
|
83
86
|
|
|
84
87
|
# Fake openai module with APIError and a default OpenAI class
|
|
85
|
-
|
|
86
|
-
functions_root = os.path.join(repo_root, 'functions')
|
|
87
|
-
module_path = os.path.join(functions_root, 'api_v0_01', '_openai_proxy.py')
|
|
88
|
-
spec = importlib.util.spec_from_file_location('api_v0_01._openai_proxy', module_path)
|
|
89
|
-
_openai = importlib.util.module_from_spec(spec)
|
|
90
|
-
spec.loader.exec_module(_openai)
|
|
88
|
+
openai_mod = types.ModuleType('openai')
|
|
91
89
|
|
|
92
90
|
class APIError(Exception):
|
|
93
91
|
pass
|
|
@@ -107,12 +105,42 @@ class DummyOpenAI:
|
|
|
107
105
|
raise NotImplementedError("parse not implemented for dummy client")
|
|
108
106
|
|
|
109
107
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
sys.modules['openai'] =
|
|
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
|
|
113
134
|
|
|
114
135
|
# Import the module under test after preparing the fake imports
|
|
115
|
-
|
|
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
|
+
|
|
116
144
|
|
|
117
145
|
# Restore original sys.modules mappings to avoid side-effects for other tests
|
|
118
146
|
for name, orig in _orig_sys_modules.items():
|
|
@@ -317,12 +345,15 @@ def test_successful_create_calls_openai_and_returns_payload():
|
|
|
317
345
|
def create(self, **params):
|
|
318
346
|
class FakeResp:
|
|
319
347
|
def model_dump(self_inner, mode='json'):
|
|
320
|
-
return {'result': 'ok', 'received_model': params.get('model')}
|
|
348
|
+
return {'result': 'ok', 'received_model': params.get('model'), 'usage': {'input_tokens': 1, 'output_tokens': 2}}
|
|
321
349
|
return FakeResp()
|
|
322
350
|
|
|
323
351
|
with patch.object(_openai, 'validate_auth_key', return_value=True), \
|
|
324
352
|
patch.object(_openai, 'verify_firebase_token', return_value={}), \
|
|
325
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()), \
|
|
326
357
|
patch.object(_openai.openai, 'OpenAI', FakeClient):
|
|
327
358
|
resp = _openai._openai_proxy(req)
|
|
328
359
|
|
|
@@ -364,12 +395,15 @@ def test_successful_parse_calls_openai_and_returns_parsed_and_usage():
|
|
|
364
395
|
# detect whether text_format was left as a string
|
|
365
396
|
is_text_format_string = isinstance(params.get('text_format'), str)
|
|
366
397
|
parsed = {'result': 'ok', 'received_model': params.get('model'), 'is_text_format_string': is_text_format_string}
|
|
367
|
-
usage = {'prompt_tokens': 5}
|
|
398
|
+
usage = {'input_tokens': 1, 'output_tokens': 2, 'prompt_tokens': 5}
|
|
368
399
|
return FakeResp(parsed, usage)
|
|
369
400
|
|
|
370
401
|
with patch.object(_openai, 'validate_auth_key', return_value=True), \
|
|
371
402
|
patch.object(_openai, 'verify_firebase_token', return_value={}), \
|
|
372
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()), \
|
|
373
407
|
patch.object(_openai.openai, 'OpenAI', FakeClient2):
|
|
374
408
|
resp = _openai._openai_proxy(req)
|
|
375
409
|
|
|
@@ -380,7 +414,7 @@ def test_successful_parse_calls_openai_and_returns_parsed_and_usage():
|
|
|
380
414
|
assert payload['payload']['output_parsed']['received_model'] == 'gem-model'
|
|
381
415
|
# The proxy should have replaced the text_format string with the model (i.e., not a string)
|
|
382
416
|
assert payload['payload']['output_parsed']['is_text_format_string'] is False
|
|
383
|
-
assert payload['payload']['usage']
|
|
417
|
+
assert payload['payload']['usage']['prompt_tokens'] == 5
|
|
384
418
|
|
|
385
419
|
|
|
386
420
|
def test_openai_api_error_with_status_code_is_returned_as_error_status():
|
|
@@ -404,6 +438,8 @@ def test_openai_api_error_with_status_code_is_returned_as_error_status():
|
|
|
404
438
|
with patch.object(_openai, 'validate_auth_key', return_value=True), \
|
|
405
439
|
patch.object(_openai, 'verify_firebase_token', return_value={}), \
|
|
406
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()), \
|
|
407
443
|
patch.object(_openai.openai, 'OpenAI', ErrClient):
|
|
408
444
|
resp = _openai._openai_proxy(req)
|
|
409
445
|
|
|
@@ -92,8 +92,8 @@ sys.modules['firebase_admin'] = firebase_admin
|
|
|
92
92
|
# -----------------------------
|
|
93
93
|
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
|
94
94
|
functions_root = os.path.join(repo_root, 'functions')
|
|
95
|
-
module_path = os.path.join(functions_root, '
|
|
96
|
-
spec = importlib.util.spec_from_file_location('
|
|
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
97
|
_redeem = importlib.util.module_from_spec(spec)
|
|
98
98
|
spec.loader.exec_module(_redeem)
|
|
99
99
|
|
|
@@ -90,11 +90,12 @@ sys.modules['firebase_admin'] = firebase_admin
|
|
|
90
90
|
# -----------------------------
|
|
91
91
|
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
|
92
92
|
functions_root = os.path.join(repo_root, 'functions')
|
|
93
|
-
module_path = os.path.join(functions_root, '
|
|
94
|
-
spec = importlib.util.spec_from_file_location('
|
|
93
|
+
module_path = os.path.join(functions_root, 'api_v1_00', '_sync_emails.py')
|
|
94
|
+
spec = importlib.util.spec_from_file_location('api_v1_00._sync_emails', module_path)
|
|
95
95
|
_sync = importlib.util.module_from_spec(spec)
|
|
96
96
|
spec.loader.exec_module(_sync)
|
|
97
97
|
|
|
98
|
+
|
|
98
99
|
# Restore original sys.modules mappings to avoid side-effects for other tests
|
|
99
100
|
for name, orig in _orig_sys_modules.items():
|
|
100
101
|
if orig is None:
|
|
@@ -210,8 +210,8 @@ sys.modules['ulid'] = ulid_mod
|
|
|
210
210
|
# Import the module under test after preparing the fake imports
|
|
211
211
|
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
|
212
212
|
functions_root = os.path.join(repo_root, 'functions')
|
|
213
|
-
module_path = os.path.join(functions_root, '
|
|
214
|
-
spec = importlib.util.spec_from_file_location('
|
|
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
215
|
_take_payment = importlib.util.module_from_spec(spec)
|
|
216
216
|
spec.loader.exec_module(_take_payment)
|
|
217
217
|
|
|
@@ -171,11 +171,12 @@ sys.modules['firebase_admin'] = firebase_admin_mod
|
|
|
171
171
|
# -----------------------------
|
|
172
172
|
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
|
173
173
|
functions_root = os.path.join(repo_root, 'functions')
|
|
174
|
-
module_path = os.path.join(functions_root, '
|
|
175
|
-
spec = importlib.util.spec_from_file_location('
|
|
174
|
+
module_path = os.path.join(functions_root, 'api_v1_00', '_use_activation_code.py')
|
|
175
|
+
spec = importlib.util.spec_from_file_location('api_v1_00._use_activation_code', module_path)
|
|
176
176
|
_use_activation = importlib.util.module_from_spec(spec)
|
|
177
177
|
spec.loader.exec_module(_use_activation)
|
|
178
178
|
|
|
179
|
+
|
|
179
180
|
# Restore original sys.modules mappings to avoid side-effects for other tests
|
|
180
181
|
for name, orig in _orig_sys_modules.items():
|
|
181
182
|
if orig is None:
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import types
|
|
5
|
+
import json
|
|
6
|
+
import base64
|
|
7
|
+
import uuid
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Helper to create simple module stubs
|
|
12
|
+
def _make_fake_firebase_functions():
|
|
13
|
+
fake_https = types.ModuleType('firebase_functions.https_fn')
|
|
14
|
+
|
|
15
|
+
class FakeResponse:
|
|
16
|
+
def __init__(self, response=None, mimetype=None, status=200, **kwargs):
|
|
17
|
+
# Mirror the small subset of the real Response API used by the code/tests
|
|
18
|
+
self.status_code = status
|
|
19
|
+
if isinstance(response, bytes):
|
|
20
|
+
self._text = response.decode('utf-8')
|
|
21
|
+
else:
|
|
22
|
+
# If a non-string is passed (unlikely), coerce to JSON text for assertions
|
|
23
|
+
self._text = response if isinstance(response, str) else (json.dumps(response) if response is not None else '')
|
|
24
|
+
self.mimetype = mimetype
|
|
25
|
+
self.headers = {'Content-Type': mimetype} if mimetype else {}
|
|
26
|
+
|
|
27
|
+
def get_data(self, as_text=False):
|
|
28
|
+
return self._text if as_text else self._text.encode('utf-8')
|
|
29
|
+
|
|
30
|
+
fake_https.Response = FakeResponse
|
|
31
|
+
# Provide a Request attribute used in type annotations in the module under test
|
|
32
|
+
fake_https.Request = types.SimpleNamespace
|
|
33
|
+
|
|
34
|
+
fake_root = types.ModuleType('firebase_functions')
|
|
35
|
+
fake_root.https_fn = fake_https
|
|
36
|
+
return fake_root, fake_https
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _make_fake_firebase_admin():
|
|
40
|
+
fake = types.ModuleType('firebase_admin')
|
|
41
|
+
# Minimal auth object so import succeeds; tests here don't rely on actual auth behaviour
|
|
42
|
+
fake.auth = types.SimpleNamespace(verify_id_token=lambda token: {'uid': 'test'})
|
|
43
|
+
return fake
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _make_fake_google_secretmanager():
|
|
47
|
+
google = types.ModuleType('google')
|
|
48
|
+
google_cloud = types.ModuleType('google.cloud')
|
|
49
|
+
secretmanager = types.ModuleType('google.cloud.secretmanager')
|
|
50
|
+
|
|
51
|
+
class FakeClient:
|
|
52
|
+
def access_secret_version(self, request):
|
|
53
|
+
raise RuntimeError("SecretManager should not be called in these tests")
|
|
54
|
+
|
|
55
|
+
secretmanager.SecretManagerServiceClient = FakeClient
|
|
56
|
+
google_cloud.secretmanager = secretmanager
|
|
57
|
+
google.cloud = google_cloud
|
|
58
|
+
return google, google_cloud, secretmanager
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_utils_module(monkeypatch):
|
|
62
|
+
"""Load a fresh copy of functions/utils.py as an isolated module.
|
|
63
|
+
|
|
64
|
+
This helper inserts necessary fake modules into sys.modules via the
|
|
65
|
+
provided monkeypatch fixture before loading the module so its import-time
|
|
66
|
+
dependencies succeed and are controllable by the tests.
|
|
67
|
+
"""
|
|
68
|
+
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
69
|
+
utils_path = os.path.join(repo_root, 'functions', 'utils.py')
|
|
70
|
+
assert os.path.exists(utils_path), f"Expected functions/utils.py to exist at {utils_path}"
|
|
71
|
+
|
|
72
|
+
# Prepare and register lightweight stubs needed at import time
|
|
73
|
+
fake_firebase_root, fake_https = _make_fake_firebase_functions()
|
|
74
|
+
monkeypatch.setitem(sys.modules, 'firebase_functions', fake_firebase_root)
|
|
75
|
+
monkeypatch.setitem(sys.modules, 'firebase_functions.https_fn', fake_https)
|
|
76
|
+
|
|
77
|
+
fake_firebase_admin = _make_fake_firebase_admin()
|
|
78
|
+
monkeypatch.setitem(sys.modules, 'firebase_admin', fake_firebase_admin)
|
|
79
|
+
|
|
80
|
+
google, google_cloud, secretmanager = _make_fake_google_secretmanager()
|
|
81
|
+
monkeypatch.setitem(sys.modules, 'google', google)
|
|
82
|
+
monkeypatch.setitem(sys.modules, 'google.cloud', google_cloud)
|
|
83
|
+
monkeypatch.setitem(sys.modules, 'google.cloud.secretmanager', secretmanager)
|
|
84
|
+
|
|
85
|
+
# Create a unique module name so each test gets a fresh module object
|
|
86
|
+
module_name = f"tests.utils_isolated_{uuid.uuid4().hex}"
|
|
87
|
+
spec = importlib.util.spec_from_file_location(module_name, utils_path)
|
|
88
|
+
module = importlib.util.module_from_spec(spec)
|
|
89
|
+
# Ensure the new module is discoverable under its name during execution
|
|
90
|
+
monkeypatch.setitem(sys.modules, module_name, module)
|
|
91
|
+
spec.loader.exec_module(module)
|
|
92
|
+
return module
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_create_json_response_returns_expected_json_and_status(monkeypatch):
|
|
96
|
+
mod = _load_utils_module(monkeypatch)
|
|
97
|
+
|
|
98
|
+
resp = mod.create_json_response(True, {"alpha": 1}, 201)
|
|
99
|
+
|
|
100
|
+
assert hasattr(resp, 'status_code')
|
|
101
|
+
assert resp.status_code == 201
|
|
102
|
+
|
|
103
|
+
body_text = resp.get_data(as_text=True)
|
|
104
|
+
parsed = json.loads(body_text)
|
|
105
|
+
assert parsed == {"success": True, "payload": {"alpha": 1}}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_get_sealedbox_private_key_raises_when_pynacl_privatekey_missing(monkeypatch):
|
|
109
|
+
# Ensure a nacl.public module exists but does NOT expose PrivateKey so the
|
|
110
|
+
# import inside get_sealedbox_private_key raises and the function reports
|
|
111
|
+
# that encryption support is unavailable.
|
|
112
|
+
nacl_mod = types.ModuleType('nacl')
|
|
113
|
+
nacl_public = types.ModuleType('nacl.public')
|
|
114
|
+
|
|
115
|
+
# Provide PublicKey/SealedBox but omit PrivateKey to trigger the import error
|
|
116
|
+
class PublicKeyStub:
|
|
117
|
+
def __init__(self, data):
|
|
118
|
+
self._data = bytes(data)
|
|
119
|
+
|
|
120
|
+
class SealedBoxStub:
|
|
121
|
+
def __init__(self, key):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
nacl_public.PublicKey = PublicKeyStub
|
|
125
|
+
nacl_public.SealedBox = SealedBoxStub
|
|
126
|
+
|
|
127
|
+
monkeypatch.setitem(sys.modules, 'nacl', nacl_mod)
|
|
128
|
+
monkeypatch.setitem(sys.modules, 'nacl.public', nacl_public)
|
|
129
|
+
|
|
130
|
+
mod = _load_utils_module(monkeypatch)
|
|
131
|
+
|
|
132
|
+
with pytest.raises(Exception) as excinfo:
|
|
133
|
+
mod.get_sealedbox_private_key()
|
|
134
|
+
|
|
135
|
+
assert 'Encryption support is unavailable' in str(excinfo.value)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_get_sealedbox_private_key_initializes_and_caches_private_key(monkeypatch):
|
|
139
|
+
# Provide a PrivateKey implementation and stub get_secret_key to return
|
|
140
|
+
# a valid 32-byte base64-encoded secret. Verify the value is cached and
|
|
141
|
+
# the secret fetch is only performed once.
|
|
142
|
+
key_bytes = b"\x02" * 32
|
|
143
|
+
|
|
144
|
+
nacl_mod = types.ModuleType('nacl')
|
|
145
|
+
nacl_public = types.ModuleType('nacl.public')
|
|
146
|
+
|
|
147
|
+
class PrivateKeyStub:
|
|
148
|
+
def __init__(self, data):
|
|
149
|
+
# Accept bytes-like input
|
|
150
|
+
self._data = bytes(data)
|
|
151
|
+
|
|
152
|
+
def __repr__(self):
|
|
153
|
+
return f"PrivateKeyStub(len={len(self._data)})"
|
|
154
|
+
|
|
155
|
+
nacl_public.PrivateKey = PrivateKeyStub
|
|
156
|
+
monkeypatch.setitem(sys.modules, 'nacl', nacl_mod)
|
|
157
|
+
monkeypatch.setitem(sys.modules, 'nacl.public', nacl_public)
|
|
158
|
+
|
|
159
|
+
mod = _load_utils_module(monkeypatch)
|
|
160
|
+
|
|
161
|
+
calls = {'count': 0}
|
|
162
|
+
|
|
163
|
+
def fake_get_secret_key(name):
|
|
164
|
+
calls['count'] += 1
|
|
165
|
+
return base64.b64encode(key_bytes).decode('ascii')
|
|
166
|
+
|
|
167
|
+
# Replace the module-level get_secret_key with our fake
|
|
168
|
+
mod.get_secret_key = fake_get_secret_key
|
|
169
|
+
|
|
170
|
+
# Ensure fresh start
|
|
171
|
+
mod._SEALEDBOX_PRIVATE_KEY = None
|
|
172
|
+
|
|
173
|
+
first = mod.get_sealedbox_private_key()
|
|
174
|
+
second = mod.get_sealedbox_private_key()
|
|
175
|
+
|
|
176
|
+
assert first is second
|
|
177
|
+
assert calls['count'] == 1
|
|
178
|
+
assert hasattr(first, '_data') and first._data == key_bytes
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_get_sealedbox_private_key_invalid_length_does_not_cache_and_raises(monkeypatch):
|
|
182
|
+
# Provide PrivateKey but have the secret manager return an invalid-length key
|
|
183
|
+
invalid_key = b"\x03" * 16
|
|
184
|
+
|
|
185
|
+
nacl_mod = types.ModuleType('nacl')
|
|
186
|
+
nacl_public = types.ModuleType('nacl.public')
|
|
187
|
+
|
|
188
|
+
class PrivateKeyStub:
|
|
189
|
+
def __init__(self, data):
|
|
190
|
+
self._data = bytes(data)
|
|
191
|
+
|
|
192
|
+
nacl_public.PrivateKey = PrivateKeyStub
|
|
193
|
+
monkeypatch.setitem(sys.modules, 'nacl', nacl_mod)
|
|
194
|
+
monkeypatch.setitem(sys.modules, 'nacl.public', nacl_public)
|
|
195
|
+
|
|
196
|
+
mod = _load_utils_module(monkeypatch)
|
|
197
|
+
|
|
198
|
+
def fake_get_secret_key(name):
|
|
199
|
+
return base64.b64encode(invalid_key).decode('ascii')
|
|
200
|
+
|
|
201
|
+
mod.get_secret_key = fake_get_secret_key
|
|
202
|
+
|
|
203
|
+
# Ensure fresh start
|
|
204
|
+
mod._SEALEDBOX_PRIVATE_KEY = None
|
|
205
|
+
|
|
206
|
+
with pytest.raises(Exception) as excinfo:
|
|
207
|
+
mod.get_sealedbox_private_key()
|
|
208
|
+
|
|
209
|
+
assert 'Failed to initialize sealed-box private key' in str(excinfo.value)
|
|
210
|
+
# Failure should not be cached
|
|
211
|
+
assert mod._SEALEDBOX_PRIVATE_KEY is None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ------------------------- Additional tests to improve coverage -------------------------
|
|
215
|
+
|
|
216
|
+
def test_get_secret_key_returns_secret_on_success(monkeypatch):
|
|
217
|
+
mod = _load_utils_module(monkeypatch)
|
|
218
|
+
|
|
219
|
+
fake_payload = types.SimpleNamespace(data=b'secret-value')
|
|
220
|
+
fake_response = types.SimpleNamespace(payload=fake_payload)
|
|
221
|
+
|
|
222
|
+
class FakeClient:
|
|
223
|
+
def access_secret_version(self, request):
|
|
224
|
+
return fake_response
|
|
225
|
+
|
|
226
|
+
# Patch the SecretManager client on the imported module
|
|
227
|
+
monkeypatch.setattr(mod.secretmanager, 'SecretManagerServiceClient', FakeClient, raising=True)
|
|
228
|
+
|
|
229
|
+
result = mod.get_secret_key('my-secret')
|
|
230
|
+
assert result == 'secret-value'
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_get_secret_key_raises_on_backend_error(monkeypatch):
|
|
234
|
+
mod = _load_utils_module(monkeypatch)
|
|
235
|
+
|
|
236
|
+
class FakeClient:
|
|
237
|
+
def access_secret_version(self, request):
|
|
238
|
+
raise RuntimeError('boom')
|
|
239
|
+
|
|
240
|
+
monkeypatch.setattr(mod.secretmanager, 'SecretManagerServiceClient', FakeClient, raising=True)
|
|
241
|
+
|
|
242
|
+
with pytest.raises(Exception) as excinfo:
|
|
243
|
+
mod.get_secret_key('does-not-matter')
|
|
244
|
+
assert 'Failed to retrieve secret key from Secret Manager' in str(excinfo.value)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_verify_firebase_token_success(monkeypatch):
|
|
248
|
+
mod = _load_utils_module(monkeypatch)
|
|
249
|
+
req = types.SimpleNamespace(headers={'Authorization': 'Bearer SOME_TOKEN'})
|
|
250
|
+
|
|
251
|
+
res = mod.verify_firebase_token(req)
|
|
252
|
+
assert res == {'uid': 'test'}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_verify_firebase_token_missing_or_invalid_header_raises(monkeypatch):
|
|
256
|
+
mod = _load_utils_module(monkeypatch)
|
|
257
|
+
req = types.SimpleNamespace(headers={})
|
|
258
|
+
|
|
259
|
+
with pytest.raises(Exception) as excinfo:
|
|
260
|
+
mod.verify_firebase_token(req)
|
|
261
|
+
# The implementation wraps the underlying message, but the cause should be present
|
|
262
|
+
assert 'Missing or invalid Authorization' in str(excinfo.value)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_verify_firebase_token_verify_id_token_failure_raises(monkeypatch):
|
|
266
|
+
mod = _load_utils_module(monkeypatch)
|
|
267
|
+
|
|
268
|
+
def fake_verify(token):
|
|
269
|
+
raise ValueError('invalid token')
|
|
270
|
+
|
|
271
|
+
# Replace the verifier with one that raises
|
|
272
|
+
mod.auth.verify_id_token = fake_verify
|
|
273
|
+
|
|
274
|
+
req = types.SimpleNamespace(headers={'Authorization': 'Bearer x'})
|
|
275
|
+
with pytest.raises(Exception) as excinfo:
|
|
276
|
+
mod.verify_firebase_token(req)
|
|
277
|
+
assert 'invalid token' in str(excinfo.value)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_get_api_key_delegates_to_get_secret_key(monkeypatch):
|
|
281
|
+
mod = _load_utils_module(monkeypatch)
|
|
282
|
+
|
|
283
|
+
called = {}
|
|
284
|
+
|
|
285
|
+
def fake_get_secret_key(name):
|
|
286
|
+
called['name'] = name
|
|
287
|
+
return 'X'
|
|
288
|
+
|
|
289
|
+
mod.get_secret_key = fake_get_secret_key
|
|
290
|
+
|
|
291
|
+
result = mod.get_api_key(mod.LlmFamily.OPENAI)
|
|
292
|
+
assert result == 'X'
|
|
293
|
+
assert called['name'] == 'openai-api-key'
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_get_request_json_plaintext_returns_parsed_json(monkeypatch):
|
|
297
|
+
mod = _load_utils_module(monkeypatch)
|
|
298
|
+
|
|
299
|
+
def get_json(silent=False):
|
|
300
|
+
return {'a': 1}
|
|
301
|
+
|
|
302
|
+
req = types.SimpleNamespace(headers={}, get_json=get_json)
|
|
303
|
+
result = mod.get_request_json(req, strict=False)
|
|
304
|
+
assert result == {'a': 1}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_get_request_json_plaintext_strict_raises_on_get_json_error(monkeypatch):
|
|
308
|
+
mod = _load_utils_module(monkeypatch)
|
|
309
|
+
|
|
310
|
+
def get_json(silent=False):
|
|
311
|
+
raise ValueError('bad json')
|
|
312
|
+
|
|
313
|
+
req = types.SimpleNamespace(headers={}, get_json=get_json)
|
|
314
|
+
with pytest.raises(Exception):
|
|
315
|
+
mod.get_request_json(req, strict=True)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_get_request_json_plaintext_non_strict_returns_none_on_get_json_error(monkeypatch):
|
|
319
|
+
mod = _load_utils_module(monkeypatch)
|
|
320
|
+
|
|
321
|
+
def get_json(silent=False):
|
|
322
|
+
raise ValueError('bad json')
|
|
323
|
+
|
|
324
|
+
req = types.SimpleNamespace(headers={}, get_json=get_json)
|
|
325
|
+
result = mod.get_request_json(req, strict=False)
|
|
326
|
+
assert result is None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _install_sealedbox_stub(monkeypatch, decrypt_behavior):
|
|
330
|
+
"""Helper: install a nacl.public.SealedBox stub whose decrypt returns or throws.
|
|
331
|
+
|
|
332
|
+
decrypt_behavior may be either:
|
|
333
|
+
- a bytes object to return from decrypt(), or
|
|
334
|
+
- a callable that accepts ciphertext and returns bytes / raises.
|
|
335
|
+
"""
|
|
336
|
+
nacl_mod = types.ModuleType('nacl')
|
|
337
|
+
nacl_public = types.ModuleType('nacl.public')
|
|
338
|
+
|
|
339
|
+
class SealedBox:
|
|
340
|
+
def __init__(self, key):
|
|
341
|
+
self._key = key
|
|
342
|
+
|
|
343
|
+
def decrypt(self, ciphertext):
|
|
344
|
+
if callable(decrypt_behavior):
|
|
345
|
+
return decrypt_behavior(ciphertext)
|
|
346
|
+
return decrypt_behavior
|
|
347
|
+
|
|
348
|
+
nacl_public.SealedBox = SealedBox
|
|
349
|
+
nacl_mod.public = nacl_public
|
|
350
|
+
|
|
351
|
+
# Ensure both module paths exist in sys.modules so `from nacl.public import SealedBox` works
|
|
352
|
+
monkeypatch.setitem(sys.modules, 'nacl', nacl_mod)
|
|
353
|
+
monkeypatch.setitem(sys.modules, 'nacl.public', nacl_public)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def test_get_request_json_encrypted_success_using_get_data(monkeypatch):
|
|
357
|
+
mod = _load_utils_module(monkeypatch)
|
|
358
|
+
|
|
359
|
+
plaintext = b'{"ok": true}'
|
|
360
|
+
ciphertext = b'FAKECIPHERTEXT'
|
|
361
|
+
body_b64 = base64.b64encode(ciphertext)
|
|
362
|
+
|
|
363
|
+
# Request exposes get_data(as_text=False)
|
|
364
|
+
def get_data(as_text=False):
|
|
365
|
+
return body_b64
|
|
366
|
+
|
|
367
|
+
req = types.SimpleNamespace(headers={mod.ENCRYPTION_HEADER_NAME: 'true'}, get_data=get_data)
|
|
368
|
+
|
|
369
|
+
# SealedBox.decrypt will return the plaintext regardless of ciphertext
|
|
370
|
+
_install_sealedbox_stub(monkeypatch, plaintext)
|
|
371
|
+
# Stub the private key loader so it doesn't call Secret Manager
|
|
372
|
+
monkeypatch.setattr(mod, 'get_sealedbox_private_key', lambda: 'DUMMY', raising=False)
|
|
373
|
+
|
|
374
|
+
res = mod.get_request_json(req, strict=True)
|
|
375
|
+
assert res == {"ok": True}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_get_request_json_encrypted_fallback_when_get_data_rejects_as_text_arg(monkeypatch):
|
|
379
|
+
mod = _load_utils_module(monkeypatch)
|
|
380
|
+
|
|
381
|
+
plaintext = b'{"ok": 1}'
|
|
382
|
+
ciphertext = b'CT'
|
|
383
|
+
body_b64 = base64.b64encode(ciphertext)
|
|
384
|
+
|
|
385
|
+
def get_data(*args, **kwargs):
|
|
386
|
+
# Simulate a stub that raises when given the as_text kwarg
|
|
387
|
+
if 'as_text' in kwargs:
|
|
388
|
+
raise TypeError('no as_text')
|
|
389
|
+
return body_b64
|
|
390
|
+
|
|
391
|
+
req = types.SimpleNamespace(headers={mod.ENCRYPTION_HEADER_NAME: 'true'}, get_data=get_data)
|
|
392
|
+
|
|
393
|
+
_install_sealedbox_stub(monkeypatch, plaintext)
|
|
394
|
+
monkeypatch.setattr(mod, 'get_sealedbox_private_key', lambda: 'DUMMY', raising=False)
|
|
395
|
+
|
|
396
|
+
res = mod.get_request_json(req, strict=True)
|
|
397
|
+
assert res == {"ok": 1}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def test_get_request_json_encrypted_uses_data_attribute_if_no_get_data(monkeypatch):
|
|
401
|
+
mod = _load_utils_module(monkeypatch)
|
|
402
|
+
|
|
403
|
+
plaintext = b'{"v": 42}'
|
|
404
|
+
ciphertext = b'CT2'
|
|
405
|
+
body_b64 = base64.b64encode(ciphertext)
|
|
406
|
+
|
|
407
|
+
req = types.SimpleNamespace(headers={mod.ENCRYPTION_HEADER_NAME: 'true'}, data=body_b64)
|
|
408
|
+
|
|
409
|
+
_install_sealedbox_stub(monkeypatch, plaintext)
|
|
410
|
+
monkeypatch.setattr(mod, 'get_sealedbox_private_key', lambda: 'DUMMY', raising=False)
|
|
411
|
+
|
|
412
|
+
res = mod.get_request_json(req, strict=True)
|
|
413
|
+
assert res == {"v": 42}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_get_request_json_encrypted_invalid_base64(monkeypatch):
|
|
417
|
+
mod = _load_utils_module(monkeypatch)
|
|
418
|
+
|
|
419
|
+
# Make b64 decoder throw to exercise that error path deterministically
|
|
420
|
+
def fake_b64decode(_):
|
|
421
|
+
raise Exception('bad base64')
|
|
422
|
+
|
|
423
|
+
monkeypatch.setattr(mod.base64, 'b64decode', fake_b64decode, raising=True)
|
|
424
|
+
|
|
425
|
+
def get_data(as_text=False):
|
|
426
|
+
return b'NOT-BASE64'
|
|
427
|
+
|
|
428
|
+
req = types.SimpleNamespace(headers={mod.ENCRYPTION_HEADER_NAME: 'true'}, get_data=get_data)
|
|
429
|
+
|
|
430
|
+
# Non-strict should return None
|
|
431
|
+
res = mod.get_request_json(req, strict=False)
|
|
432
|
+
assert res is None
|
|
433
|
+
|
|
434
|
+
# Strict should raise
|
|
435
|
+
with pytest.raises(Exception):
|
|
436
|
+
mod.get_request_json(req, strict=True)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_get_request_json_encrypted_decrypt_failure(monkeypatch):
|
|
440
|
+
mod = _load_utils_module(monkeypatch)
|
|
441
|
+
|
|
442
|
+
ciphertext = b'CT'
|
|
443
|
+
body_b64 = base64.b64encode(ciphertext)
|
|
444
|
+
|
|
445
|
+
# SealedBox.decrypt will raise to simulate decryption failure
|
|
446
|
+
def raise_on_decrypt(_):
|
|
447
|
+
raise RuntimeError('bad decrypt')
|
|
448
|
+
|
|
449
|
+
_install_sealedbox_stub(monkeypatch, raise_on_decrypt)
|
|
450
|
+
monkeypatch.setattr(mod, 'get_sealedbox_private_key', lambda: 'DUMMY', raising=False)
|
|
451
|
+
|
|
452
|
+
def get_data(as_text=False):
|
|
453
|
+
return body_b64
|
|
454
|
+
|
|
455
|
+
req = types.SimpleNamespace(headers={mod.ENCRYPTION_HEADER_NAME: 'true'}, get_data=get_data)
|
|
456
|
+
|
|
457
|
+
# Non-strict returns None
|
|
458
|
+
assert mod.get_request_json(req, strict=False) is None
|
|
459
|
+
|
|
460
|
+
# Strict raises
|
|
461
|
+
with pytest.raises(Exception):
|
|
462
|
+
mod.get_request_json(req, strict=True)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def test_get_request_json_encrypted_invalid_json_after_decrypt(monkeypatch):
|
|
466
|
+
mod = _load_utils_module(monkeypatch)
|
|
467
|
+
|
|
468
|
+
# Decrypt returns bytes that are not valid JSON
|
|
469
|
+
_install_sealedbox_stub(monkeypatch, b'not-json')
|
|
470
|
+
monkeypatch.setattr(mod, 'get_sealedbox_private_key', lambda: 'DUMMY', raising=False)
|
|
471
|
+
|
|
472
|
+
body_b64 = base64.b64encode(b'CT')
|
|
473
|
+
|
|
474
|
+
def get_data(as_text=False):
|
|
475
|
+
return body_b64
|
|
476
|
+
|
|
477
|
+
req = types.SimpleNamespace(headers={mod.ENCRYPTION_HEADER_NAME: 'true'}, get_data=get_data)
|
|
478
|
+
|
|
479
|
+
assert mod.get_request_json(req, strict=False) is None
|
|
480
|
+
with pytest.raises(Exception):
|
|
481
|
+
mod.get_request_json(req, strict=True)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# End of additional tests
|
|
Binary file
|
|
Binary file
|