74 lines
2.7 KiB
Python
74 lines
2.7 KiB
Python
# -*- coding: utf-8 -*-
|
|||
|
|
"""The LinkedIn OAuth callback endpoint's guidance when opened by a person.
|
||
|
|
|
||
|
|
Real incident: the Agent -> MCP page tells the operator to copy the
|
||
|
|
callback URL so it can be registered in the LinkedIn Developer Portal.
|
||
|
|
Someone copied it and opened it in a browser tab instead, landing on this
|
||
|
|
endpoint with no query string at all, and got "Missing code or state." —
|
||
|
|
accurate, and useless to the person reading it.
|
||
|
|
|
||
|
|
Only LinkedIn is supposed to call this URL, carrying ?code=...&state=...
|
||
|
|
after a real login. A bare visit is therefore a strong signal that
|
||
|
|
somebody misunderstood the copy step, and the response is the only place
|
||
|
|
left to tell them so.
|
||
|
|
"""
|
||
|
|
from fastapi import FastAPI
|
||
|
|
from fastapi.testclient import TestClient
|
||
|
|
|
||
|
|
from adclaw.app.routers import mcp as mcp_router
|
||
|
|
|
||
|
|
|
||
|
|
def _client() -> TestClient:
|
||
|
|
app = FastAPI()
|
||
|
|
app.include_router(mcp_router.router, prefix="/api")
|
||
|
|
return TestClient(app)
|
||
|
|
|
||
|
|
|
||
|
|
def test_opening_the_callback_directly_explains_what_it_is_for():
|
||
|
|
response = _client().get("/api/mcp/linkedin/oauth/callback")
|
||
|
|
|
||
|
|
assert response.status_code == 400
|
||
|
|
body = response.text
|
||
|
|
# Names what the URL is actually for, rather than the raw symptom.
|
||
|
|
assert "not meant to be opened directly" in body
|
||
|
|
assert "Authorized redirect URLs" in body
|
||
|
|
assert "Authenticate" in body
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_old_bare_symptom_message_is_gone():
|
||
|
|
""""Missing code or state" told the operator nothing they could act
|
||
|
|
on. If it ever comes back, this endpoint has lost the only guidance a
|
||
|
|
misdirected person gets."""
|
||
|
|
body = _client().get("/api/mcp/linkedin/oauth/callback").text
|
||
|
|
|
||
|
|
assert "Missing code or state" not in body
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_real_linkedin_error_is_still_shown_verbatim():
|
||
|
|
"""LinkedIn's own refusal (user denied, app misconfigured) must not be
|
||
|
|
replaced by the copy-step guidance — that is a different failure and
|
||
|
|
the operator needs LinkedIn's own words."""
|
||
|
|
response = _client().get(
|
||
|
|
"/api/mcp/linkedin/oauth/callback",
|
||
|
|
params={
|
||
|
|
"error": "user_cancelled_login",
|
||
|
|
"error_description": "The user cancelled the login.",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 400
|
||
|
|
assert "The user cancelled the login." in response.text
|
||
|
|
assert "not meant to be opened directly" not in response.text
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_code_without_state_is_not_treated_as_a_stray_visit():
|
||
|
|
"""Half a callback is a real OAuth problem, not someone pasting a URL,
|
||
|
|
but it lands in the same branch — the guidance is still the most
|
||
|
|
useful thing to say, so this only pins that it does not crash."""
|
||
|
|
response = _client().get(
|
||
|
|
"/api/mcp/linkedin/oauth/callback",
|
||
|
|
params={"code": "abc123"},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 400
|