feat: refine email verification login flow
This commit is contained in:
@@ -10,6 +10,7 @@ from flask import session
|
||||
|
||||
from app import create_app
|
||||
from app.config import Config
|
||||
from app.email import EmailDeliveryError
|
||||
from app.extensions import db
|
||||
from app.models import EmailVerificationCode, TrustedDevice, User
|
||||
from app.routes import grant_fresh_authorization, has_fresh_authorization, valid_password
|
||||
@@ -60,6 +61,28 @@ class EmailAuthenticationTest(unittest.TestCase):
|
||||
).isoformat()
|
||||
self.assertFalse(has_fresh_authorization(user, "password_change"))
|
||||
|
||||
def test_account_security_uses_custom_email_validation_and_password_autofill(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
app = self.create_app(directory)
|
||||
with app.app_context():
|
||||
user = User(username="Alice", email="alice@example.com", is_active_account=True)
|
||||
user.set_password("Password-1234!")
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
user_id = user.id
|
||||
|
||||
client = app.test_client()
|
||||
with app.app_context():
|
||||
self.login_as(client, db.session.get(User, user_id))
|
||||
response = client.get("/account/security")
|
||||
html = response.get_data(as_text=True)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('novalidate data-change-email-form', html)
|
||||
self.assertIn('autocomplete="current-password"', html)
|
||||
self.assertIn('autocomplete="email"', html)
|
||||
self.assertIn('请输入有效的新登录邮箱。', html)
|
||||
|
||||
def create_app(self, directory: str):
|
||||
class TestConfig(Config):
|
||||
TESTING = True
|
||||
@@ -143,7 +166,118 @@ class EmailAuthenticationTest(unittest.TestCase):
|
||||
verify = client.get("/verify/register")
|
||||
response = client.post("/verify/register", data={"csrf_token": self.csrf(verify), "code": "123456"})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
with app.app_context(): self.assertTrue(User.query.filter_by(email="alice@example.com").one().is_active_account)
|
||||
self.assertEqual(response.location, "/home")
|
||||
self.assertEqual(client.get("/home").status_code, 200)
|
||||
with app.app_context():
|
||||
self.assertTrue(User.query.filter_by(email="alice@example.com").one().is_active_account)
|
||||
self.assertEqual(TrustedDevice.query.count(), 0)
|
||||
|
||||
@patch("app.routes.send_transactional_email")
|
||||
@patch("app.routes.secrets.randbelow", return_value=123456)
|
||||
def test_login_accepts_username_or_email(self, _random, _send):
|
||||
with TemporaryDirectory() as directory:
|
||||
app = self.create_app(directory)
|
||||
with app.app_context():
|
||||
user = User(username="Alice", email="alice@example.com", is_active_account=True)
|
||||
user.set_password("Password-1234!")
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
for identifier in ("Alice", "alice@example.com"):
|
||||
client = app.test_client()
|
||||
page = client.get("/login")
|
||||
with client.session_transaction() as state:
|
||||
captcha = state["captcha"]
|
||||
response = client.post("/login", data={
|
||||
"csrf_token": self.csrf(page), "identifier": identifier,
|
||||
"password": "Password-1234!", "captcha": captcha,
|
||||
})
|
||||
self.assertEqual(response.location, "/verify/login")
|
||||
with app.app_context():
|
||||
EmailVerificationCode.query.delete()
|
||||
db.session.commit()
|
||||
|
||||
def test_login_reports_verification_code_cooldown(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
app = self.create_app(directory)
|
||||
with app.app_context():
|
||||
user = User(username="Alice", email="alice@example.com", is_active_account=True)
|
||||
user.set_password("Password-1234!")
|
||||
db.session.add(user)
|
||||
db.session.flush()
|
||||
db.session.add(EmailVerificationCode(
|
||||
email=user.email,
|
||||
purpose="login",
|
||||
code_hash="a" * 64,
|
||||
expires_at=utc_now() + timedelta(minutes=10),
|
||||
requested_ip="127.0.0.1",
|
||||
))
|
||||
db.session.commit()
|
||||
|
||||
client = app.test_client()
|
||||
page = client.get("/login")
|
||||
with client.session_transaction() as state:
|
||||
captcha = state["captcha"]
|
||||
response = client.post("/login", data={
|
||||
"csrf_token": self.csrf(page), "identifier": "Alice",
|
||||
"password": "Password-1234!", "captcha": captcha,
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.location, "/verify/login")
|
||||
with app.app_context():
|
||||
self.assertEqual(EmailVerificationCode.query.count(), 1)
|
||||
|
||||
verify_page = client.get("/verify/login")
|
||||
html = verify_page.get_data(as_text=True)
|
||||
self.assertIn("邮箱二次认证", html)
|
||||
self.assertIn("邮箱验证码已发送,请输入验证码完成二次认证。", html)
|
||||
self.assertIn('name="trust_device"', html)
|
||||
|
||||
def test_invalid_graphic_captcha_does_not_start_email_verification(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
app = self.create_app(directory)
|
||||
with app.app_context():
|
||||
user = User(username="Alice", email="alice@example.com", is_active_account=True)
|
||||
user.set_password("Password-1234!")
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
client = app.test_client()
|
||||
page = client.get("/login")
|
||||
response = client.post("/login", data={
|
||||
"csrf_token": self.csrf(page), "identifier": "Alice",
|
||||
"password": "Password-1234!", "captcha": "WRONG",
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("图形验证码错误", response.get_data(as_text=True))
|
||||
self.assertIn('window.__authErrorField = "captcha"', response.get_data(as_text=True))
|
||||
with app.app_context():
|
||||
self.assertEqual(EmailVerificationCode.query.count(), 0)
|
||||
|
||||
@patch("app.routes.send_transactional_email", side_effect=EmailDeliveryError("delivery failed"))
|
||||
def test_email_delivery_failure_does_not_mark_graphic_captcha(self, _send):
|
||||
with TemporaryDirectory() as directory:
|
||||
app = self.create_app(directory)
|
||||
with app.app_context():
|
||||
user = User(username="Alice", email="alice@example.com", is_active_account=True)
|
||||
user.set_password("Password-1234!")
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
client = app.test_client()
|
||||
page = client.get("/login")
|
||||
with client.session_transaction() as state:
|
||||
captcha = state["captcha"]
|
||||
response = client.post("/login", data={
|
||||
"csrf_token": self.csrf(page), "identifier": "Alice",
|
||||
"password": "Password-1234!", "captcha": captcha,
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 503)
|
||||
self.assertIn("验证码发送失败,请稍后重试。", response.get_data(as_text=True))
|
||||
self.assertIn("window.__authErrorField = null", response.get_data(as_text=True))
|
||||
|
||||
@patch("app.routes.send_transactional_email")
|
||||
@patch("app.routes.secrets.randbelow", return_value=123456)
|
||||
@@ -154,10 +288,10 @@ class EmailAuthenticationTest(unittest.TestCase):
|
||||
user = User(username="Alice", email="alice@example.com", is_active_account=True); user.set_password("Password-1234!"); db.session.add(user); db.session.commit()
|
||||
client = app.test_client(); page = client.get("/login")
|
||||
with client.session_transaction() as state: captcha = state["captcha"]
|
||||
response = client.post("/login", data={"csrf_token": self.csrf(page), "email": "alice@example.com", "password": "Password-1234!", "captcha": captcha})
|
||||
response = client.post("/login", data={"csrf_token": self.csrf(page), "identifier": "alice@example.com", "password": "Password-1234!", "captcha": captcha})
|
||||
self.assertEqual(response.location, "/verify/login")
|
||||
verify = client.get("/verify/login")
|
||||
response = client.post("/verify/login", data={"csrf_token": self.csrf(verify), "code": "123456"})
|
||||
response = client.post("/verify/login", data={"csrf_token": self.csrf(verify), "code": "123456", "trust_device": "on"})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
with app.app_context(): self.assertEqual(TrustedDevice.query.count(), 1)
|
||||
|
||||
@@ -190,7 +324,7 @@ class EmailAuthenticationTest(unittest.TestCase):
|
||||
user = User(username="Alice", email="alice@example.com", is_active_account=True); user.set_password("Password-1234!"); db.session.add(user); db.session.commit()
|
||||
page = client.get("/login")
|
||||
with client.session_transaction() as state: captcha = state["captcha"]
|
||||
client.post("/login", data={"csrf_token": self.csrf(page), "email": "alice@example.com", "password": "Password-1234!", "captcha": captcha})
|
||||
client.post("/login", data={"csrf_token": self.csrf(page), "identifier": "alice@example.com", "password": "Password-1234!", "captcha": captcha})
|
||||
for _ in range(5):
|
||||
page = client.get("/verify/login"); client.post("/verify/login", data={"csrf_token": self.csrf(page), "code": "000000"})
|
||||
page = client.get("/verify/login"); response = client.post("/verify/login", data={"csrf_token": self.csrf(page), "code": "123456"})
|
||||
|
||||
Reference in New Issue
Block a user