roksta 0.2.4__cp314-cp314t-win_amd64.whl → 0.2.6__cp314-cp314t-win_amd64.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/__init__.cp314t-win_amd64.pyd +0 -0
- roksta/ai/__init__.cp314t-win_amd64.pyd +0 -0
- roksta/ai/call_ai.cp314t-win_amd64.pyd +0 -0
- roksta/ai/gemini.cp314t-win_amd64.pyd +0 -0
- roksta/ai/generic.cp314t-win_amd64.pyd +0 -0
- roksta/ai/llm.cp314t-win_amd64.pyd +0 -0
- roksta/ai/openai.cp314t-win_amd64.pyd +0 -0
- roksta/ai/tools.cp314t-win_amd64.pyd +0 -0
- roksta/analytics.cp314t-win_amd64.pyd +0 -0
- roksta/balance.cp314t-win_amd64.pyd +0 -0
- roksta/build_project.cp314t-win_amd64.pyd +0 -0
- roksta/chat_workflow.cp314t-win_amd64.pyd +0 -0
- roksta/check_for_updates.cp314t-win_amd64.pyd +0 -0
- roksta/checkpoints.cp314t-win_amd64.pyd +0 -0
- roksta/clarify_goal.cp314t-win_amd64.pyd +0 -0
- roksta/codebase_listing.cp314t-win_amd64.pyd +0 -0
- roksta/command_handlers.cp314t-win_amd64.pyd +0 -0
- roksta/create_default_config.cp314t-win_amd64.pyd +0 -0
- roksta/default_config.cp314t-win_amd64.pyd +0 -0
- roksta/enums.cp314t-win_amd64.pyd +0 -0
- roksta/env.cp314t-win_amd64.pyd +0 -0
- roksta/extended_text_area.cp314t-win_amd64.pyd +0 -0
- roksta/firebase.cp314t-win_amd64.pyd +0 -0
- roksta/firebase_auth_web.cp314t-win_amd64.pyd +0 -0
- roksta/firebase_config.cp314t-win_amd64.pyd +0 -0
- roksta/fix_tests.cp314t-win_amd64.pyd +0 -0
- roksta/gen_codebase_summaries.cp314t-win_amd64.pyd +0 -0
- roksta/gen_one_line_goal.cp314t-win_amd64.pyd +0 -0
- roksta/get_codebase_structure.cp314t-win_amd64.pyd +0 -0
- roksta/get_failing_tests.cp314t-win_amd64.pyd +0 -0
- roksta/goal_workflow.cp314t-win_amd64.pyd +0 -0
- roksta/init_codebase.cp314t-win_amd64.pyd +0 -0
- roksta/lint_code.cp314t-win_amd64.pyd +0 -0
- roksta/logger.cp314t-win_amd64.pyd +0 -0
- roksta/main.cp314t-win_amd64.pyd +0 -0
- roksta/make_issue.cp314t-win_amd64.pyd +0 -0
- roksta/new_features.cp314t-win_amd64.pyd +0 -0
- roksta/parse_readme.cp314t-win_amd64.pyd +0 -0
- roksta/propose_solution.cp314t-win_amd64.pyd +0 -0
- roksta/response_formats.cp314t-win_amd64.pyd +0 -0
- roksta/rewrite_goal.cp314t-win_amd64.pyd +0 -0
- roksta/roksta.cp314t-win_amd64.pyd +0 -0
- roksta/run_cli_goal.cp314t-win_amd64.pyd +0 -0
- roksta/select_files.cp314t-win_amd64.pyd +0 -0
- roksta/tips.cp314t-win_amd64.pyd +0 -0
- roksta/utils.cp314t-win_amd64.pyd +0 -0
- roksta/write_code.cp314t-win_amd64.pyd +0 -0
- {roksta-0.2.4.dist-info → roksta-0.2.6.dist-info}/METADATA +1 -1
- roksta-0.2.6.dist-info/RECORD +78 -0
- {roksta-0.2.4.dist-info → roksta-0.2.6.dist-info}/top_level.txt +1 -0
- tests/__init__.py +2 -0
- tests/conftest.py +169 -0
- tests/functions/__init__.py +2 -0
- tests/functions/api_v0_01/__init__.py +2 -0
- tests/functions/api_v0_01/test__analytics.py +417 -0
- tests/functions/api_v0_01/test__gemini_proxy.py +307 -0
- tests/functions/api_v0_01/test__generic_proxy.py +399 -0
- tests/functions/api_v0_01/test__get_payment_details.py +356 -0
- tests/functions/api_v0_01/test__openai_proxy.py +413 -0
- tests/functions/api_v0_01/test__redeem_credit_code.py +167 -0
- tests/functions/api_v0_01/test__sync_emails.py +324 -0
- tests/functions/api_v0_01/test__take_payment.py +491 -0
- tests/functions/api_v0_01/test__use_activation_code.py +437 -0
- tests/functions/api_v1_00/__init__.py +2 -0
- tests/functions/api_v1_00/test__analytics.py +416 -0
- tests/functions/api_v1_00/test__gemini_proxy.py +352 -0
- tests/functions/api_v1_00/test__generic_proxy.py +428 -0
- tests/functions/api_v1_00/test__get_payment_details.py +356 -0
- tests/functions/api_v1_00/test__openai_proxy.py +449 -0
- tests/functions/api_v1_00/test__redeem_credit_code.py +167 -0
- tests/functions/api_v1_00/test__sync_emails.py +325 -0
- tests/functions/api_v1_00/test__take_payment.py +491 -0
- tests/functions/api_v1_00/test__use_activation_code.py +438 -0
- tests/functions/test_auth.py +24 -0
- tests/functions/test_main_functions.py +73 -0
- tests/functions/test_utils_functions.py +222 -0
- roksta-0.2.4.dist-info/RECORD +0 -51
- {roksta-0.2.4.dist-info → roksta-0.2.6.dist-info}/WHEEL +0 -0
- {roksta-0.2.4.dist-info → roksta-0.2.6.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import types
|
|
5
|
+
import importlib
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
from datetime import datetime, timezone, timedelta
|
|
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 _use_activation_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 = [
|
|
22
|
+
'firebase_functions',
|
|
23
|
+
'utils',
|
|
24
|
+
'auth',
|
|
25
|
+
'firebase_admin',
|
|
26
|
+
'firebase_admin.firestore',
|
|
27
|
+
]
|
|
28
|
+
for name in _names_to_fake:
|
|
29
|
+
_orig_sys_modules[name] = sys.modules.get(name)
|
|
30
|
+
|
|
31
|
+
# Fake firebase_functions.https_fn.Response to capture returned data
|
|
32
|
+
firebase_functions = types.ModuleType('firebase_functions')
|
|
33
|
+
|
|
34
|
+
class FakeResponse:
|
|
35
|
+
def __init__(self, response=None, mimetype=None, status=200, **kwargs):
|
|
36
|
+
# Mirror the small subset of the interface tests expect
|
|
37
|
+
self.status_code = status
|
|
38
|
+
if isinstance(response, (dict, list)):
|
|
39
|
+
self._body_text = json.dumps(response)
|
|
40
|
+
else:
|
|
41
|
+
self._body_text = '' if response is None else response
|
|
42
|
+
self.headers = kwargs.get('headers', {})
|
|
43
|
+
|
|
44
|
+
def get_data(self, as_text=False):
|
|
45
|
+
if as_text:
|
|
46
|
+
return self._body_text
|
|
47
|
+
return self._body_text.encode('utf-8')
|
|
48
|
+
|
|
49
|
+
firebase_functions.https_fn = types.SimpleNamespace(Request=object, Response=FakeResponse)
|
|
50
|
+
sys.modules['firebase_functions'] = firebase_functions
|
|
51
|
+
|
|
52
|
+
# Fake utils module (create_json_response, verify_firebase_token)
|
|
53
|
+
utils_mod = types.ModuleType('utils')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _fake_create_json_response(success: bool, payload: any, status_code: int):
|
|
57
|
+
response_body = {"success": success, "payload": payload}
|
|
58
|
+
return firebase_functions.https_fn.Response(response=response_body, status=status_code, headers={'Content-Type': 'application/json'})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _fake_verify_firebase_token(req):
|
|
62
|
+
# Default: successful verification returning a uid
|
|
63
|
+
return {'uid': 'user_1'}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
utils_mod.create_json_response = _fake_create_json_response
|
|
67
|
+
utils_mod.verify_firebase_token = _fake_verify_firebase_token
|
|
68
|
+
sys.modules['utils'] = utils_mod
|
|
69
|
+
|
|
70
|
+
# Fake top-level auth module (validate_auth_key will be patched per-test as needed)
|
|
71
|
+
auth_mod = types.ModuleType('auth')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _simple_validate_auth_key(val: str) -> bool:
|
|
75
|
+
return bool(val)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
auth_mod.validate_auth_key = _simple_validate_auth_key
|
|
79
|
+
sys.modules['auth'] = auth_mod
|
|
80
|
+
|
|
81
|
+
# Fake firebase_admin.firestore using an in-memory dict
|
|
82
|
+
firebase_admin_mod = types.ModuleType('firebase_admin')
|
|
83
|
+
|
|
84
|
+
# In-memory fake DB used by the fake Firestore implementation
|
|
85
|
+
fake_db_data = {}
|
|
86
|
+
|
|
87
|
+
class DocumentSnapshot:
|
|
88
|
+
def __init__(self, exists, data):
|
|
89
|
+
self.exists = exists
|
|
90
|
+
self._data = data
|
|
91
|
+
|
|
92
|
+
def to_dict(self):
|
|
93
|
+
return self._data
|
|
94
|
+
|
|
95
|
+
def get(self, key):
|
|
96
|
+
if not self._data:
|
|
97
|
+
return None
|
|
98
|
+
return self._data.get(key)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class DocumentRef:
|
|
102
|
+
def __init__(self, collection_name, doc_id):
|
|
103
|
+
self.collection = collection_name
|
|
104
|
+
self.doc_id = doc_id
|
|
105
|
+
|
|
106
|
+
def get(self, transaction=None):
|
|
107
|
+
coll = fake_db_data.get(self.collection, {})
|
|
108
|
+
if self.doc_id in coll:
|
|
109
|
+
return DocumentSnapshot(True, coll[self.doc_id])
|
|
110
|
+
return DocumentSnapshot(False, None)
|
|
111
|
+
|
|
112
|
+
def update(self, update_dict):
|
|
113
|
+
coll = fake_db_data.setdefault(self.collection, {})
|
|
114
|
+
doc = coll.setdefault(self.doc_id, {})
|
|
115
|
+
if isinstance(doc, dict):
|
|
116
|
+
doc.update(update_dict)
|
|
117
|
+
else:
|
|
118
|
+
coll[self.doc_id] = update_dict
|
|
119
|
+
|
|
120
|
+
def set(self, data):
|
|
121
|
+
coll = fake_db_data.setdefault(self.collection, {})
|
|
122
|
+
coll[self.doc_id] = data
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class CollectionRef:
|
|
126
|
+
def __init__(self, name):
|
|
127
|
+
self.name = name
|
|
128
|
+
|
|
129
|
+
def document(self, doc_id):
|
|
130
|
+
return DocumentRef(self.name, doc_id)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class FakeTransaction:
|
|
134
|
+
def __init__(self):
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
def update(self, doc_ref, update_dict):
|
|
138
|
+
coll = fake_db_data.setdefault(doc_ref.collection, {})
|
|
139
|
+
doc = coll.setdefault(doc_ref.doc_id, {})
|
|
140
|
+
if isinstance(doc, dict):
|
|
141
|
+
doc.update(update_dict)
|
|
142
|
+
else:
|
|
143
|
+
coll[doc_ref.doc_id] = update_dict
|
|
144
|
+
|
|
145
|
+
def set(self, doc_ref, data):
|
|
146
|
+
coll = fake_db_data.setdefault(doc_ref.collection, {})
|
|
147
|
+
coll[doc_ref.doc_id] = data
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class FakeFirestoreClient:
|
|
151
|
+
def __init__(self):
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
def collection(self, name):
|
|
155
|
+
return CollectionRef(name)
|
|
156
|
+
|
|
157
|
+
def transaction(self):
|
|
158
|
+
return FakeTransaction()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
fake_firestore_mod = types.ModuleType('firebase_admin.firestore')
|
|
162
|
+
fake_firestore_mod.client = lambda: FakeFirestoreClient()
|
|
163
|
+
fake_firestore_mod.transactional = lambda f: f
|
|
164
|
+
fake_firestore_mod.SERVER_TIMESTAMP = object()
|
|
165
|
+
sys.modules['firebase_admin.firestore'] = fake_firestore_mod
|
|
166
|
+
firebase_admin_mod.firestore = fake_firestore_mod
|
|
167
|
+
sys.modules['firebase_admin'] = firebase_admin_mod
|
|
168
|
+
|
|
169
|
+
# -----------------------------
|
|
170
|
+
# Import the module under test after preparing the fake imports
|
|
171
|
+
# -----------------------------
|
|
172
|
+
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
|
173
|
+
functions_root = os.path.join(repo_root, 'functions')
|
|
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
|
+
_use_activation = importlib.util.module_from_spec(spec)
|
|
177
|
+
spec.loader.exec_module(_use_activation)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Restore original sys.modules mappings to avoid side-effects for other tests
|
|
181
|
+
for name, orig in _orig_sys_modules.items():
|
|
182
|
+
if orig is None:
|
|
183
|
+
try:
|
|
184
|
+
del sys.modules[name]
|
|
185
|
+
except KeyError:
|
|
186
|
+
pass
|
|
187
|
+
else:
|
|
188
|
+
sys.modules[name] = orig
|
|
189
|
+
|
|
190
|
+
# Import env constants for auth header usage in tests
|
|
191
|
+
import env as functions_env # noqa: E402
|
|
192
|
+
AUTH_HEADER_NAME = functions_env.AUTH_HEADER_NAME
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# Simple helper request stub used in the tests
|
|
196
|
+
class DummyRequest:
|
|
197
|
+
def __init__(self, headers=None, method='POST', json_data=None, raise_on_get_json=False):
|
|
198
|
+
self.headers = headers or {}
|
|
199
|
+
self.method = method
|
|
200
|
+
self._json_data = json_data
|
|
201
|
+
self._raise = raise_on_get_json
|
|
202
|
+
|
|
203
|
+
def get_json(self, silent=True):
|
|
204
|
+
if self._raise:
|
|
205
|
+
raise Exception('Malformed JSON')
|
|
206
|
+
return self._json_data
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _parse_response(resp):
|
|
210
|
+
data = resp.get_data(as_text=True)
|
|
211
|
+
return json.loads(data)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# -----------------------------
|
|
215
|
+
# Tests
|
|
216
|
+
# -----------------------------
|
|
217
|
+
|
|
218
|
+
def test_missing_auth_header_returns_401():
|
|
219
|
+
fake_db_data.clear()
|
|
220
|
+
req = DummyRequest(headers={}, method='POST')
|
|
221
|
+
# Ensure token verification isn't called when header missing
|
|
222
|
+
with patch.object(_use_activation, 'verify_firebase_token', side_effect=Exception('Should not be called')):
|
|
223
|
+
resp = _use_activation._use_activation_code(req)
|
|
224
|
+
|
|
225
|
+
assert resp.status_code == 401
|
|
226
|
+
payload = _parse_response(resp)
|
|
227
|
+
assert payload['success'] is False
|
|
228
|
+
assert 'Missing app authentication key' in payload['payload']
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_invalid_auth_key_returns_403():
|
|
232
|
+
fake_db_data.clear()
|
|
233
|
+
headers = {AUTH_HEADER_NAME: 'bad-token'}
|
|
234
|
+
req = DummyRequest(headers=headers, method='POST')
|
|
235
|
+
with patch.object(_use_activation, 'validate_auth_key', return_value=False):
|
|
236
|
+
resp = _use_activation._use_activation_code(req)
|
|
237
|
+
|
|
238
|
+
assert resp.status_code == 403
|
|
239
|
+
payload = _parse_response(resp)
|
|
240
|
+
assert payload['success'] is False
|
|
241
|
+
assert 'Invalid app authentication key' in payload['payload']
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_verify_firebase_token_failure_returns_403():
|
|
245
|
+
fake_db_data.clear()
|
|
246
|
+
headers = {AUTH_HEADER_NAME: 'ok'}
|
|
247
|
+
req = DummyRequest(headers=headers, method='POST')
|
|
248
|
+
with patch.object(_use_activation, 'validate_auth_key', return_value=True), patch.object(_use_activation, 'verify_firebase_token', side_effect=Exception('boom')):
|
|
249
|
+
resp = _use_activation._use_activation_code(req)
|
|
250
|
+
|
|
251
|
+
assert resp.status_code == 403
|
|
252
|
+
payload = _parse_response(resp)
|
|
253
|
+
assert payload['success'] is False
|
|
254
|
+
assert 'Authentication failed' in payload['payload']
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_missing_json_payload_returns_400():
|
|
258
|
+
fake_db_data.clear()
|
|
259
|
+
headers = {AUTH_HEADER_NAME: 'ok'}
|
|
260
|
+
req = DummyRequest(headers=headers, method='POST', json_data=None)
|
|
261
|
+
with patch.object(_use_activation, 'validate_auth_key', return_value=True), patch.object(_use_activation, 'verify_firebase_token', return_value={'uid': 'user_1'}):
|
|
262
|
+
resp = _use_activation._use_activation_code(req)
|
|
263
|
+
|
|
264
|
+
assert resp.status_code == 400
|
|
265
|
+
payload = _parse_response(resp)
|
|
266
|
+
assert payload['success'] is False
|
|
267
|
+
assert 'Invalid or missing JSON payload' in payload['payload']
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_missing_activation_code_returns_400():
|
|
271
|
+
fake_db_data.clear()
|
|
272
|
+
headers = {AUTH_HEADER_NAME: 'ok'}
|
|
273
|
+
# Provide a non-empty JSON payload without 'activation_code' to trigger the missing key branch
|
|
274
|
+
req = DummyRequest(headers=headers, method='POST', json_data={'foo': 'bar'})
|
|
275
|
+
with patch.object(_use_activation, 'validate_auth_key', return_value=True), patch.object(_use_activation, 'verify_firebase_token', return_value={'uid': 'user_1'}):
|
|
276
|
+
resp = _use_activation._use_activation_code(req)
|
|
277
|
+
|
|
278
|
+
assert resp.status_code == 400
|
|
279
|
+
payload = _parse_response(resp)
|
|
280
|
+
assert payload['success'] is False
|
|
281
|
+
assert "Missing 'activation_code'" in payload['payload']
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_user_does_not_exist_returns_404():
|
|
285
|
+
fake_db_data.clear()
|
|
286
|
+
headers = {AUTH_HEADER_NAME: 'ok'}
|
|
287
|
+
req = DummyRequest(headers=headers, method='POST', json_data={'activation_code': 'code1'})
|
|
288
|
+
with patch.object(_use_activation, 'validate_auth_key', return_value=True), patch.object(_use_activation, 'verify_firebase_token', return_value={'uid': 'user_1'}):
|
|
289
|
+
resp = _use_activation._use_activation_code(req)
|
|
290
|
+
|
|
291
|
+
assert resp.status_code == 404
|
|
292
|
+
payload = _parse_response(resp)
|
|
293
|
+
assert payload['success'] is False
|
|
294
|
+
assert 'User does not exist' in payload['payload']
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def test_user_already_active_returns_200():
|
|
298
|
+
fake_db_data.clear()
|
|
299
|
+
fake_db_data.setdefault('users', {})['user_1'] = {'active': True}
|
|
300
|
+
|
|
301
|
+
headers = {AUTH_HEADER_NAME: 'ok'}
|
|
302
|
+
req = DummyRequest(headers=headers, method='POST', json_data={'activation_code': 'code1'})
|
|
303
|
+
|
|
304
|
+
with patch.object(_use_activation, 'validate_auth_key', return_value=True), patch.object(_use_activation, 'verify_firebase_token', return_value={'uid': 'user_1'}):
|
|
305
|
+
resp = _use_activation._use_activation_code(req)
|
|
306
|
+
|
|
307
|
+
assert resp.status_code == 200
|
|
308
|
+
payload = _parse_response(resp)
|
|
309
|
+
assert payload['success'] is True
|
|
310
|
+
assert 'Account is already active' in payload['payload']
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_activation_code_not_found_returns_404():
|
|
314
|
+
fake_db_data.clear()
|
|
315
|
+
# user exists and inactive
|
|
316
|
+
fake_db_data.setdefault('users', {})['user_1'] = {'active': False}
|
|
317
|
+
|
|
318
|
+
headers = {AUTH_HEADER_NAME: 'ok'}
|
|
319
|
+
req = DummyRequest(headers=headers, method='POST', json_data={'activation_code': 'CodeX'})
|
|
320
|
+
|
|
321
|
+
with patch.object(_use_activation, 'validate_auth_key', return_value=True), patch.object(_use_activation, 'verify_firebase_token', return_value={'uid': 'user_1'}):
|
|
322
|
+
resp = _use_activation._use_activation_code(req)
|
|
323
|
+
|
|
324
|
+
assert resp.status_code == 404
|
|
325
|
+
payload = _parse_response(resp)
|
|
326
|
+
assert payload['success'] is False
|
|
327
|
+
assert 'Invalid activation code' in payload['payload']
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def test_activation_code_missing_expiry_returns_500():
|
|
331
|
+
fake_db_data.clear()
|
|
332
|
+
# user exists and inactive
|
|
333
|
+
fake_db_data.setdefault('users', {})['user_1'] = {'active': False}
|
|
334
|
+
# activation code exists but missing expiry_date
|
|
335
|
+
fake_db_data.setdefault('activation_codes', {})['codea'] = {'use_count': 0, 'max_uses': 1}
|
|
336
|
+
|
|
337
|
+
headers = {AUTH_HEADER_NAME: 'ok'}
|
|
338
|
+
req = DummyRequest(headers=headers, method='POST', json_data={'activation_code': 'codeA'})
|
|
339
|
+
|
|
340
|
+
with patch.object(_use_activation, 'validate_auth_key', return_value=True), patch.object(_use_activation, 'verify_firebase_token', return_value={'uid': 'user_1'}):
|
|
341
|
+
resp = _use_activation._use_activation_code(req)
|
|
342
|
+
|
|
343
|
+
assert resp.status_code == 500
|
|
344
|
+
payload = _parse_response(resp)
|
|
345
|
+
assert payload['success'] is False
|
|
346
|
+
assert 'Activation code expiry information is missing' in payload['payload']
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_activation_code_expired_returns_400():
|
|
350
|
+
fake_db_data.clear()
|
|
351
|
+
fake_db_data.setdefault('users', {})['user_1'] = {'active': False}
|
|
352
|
+
past = datetime.now(timezone.utc) - timedelta(days=1)
|
|
353
|
+
fake_db_data.setdefault('activation_codes', {})['expiredcode'] = {'expiry_date': past, 'use_count': 0, 'max_uses': 5}
|
|
354
|
+
|
|
355
|
+
headers = {AUTH_HEADER_NAME: 'ok'}
|
|
356
|
+
req = DummyRequest(headers=headers, method='POST', json_data={'activation_code': 'expiredCode'})
|
|
357
|
+
|
|
358
|
+
with patch.object(_use_activation, 'validate_auth_key', return_value=True), patch.object(_use_activation, 'verify_firebase_token', return_value={'uid': 'user_1'}):
|
|
359
|
+
resp = _use_activation._use_activation_code(req)
|
|
360
|
+
|
|
361
|
+
assert resp.status_code == 400
|
|
362
|
+
payload = _parse_response(resp)
|
|
363
|
+
assert payload['success'] is False
|
|
364
|
+
assert 'Activation code has expired' in payload['payload']
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def test_activation_code_usage_limit_reached_returns_400():
|
|
368
|
+
fake_db_data.clear()
|
|
369
|
+
fake_db_data.setdefault('users', {})['user_1'] = {'active': False}
|
|
370
|
+
fake_db_data.setdefault('activation_codes', {})['limitcode'] = {'expiry_date': datetime.now(timezone.utc) + timedelta(days=1), 'use_count': 5, 'max_uses': 5}
|
|
371
|
+
|
|
372
|
+
headers = {AUTH_HEADER_NAME: 'ok'}
|
|
373
|
+
req = DummyRequest(headers=headers, method='POST', json_data={'activation_code': 'limitCode'})
|
|
374
|
+
|
|
375
|
+
with patch.object(_use_activation, 'validate_auth_key', return_value=True), patch.object(_use_activation, 'verify_firebase_token', return_value={'uid': 'user_1'}):
|
|
376
|
+
resp = _use_activation._use_activation_code(req)
|
|
377
|
+
|
|
378
|
+
assert resp.status_code == 400
|
|
379
|
+
payload = _parse_response(resp)
|
|
380
|
+
assert payload['success'] is False
|
|
381
|
+
assert 'Usage limit has been hit' in payload['payload']
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def test_user_already_used_code_returns_400():
|
|
385
|
+
fake_db_data.clear()
|
|
386
|
+
fake_db_data.setdefault('users', {})['user_1'] = {'active': False}
|
|
387
|
+
code = 'multi'
|
|
388
|
+
fake_db_data.setdefault('activation_codes', {})[code] = {'expiry_date': datetime.now(timezone.utc) + timedelta(days=1), 'use_count': 0, 'max_uses': 5}
|
|
389
|
+
# Create an activation record indicating user already used the code
|
|
390
|
+
fake_db_data.setdefault('activations', {})[f"{code}_user_1"] = {'activation_code': code, 'user_id': 'user_1', 'timestamp': fake_firestore_mod.SERVER_TIMESTAMP}
|
|
391
|
+
|
|
392
|
+
headers = {AUTH_HEADER_NAME: 'ok'}
|
|
393
|
+
req = DummyRequest(headers=headers, method='POST', json_data={'activation_code': 'MULTI'})
|
|
394
|
+
|
|
395
|
+
with patch.object(_use_activation, 'validate_auth_key', return_value=True), patch.object(_use_activation, 'verify_firebase_token', return_value={'uid': 'user_1'}):
|
|
396
|
+
resp = _use_activation._use_activation_code(req)
|
|
397
|
+
|
|
398
|
+
assert resp.status_code == 400
|
|
399
|
+
payload = _parse_response(resp)
|
|
400
|
+
assert payload['success'] is False
|
|
401
|
+
assert 'User has already used this activation code' in payload['payload']
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def test_successful_activation_returns_200_and_updates_db():
|
|
405
|
+
fake_db_data.clear()
|
|
406
|
+
fake_db_data.setdefault('users', {})['user_1'] = {'active': False}
|
|
407
|
+
code = 'successcode'
|
|
408
|
+
fake_db_data.setdefault('activation_codes', {})[code] = {'expiry_date': datetime.now(timezone.utc) + timedelta(days=1), 'use_count': 2, 'max_uses': 5}
|
|
409
|
+
|
|
410
|
+
headers = {AUTH_HEADER_NAME: 'ok'}
|
|
411
|
+
req = DummyRequest(headers=headers, method='POST', json_data={'activation_code': 'SuccessCode'})
|
|
412
|
+
|
|
413
|
+
with patch.object(_use_activation, 'validate_auth_key', return_value=True), patch.object(_use_activation, 'verify_firebase_token', return_value={'uid': 'user_1'}):
|
|
414
|
+
resp = _use_activation._use_activation_code(req)
|
|
415
|
+
|
|
416
|
+
assert resp.status_code == 200
|
|
417
|
+
payload = _parse_response(resp)
|
|
418
|
+
assert payload['success'] is True
|
|
419
|
+
assert 'Activation code used successfully' in payload['payload']
|
|
420
|
+
|
|
421
|
+
# Verify database updates
|
|
422
|
+
user_doc = fake_db_data.get('users', {}).get('user_1')
|
|
423
|
+
assert user_doc is not None
|
|
424
|
+
assert user_doc.get('active') is True
|
|
425
|
+
|
|
426
|
+
code_doc = fake_db_data.get('activation_codes', {}).get(code)
|
|
427
|
+
assert code_doc is not None
|
|
428
|
+
assert code_doc.get('use_count') == 3
|
|
429
|
+
|
|
430
|
+
activation_key = f"{code}_user_1"
|
|
431
|
+
activations = fake_db_data.get('activations', {})
|
|
432
|
+
assert activation_key in activations
|
|
433
|
+
act = activations[activation_key]
|
|
434
|
+
assert act['user_id'] == 'user_1'
|
|
435
|
+
assert act['activation_code'] == code
|
|
436
|
+
assert 'timestamp' in act
|
|
437
|
+
# timestamp should be the firestore.SERVER_TIMESTAMP sentinel
|
|
438
|
+
assert act['timestamp'] is fake_firestore_mod.SERVER_TIMESTAMP
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from cryptography.fernet import Fernet
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_validate_auth_key_returns_true_for_valid_token():
|
|
6
|
+
"""Encrypt the known AUTH_KEY with the module's FERNET_KEY and verify validation passes."""
|
|
7
|
+
auth = importlib.import_module('auth')
|
|
8
|
+
cipher = Fernet(auth.FERNET_KEY)
|
|
9
|
+
token = cipher.encrypt(auth.AUTH_KEY.encode()).decode('utf-8')
|
|
10
|
+
assert auth.validate_auth_key(token) is True
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_validate_auth_key_returns_false_for_invalid_token_string():
|
|
14
|
+
"""Passing a non-Fernet string should return False rather than raise."""
|
|
15
|
+
auth = importlib.import_module('auth')
|
|
16
|
+
assert auth.validate_auth_key('not-a-token') is False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_validate_auth_key_returns_false_for_wrong_plaintext():
|
|
20
|
+
"""Encrypting a different plaintext with the correct key should not validate."""
|
|
21
|
+
auth = importlib.import_module('auth')
|
|
22
|
+
cipher = Fernet(auth.FERNET_KEY)
|
|
23
|
+
token = cipher.encrypt(b'WRONG').decode('utf-8')
|
|
24
|
+
assert auth.validate_auth_key(token) is False
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import sys
|
|
3
|
+
import types
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_import_initializes_firebase_and_stripe_webhook_delegates():
|
|
7
|
+
# Save originals
|
|
8
|
+
orig = {}
|
|
9
|
+
for name in ('firebase_admin', 'firebase_functions', '_stripe_webhook', 'env', 'main'):
|
|
10
|
+
orig[name] = sys.modules.get(name)
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
# Stub env so DEPLOY_VERSIONS is empty and main won't try to dynamically import endpoints
|
|
14
|
+
env_mod = types.ModuleType('env')
|
|
15
|
+
env_mod.DEPLOY_VERSIONS = []
|
|
16
|
+
sys.modules['env'] = env_mod
|
|
17
|
+
|
|
18
|
+
# Stub firebase_admin.initialize_app and record invocation
|
|
19
|
+
init_called = {'called': False}
|
|
20
|
+
|
|
21
|
+
def fake_initialize_app():
|
|
22
|
+
init_called['called'] = True
|
|
23
|
+
|
|
24
|
+
firebase_admin_mod = types.ModuleType('firebase_admin')
|
|
25
|
+
firebase_admin_mod.initialize_app = fake_initialize_app
|
|
26
|
+
sys.modules['firebase_admin'] = firebase_admin_mod
|
|
27
|
+
|
|
28
|
+
# Stub firebase_functions.https_fn.on_request decorator used by main
|
|
29
|
+
def on_request(memory=None, timeout_sec=None):
|
|
30
|
+
def decorator(fn):
|
|
31
|
+
# return the function unchanged; this keeps behavior simple for testing
|
|
32
|
+
return fn
|
|
33
|
+
return decorator
|
|
34
|
+
|
|
35
|
+
firebase_functions_mod = types.ModuleType('firebase_functions')
|
|
36
|
+
firebase_functions_mod.https_fn = types.SimpleNamespace(on_request=on_request, Request=object, Response=object)
|
|
37
|
+
sys.modules['firebase_functions'] = firebase_functions_mod
|
|
38
|
+
|
|
39
|
+
# Stub the _stripe_webhook implementation that main imports
|
|
40
|
+
sentinel = {'sentinel': True}
|
|
41
|
+
|
|
42
|
+
def _stripe_webhook(req):
|
|
43
|
+
return sentinel
|
|
44
|
+
|
|
45
|
+
stripe_mod = types.ModuleType('_stripe_webhook')
|
|
46
|
+
stripe_mod._stripe_webhook = _stripe_webhook
|
|
47
|
+
sys.modules['_stripe_webhook'] = stripe_mod
|
|
48
|
+
|
|
49
|
+
# Ensure main is imported fresh
|
|
50
|
+
if 'main' in sys.modules:
|
|
51
|
+
del sys.modules['main']
|
|
52
|
+
|
|
53
|
+
main = importlib.import_module('main')
|
|
54
|
+
|
|
55
|
+
# initialize_app should have been called during import
|
|
56
|
+
assert init_called['called'] is True
|
|
57
|
+
|
|
58
|
+
# The stripe_webhook exported function should delegate to our stub
|
|
59
|
+
class DummyRequest:
|
|
60
|
+
def __init__(self, headers=None):
|
|
61
|
+
self.headers = headers or {}
|
|
62
|
+
|
|
63
|
+
resp = main.stripe_webhook(DummyRequest())
|
|
64
|
+
assert resp == sentinel
|
|
65
|
+
|
|
66
|
+
finally:
|
|
67
|
+
# Restore originals
|
|
68
|
+
for name, val in orig.items():
|
|
69
|
+
if val is None:
|
|
70
|
+
if name in sys.modules:
|
|
71
|
+
del sys.modules[name]
|
|
72
|
+
else:
|
|
73
|
+
sys.modules[name] = val
|