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.
- jac_scale/abstractions/config/app_config.jac +5 -2
- jac_scale/config_loader.jac +2 -1
- 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/config_loader.impl.jac +28 -3
- jac_scale/impl/context.impl.jac +1 -0
- jac_scale/impl/serve.impl.jac +749 -266
- jac_scale/impl/user_manager.impl.jac +349 -0
- jac_scale/impl/webhook.impl.jac +212 -0
- jac_scale/jserver/impl/jfast_api.impl.jac +4 -0
- jac_scale/memory_hierarchy.jac +3 -1
- jac_scale/plugin.jac +46 -3
- jac_scale/plugin_config.jac +28 -1
- jac_scale/serve.jac +33 -16
- 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 +89 -0
- jac_scale/tests/fixtures/test_restspec.jac +88 -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 +289 -0
- jac_scale/tests/test_serve.py +411 -4
- 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/webhook.jac +93 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/METADATA +11 -4
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/RECORD +36 -23
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/WHEEL +1 -1
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/entry_points.txt +0 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/top_level.txt +0 -0
jac_scale/tests/test_serve.py
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import gc
|
|
5
5
|
import glob
|
|
6
|
+
import hashlib
|
|
7
|
+
import hmac
|
|
8
|
+
import json
|
|
6
9
|
import socket
|
|
7
10
|
import subprocess
|
|
8
11
|
import time
|
|
@@ -260,7 +263,7 @@ class TestJacScaleServe:
|
|
|
260
263
|
def _create_expired_token(self, username: str, days_ago: int = 1) -> str:
|
|
261
264
|
"""Create an expired JWT token for testing."""
|
|
262
265
|
# Use the same secret as the server (default)
|
|
263
|
-
secret = "
|
|
266
|
+
secret = "supersecretkey_for_testing_only!"
|
|
264
267
|
algorithm = "HS256"
|
|
265
268
|
|
|
266
269
|
past_time = datetime.now(UTC) - timedelta(days=days_ago)
|
|
@@ -273,7 +276,7 @@ class TestJacScaleServe:
|
|
|
273
276
|
|
|
274
277
|
def _create_very_old_token(self, username: str, days_ago: int = 15) -> str:
|
|
275
278
|
"""Create a token that's too old to refresh."""
|
|
276
|
-
secret = "
|
|
279
|
+
secret = "supersecretkey_for_testing_only!"
|
|
277
280
|
algorithm = "HS256"
|
|
278
281
|
|
|
279
282
|
past_time = datetime.now(UTC) - timedelta(days=days_ago)
|
|
@@ -505,7 +508,7 @@ class TestJacScaleServe:
|
|
|
505
508
|
new_token = refresh_result["token"]
|
|
506
509
|
|
|
507
510
|
# Decode both tokens and verify username is preserved
|
|
508
|
-
secret = "
|
|
511
|
+
secret = "supersecretkey_for_testing_only!"
|
|
509
512
|
algorithm = "HS256"
|
|
510
513
|
|
|
511
514
|
original_payload = pyjwt.decode(original_token, secret, algorithms=[algorithm])
|
|
@@ -535,7 +538,7 @@ class TestJacScaleServe:
|
|
|
535
538
|
new_token = refresh_result["token"]
|
|
536
539
|
|
|
537
540
|
# Decode tokens and compare expiration times
|
|
538
|
-
secret = "
|
|
541
|
+
secret = "supersecretkey_for_testing_only!"
|
|
539
542
|
algorithm = "HS256"
|
|
540
543
|
|
|
541
544
|
original_payload = pyjwt.decode(original_token, secret, algorithms=[algorithm])
|
|
@@ -1561,6 +1564,356 @@ class TestJacScaleServe:
|
|
|
1561
1564
|
)
|
|
1562
1565
|
assert old_password_response.status_code == 401
|
|
1563
1566
|
|
|
1567
|
+
# ========================================================================
|
|
1568
|
+
# Webhook Walker Tests
|
|
1569
|
+
# ========================================================================
|
|
1570
|
+
|
|
1571
|
+
def _generate_webhook_signature(self, payload: bytes, secret: str) -> str:
|
|
1572
|
+
"""Generate HMAC-SHA256 signature for webhook payload."""
|
|
1573
|
+
return hmac.new(secret.encode("utf-8"), payload, hashlib.sha256).hexdigest()
|
|
1574
|
+
|
|
1575
|
+
def test_webhook_endpoint_exists_for_webhook_walkers(self) -> None:
|
|
1576
|
+
"""Test 1: Verify webhook endpoints are registered for walkers with @restspec(webhook=True)."""
|
|
1577
|
+
response = requests.get(f"{self.base_url}/openapi.json", timeout=5)
|
|
1578
|
+
assert response.status_code == 200
|
|
1579
|
+
schema = response.json()
|
|
1580
|
+
paths = schema.get("paths", {})
|
|
1581
|
+
|
|
1582
|
+
# PaymentReceived has @restspec(webhook=True) -> should have /webhook/ endpoint
|
|
1583
|
+
assert "/webhook/PaymentReceived" in paths, (
|
|
1584
|
+
f"Expected /webhook/PaymentReceived in paths: {list(paths.keys())}"
|
|
1585
|
+
)
|
|
1586
|
+
# MinimalWebhook has @restspec(webhook=True) -> should have /webhook/ endpoint
|
|
1587
|
+
assert "/webhook/MinimalWebhook" in paths, (
|
|
1588
|
+
f"Expected /webhook/MinimalWebhook in paths: {list(paths.keys())}"
|
|
1589
|
+
)
|
|
1590
|
+
|
|
1591
|
+
def test_normal_walker_not_in_webhook_endpoint(self) -> None:
|
|
1592
|
+
"""Test 2: Verify normal walkers (without @restspec(webhook=True)) are NOT in /webhook/."""
|
|
1593
|
+
response = requests.get(f"{self.base_url}/openapi.json", timeout=5)
|
|
1594
|
+
assert response.status_code == 200
|
|
1595
|
+
schema = response.json()
|
|
1596
|
+
paths = schema.get("paths", {})
|
|
1597
|
+
|
|
1598
|
+
# NormalPayment does NOT have @restspec(webhook=True) -> should NOT have /webhook/ endpoint
|
|
1599
|
+
assert "/webhook/NormalPayment" not in paths, (
|
|
1600
|
+
"NormalPayment should NOT have webhook endpoint but found in paths"
|
|
1601
|
+
)
|
|
1602
|
+
# NormalPayment should be accessible via /walker/ endpoint
|
|
1603
|
+
assert "/walker/NormalPayment" in paths or "/walker/{walker_name}" in paths, (
|
|
1604
|
+
"NormalPayment should be accessible via /walker/ endpoint"
|
|
1605
|
+
)
|
|
1606
|
+
|
|
1607
|
+
def test_normal_walker_accessible_via_walker_endpoint(self) -> None:
|
|
1608
|
+
"""Test 2b: Verify NormalPayment (no webhook restspec) works via /walker/ endpoint."""
|
|
1609
|
+
# Create user and get token
|
|
1610
|
+
username = f"normal_walker_user_{uuid.uuid4().hex[:8]}"
|
|
1611
|
+
register_response = requests.post(
|
|
1612
|
+
f"{self.base_url}/user/register",
|
|
1613
|
+
json={"username": username, "password": "password123"},
|
|
1614
|
+
timeout=10,
|
|
1615
|
+
)
|
|
1616
|
+
assert register_response.status_code == 201
|
|
1617
|
+
register_data = cast(
|
|
1618
|
+
dict[str, Any],
|
|
1619
|
+
self._extract_transport_response_data(register_response.json()),
|
|
1620
|
+
)
|
|
1621
|
+
token = register_data["token"]
|
|
1622
|
+
|
|
1623
|
+
# Call NormalPayment via /walker/ endpoint
|
|
1624
|
+
response = requests.post(
|
|
1625
|
+
f"{self.base_url}/walker/NormalPayment",
|
|
1626
|
+
json={
|
|
1627
|
+
"payment_id": "PAY-NORMAL-001",
|
|
1628
|
+
"order_id": "ORD-NORMAL-001",
|
|
1629
|
+
"amount": 50.00,
|
|
1630
|
+
"currency": "EUR",
|
|
1631
|
+
},
|
|
1632
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
1633
|
+
timeout=10,
|
|
1634
|
+
)
|
|
1635
|
+
|
|
1636
|
+
assert response.status_code == 200, (
|
|
1637
|
+
f"Expected 200, got {response.status_code}: {response.text}"
|
|
1638
|
+
)
|
|
1639
|
+
data = cast(
|
|
1640
|
+
dict[str, Any], self._extract_transport_response_data(response.json())
|
|
1641
|
+
)
|
|
1642
|
+
assert "reports" in data
|
|
1643
|
+
report = data["reports"][0]
|
|
1644
|
+
assert report["status"] == "success"
|
|
1645
|
+
assert report["payment_id"] == "PAY-NORMAL-001"
|
|
1646
|
+
assert report["transport"] == "http"
|
|
1647
|
+
|
|
1648
|
+
def test_webhook_requires_api_key(self) -> None:
|
|
1649
|
+
"""Test that webhook endpoints require API key authentication."""
|
|
1650
|
+
payload = json.dumps({})
|
|
1651
|
+
|
|
1652
|
+
# Try to call MinimalWebhook without API key
|
|
1653
|
+
response = requests.post(
|
|
1654
|
+
f"{self.base_url}/webhook/MinimalWebhook",
|
|
1655
|
+
data=payload,
|
|
1656
|
+
headers={"Content-Type": "application/json"},
|
|
1657
|
+
timeout=5,
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
# Should fail - 401 (our custom error) or 422 (FastAPI validation for missing header)
|
|
1661
|
+
assert response.status_code in (401, 422), (
|
|
1662
|
+
f"Expected 401 or 422, got {response.status_code}: {response.text}"
|
|
1663
|
+
)
|
|
1664
|
+
|
|
1665
|
+
def test_webhook_invalid_api_key(self) -> None:
|
|
1666
|
+
"""Test that webhook endpoints reject invalid API keys."""
|
|
1667
|
+
payload = json.dumps({})
|
|
1668
|
+
|
|
1669
|
+
# Try with invalid API key
|
|
1670
|
+
response = requests.post(
|
|
1671
|
+
f"{self.base_url}/webhook/MinimalWebhook",
|
|
1672
|
+
data=payload,
|
|
1673
|
+
headers={
|
|
1674
|
+
"Content-Type": "application/json",
|
|
1675
|
+
"X-API-Key": "invalid_key_12345",
|
|
1676
|
+
},
|
|
1677
|
+
timeout=5,
|
|
1678
|
+
)
|
|
1679
|
+
|
|
1680
|
+
# Should fail with 401 Unauthorized
|
|
1681
|
+
assert response.status_code == 401, (
|
|
1682
|
+
f"Expected 401, got {response.status_code}: {response.text}"
|
|
1683
|
+
)
|
|
1684
|
+
|
|
1685
|
+
def test_minimal_webhook_with_valid_api_key(self) -> None:
|
|
1686
|
+
"""Test 3: MinimalWebhook (with @restspec(webhook=True)) works with valid API key."""
|
|
1687
|
+
# Create user and get auth token
|
|
1688
|
+
username = f"minimal_webhook_user_{uuid.uuid4().hex[:8]}"
|
|
1689
|
+
register_response = requests.post(
|
|
1690
|
+
f"{self.base_url}/user/register",
|
|
1691
|
+
json={"username": username, "password": "password123"},
|
|
1692
|
+
timeout=10,
|
|
1693
|
+
)
|
|
1694
|
+
assert register_response.status_code == 201
|
|
1695
|
+
register_data = cast(
|
|
1696
|
+
dict[str, Any],
|
|
1697
|
+
self._extract_transport_response_data(register_response.json()),
|
|
1698
|
+
)
|
|
1699
|
+
token = register_data["token"]
|
|
1700
|
+
|
|
1701
|
+
# Create API key
|
|
1702
|
+
api_key_response = requests.post(
|
|
1703
|
+
f"{self.base_url}/api-key/create",
|
|
1704
|
+
json={"name": "minimal_webhook_key", "expiry_days": 30},
|
|
1705
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
1706
|
+
timeout=10,
|
|
1707
|
+
)
|
|
1708
|
+
assert api_key_response.status_code == 201, (
|
|
1709
|
+
f"Failed to create API key: {api_key_response.text}"
|
|
1710
|
+
)
|
|
1711
|
+
api_key_data = cast(
|
|
1712
|
+
dict[str, Any],
|
|
1713
|
+
self._extract_transport_response_data(api_key_response.json()),
|
|
1714
|
+
)
|
|
1715
|
+
api_key = api_key_data["api_key"]
|
|
1716
|
+
|
|
1717
|
+
# Call MinimalWebhook with valid API key (empty payload - no fields required)
|
|
1718
|
+
payload = json.dumps({})
|
|
1719
|
+
payload_bytes = payload.encode("utf-8")
|
|
1720
|
+
signature = self._generate_webhook_signature(payload_bytes, api_key)
|
|
1721
|
+
response = requests.post(
|
|
1722
|
+
f"{self.base_url}/webhook/MinimalWebhook",
|
|
1723
|
+
data=payload,
|
|
1724
|
+
headers={
|
|
1725
|
+
"Content-Type": "application/json",
|
|
1726
|
+
"X-API-Key": api_key,
|
|
1727
|
+
"X-Webhook-Signature": signature,
|
|
1728
|
+
},
|
|
1729
|
+
timeout=10,
|
|
1730
|
+
)
|
|
1731
|
+
|
|
1732
|
+
assert response.status_code == 200, (
|
|
1733
|
+
f"Expected 200, got {response.status_code}: {response.text}"
|
|
1734
|
+
)
|
|
1735
|
+
data = cast(
|
|
1736
|
+
dict[str, Any], self._extract_transport_response_data(response.json())
|
|
1737
|
+
)
|
|
1738
|
+
assert "reports" in data
|
|
1739
|
+
assert data["reports"][0]["status"] == "received"
|
|
1740
|
+
assert data["reports"][0]["transport"] == "webhook"
|
|
1741
|
+
|
|
1742
|
+
def test_webhook_payment_received_with_fields(self) -> None:
|
|
1743
|
+
"""Test 1: PaymentReceived webhook walker with multiple fields + @restspec(webhook=True)."""
|
|
1744
|
+
# Create user and get API key
|
|
1745
|
+
username = f"payment_user_{uuid.uuid4().hex[:8]}"
|
|
1746
|
+
register_response = requests.post(
|
|
1747
|
+
f"{self.base_url}/user/register",
|
|
1748
|
+
json={"username": username, "password": "password123"},
|
|
1749
|
+
timeout=10,
|
|
1750
|
+
)
|
|
1751
|
+
assert register_response.status_code == 201
|
|
1752
|
+
register_data = cast(
|
|
1753
|
+
dict[str, Any],
|
|
1754
|
+
self._extract_transport_response_data(register_response.json()),
|
|
1755
|
+
)
|
|
1756
|
+
token = register_data["token"]
|
|
1757
|
+
|
|
1758
|
+
api_key_response = requests.post(
|
|
1759
|
+
f"{self.base_url}/api-key/create",
|
|
1760
|
+
json={"name": "payment_webhook_key", "expiry_days": 30},
|
|
1761
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
1762
|
+
timeout=10,
|
|
1763
|
+
)
|
|
1764
|
+
assert api_key_response.status_code == 201
|
|
1765
|
+
api_key_data = cast(
|
|
1766
|
+
dict[str, Any],
|
|
1767
|
+
self._extract_transport_response_data(api_key_response.json()),
|
|
1768
|
+
)
|
|
1769
|
+
api_key = api_key_data["api_key"]
|
|
1770
|
+
|
|
1771
|
+
# Send payment webhook
|
|
1772
|
+
payload = json.dumps(
|
|
1773
|
+
{
|
|
1774
|
+
"payment_id": "PAY-12345",
|
|
1775
|
+
"order_id": "ORD-67890",
|
|
1776
|
+
"amount": 99.99,
|
|
1777
|
+
"currency": "USD",
|
|
1778
|
+
}
|
|
1779
|
+
)
|
|
1780
|
+
payload_bytes = payload.encode("utf-8")
|
|
1781
|
+
signature = self._generate_webhook_signature(payload_bytes, api_key)
|
|
1782
|
+
|
|
1783
|
+
response = requests.post(
|
|
1784
|
+
f"{self.base_url}/webhook/PaymentReceived",
|
|
1785
|
+
data=payload,
|
|
1786
|
+
headers={
|
|
1787
|
+
"Content-Type": "application/json",
|
|
1788
|
+
"X-API-Key": api_key,
|
|
1789
|
+
"X-Webhook-Signature": signature,
|
|
1790
|
+
},
|
|
1791
|
+
timeout=10,
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1794
|
+
assert response.status_code == 200, (
|
|
1795
|
+
f"Expected 200, got {response.status_code}: {response.text}"
|
|
1796
|
+
)
|
|
1797
|
+
data = cast(
|
|
1798
|
+
dict[str, Any], self._extract_transport_response_data(response.json())
|
|
1799
|
+
)
|
|
1800
|
+
assert "reports" in data
|
|
1801
|
+
report = data["reports"][0]
|
|
1802
|
+
assert report["status"] == "success"
|
|
1803
|
+
assert report["payment_id"] == "PAY-12345"
|
|
1804
|
+
assert report["order_id"] == "ORD-67890"
|
|
1805
|
+
assert report["amount"] == 99.99
|
|
1806
|
+
assert report["currency"] == "USD"
|
|
1807
|
+
|
|
1808
|
+
def test_webhook_not_accessible_via_regular_walker_endpoint(self) -> None:
|
|
1809
|
+
"""Test that webhook walkers (with @restspec(webhook=True)) are NOT accessible via /walker/."""
|
|
1810
|
+
# Create user and get token
|
|
1811
|
+
username = f"webhook_path_user_{uuid.uuid4().hex[:8]}"
|
|
1812
|
+
register_response = requests.post(
|
|
1813
|
+
f"{self.base_url}/user/register",
|
|
1814
|
+
json={"username": username, "password": "password123"},
|
|
1815
|
+
timeout=10,
|
|
1816
|
+
)
|
|
1817
|
+
assert register_response.status_code == 201
|
|
1818
|
+
register_data = cast(
|
|
1819
|
+
dict[str, Any],
|
|
1820
|
+
self._extract_transport_response_data(register_response.json()),
|
|
1821
|
+
)
|
|
1822
|
+
token = register_data["token"]
|
|
1823
|
+
|
|
1824
|
+
# Try to access PaymentReceived (webhook walker) via /walker/ endpoint
|
|
1825
|
+
response = requests.post(
|
|
1826
|
+
f"{self.base_url}/walker/PaymentReceived",
|
|
1827
|
+
json={
|
|
1828
|
+
"payment_id": "PAY-TEST",
|
|
1829
|
+
"order_id": "ORD-TEST",
|
|
1830
|
+
"amount": 10.00,
|
|
1831
|
+
},
|
|
1832
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
1833
|
+
timeout=10,
|
|
1834
|
+
)
|
|
1835
|
+
|
|
1836
|
+
# Webhook walkers should not be accessible via /walker/ endpoint
|
|
1837
|
+
# In static mode: 404 (Not Found) or 405 (Method Not Allowed - no POST registered)
|
|
1838
|
+
# In dynamic mode: 400 (Bad Request - explicitly rejected)
|
|
1839
|
+
assert response.status_code in (400, 404, 405), (
|
|
1840
|
+
f"Expected 400/404/405, got {response.status_code}: {response.text}"
|
|
1841
|
+
)
|
|
1842
|
+
|
|
1843
|
+
def test_webhook_revoked_api_key(self) -> None:
|
|
1844
|
+
"""Test that revoked API keys are rejected."""
|
|
1845
|
+
# Create user and get auth token
|
|
1846
|
+
username = f"webhook_revoke_user_{uuid.uuid4().hex[:8]}"
|
|
1847
|
+
register_response = requests.post(
|
|
1848
|
+
f"{self.base_url}/user/register",
|
|
1849
|
+
json={"username": username, "password": "password123"},
|
|
1850
|
+
timeout=10,
|
|
1851
|
+
)
|
|
1852
|
+
assert register_response.status_code == 201
|
|
1853
|
+
register_data = cast(
|
|
1854
|
+
dict[str, Any],
|
|
1855
|
+
self._extract_transport_response_data(register_response.json()),
|
|
1856
|
+
)
|
|
1857
|
+
token = register_data["token"]
|
|
1858
|
+
|
|
1859
|
+
# Create API key
|
|
1860
|
+
api_key_response = requests.post(
|
|
1861
|
+
f"{self.base_url}/api-key/create",
|
|
1862
|
+
json={"name": "key_to_revoke", "expiry_days": 30},
|
|
1863
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
1864
|
+
timeout=10,
|
|
1865
|
+
)
|
|
1866
|
+
assert api_key_response.status_code == 201
|
|
1867
|
+
api_key_data = cast(
|
|
1868
|
+
dict[str, Any],
|
|
1869
|
+
self._extract_transport_response_data(api_key_response.json()),
|
|
1870
|
+
)
|
|
1871
|
+
api_key = api_key_data["api_key"]
|
|
1872
|
+
api_key_id = api_key_data["api_key_id"]
|
|
1873
|
+
|
|
1874
|
+
# Verify API key works with MinimalWebhook
|
|
1875
|
+
payload = json.dumps({})
|
|
1876
|
+
payload_bytes = payload.encode("utf-8")
|
|
1877
|
+
signature = self._generate_webhook_signature(payload_bytes, api_key)
|
|
1878
|
+
response = requests.post(
|
|
1879
|
+
f"{self.base_url}/webhook/MinimalWebhook",
|
|
1880
|
+
data=payload,
|
|
1881
|
+
headers={
|
|
1882
|
+
"Content-Type": "application/json",
|
|
1883
|
+
"X-API-Key": api_key,
|
|
1884
|
+
"X-Webhook-Signature": signature,
|
|
1885
|
+
},
|
|
1886
|
+
timeout=10,
|
|
1887
|
+
)
|
|
1888
|
+
assert response.status_code == 200
|
|
1889
|
+
|
|
1890
|
+
# Revoke the API key
|
|
1891
|
+
revoke_response = requests.delete(
|
|
1892
|
+
f"{self.base_url}/api-key/{api_key_id}",
|
|
1893
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
1894
|
+
timeout=10,
|
|
1895
|
+
)
|
|
1896
|
+
assert revoke_response.status_code == 200, (
|
|
1897
|
+
f"Failed to revoke key: {revoke_response.text}"
|
|
1898
|
+
)
|
|
1899
|
+
|
|
1900
|
+
# Try to use revoked key (same payload and signature)
|
|
1901
|
+
response = requests.post(
|
|
1902
|
+
f"{self.base_url}/webhook/MinimalWebhook",
|
|
1903
|
+
data=payload,
|
|
1904
|
+
headers={
|
|
1905
|
+
"Content-Type": "application/json",
|
|
1906
|
+
"X-API-Key": api_key,
|
|
1907
|
+
"X-Webhook-Signature": signature,
|
|
1908
|
+
},
|
|
1909
|
+
timeout=10,
|
|
1910
|
+
)
|
|
1911
|
+
|
|
1912
|
+
# Should fail with 401
|
|
1913
|
+
assert response.status_code == 401, (
|
|
1914
|
+
f"Expected 401 for revoked key, got {response.status_code}: {response.text}"
|
|
1915
|
+
)
|
|
1916
|
+
|
|
1564
1917
|
|
|
1565
1918
|
class TestJacScaleServeDevMode:
|
|
1566
1919
|
"""Test jac-scale serve with --dev mode (dynamic routing).
|
|
@@ -1833,3 +2186,57 @@ class TestJacScaleServeDevMode:
|
|
|
1833
2186
|
assert reports[1]["status"] == "after_async_wait"
|
|
1834
2187
|
assert reports[2]["status"] == "completed"
|
|
1835
2188
|
assert "task" in reports[2]
|
|
2189
|
+
|
|
2190
|
+
def test_walker_stream_response(self) -> None:
|
|
2191
|
+
"""Test that walker streaming responses work correctly."""
|
|
2192
|
+
response = requests.post(
|
|
2193
|
+
f"{self.base_url}/walker/WalkerStream",
|
|
2194
|
+
json={"count": 3},
|
|
2195
|
+
timeout=30,
|
|
2196
|
+
stream=True,
|
|
2197
|
+
)
|
|
2198
|
+
|
|
2199
|
+
assert response.status_code == 200, (
|
|
2200
|
+
f"Failed with status {response.status_code}: {response.text}"
|
|
2201
|
+
)
|
|
2202
|
+
assert response.headers["content-type"] == "text/event-stream; charset=utf-8"
|
|
2203
|
+
assert response.headers.get("cache-control") == "no-cache"
|
|
2204
|
+
assert response.headers.get("connection") == "close"
|
|
2205
|
+
|
|
2206
|
+
# Collect streaming content
|
|
2207
|
+
content = ""
|
|
2208
|
+
for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
|
|
2209
|
+
if chunk:
|
|
2210
|
+
content += chunk
|
|
2211
|
+
|
|
2212
|
+
# The generator yields "Report 0", "Report 1", "Report 2" without delimiters
|
|
2213
|
+
# They get concatenated together in the stream
|
|
2214
|
+
expected = "Report 0Report 1Report 2"
|
|
2215
|
+
assert content == expected, f"Expected '{expected}', got '{content}'"
|
|
2216
|
+
|
|
2217
|
+
def test_function_stream_response(self) -> None:
|
|
2218
|
+
"""Test that function streaming responses work correctly."""
|
|
2219
|
+
response = requests.post(
|
|
2220
|
+
f"{self.base_url}/function/FunctionStream",
|
|
2221
|
+
json={"count": 2},
|
|
2222
|
+
timeout=30,
|
|
2223
|
+
stream=True,
|
|
2224
|
+
)
|
|
2225
|
+
|
|
2226
|
+
assert response.status_code == 200, (
|
|
2227
|
+
f"Failed with status {response.status_code}: {response.text}"
|
|
2228
|
+
)
|
|
2229
|
+
assert response.headers["content-type"] == "text/event-stream; charset=utf-8"
|
|
2230
|
+
assert response.headers.get("cache-control") == "no-cache"
|
|
2231
|
+
assert response.headers.get("connection") == "close"
|
|
2232
|
+
|
|
2233
|
+
# Collect streaming content
|
|
2234
|
+
content = ""
|
|
2235
|
+
for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
|
|
2236
|
+
if chunk:
|
|
2237
|
+
content += chunk
|
|
2238
|
+
|
|
2239
|
+
# The generator yields "Func 0", "Func 1" without delimiters
|
|
2240
|
+
# They get concatenated together in the stream
|
|
2241
|
+
expected = "Func 0Func 1"
|
|
2242
|
+
assert content == expected, f"Expected '{expected}', got '{content}'"
|