jac-scale 0.1.1__py3-none-any.whl → 0.1.3__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.
- jac_scale/abstractions/config/app_config.jac +5 -2
- jac_scale/context.jac +2 -1
- jac_scale/factories/storage_factory.jac +75 -0
- jac_scale/google_sso_provider.jac +85 -0
- jac_scale/impl/context.impl.jac +3 -0
- jac_scale/impl/serve.impl.jac +82 -234
- jac_scale/impl/user_manager.impl.jac +349 -0
- jac_scale/memory_hierarchy.jac +3 -1
- jac_scale/plugin.jac +46 -3
- jac_scale/plugin_config.jac +27 -0
- jac_scale/serve.jac +3 -12
- jac_scale/sso_provider.jac +72 -0
- jac_scale/targets/kubernetes/kubernetes_config.jac +9 -15
- jac_scale/targets/kubernetes/kubernetes_target.jac +174 -15
- jac_scale/tests/fixtures/scale-feats/components/Button.cl.jac +32 -0
- jac_scale/tests/fixtures/scale-feats/main.jac +147 -0
- jac_scale/tests/fixtures/test_api.jac +29 -0
- jac_scale/tests/fixtures/test_restspec.jac +37 -0
- jac_scale/tests/test_deploy_k8s.py +2 -1
- jac_scale/tests/test_examples.py +180 -5
- jac_scale/tests/test_hooks.py +39 -0
- jac_scale/tests/test_restspec.py +192 -0
- jac_scale/tests/test_serve.py +54 -0
- jac_scale/tests/test_sso.py +273 -284
- jac_scale/tests/test_storage.py +274 -0
- jac_scale/user_manager.jac +49 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/METADATA +9 -2
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/RECORD +31 -20
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/WHEEL +1 -1
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/entry_points.txt +0 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/top_level.txt +0 -0
jac_scale/tests/test_examples.py
CHANGED
|
@@ -19,6 +19,8 @@ JacClientExamples = (
|
|
|
19
19
|
/ "examples"
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
+
JacScaleFixtures = Path(__file__).parent / "fixtures"
|
|
23
|
+
|
|
22
24
|
|
|
23
25
|
def get_free_port() -> int:
|
|
24
26
|
"""Get a free port by binding to port 0 and releasing it."""
|
|
@@ -184,6 +186,7 @@ class JacScaleTestRunner:
|
|
|
184
186
|
"node_modules",
|
|
185
187
|
".jac",
|
|
186
188
|
"package-lock.json",
|
|
189
|
+
"uploads", # Storage API test uploads
|
|
187
190
|
]
|
|
188
191
|
for dir_name in dirs_to_clean:
|
|
189
192
|
dir_path = example_dir / dir_name
|
|
@@ -371,9 +374,7 @@ class JacScaleTestRunner:
|
|
|
371
374
|
return response.text
|
|
372
375
|
return f"Request failed after {max_retries} retries (all timeouts)"
|
|
373
376
|
|
|
374
|
-
def spawn_walker(
|
|
375
|
-
self, walker_name: str, **kwargs: dict[str, Any]
|
|
376
|
-
) -> dict[str, Any]:
|
|
377
|
+
def spawn_walker(self, walker_name: str, **kwargs: object) -> dict[str, Any]:
|
|
377
378
|
"""Spawn a walker with the given parameters.
|
|
378
379
|
|
|
379
380
|
Args:
|
|
@@ -387,9 +388,73 @@ class JacScaleTestRunner:
|
|
|
387
388
|
"POST", f"/walker/{walker_name}", data=kwargs, use_token=True
|
|
388
389
|
)
|
|
389
390
|
|
|
390
|
-
def
|
|
391
|
-
self,
|
|
391
|
+
def upload_file(
|
|
392
|
+
self,
|
|
393
|
+
walker_name: str,
|
|
394
|
+
file_field: str,
|
|
395
|
+
filename: str,
|
|
396
|
+
content: bytes,
|
|
397
|
+
content_type: str = "text/plain",
|
|
398
|
+
extra_data: dict[str, Any] | None = None,
|
|
399
|
+
timeout: int = 10,
|
|
392
400
|
) -> dict[str, Any]:
|
|
401
|
+
"""Upload a file to a walker endpoint using multipart/form-data.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
walker_name: Name of the walker to call
|
|
405
|
+
file_field: Name of the file field in the walker
|
|
406
|
+
filename: Name of the file being uploaded
|
|
407
|
+
content: File content as bytes
|
|
408
|
+
content_type: MIME type of the file
|
|
409
|
+
extra_data: Additional form data fields
|
|
410
|
+
timeout: Request timeout in seconds
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Walker execution response
|
|
414
|
+
"""
|
|
415
|
+
import io
|
|
416
|
+
|
|
417
|
+
url = f"{self.base_url}/walker/{walker_name}"
|
|
418
|
+
headers = {}
|
|
419
|
+
|
|
420
|
+
if self.token:
|
|
421
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
422
|
+
|
|
423
|
+
files = {file_field: (filename, io.BytesIO(content), content_type)}
|
|
424
|
+
data = extra_data or {}
|
|
425
|
+
|
|
426
|
+
response = requests.post(
|
|
427
|
+
url, headers=headers, files=files, data=data, timeout=timeout
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
json_response: Any = response.json()
|
|
431
|
+
|
|
432
|
+
# Handle jac-scale's tuple response format [status, body]
|
|
433
|
+
if isinstance(json_response, list) and len(json_response) == 2:
|
|
434
|
+
json_response = json_response[1]
|
|
435
|
+
|
|
436
|
+
# Handle TransportResponse envelope format
|
|
437
|
+
if (
|
|
438
|
+
isinstance(json_response, dict)
|
|
439
|
+
and "ok" in json_response
|
|
440
|
+
and "data" in json_response
|
|
441
|
+
):
|
|
442
|
+
if json_response.get("ok") and json_response.get("data") is not None:
|
|
443
|
+
return json_response["data"]
|
|
444
|
+
elif not json_response.get("ok") and json_response.get("error"):
|
|
445
|
+
error_info = json_response["error"]
|
|
446
|
+
result: dict[str, Any] = {
|
|
447
|
+
"error": error_info.get("message", "Unknown error")
|
|
448
|
+
}
|
|
449
|
+
if "code" in error_info:
|
|
450
|
+
result["error_code"] = error_info["code"]
|
|
451
|
+
if "details" in error_info:
|
|
452
|
+
result["error_details"] = error_info["details"]
|
|
453
|
+
return result
|
|
454
|
+
|
|
455
|
+
return json_response # type: ignore[return-value]
|
|
456
|
+
|
|
457
|
+
def call_function(self, function_name: str, **kwargs: object) -> dict[str, Any]:
|
|
393
458
|
"""Call a function with the given parameters.
|
|
394
459
|
|
|
395
460
|
Args:
|
|
@@ -482,3 +547,113 @@ class TestJacClientExamples:
|
|
|
482
547
|
) as runner:
|
|
483
548
|
assert "/static/client.js" in runner.request_raw("GET", "/cl/app")
|
|
484
549
|
assert "import styled from" in runner.request_raw("GET", "/styled.js")
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class TestJacScaleFeatures:
|
|
553
|
+
"""Test jac-scale specific features using dedicated fixtures."""
|
|
554
|
+
|
|
555
|
+
def test_storage_api(self) -> None:
|
|
556
|
+
"""Test Storage API with file upload, list, download, copy, move, delete."""
|
|
557
|
+
example_file = JacScaleFixtures / "scale-feats" / "main.jac"
|
|
558
|
+
with JacScaleTestRunner(
|
|
559
|
+
example_file, session_name="storage_api_test", setup_npm=False
|
|
560
|
+
) as runner:
|
|
561
|
+
# Health check
|
|
562
|
+
health = runner.spawn_walker("health")
|
|
563
|
+
assert health.get("reports", [[]])[0].get("status") == "ok"
|
|
564
|
+
|
|
565
|
+
# Register user for auth
|
|
566
|
+
runner.create_user("storage_user", "testpass123")
|
|
567
|
+
|
|
568
|
+
# Test file upload
|
|
569
|
+
test_content = b"Test content for storage API"
|
|
570
|
+
upload_result = runner.upload_file(
|
|
571
|
+
walker_name="upload_file",
|
|
572
|
+
file_field="file",
|
|
573
|
+
filename="test_file.txt",
|
|
574
|
+
content=test_content,
|
|
575
|
+
content_type="text/plain",
|
|
576
|
+
extra_data={"folder": "test_docs"},
|
|
577
|
+
)
|
|
578
|
+
reports = upload_result.get("reports", [])
|
|
579
|
+
assert len(reports) > 0, "Upload should return reports"
|
|
580
|
+
file_info = reports[0]
|
|
581
|
+
assert file_info["success"] is True
|
|
582
|
+
assert file_info["original_filename"] == "test_file.txt"
|
|
583
|
+
assert "storage_path" in file_info
|
|
584
|
+
uploaded_path = file_info["storage_path"]
|
|
585
|
+
assert uploaded_path.startswith("test_docs/")
|
|
586
|
+
assert file_info["size"] == len(test_content)
|
|
587
|
+
|
|
588
|
+
# Test file exists
|
|
589
|
+
exists_result = runner.spawn_walker("file_exists", path=uploaded_path)
|
|
590
|
+
reports = exists_result.get("reports", [])
|
|
591
|
+
assert len(reports) > 0
|
|
592
|
+
assert reports[0]["exists"] is True
|
|
593
|
+
|
|
594
|
+
# Test list files
|
|
595
|
+
list_result = runner.spawn_walker("list_files", folder="", recursive=True)
|
|
596
|
+
reports = list_result.get("reports", [])
|
|
597
|
+
assert len(reports) > 0
|
|
598
|
+
list_info = reports[0]
|
|
599
|
+
assert "files" in list_info
|
|
600
|
+
assert list_info["count"] >= 1
|
|
601
|
+
file_paths = [f["path"] for f in list_info["files"]]
|
|
602
|
+
assert uploaded_path in file_paths
|
|
603
|
+
|
|
604
|
+
# Test download file
|
|
605
|
+
download_result = runner.spawn_walker("download_file", path=uploaded_path)
|
|
606
|
+
reports = download_result.get("reports", [])
|
|
607
|
+
assert len(reports) > 0
|
|
608
|
+
assert reports[0]["success"] is True
|
|
609
|
+
assert reports[0]["content_length"] == len(test_content)
|
|
610
|
+
|
|
611
|
+
# Test copy file
|
|
612
|
+
copy_dest = "test_docs/copied_file.txt"
|
|
613
|
+
copy_result = runner.spawn_walker(
|
|
614
|
+
"copy_file", source=uploaded_path, destination=copy_dest
|
|
615
|
+
)
|
|
616
|
+
reports = copy_result.get("reports", [])
|
|
617
|
+
assert len(reports) > 0
|
|
618
|
+
assert reports[0]["success"] is True
|
|
619
|
+
|
|
620
|
+
# Verify copy exists
|
|
621
|
+
exists_result = runner.spawn_walker("file_exists", path=copy_dest)
|
|
622
|
+
assert exists_result.get("reports", [[]])[0]["exists"] is True
|
|
623
|
+
|
|
624
|
+
# Test move file
|
|
625
|
+
move_dest = "test_docs/moved_file.txt"
|
|
626
|
+
move_result = runner.spawn_walker(
|
|
627
|
+
"move_file", source=copy_dest, destination=move_dest
|
|
628
|
+
)
|
|
629
|
+
reports = move_result.get("reports", [])
|
|
630
|
+
assert len(reports) > 0
|
|
631
|
+
assert reports[0]["success"] is True
|
|
632
|
+
|
|
633
|
+
# Verify move: source gone, dest exists
|
|
634
|
+
exists_result = runner.spawn_walker("file_exists", path=copy_dest)
|
|
635
|
+
assert exists_result.get("reports", [[]])[0]["exists"] is False
|
|
636
|
+
exists_result = runner.spawn_walker("file_exists", path=move_dest)
|
|
637
|
+
assert exists_result.get("reports", [[]])[0]["exists"] is True
|
|
638
|
+
|
|
639
|
+
# Test delete file
|
|
640
|
+
delete_result = runner.spawn_walker("delete_file", path=uploaded_path)
|
|
641
|
+
reports = delete_result.get("reports", [])
|
|
642
|
+
assert len(reports) > 0
|
|
643
|
+
assert reports[0]["success"] is True
|
|
644
|
+
|
|
645
|
+
# Verify deleted
|
|
646
|
+
exists_result = runner.spawn_walker("file_exists", path=uploaded_path)
|
|
647
|
+
assert exists_result.get("reports", [[]])[0]["exists"] is False
|
|
648
|
+
|
|
649
|
+
# Test delete nonexistent file
|
|
650
|
+
delete_result = runner.spawn_walker(
|
|
651
|
+
"delete_file", path="nonexistent/file.txt"
|
|
652
|
+
)
|
|
653
|
+
reports = delete_result.get("reports", [])
|
|
654
|
+
assert len(reports) > 0
|
|
655
|
+
assert reports[0]["success"] is False
|
|
656
|
+
assert reports[0]["error"] == "File not found"
|
|
657
|
+
|
|
658
|
+
# Cleanup: delete moved file
|
|
659
|
+
runner.spawn_walker("delete_file", path=move_dest)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Test hook registration for JacScale."""
|
|
2
|
+
|
|
3
|
+
from jac_scale.plugin import JacScalePlugin
|
|
4
|
+
from jac_scale.user_manager import JacScaleUserManager
|
|
5
|
+
from jaclang.pycore.runtime import plugin_manager as pm
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_get_user_manager_implementation():
|
|
9
|
+
"""Test that the plugin method returns the correct class instance."""
|
|
10
|
+
# It returns an Instance, as seen in plugin.jac implementation
|
|
11
|
+
user_manager = JacScalePlugin.get_user_manager(base_path="")
|
|
12
|
+
assert isinstance(user_manager, JacScaleUserManager)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_hook_registration():
|
|
16
|
+
"""Test that the hook is registered with Jac plugin manager."""
|
|
17
|
+
# Create plugin instance
|
|
18
|
+
plugin = JacScalePlugin()
|
|
19
|
+
|
|
20
|
+
# Register manually for the test
|
|
21
|
+
if not pm.is_registered(plugin):
|
|
22
|
+
pm.register(plugin)
|
|
23
|
+
|
|
24
|
+
# Check hook implementations
|
|
25
|
+
hook_impls = pm.hook.get_user_manager.get_hookimpls()
|
|
26
|
+
|
|
27
|
+
# Verify our plugin's method is in the implementations
|
|
28
|
+
found = False
|
|
29
|
+
for impl in hook_impls:
|
|
30
|
+
# Check if the function belongs to our plugin class or module
|
|
31
|
+
if (
|
|
32
|
+
impl.plugin_name == "scale"
|
|
33
|
+
or isinstance(impl.plugin, JacScalePlugin)
|
|
34
|
+
or impl.function.__qualname__ == "JacScalePlugin.get_user_manager"
|
|
35
|
+
):
|
|
36
|
+
found = True
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
assert found, "JacScalePlugin.get_user_manager not found in hook implementations"
|
|
@@ -0,0 +1,192 @@
|
|
|
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_custom_method_func(self) -> None:
|
|
143
|
+
"""Test function with custom GET method."""
|
|
144
|
+
requests.post(
|
|
145
|
+
f"{self.base_url}/user/register", json={"username": "u1", "password": "p1"}
|
|
146
|
+
)
|
|
147
|
+
login = requests.post(
|
|
148
|
+
f"{self.base_url}/user/login", json={"username": "u1", "password": "p1"}
|
|
149
|
+
)
|
|
150
|
+
token = self._extract_data(login.json())["token"]
|
|
151
|
+
|
|
152
|
+
response = requests.get(
|
|
153
|
+
f"{self.base_url}/function/get_func",
|
|
154
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
155
|
+
timeout=5,
|
|
156
|
+
)
|
|
157
|
+
assert response.status_code == 200
|
|
158
|
+
data = self._extract_data(response.json())
|
|
159
|
+
assert data["result"]["message"] == "get_func executed"
|
|
160
|
+
|
|
161
|
+
def test_custom_path_func(self) -> None:
|
|
162
|
+
"""Test function with custom path."""
|
|
163
|
+
# Use existing user token if possible, but simplest is fresh login
|
|
164
|
+
login = requests.post(
|
|
165
|
+
f"{self.base_url}/user/login", json={"username": "u1", "password": "p1"}
|
|
166
|
+
)
|
|
167
|
+
token = self._extract_data(login.json())["token"]
|
|
168
|
+
|
|
169
|
+
response = requests.get(
|
|
170
|
+
f"{self.base_url}/custom/func",
|
|
171
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
172
|
+
timeout=5,
|
|
173
|
+
)
|
|
174
|
+
assert response.status_code == 200
|
|
175
|
+
data = self._extract_data(response.json())
|
|
176
|
+
assert data["result"]["message"] == "custom_path_func executed"
|
|
177
|
+
assert data["result"]["path"] == "/custom/func"
|
|
178
|
+
|
|
179
|
+
def test_openapi_specs(self) -> None:
|
|
180
|
+
"""Verify OpenAPI documentation reflects custom paths and methods."""
|
|
181
|
+
spec = requests.get(f"{self.base_url}/openapi.json").json()
|
|
182
|
+
paths = spec["paths"]
|
|
183
|
+
|
|
184
|
+
assert "/custom/walker" in paths
|
|
185
|
+
assert "get" in paths["/custom/walker"]
|
|
186
|
+
|
|
187
|
+
assert "/custom/func" in paths
|
|
188
|
+
assert "get" in paths["/custom/func"]
|
|
189
|
+
|
|
190
|
+
assert "/walker/GetWalker" in paths
|
|
191
|
+
assert "get" in paths["/walker/GetWalker"]
|
|
192
|
+
assert "post" not in paths["/walker/GetWalker"]
|
jac_scale/tests/test_serve.py
CHANGED
|
@@ -1833,3 +1833,57 @@ class TestJacScaleServeDevMode:
|
|
|
1833
1833
|
assert reports[1]["status"] == "after_async_wait"
|
|
1834
1834
|
assert reports[2]["status"] == "completed"
|
|
1835
1835
|
assert "task" in reports[2]
|
|
1836
|
+
|
|
1837
|
+
def test_walker_stream_response(self) -> None:
|
|
1838
|
+
"""Test that walker streaming responses work correctly."""
|
|
1839
|
+
response = requests.post(
|
|
1840
|
+
f"{self.base_url}/walker/WalkerStream",
|
|
1841
|
+
json={"count": 3},
|
|
1842
|
+
timeout=30,
|
|
1843
|
+
stream=True,
|
|
1844
|
+
)
|
|
1845
|
+
|
|
1846
|
+
assert response.status_code == 200, (
|
|
1847
|
+
f"Failed with status {response.status_code}: {response.text}"
|
|
1848
|
+
)
|
|
1849
|
+
assert response.headers["content-type"] == "text/event-stream; charset=utf-8"
|
|
1850
|
+
assert response.headers.get("cache-control") == "no-cache"
|
|
1851
|
+
assert response.headers.get("connection") == "close"
|
|
1852
|
+
|
|
1853
|
+
# Collect streaming content
|
|
1854
|
+
content = ""
|
|
1855
|
+
for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
|
|
1856
|
+
if chunk:
|
|
1857
|
+
content += chunk
|
|
1858
|
+
|
|
1859
|
+
# The generator yields "Report 0", "Report 1", "Report 2" without delimiters
|
|
1860
|
+
# They get concatenated together in the stream
|
|
1861
|
+
expected = "Report 0Report 1Report 2"
|
|
1862
|
+
assert content == expected, f"Expected '{expected}', got '{content}'"
|
|
1863
|
+
|
|
1864
|
+
def test_function_stream_response(self) -> None:
|
|
1865
|
+
"""Test that function streaming responses work correctly."""
|
|
1866
|
+
response = requests.post(
|
|
1867
|
+
f"{self.base_url}/function/FunctionStream",
|
|
1868
|
+
json={"count": 2},
|
|
1869
|
+
timeout=30,
|
|
1870
|
+
stream=True,
|
|
1871
|
+
)
|
|
1872
|
+
|
|
1873
|
+
assert response.status_code == 200, (
|
|
1874
|
+
f"Failed with status {response.status_code}: {response.text}"
|
|
1875
|
+
)
|
|
1876
|
+
assert response.headers["content-type"] == "text/event-stream; charset=utf-8"
|
|
1877
|
+
assert response.headers.get("cache-control") == "no-cache"
|
|
1878
|
+
assert response.headers.get("connection") == "close"
|
|
1879
|
+
|
|
1880
|
+
# Collect streaming content
|
|
1881
|
+
content = ""
|
|
1882
|
+
for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
|
|
1883
|
+
if chunk:
|
|
1884
|
+
content += chunk
|
|
1885
|
+
|
|
1886
|
+
# The generator yields "Func 0", "Func 1" without delimiters
|
|
1887
|
+
# They get concatenated together in the stream
|
|
1888
|
+
expected = "Func 0Func 1"
|
|
1889
|
+
assert content == expected, f"Expected '{expected}', got '{content}'"
|