Schwab requires an HTTPS callback. The usual answer is https://127.0.0.1:8182 behind a self-signed certificate, which means clicking through a browser warning on every re-authentication — and the refresh token expires weekly. There are also reports of Schwab refusing to register apps whose callback is a loopback address. This app already terminates real HTTPS, so it can take the redirect itself. Unauthenticated by necessity: the provider redirects a browser here and cannot attach the chart token, so it sits alongside /health and /version. It is inert — nothing is stored, and the page echoes only the query string of the request that produced it, which the caller already has in their address bar. Retaining the code would let a later anonymous visitor read it. The path and the page are both deliberately unrevealing. That is not a security control; it just avoids advertising which brokerage this host talks to. Treat the path as fixed — changing a registered callback means editing the app, which can send it back through approval. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
41 lines
1.4 KiB
Python
41 lines
1.4 KiB
Python
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.api.schwab_auth import router
|
|
|
|
|
|
def client() -> TestClient:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
return TestClient(app)
|
|
|
|
|
|
def test_callback_needs_no_chart_token():
|
|
# Schwab redirects a browser here and cannot attach the token, so this
|
|
# endpoint has to stay open the way /health and /version do.
|
|
assert client().get("/api/qt").status_code == 200
|
|
|
|
|
|
def test_page_does_not_name_the_brokerage():
|
|
# The path is neutral so the host does not advertise who it trades with;
|
|
# the page saying it anyway would defeat that.
|
|
assert "chwab" not in client().get("/api/qt").text
|
|
|
|
|
|
def test_landing_here_directly_explains_itself():
|
|
body = client().get("/api/qt").text
|
|
assert "Register this exact URL" in body
|
|
assert "code" not in body.split("<style>")[0]
|
|
|
|
|
|
def test_authorisation_code_is_echoed_for_the_manual_flow():
|
|
response = client().get("/api/qt", params={"code": "abc123", "session": "s"})
|
|
assert "abc123" in response.text
|
|
assert response.headers["cache-control"] == "no-store"
|
|
|
|
|
|
def test_the_code_is_not_retained_for_a_later_visitor():
|
|
session = client()
|
|
session.get("/api/qt", params={"code": "secret-code"})
|
|
# A second, code-less request must not replay the first one's code.
|
|
assert "secret-code" not in session.get("/api/qt").text
|