Flatten budget-app contents into repo root
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import time
|
||||
from io import BytesIO
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from PIL import Image
|
||||
|
||||
from app import mfa
|
||||
from app.main import app
|
||||
from tests.test_v5 import H, user
|
||||
|
||||
|
||||
def login(email, password, code=None):
|
||||
c = TestClient(app)
|
||||
c.headers.update(H)
|
||||
body = {"email": email, "password": password}
|
||||
if code is not None:
|
||||
body["code"] = code
|
||||
return c, c.post("/api/auth/login", json=body)
|
||||
|
||||
|
||||
def now_code(secret, offset=0):
|
||||
return mfa.code_at(secret, int(time.time() // mfa.STEP) + offset)
|
||||
|
||||
|
||||
def test_rfc6238_vectors():
|
||||
import base64
|
||||
s = base64.b32encode(b"12345678901234567890").decode().rstrip("=")
|
||||
assert mfa.code_at(s, 59 // 30) == "287082" and mfa.code_at(s, 1111111109 // 30) == "081804"
|
||||
|
||||
|
||||
def test_mfa_set_up_sign_in_recovery_and_reset(client):
|
||||
ana = user(client, "ana@t.local", "editor")
|
||||
setup = ana.post("/api/auth/mfa/setup").json()
|
||||
assert setup["uri"].startswith("otpauth://totp/") and "<svg" in setup["qr_svg"]
|
||||
secret = setup["secret"]
|
||||
# not on until a code is confirmed
|
||||
assert ana.get("/api/auth/me").json()["mfa_enabled"] is False
|
||||
assert ana.post("/api/auth/mfa/enable", json={"code": "000000" if now_code(secret) != "000000" else "111111"}).status_code == 400
|
||||
r = ana.post("/api/auth/mfa/enable", json={"code": now_code(secret)}).json()
|
||||
codes = r["recovery_codes"]
|
||||
assert r["user"]["mfa_enabled"] and len(codes) == 8 and r["user"]["mfa_recovery_left"] == 8
|
||||
listed = {u["email"]: u["mfa_enabled"] for u in client.get("/api/users").json()}
|
||||
assert listed == {"admin@test.local": False, "ana@t.local": True}
|
||||
|
||||
# sign-in now asks for the code; no session without it
|
||||
c, res = login("ana@t.local", "password-123")
|
||||
assert res.status_code == 200 and res.json() == {"mfa_required": True}
|
||||
assert c.get("/api/auth/me").status_code == 401
|
||||
_, res = login("ana@t.local", "password-123", "123456" if now_code(secret, 1) != "123456" else "654321")
|
||||
assert res.status_code == 401 and res.json()["detail"] == "invalid_mfa_code"
|
||||
# the code from the enable step was used already; the next one works once
|
||||
c, res = login("ana@t.local", "password-123", now_code(secret, 1))
|
||||
assert res.status_code == 200 and res.json()["email"] == "ana@t.local" and c.get("/api/auth/me").status_code == 200
|
||||
_, res = login("ana@t.local", "password-123", now_code(secret, 1))
|
||||
assert res.status_code == 401 # replay refused
|
||||
# a recovery code works once
|
||||
c, res = login("ana@t.local", "password-123", codes[0].upper())
|
||||
assert res.status_code == 200 and res.json()["mfa_recovery_left"] == 7
|
||||
assert login("ana@t.local", "password-123", codes[0])[1].status_code == 401
|
||||
|
||||
# an administrator resets it (lost phone)
|
||||
me = c.get("/api/auth/me").json()["id"]
|
||||
assert ana.post(f"/api/users/{me}/mfa/reset").status_code == 403
|
||||
assert client.post(f"/api/users/{me}/mfa/reset").json()["mfa_enabled"] is False
|
||||
assert login("ana@t.local", "password-123")[1].json()["email"] == "ana@t.local"
|
||||
acts = {r["action"] for r in client.get("/api/activity?entity=user").json()["rows"]}
|
||||
assert {"mfa_enabled", "mfa_failed", "mfa_recovery_used", "mfa_reset"} <= acts
|
||||
|
||||
|
||||
def test_mfa_locks_after_wrong_codes_and_turns_off_with_password_and_code(client):
|
||||
ana = user(client, "ana@t.local", "viewer")
|
||||
secret = ana.post("/api/auth/mfa/setup").json()["secret"]
|
||||
ana.post("/api/auth/mfa/enable", json={"code": now_code(secret)})
|
||||
for _ in range(mfa.MAX_FAILED - 1):
|
||||
assert login("ana@t.local", "password-123", "999999x")[1].status_code == 401
|
||||
assert login("ana@t.local", "password-123", "999999x")[1].status_code == 429
|
||||
assert login("ana@t.local", "password-123", now_code(secret, 1))[1].status_code == 429 # locked for a while
|
||||
assert ana.post("/api/auth/mfa/disable", json={"password": "wrong", "code": ""}).status_code == 400
|
||||
from app.db import SessionLocal
|
||||
from app.models import User
|
||||
with SessionLocal() as db:
|
||||
u = db.query(User).filter_by(email="ana@t.local").one()
|
||||
u.mfa_locked_until = None
|
||||
db.commit()
|
||||
assert ana.post("/api/auth/mfa/disable", json={"password": "password-123", "code": now_code(secret, 1)}).json()["mfa_enabled"] is False
|
||||
|
||||
|
||||
def test_avatar_upload_resize_and_remove(client):
|
||||
ana = user(client, "ana@t.local", "viewer")
|
||||
buf = BytesIO()
|
||||
Image.new("RGB", (900, 600), (200, 30, 30)).save(buf, "PNG")
|
||||
assert ana.put("/api/auth/me/avatar", files={"file": ("x.txt", b"not a picture")}).status_code == 400
|
||||
me = ana.put("/api/auth/me/avatar", files={"file": ("me.png", buf.getvalue(), "image/png")}).json()
|
||||
assert me["avatar_url"].startswith(f"/api/users/{me['id']}/avatar?v=")
|
||||
pic = client.get(me["avatar_url"]) # other users see it in lists
|
||||
assert pic.status_code == 200 and pic.headers["content-type"] == "image/jpeg"
|
||||
assert Image.open(BytesIO(pic.content)).size == (256, 256)
|
||||
assert ana.delete("/api/auth/me/avatar").json()["avatar_url"] is None
|
||||
assert client.get(f"/api/users/{me['id']}/avatar").status_code == 404
|
||||
|
||||
|
||||
def test_theme_is_saved(client):
|
||||
assert client.get("/api/auth/me").json()["theme"] == "system"
|
||||
assert client.put("/api/auth/me", json={"theme": "dark"}).json()["theme"] == "dark"
|
||||
assert client.put("/api/auth/me", json={"theme": "pink"}).json()["theme"] == "dark"
|
||||
Reference in New Issue
Block a user