refactor(auth): one Google connect route for a sign-in and for YouTube access
/auth/link and /auth/upgrade become /auth/google/connect?access=identity|read|write: one handler, one table of what each access asks Google for, the same callback markers. An unknown access is refused (422) instead of quietly meaning read, and an instance without Google answers ?link=error for a grant too, not a silent return to the app. The frontend builds the URL in lib/googleConnect, and a contract test holds it to the backend route and its access values.
This commit is contained in:
@@ -23,8 +23,10 @@ class _FakeGoogle:
|
||||
|
||||
def __init__(self, userinfo: dict):
|
||||
self.userinfo = userinfo
|
||||
self.asked: dict = {}
|
||||
|
||||
async def authorize_redirect(self, _request, _redirect_uri, **_kw):
|
||||
async def authorize_redirect(self, _request, _redirect_uri, **kw):
|
||||
self.asked = kw
|
||||
return RedirectResponse(url="/fake-google")
|
||||
|
||||
async def authorize_access_token(self, _request):
|
||||
@@ -218,12 +220,20 @@ def test_a_refused_sign_in_records_a_request_in_the_reported_language(
|
||||
assert _rows(db, User, User.email, "new@example.com") == []
|
||||
|
||||
|
||||
def _start_link(client, email: str):
|
||||
def _password_login(client, email: str):
|
||||
assert (
|
||||
client.post("/auth/password-login", json={"email": email, "password": _PW}).status_code
|
||||
== 200
|
||||
)
|
||||
assert client.get("/auth/link", follow_redirects=False).headers["location"] == "/fake-google"
|
||||
|
||||
|
||||
def _connect(client, access: str):
|
||||
return client.get(f"/auth/google/connect?access={access}", follow_redirects=False)
|
||||
|
||||
|
||||
def _start_link(client, email: str):
|
||||
_password_login(client, email)
|
||||
assert _connect(client, "identity").headers["location"] == "/fake-google"
|
||||
|
||||
|
||||
def test_linking_an_identity_another_account_holds_is_a_conflict(client, db, google, make_user):
|
||||
@@ -258,3 +268,72 @@ def test_linking_a_free_identity_attaches_it(client, db, google, make_user):
|
||||
assert r.headers["location"] == "/?link=ok"
|
||||
db.refresh(me)
|
||||
assert (me.google_sub, me.google_email) == ("sub-free", "me3.google@example.com")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("access", "scope", "prompt"),
|
||||
[
|
||||
("identity", "openid email profile", "select_account"),
|
||||
("read", auth_routes.READ_SCOPES, "consent"),
|
||||
("write", auth_routes.WRITE_SCOPES, "consent"),
|
||||
],
|
||||
)
|
||||
def test_one_connect_route_asks_google_for_what_the_access_needs(
|
||||
client, google, make_user, access, scope, prompt
|
||||
):
|
||||
make_user(email=f"ask-{access}@example.com")
|
||||
_password_login(client, f"ask-{access}@example.com")
|
||||
|
||||
assert _connect(client, access).headers["location"] == "/fake-google"
|
||||
|
||||
assert (google.asked["scope"], google.asked["prompt"]) == (scope, prompt)
|
||||
assert google.asked["access_type"] == "offline"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("access", ["read", "write"])
|
||||
def test_a_youtube_grant_attaches_to_the_password_account_and_returns_to_the_app(
|
||||
client, db, google, make_user, access
|
||||
):
|
||||
me = make_user(email=f"grant-{access}@example.com")
|
||||
_password_login(client, f"grant-{access}@example.com")
|
||||
_connect(client, access)
|
||||
|
||||
r = _sign_in(client, google, sub=f"sub-grant-{access}", email=f"g-{access}@example.com")
|
||||
|
||||
# Back to the app, not the "linked" notice: the onboarding wizard picks up where it was.
|
||||
assert r.headers["location"] == "/"
|
||||
db.refresh(me)
|
||||
assert me.google_sub == f"sub-grant-{access}"
|
||||
assert _rows(db, User, User.google_sub, f"sub-grant-{access}") == [me]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("access", ["", "admin", "READ"])
|
||||
def test_an_unknown_access_starts_no_connect(client, google, make_user, access):
|
||||
make_user(email="odd@example.com")
|
||||
_password_login(client, "odd@example.com")
|
||||
|
||||
assert _connect(client, access).status_code == 422
|
||||
assert google.asked == {}
|
||||
|
||||
|
||||
def test_a_connect_needs_a_signed_in_account(client, google):
|
||||
assert _connect(client, "identity").status_code == 401
|
||||
assert google.asked == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("access", ["identity", "read", "write"])
|
||||
def test_without_google_configured_a_connect_says_it_failed(client, make_user, monkeypatch, access):
|
||||
monkeypatch.setattr(auth_routes, "google_oauth", lambda _db: None)
|
||||
make_user(email=f"nog-{access}@example.com")
|
||||
_password_login(client, f"nog-{access}@example.com")
|
||||
|
||||
assert _connect(client, access).headers["location"] == "/?link=error"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["/auth/link", "/auth/upgrade", "/auth/upgrade?access=write"])
|
||||
def test_the_old_connect_routes_are_gone(client, google, make_user, path):
|
||||
make_user(email="old@example.com")
|
||||
_password_login(client, "old@example.com")
|
||||
|
||||
assert client.get(path, follow_redirects=False).status_code == 404
|
||||
assert google.asked == {}
|
||||
|
||||
Reference in New Issue
Block a user