feat: add initial frontend setup with styles, types, and testing framework
- Created a global CSS file for styling the frontend with responsive design. - Introduced TypeScript types for various entities including Research, Alpha, and Account. - Implemented Playwright tests for account management and data synchronization workflows. - Configured TypeScript with strict settings and included necessary libraries. - Set up Vite as the build tool with React plugin and API proxy configuration. - Added a Python script to initialize environment variables securely.
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from email.utils import format_datetime
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.alphas import pnl_points, sanitize
|
||||
from app.worldquant import VerificationRequired, WqClient, WqError
|
||||
|
||||
|
||||
async def test_cookie_auth_expiry_and_read_only_boundary(settings):
|
||||
calls, auth_count = [], 0
|
||||
|
||||
def handler(request):
|
||||
nonlocal auth_count
|
||||
calls.append((request.method, request.url.path))
|
||||
if request.url.path == "/authentication":
|
||||
auth_count += 1
|
||||
return httpx.Response(201, headers={"Set-Cookie": f"t=session{auth_count}; Path=/"}, json={})
|
||||
assert "Authorization" not in request.headers
|
||||
if request.headers.get("Cookie") == "t=session1":
|
||||
return httpx.Response(401)
|
||||
return httpx.Response(200, json={"id": "user1"})
|
||||
|
||||
client = WqClient(settings, transport=httpx.MockTransport(handler))
|
||||
await client.authenticate("person@example.com", "secret")
|
||||
assert await client.profile() == {"id": "user1"}
|
||||
assert auth_count == 2
|
||||
await asyncio.gather(*(client.authenticate("person@example.com", "secret") for _ in range(5)))
|
||||
assert auth_count == 2
|
||||
assert all(method == "GET" or path == "/authentication" for method, path in calls)
|
||||
client.disconnect()
|
||||
assert client.credentials is None and not list(client.client.cookies.items())
|
||||
await client.close()
|
||||
|
||||
|
||||
async def test_persona_verification_uses_same_cookie_and_safe_location(settings):
|
||||
complete = False
|
||||
|
||||
def handler(request):
|
||||
if request.url.path == "/authentication":
|
||||
return httpx.Response(
|
||||
401,
|
||||
headers={
|
||||
"WWW-Authenticate": "persona",
|
||||
"Location": "/authentication/persona/challenge",
|
||||
"Set-Cookie": "challenge=abc; Path=/",
|
||||
},
|
||||
)
|
||||
assert request.headers.get("Cookie") == "challenge=abc"
|
||||
return httpx.Response(201 if complete else 202, json={})
|
||||
|
||||
client = WqClient(settings, transport=httpx.MockTransport(handler))
|
||||
with pytest.raises(VerificationRequired) as error:
|
||||
await client.authenticate("test@example.com", "secret")
|
||||
assert error.value.url.endswith("/authentication/persona/challenge")
|
||||
with pytest.raises(VerificationRequired):
|
||||
await client.verify()
|
||||
complete = True
|
||||
await client.verify()
|
||||
assert client.authenticated and client.verification_url is None
|
||||
await client.close()
|
||||
unsafe = WqClient(
|
||||
settings,
|
||||
transport=httpx.MockTransport(
|
||||
lambda r: httpx.Response(
|
||||
401, headers={"WWW-Authenticate": "persona", "Location": "https://evil.example/secret"}
|
||||
)
|
||||
),
|
||||
)
|
||||
with pytest.raises(WqError) as error:
|
||||
await unsafe.authenticate("test@example.com", "secret")
|
||||
assert error.value.code == "invalid_verification"
|
||||
await unsafe.close()
|
||||
|
||||
|
||||
async def test_retry_after_network_budget_pending_and_permissions(settings):
|
||||
delays, requests = [], 0
|
||||
|
||||
async def sleep(delay):
|
||||
delays.append(delay)
|
||||
|
||||
def handler(request):
|
||||
nonlocal requests
|
||||
requests += 1
|
||||
if requests == 1:
|
||||
return httpx.Response(429, headers={"Retry-After": "120"})
|
||||
if requests == 2:
|
||||
raise httpx.ConnectError("contains private body", request=request)
|
||||
return httpx.Response(200, json={"id": "ready"})
|
||||
|
||||
client = WqClient(settings, transport=httpx.MockTransport(handler), sleep=sleep)
|
||||
client.credentials, client.authenticated = ("test@example.com", "secret"), True
|
||||
assert await client.profile() == {"id": "ready"}
|
||||
assert delays[0] == 120 and len(delays) == 2
|
||||
date = format_datetime(datetime.now(timezone.utc) + timedelta(seconds=60), usegmt=True)
|
||||
assert 58 < client.retry_delay(date, 0) <= 60
|
||||
await client.close()
|
||||
for status, code in [(403, "access_denied"), (404, "not_found"), (503, "retry_exhausted")]:
|
||||
c = WqClient(
|
||||
settings,
|
||||
transport=httpx.MockTransport(lambda r: httpx.Response(status, text="private data")),
|
||||
sleep=sleep,
|
||||
)
|
||||
c.credentials, c.authenticated = ("test@example.com", "secret"), True
|
||||
with pytest.raises(WqError) as error:
|
||||
await c.profile()
|
||||
assert error.value.code == code and "private" not in str(error.value)
|
||||
await c.close()
|
||||
responses = iter(
|
||||
[httpx.Response(202, headers={"Retry-After": "1"}), httpx.Response(200, json={"records": []})]
|
||||
)
|
||||
c = WqClient(settings, transport=httpx.MockTransport(lambda r: next(responses)), sleep=sleep)
|
||||
c.credentials, c.authenticated = ("test@example.com", "secret"), True
|
||||
assert await c.pnl("abc") == {"records": []}
|
||||
await c.close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw,expected",
|
||||
[
|
||||
(
|
||||
{
|
||||
"schema": {"properties": [{"name": "date"}, {"name": "pnl"}]},
|
||||
"records": [["2025-01-01", 10], ["2025-01-02", None]],
|
||||
},
|
||||
[{"date": "2025-01-01", "value": 10.0}, {"date": "2025-01-02", "value": None}],
|
||||
),
|
||||
(
|
||||
{"schema": {"properties": [{"name": "pnl"}, {"name": "date"}]}, "records": [[20, "2025-01-01"]]},
|
||||
[{"date": "2025-01-01", "value": 20.0}],
|
||||
),
|
||||
(
|
||||
{"schema": {"properties": {"date": {}, "pnl": {}}}, "records": [["2025-01-01", "NaN"]]},
|
||||
[{"date": "2025-01-01", "value": None}],
|
||||
),
|
||||
(
|
||||
{"records": [{"timestamp": 1735689600000, "value": "5"}]},
|
||||
[{"date": "2025-01-01T00:00:00+00:00", "value": 5.0}],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_pnl_schemas_preserve_null(raw, expected):
|
||||
assert pnl_points(raw) == expected
|
||||
|
||||
|
||||
def test_unknown_pnl_schema_fails_instead_of_fabricating():
|
||||
with pytest.raises(ValueError):
|
||||
pnl_points({"records": [["2025-01-01", 100]]})
|
||||
|
||||
|
||||
def test_sensitive_response_keys_are_removed_recursively():
|
||||
assert sanitize(
|
||||
{
|
||||
"id": "a1",
|
||||
"accessToken": "secret",
|
||||
"nested": [{"refresh_token": "secret", "client-secret": "secret", "value": 1}],
|
||||
"Set-Cookie": "secret",
|
||||
}
|
||||
) == {"id": "a1", "nested": [{"value": 1}]}
|
||||
|
||||
|
||||
async def test_concurrent_expiry_authenticates_once(settings):
|
||||
auth_count = 0
|
||||
first_requests = 0
|
||||
both_expired = asyncio.Event()
|
||||
|
||||
async def handler(request):
|
||||
nonlocal auth_count, first_requests
|
||||
if request.method == "POST":
|
||||
auth_count += 1
|
||||
return httpx.Response(201, json={})
|
||||
if first_requests < 2:
|
||||
first_requests += 1
|
||||
if first_requests == 2:
|
||||
both_expired.set()
|
||||
await both_expired.wait()
|
||||
return httpx.Response(401)
|
||||
return httpx.Response(200, json={"id": "user"})
|
||||
|
||||
client = WqClient(settings, transport=httpx.MockTransport(handler))
|
||||
await client.authenticate("test@example.com", "secret")
|
||||
assert await asyncio.gather(client.profile(), client.profile()) == [{"id": "user"}, {"id": "user"}]
|
||||
assert auth_count == 2 # Initial connection plus one shared reauthentication.
|
||||
await client.close()
|
||||
Reference in New Issue
Block a user