여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다. - ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제 - 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다 - eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다 파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이). site·frontend·admin tsc, site vitest 105 passed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
199 lines
9.4 KiB
Python
199 lines
9.4 KiB
Python
"""auth 도메인 e2e — 로그인 / 내정보 / 인증거부 흐름."""
|
|
|
|
|
|
async def test_login_and_me_flow(auth_headers, client):
|
|
"""검증: 시드된 유저가 로그인해 받은 토큰으로 /me 호출."""
|
|
h = await auth_headers("user1", name="홍길동")
|
|
|
|
r = await client.get("/v1/auth/me", headers=h)
|
|
assert r.status_code == 200
|
|
me = r.json()
|
|
assert me["id"] == "user1"
|
|
assert me["name"] == "홍길동"
|
|
# 소속사 필드는 없다.
|
|
assert "company" not in me
|
|
|
|
|
|
async def test_login_with_wrong_password(auth_headers, client):
|
|
"""검증: 존재하는 계정에 '틀린 비밀번호'로 로그인."""
|
|
await auth_headers("user2") # pw1234 로 시드
|
|
|
|
r = await client.post("/v1/auth/login", json={"id": "user2", "password": "wrong"})
|
|
body = r.json()
|
|
assert body["result"]["success"] is False
|
|
assert body["result"]["code"] == 1100
|
|
assert body.get("access_token", "") == ""
|
|
|
|
|
|
async def test_login_nonexistent_account(client):
|
|
"""검증: 존재하지 않는 계정으로 로그인."""
|
|
r = await client.post("/v1/auth/login", json={"id": "ghost", "password": "whatever"})
|
|
assert r.json()["result"]["success"] is False
|
|
|
|
|
|
async def test_me_without_token_is_rejected(client):
|
|
"""검증: 토큰 없이 보호 엔드포인트 /me 호출."""
|
|
r = await client.get("/v1/auth/me")
|
|
assert r.status_code in (401, 403)
|
|
|
|
|
|
# ── 가입(id/pw) ──────────────────────────────────────────────────────────────
|
|
_SIGNUP = {"id": "sajang1", "password": "pw12345678", "name": "김사장", "email": "boss@example.com"}
|
|
|
|
|
|
async def test_signup_creates_account_and_logs_in(client, db_engine):
|
|
"""검증: 가입 → 받은 토큰으로 곧바로 /me."""
|
|
r = await client.post("/v1/auth/signup", json=_SIGNUP)
|
|
body = r.json()
|
|
assert body["result"]["success"] is True
|
|
assert body["access_token"] and body["refresh_token"]
|
|
|
|
me = (await client.get("/v1/auth/me", headers={"Authorization": f"Bearer {body['access_token']}"})).json()
|
|
assert me["id"] == "sajang1"
|
|
assert me["email"] == "boss@example.com"
|
|
assert me["provider"] == 1 # AuthProvider.LOCAL
|
|
|
|
|
|
async def test_signup_rejects_duplicate_id(client, db_engine):
|
|
"""검증: 같은 아이디로 두 번 가입."""
|
|
await client.post("/v1/auth/signup", json=_SIGNUP)
|
|
r = await client.post("/v1/auth/signup", json={**_SIGNUP, "email": "other@example.com"})
|
|
assert r.json()["result"]["code"] == 1101
|
|
|
|
|
|
async def test_signup_rejects_duplicate_email(client, db_engine):
|
|
"""검증: 아이디는 다른데 이메일이 같은 가입."""
|
|
await client.post("/v1/auth/signup", json=_SIGNUP)
|
|
r = await client.post("/v1/auth/signup", json={**_SIGNUP, "id": "sajang2"})
|
|
assert r.json()["result"]["code"] == 1101
|
|
|
|
|
|
async def test_signup_rejects_weak_input(client, db_engine):
|
|
"""검증: 짧은 비밀번호 / 규칙에 안 맞는 아이디 / 형식이 아닌 이메일."""
|
|
for bad in (
|
|
{**_SIGNUP, "password": "short"},
|
|
{**_SIGNUP, "id": "1abc"}, # 영문으로 시작해야 한다
|
|
{**_SIGNUP, "id": "ab"}, # 4자 미만
|
|
{**_SIGNUP, "id": "google_12345"}, # 구글 계정 아이디 접두어는 선점 금지
|
|
{**_SIGNUP, "email": "not-an-email"},
|
|
):
|
|
r = await client.post("/v1/auth/signup", json=bad)
|
|
assert r.json()["result"]["code"] == 101, bad
|
|
|
|
|
|
# ── 구글 로그인 ──────────────────────────────────────────────────────────────
|
|
def _stub_google(monkeypatch, *, sub="1234567890", email="g@example.com", name="구글유저"):
|
|
"""ID 토큰 검증을 대역으로 바꾼다."""
|
|
from services.external.google_identity import GoogleAccount
|
|
|
|
async def _verify(_credential):
|
|
return GoogleAccount(sub=sub, email=email, name=name)
|
|
|
|
monkeypatch.setattr("services.auth_service.verify_id_token", _verify)
|
|
|
|
|
|
async def test_google_login_creates_then_reuses_account(client, db_engine, monkeypatch):
|
|
"""검증: 같은 구글 계정으로 두 번 로그인."""
|
|
_stub_google(monkeypatch)
|
|
|
|
first = (await client.post("/v1/auth/google", json={"credential": "x"})).json()
|
|
assert first["result"]["success"] is True
|
|
me1 = (await client.get("/v1/auth/me", headers={"Authorization": f"Bearer {first['access_token']}"})).json()
|
|
assert me1["provider"] == 2 # AuthProvider.GOOGLE
|
|
assert me1["id"] == "google_1234567890"
|
|
|
|
second = (await client.post("/v1/auth/google", json={"credential": "x"})).json()
|
|
me2 = (await client.get("/v1/auth/me", headers={"Authorization": f"Bearer {second['access_token']}"})).json()
|
|
assert me2["user_id"] == me1["user_id"]
|
|
|
|
|
|
async def test_google_login_follows_sub_not_email(client, db_engine, monkeypatch):
|
|
"""검증: 같은 sub 인데 구글 쪽 이메일이 바뀐 경우."""
|
|
_stub_google(monkeypatch, email="before@example.com")
|
|
first = (await client.post("/v1/auth/google", json={"credential": "x"})).json()
|
|
me1 = (await client.get("/v1/auth/me", headers={"Authorization": f"Bearer {first['access_token']}"})).json()
|
|
|
|
_stub_google(monkeypatch, email="after@example.com")
|
|
second = (await client.post("/v1/auth/google", json={"credential": "x"})).json()
|
|
me2 = (await client.get("/v1/auth/me", headers={"Authorization": f"Bearer {second['access_token']}"})).json()
|
|
assert me2["user_id"] == me1["user_id"]
|
|
|
|
|
|
async def test_google_login_refuses_to_link_existing_local_account(client, db_engine, monkeypatch):
|
|
"""검증: id/pw 로 이미 가입된 이메일로 구글 로그인."""
|
|
await client.post("/v1/auth/signup", json=_SIGNUP)
|
|
|
|
_stub_google(monkeypatch, email=_SIGNUP["email"])
|
|
r = await client.post("/v1/auth/google", json={"credential": "x"})
|
|
assert r.json()["result"]["code"] == 1105
|
|
|
|
|
|
async def test_password_login_against_google_account_is_refused(client, db_engine, monkeypatch):
|
|
"""검증: 구글로 만들어진 계정에 id/pw 로그인 시도."""
|
|
_stub_google(monkeypatch)
|
|
await client.post("/v1/auth/google", json={"credential": "x"})
|
|
|
|
r = await client.post("/v1/auth/login", json={"id": "google_1234567890", "password": "whatever"})
|
|
assert r.json()["result"]["code"] == 1105
|
|
|
|
|
|
async def test_google_login_is_off_when_client_id_is_empty(client, db_engine):
|
|
"""검증: GOOGLE_CLIENT_ID 가 비어 있을 때(테스트 기본값) 구글 로그인 호출."""
|
|
r = await client.post("/v1/auth/google", json={"credential": "anything"})
|
|
assert r.json()["result"]["code"] == 1106
|
|
|
|
|
|
# ── refresh 토큰 무효화(token_version) ────────────────────────────────────────
|
|
async def test_refresh_token_reissues_access_token(auth_headers, client):
|
|
"""검증: 정상적인 refresh 토큰으로 재발급."""
|
|
h = await auth_headers("refuser1")
|
|
login = (await client.post("/v1/auth/login", json={"id": "refuser1", "password": "pw1234"})).json()
|
|
refresh_token = login["refresh_token"]
|
|
|
|
r = await client.post("/v1/auth/refresh_token", headers={"Authorization": f"Bearer {refresh_token}"})
|
|
body = r.json()
|
|
assert body["result"]["success"] is True
|
|
assert body["access_token"]
|
|
# 새 access 토큰이 실제로 먹힌다.
|
|
me = (await client.get("/v1/auth/me", headers={"Authorization": f"Bearer {body['access_token']}"})).json()
|
|
assert me["id"] == "refuser1"
|
|
del h # auth_headers 는 시드 용도로만 쓴다
|
|
|
|
|
|
async def test_refresh_token_is_revoked_after_password_change(auth_headers, client):
|
|
"""검증: refresh 토큰을 받은 **뒤에** 비밀번호를 바꾼다."""
|
|
h = await auth_headers("refuser2")
|
|
login = (await client.post("/v1/auth/login", json={"id": "refuser2", "password": "pw1234"})).json()
|
|
old_refresh_token = login["refresh_token"]
|
|
|
|
upd = await client.patch("/v1/auth/me", headers=h, json={"password": "newpassword123"})
|
|
assert upd.json()["result"]["success"] is True
|
|
|
|
r = await client.post("/v1/auth/refresh_token", headers={"Authorization": f"Bearer {old_refresh_token}"})
|
|
body = r.json()
|
|
assert body["result"]["success"] is False
|
|
assert body["result"]["code"] == 1108, body # ACCOUNT_SESSION_REVOKED
|
|
assert body.get("access_token", "") == ""
|
|
|
|
# 새로 로그인하면(새 비밀번호로) 새 refresh 토큰은 당연히 먹힌다.
|
|
relogin = (await client.post("/v1/auth/login", json={"id": "refuser2", "password": "newpassword123"})).json()
|
|
r2 = await client.post("/v1/auth/refresh_token", headers={"Authorization": f"Bearer {relogin['refresh_token']}"})
|
|
assert r2.json()["result"]["success"] is True
|
|
|
|
|
|
async def test_refresh_token_is_revoked_when_account_blocked(auth_headers, client, db_engine):
|
|
"""검증: refresh 토큰을 받은 뒤 계정이 차단(UserStatus.INACTIVE)된다."""
|
|
from sqlalchemy import text
|
|
|
|
h = await auth_headers("refuser3")
|
|
login = (await client.post("/v1/auth/login", json={"id": "refuser3", "password": "pw1234"})).json()
|
|
del h
|
|
|
|
async with db_engine.begin() as c:
|
|
await c.execute(text("UPDATE users SET status = 2 WHERE id = 'refuser3'")) # UserStatus.INACTIVE
|
|
|
|
r = await client.post("/v1/auth/refresh_token", headers={"Authorization": f"Bearer {login['refresh_token']}"})
|
|
body = r.json()
|
|
assert body["result"]["success"] is False
|
|
assert body["result"]["code"] == 1102 # ACCOUNT_BLOCKED_USER
|