jac-scale 0.1.1__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. jac_scale/abstractions/config/app_config.jac +5 -2
  2. jac_scale/config_loader.jac +2 -1
  3. jac_scale/context.jac +2 -1
  4. jac_scale/factories/storage_factory.jac +75 -0
  5. jac_scale/google_sso_provider.jac +85 -0
  6. jac_scale/impl/config_loader.impl.jac +28 -3
  7. jac_scale/impl/context.impl.jac +1 -0
  8. jac_scale/impl/serve.impl.jac +749 -266
  9. jac_scale/impl/user_manager.impl.jac +349 -0
  10. jac_scale/impl/webhook.impl.jac +212 -0
  11. jac_scale/jserver/impl/jfast_api.impl.jac +4 -0
  12. jac_scale/memory_hierarchy.jac +3 -1
  13. jac_scale/plugin.jac +46 -3
  14. jac_scale/plugin_config.jac +28 -1
  15. jac_scale/serve.jac +33 -16
  16. jac_scale/sso_provider.jac +72 -0
  17. jac_scale/targets/kubernetes/kubernetes_config.jac +9 -15
  18. jac_scale/targets/kubernetes/kubernetes_target.jac +174 -15
  19. jac_scale/tests/fixtures/scale-feats/components/Button.cl.jac +32 -0
  20. jac_scale/tests/fixtures/scale-feats/main.jac +147 -0
  21. jac_scale/tests/fixtures/test_api.jac +89 -0
  22. jac_scale/tests/fixtures/test_restspec.jac +88 -0
  23. jac_scale/tests/test_deploy_k8s.py +2 -1
  24. jac_scale/tests/test_examples.py +180 -5
  25. jac_scale/tests/test_hooks.py +39 -0
  26. jac_scale/tests/test_restspec.py +289 -0
  27. jac_scale/tests/test_serve.py +411 -4
  28. jac_scale/tests/test_sso.py +273 -284
  29. jac_scale/tests/test_storage.py +274 -0
  30. jac_scale/user_manager.jac +49 -0
  31. jac_scale/webhook.jac +93 -0
  32. {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/METADATA +11 -4
  33. {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/RECORD +36 -23
  34. {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/WHEEL +1 -1
  35. {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/entry_points.txt +0 -0
  36. {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,289 @@
1
+ """Test for restspec decorator functionality."""
2
+
3
+ import contextlib
4
+ import gc
5
+ import glob
6
+ import socket
7
+ import subprocess
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import requests
13
+
14
+
15
+ def get_free_port() -> int:
16
+ """Get a free port by binding to port 0 and releasing it."""
17
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
18
+ s.bind(("", 0))
19
+ s.listen(1)
20
+ port = s.getsockname()[1]
21
+ return port
22
+
23
+
24
+ class TestRestSpec:
25
+ """Test restspec decorator functionality."""
26
+
27
+ fixtures_dir: Path
28
+ test_file: Path
29
+ port: int
30
+ base_url: str
31
+ server_process: subprocess.Popen[str] | None = None
32
+
33
+ @classmethod
34
+ def setup_class(cls) -> None:
35
+ """Set up test class - runs once for all tests."""
36
+ cls.fixtures_dir = Path(__file__).parent / "fixtures"
37
+ cls.test_file = cls.fixtures_dir / "test_restspec.jac"
38
+
39
+ if not cls.test_file.exists():
40
+ raise FileNotFoundError(f"Test fixture not found: {cls.test_file}")
41
+
42
+ cls.port = get_free_port()
43
+ cls.base_url = f"http://localhost:{cls.port}"
44
+
45
+ cls._cleanup_db_files()
46
+ cls._start_server()
47
+
48
+ @classmethod
49
+ def teardown_class(cls) -> None:
50
+ """Tear down test class - runs once after all tests."""
51
+ if cls.server_process:
52
+ cls.server_process.terminate()
53
+ try:
54
+ cls.server_process.wait(timeout=5)
55
+ except subprocess.TimeoutExpired:
56
+ cls.server_process.kill()
57
+ cls.server_process.wait()
58
+
59
+ time.sleep(0.5)
60
+ gc.collect()
61
+ cls._cleanup_db_files()
62
+
63
+ @classmethod
64
+ def _start_server(cls) -> None:
65
+ """Start the jac-scale server in a subprocess."""
66
+ import sys
67
+
68
+ jac_executable = Path(sys.executable).parent / "jac"
69
+ cmd = [
70
+ str(jac_executable),
71
+ "start",
72
+ cls.test_file.name,
73
+ "--port",
74
+ str(cls.port),
75
+ ]
76
+
77
+ cls.server_process = subprocess.Popen(
78
+ cmd,
79
+ stdout=subprocess.PIPE,
80
+ stderr=subprocess.PIPE,
81
+ text=True,
82
+ cwd=str(cls.fixtures_dir),
83
+ )
84
+
85
+ # Wait for server
86
+ max_attempts = 50
87
+ for _ in range(max_attempts):
88
+ if cls.server_process.poll() is not None:
89
+ stdout, stderr = cls.server_process.communicate()
90
+ raise RuntimeError(f"Server died: {stdout}\n{stderr}")
91
+ try:
92
+ requests.get(f"{cls.base_url}/docs", timeout=1)
93
+ return
94
+ except requests.RequestException:
95
+ time.sleep(0.5)
96
+
97
+ cls.server_process.kill()
98
+ raise RuntimeError("Server failed to start")
99
+
100
+ @classmethod
101
+ def _cleanup_db_files(cls) -> None:
102
+ """Cleanup database files."""
103
+ import shutil
104
+
105
+ for pattern in ["*.db", "*.db-wal", "*.db-shm", "anchor_store*"]:
106
+ for f in glob.glob(pattern):
107
+ with contextlib.suppress(Exception):
108
+ Path(f).unlink()
109
+ for pattern in ["*.db", "*.db-wal", "*.db-shm"]:
110
+ for f in glob.glob(str(cls.fixtures_dir / pattern)):
111
+ with contextlib.suppress(Exception):
112
+ Path(f).unlink()
113
+ client_dir = cls.fixtures_dir / ".jac"
114
+ if client_dir.exists():
115
+ with contextlib.suppress(Exception):
116
+ shutil.rmtree(client_dir)
117
+
118
+ def _extract_data(self, response: dict[str, Any] | list[Any]) -> Any: # noqa: ANN401
119
+ # Handle tuple response [status, body]
120
+ if isinstance(response, list) and len(response) == 2:
121
+ response = response[1]
122
+
123
+ if isinstance(response, dict) and "data" in response:
124
+ return response["data"]
125
+ return response
126
+
127
+ def test_custom_method_walker(self) -> None:
128
+ """Test walker with custom GET method."""
129
+ response = requests.get(f"{self.base_url}/walker/GetWalker", timeout=5)
130
+ assert response.status_code == 200
131
+ data = self._extract_data(response.json())
132
+ assert data["reports"][0]["message"] == "GetWalker executed"
133
+
134
+ def test_custom_path_walker(self) -> None:
135
+ """Test walker with custom path."""
136
+ response = requests.get(f"{self.base_url}/custom/walker", timeout=5)
137
+ assert response.status_code == 200
138
+ data = self._extract_data(response.json())
139
+ assert data["reports"][0]["message"] == "CustomPathWalker executed"
140
+ assert data["reports"][0]["path"] == "/custom/walker"
141
+
142
+ def test_post_method_walker(self) -> None:
143
+ """Test walker with explicit POST method."""
144
+ response = requests.post(f"{self.base_url}/walker/PostWalker", timeout=5)
145
+ assert response.status_code == 200
146
+ data = self._extract_data(response.json())
147
+ assert data["reports"][0]["message"] == "PostWalker executed"
148
+ assert data["reports"][0]["method"] == "POST"
149
+
150
+ def test_default_method_walker(self) -> None:
151
+ """Test walker with default method (POST)."""
152
+ response = requests.post(f"{self.base_url}/walker/DefaultWalker", timeout=5)
153
+ assert response.status_code == 200
154
+ data = self._extract_data(response.json())
155
+ assert data["reports"][0]["message"] == "DefaultWalker executed"
156
+ assert data["reports"][0]["method"] == "DEFAULT"
157
+
158
+ def test_custom_method_func(self) -> None:
159
+ """Test function with custom GET method."""
160
+ requests.post(
161
+ f"{self.base_url}/user/register", json={"username": "u1", "password": "p1"}
162
+ )
163
+ login = requests.post(
164
+ f"{self.base_url}/user/login", json={"username": "u1", "password": "p1"}
165
+ )
166
+ token = self._extract_data(login.json())["token"]
167
+
168
+ response = requests.get(
169
+ f"{self.base_url}/function/get_func",
170
+ headers={"Authorization": f"Bearer {token}"},
171
+ timeout=5,
172
+ )
173
+ assert response.status_code == 200
174
+ data = self._extract_data(response.json())
175
+ assert data["result"]["message"] == "get_func executed"
176
+
177
+ def test_custom_path_func(self) -> None:
178
+ """Test function with custom path."""
179
+ # Use existing user token if possible, but simplest is fresh login
180
+ login = requests.post(
181
+ f"{self.base_url}/user/login", json={"username": "u1", "password": "p1"}
182
+ )
183
+ token = self._extract_data(login.json())["token"]
184
+
185
+ response = requests.get(
186
+ f"{self.base_url}/custom/func",
187
+ headers={"Authorization": f"Bearer {token}"},
188
+ timeout=5,
189
+ )
190
+ assert response.status_code == 200
191
+ data = self._extract_data(response.json())
192
+ assert data["result"]["message"] == "custom_path_func executed"
193
+ assert data["result"]["path"] == "/custom/func"
194
+
195
+ def test_post_method_func(self) -> None:
196
+ """Test function with explicit POST method."""
197
+ # Use existing user token if possible, but simplest is fresh login
198
+ login = requests.post(
199
+ f"{self.base_url}/user/login", json={"username": "u1", "password": "p1"}
200
+ )
201
+ token = self._extract_data(login.json())["token"]
202
+
203
+ response = requests.post(
204
+ f"{self.base_url}/function/post_func",
205
+ headers={"Authorization": f"Bearer {token}"},
206
+ timeout=5,
207
+ )
208
+ assert response.status_code == 200
209
+ data = self._extract_data(response.json())
210
+ assert data["result"]["message"] == "post_func executed"
211
+ assert data["result"]["method"] == "POST"
212
+
213
+ def test_default_method_func(self) -> None:
214
+ """Test function with default method (POST)."""
215
+ # Use existing user token if possible, but simplest is fresh login
216
+ login = requests.post(
217
+ f"{self.base_url}/user/login", json={"username": "u1", "password": "p1"}
218
+ )
219
+ token = self._extract_data(login.json())["token"]
220
+
221
+ response = requests.post(
222
+ f"{self.base_url}/function/default_func",
223
+ headers={"Authorization": f"Bearer {token}"},
224
+ timeout=5,
225
+ )
226
+ assert response.status_code == 200
227
+ data = self._extract_data(response.json())
228
+ assert data["result"]["message"] == "default_func executed"
229
+ assert data["result"]["method"] == "DEFAULT"
230
+
231
+ def test_get_walker_with_params(self) -> None:
232
+ """Test walker with GET method and query parameters."""
233
+ # Parameters should be passed as query string
234
+ params: dict[str, str | int] = {"name": "Alice", "age": 30}
235
+ response = requests.get(
236
+ f"{self.base_url}/walker/GetWalkerWithParams",
237
+ params=params,
238
+ timeout=5,
239
+ )
240
+ assert response.status_code == 200
241
+ data = self._extract_data(response.json())
242
+ assert data["reports"][0]["message"] == "GetWalkerWithParams executed"
243
+ assert data["reports"][0]["name"] == "Alice"
244
+ assert data["reports"][0]["age"] == 30
245
+
246
+ def test_get_func_with_params(self) -> None:
247
+ """Test function with GET method and query parameters."""
248
+ # Use existing user token if possible, but simplest is fresh login
249
+ login = requests.post(
250
+ f"{self.base_url}/user/login", json={"username": "u1", "password": "p1"}
251
+ )
252
+ token = self._extract_data(login.json())["token"]
253
+
254
+ # Parameters should be passed as query string
255
+ params: dict[str, str | int] = {"name": "Bob", "age": 40}
256
+ response = requests.get(
257
+ f"{self.base_url}/function/get_func_with_params",
258
+ headers={"Authorization": f"Bearer {token}"},
259
+ params=params,
260
+ timeout=5,
261
+ )
262
+ assert response.status_code == 200
263
+ data = self._extract_data(response.json())
264
+ assert data["result"]["message"] == "get_func_with_params executed"
265
+ assert data["result"]["name"] == "Bob"
266
+ assert data["result"]["age"] == 40
267
+
268
+ def test_openapi_specs(self) -> None:
269
+ """Verify OpenAPI documentation reflects custom paths and methods."""
270
+ spec = requests.get(f"{self.base_url}/openapi.json").json()
271
+ paths = spec["paths"]
272
+
273
+ assert "/custom/walker" in paths
274
+ assert "get" in paths["/custom/walker"]
275
+
276
+ assert "/custom/func" in paths
277
+ assert "get" in paths["/custom/func"]
278
+
279
+ assert "/walker/GetWalker" in paths
280
+ assert "get" in paths["/walker/GetWalker"]
281
+ assert "post" not in paths["/walker/GetWalker"]
282
+
283
+ assert "/walker/PostWalker" in paths
284
+ assert "post" in paths["/walker/PostWalker"]
285
+ assert "get" not in paths["/walker/PostWalker"]
286
+
287
+ assert "/walker/DefaultWalker" in paths
288
+ assert "post" in paths["/walker/DefaultWalker"]
289
+ assert "get" not in paths["/walker/DefaultWalker"]