2024年1月21日 星期日

Flask網頁開發 - 第二版 - 使用者身分驗證 (3)

  我的Flask網頁開發學習,也是走走停停,三天打魚,兩天晒網。但意外的是,學習與學習之間的空檔,讓我在快要忘記先前所學的技術之際,透過再次複習,不只重拾記憶,更可以深深地加強自己對於這項技術的整體架構與掌握度。

  一如先前的預測,未來我的ESG工作,將不再只是單純做資料處理、資料維運、資料科學的任務,而必須進一步加值我們團隊打造的ESG產品,製作成方便客戶使用的「工具」,實踐組織的ESG目標。

  目前我自己的網頁開發能力仍不純熟,即便自己對此沒有興趣,但著眼於網頁開發是簡報呈現與工具提供的方式,因此我還是有動機持續學習、應用的。近幾年的目標是希望我能夠將ESG計算工具的產品,先透過Flask做簡易的產品原型Demo,這對於研發而言也是極其重要的。

第二部分、範例:社群部落格app

第八章、使用者身分驗證-密碼雜湊與驗證藍圖(8a8b)

l   密碼雜湊與驗證藍圖:
git checkout 8a
git checkout 8b

l   所需使用的套件:
Flask-Login
-管理已登入的使用者session
Werkzeug
-密碼雜湊化與驗證。
itsdangerous
-以加密的方式保護權證的生成與驗證。

l   app/models.py
from werkzeug.security import generate_password_hash, check_password_hash
from . import db

class User(db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    role_id = db.Column(db.Integer, db.ForeignKey("roles.id"))
    password_hash = db.Column(db.String(128))
   
    # @porperty
裝飾器將方法定義成屬性,因此它不需要()
    @property
    def password(self):
        raise AttributeError("password is not a readable attribute")
   
    @password.setter
    def password(self, password):
        # generate_password_hash(password, method="pbkdf2:sha256", salt_length=8)
,此函式接收純文字密碼,回傳密碼雜湊字串
        self.password_hash = generate_password_hash(password)
   
    def verify_password(self, password):
        # check_password_hash(hash, password)
,此函式回傳值True代表使用者密碼是正確的
        return check_password_hash(self.password_hash, password)
   
    def __repr__(self):
        return "<User %r>" % self.username

l   tests/test_user_model.py
import unittest
from app import create_app, db
from app.models import User

class UserModelTestCase(unittest.TestCase):
    #
flask shell測試密碼雜湊,u1.password_hash應有值
    # u1 = User()
    # u1.password = "cat"
    def test_password_setter(self):
        u = User(password="cat")
        self.assertTrue(u.password_hash is not None)
   
    #
flask shell測試密碼雜湊,直接讀取u1.password會造成AttributeError
    def test_no_password_getter(self):
        u = User(password="cat")
        with self.assertRaises(AttributeError):
            u.password
   
    #
flask shell測試密碼雜湊,輸入正確密碼才可得True
    def test_password_verification(self):
        u = User(password="cat")
        self.assertTrue(u.verify_password("cat"))
        self.assertFalse(u.verify_password("dog"))
   
    #
flask shell測試密碼雜湊,即便密碼相同,u1.password_hashu2.password_hash也不相同
    # u2 = User()
    # u2.password = "cat"
    def test_password_salts_are_random(self):
        u = User(password="cat")
        u2 = User(password="cat")
        self.assertTrue(u.password_hash != u2.password_hash)

l   將使用者身分驗證系統的路由加入為第二個藍圖:
# app/auth/__init__.py
from flask import Blueprint
auth = Blueprint("auth", __name__)
from . import views

# app/auth/views.py
from flask import render_template
from . import auth
@auth.route("/login")
def login():
    return render_template("auth/login.html")

# app/__init__.py
from .auth import auth as auth_blueprint
# url_prefix
引數是選用的,當你使用它時,在藍圖裡面定義的所有路由都會被加上前置詞,本例路由會被註冊成/auth/login,且開發web伺服器底下的完整URL會變成http://localhost:5000/auth/login
app.register_blueprint(auth_blueprint, url_prefix="/auth")

第八章、使用者身分驗證-登入登出與使用者註冊(8c8d)

l   登入登出與使用者註冊:
git checkout 8c
git checkout 8d

l   Flask-Login是一種小型但是相當實用的擴充套件,專門用來管理使用者身分驗證系統,且不需要綁定特定的身分驗證機制:
pip install flask-login

l   Flask-Login會與appUser物件密切合作,為了能夠使用appUser模型,Flask-Login要求它實作一些常見的屬性與方法:

特性/方法

說明

is_authenticated

如果使用者提供有效的登入憑證,它必須是True

is_active

如果允許使用者登入,它必須是True

is_anonymous

用來代表匿名使用者,它必須是True

get_id()

必須回傳使用者獨有的識別碼,編碼成Unicode字串

l   app/models.py
from flask_login import UserMixin
from . import login_manager

#
修改User模型來支援使用者登入
class User(UserMixin, db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(64), unique=True, index=True)
    username = db.Column(db.String(64), unique=True, index=True)
    role_id = db.Column(db.Integer, db.ForeignKey("roles.id"))
    password_hash = db.Column(db.String(128))

#
載入使用者的函式,當Flask-Login需要取得已登入的使用者的資訊時就會呼叫它
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

l   app/__init__.py
from flask_login import LoginManager

login_manager = LoginManager()
# login_view
屬性的用途是設定登入網頁的端點,因為登入路由在藍圖裡面,它的前面必須加上藍圖名稱
login_manager.login_view = "auth.login"

def create_app(config_name):
    # ...
    login_manager.init_app(app)
    # ...

l   app/auth/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User

class LoginForm(FlaskForm):
    # Email()
驗證需要先pip install email_validator
    email = StringField("Email", validators=[DataRequired(), Length(1, 64), Email()])
    password = PasswordField("Password", validators=[DataRequired()])
    remember_me = BooleanField("Keep me logged in")
    submit = SubmitField("Log In")

class RegistrationForm(FlaskForm):
    email = StringField("Email", validators=[DataRequired(), Length(1, 64), Email()])
    # 0: flags – The regexp flags to use, for example re.IGNORECASE.
    username = StringField("Username", validators=[DataRequired(), Length(1, 64), Regexp("^[A-Za-z][A-Za-z0-9_.]*$", 0, "Usernames must have only letters, numbers, dots or underscores")])
    password = PasswordField("Password", validators=[DataRequired(), EqualTo("password2", message="Passwords must match.")])
    password2 = PasswordField("Confirm password", validators=[DataRequired()])
    submit = SubmitField("Register")
   
    #
當提交表單時,Flask-WTF將自動驗證表單資料,並呼叫表單類別中定義的驗證方法
    def validate_email(self, field):
        if User.query.filter_by(email=field.data.lower()).first():
            raise ValidationError("Email already registered.")
   
    def validate_username(self, field):
        if User.query.filter_by(username=field.data).first():
            raise ValidationError("Username already in use.")

l   app/auth/views.py
from flask import render_template, redirect, request, url_for, flash
from flask_login import login_user, logout_user, login_required
from . import auth
from .. import db
from ..models import User
from .forms import LoginForm, RegistrationForm

@auth.route("/login", methods=["GET", "POST"])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data.lower()).first()
        if user is not None and user.verify_password(form.password.data):
            # login_user()
函式來幫使用者session記錄使用者已經登入
            #
選用的REMEMBER_COOKIE_DURATION組態選項可用來改變cookie的一年期限預設值
            login_user(user, form.remember_me.data)
            #
如果顯示給使用者看的登入表單想要防止使用者在未經授權的情況下前往受保護的URLFlask-Login就會將那個原始的URL存放在next查詢字串引數;如果沒有next查詢字串引數,就會轉址到首頁
            next = request.args.get("next")
            if next is None or not next.startswith("/"):
                next = url_for("main.index")
            return redirect(next)
        flash("Invalid email or password.")
    return render_template("auth/login.html", form=form)

@auth.route("/logout")
#
保護路由,只允許通過驗證的使用者造訪它
@login_required
def logout():
    logout_user()
    flash("You have been logged out.")
    return redirect(url_for("main.index"))

@auth.route("/register", methods=["GET", "POST"])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(email=form.email.data.lower(), username=form.username.data, password=form.password.data)
        db.session.add(user)
        db.session.commit()
        flash("You can now login.")
        return redirect(url_for("auth.login"))
    return render_template('auth/register.html', form=form)

l   app/templates/base.html
<!-- ... -->
<ul class="nav navbar-nav navbar-right">
    {% if current_user.is_authenticated %}
    <li><a href="{{ url_for('auth.logout') }}">Log Out</a></li>
    {% else %}
    <li><a href="{{ url_for('auth.login') }}">Log In</a></li>
    {% endif %}
</ul>
<!-- ... -->

l   app/templates/auth/login.html
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Login{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Login</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
    <br>
    <p>New user? <a href="{{ url_for('auth.register') }}">Click here to register</a>.</p>
</div>
{% endblock %}

l   app/templates/auth/register.html
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Register{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Register</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}

第八章、使用者身分驗證-帳號認證(8e)

l   帳號認證:
git checkout 8e

l   展示itsdangerous如何產生一個含有使用者id的已簽署權杖:
#
flask shell展示
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
# expires_in
引數可設定權杖的到期時間,其單位是秒
s = Serializer(app.config["SECRET_KEY"], expires_in=3600)
token = s.dumps({"confirm": 23})
token
à b'eyJhbGciOiJIUzUxMiIsImlhdCI6MTY4Mjc0NTY2MSwiZXhwIjoxNjgyNzQ5MjYxfQ.eyJjb25maXJtIjoyM30.ZBVS3Y-H_ooMCa8cfDrxtZCtcAX00AI0RYIdETcyMpkOPyeIfxcVXF74w3YQUvZNm0XfVckQOWf7jZ8jzwcYTw'
data = s.loads(token)
data
à {'confirm': 23}

l   app/models.py
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app

class User(UserMixin, db.Model):
    # ...
    confirmed = db.Column(db.Boolean, default=False)
   
    def generate_confirmation_token(self, expiration=3600):
        s = Serializer(current_app.config["SECRET_KEY"], expiration)
        return s.dumps({"confirm": self.id}).decode("utf-8")
   
    def confirm(self, token):
        s = Serializer(current_app.config["SECRET_KEY"])
        try:
            data = s.loads(token.encode("utf-8"))
        except:
            return False
        if data.get("confirm") != self.id:
            return False
        self.confirmed = True
        db.session.add(self)
        return True

l   app/auth/views.py
# ...
#
可以直接利用current_user隨處取得目前的使用者相關資訊(依個人實作User Class設置而定)、登入狀態以及利用裝飾器@login_request驗證登入狀況
from flask_login import login_user, logout_user, login_required, current_user
# ...

#
before_app_request處理常式來過濾未確認的帳號
@auth.before_app_request
def before_request():
    # current_user.is_authenticated
已登入
    # not current_user.confirmed
未啟動(本書範例User Classconfirmed)
    # flask
透過了endpoint來記錄了跟rule(url)view_function的關聯,所以有endpoint,就有辦法找到相關的ruleview_function (https://hackmd.io/@shaoeChen/HJiZtEngG/https%3A%2F%2Fhackmd.io%2Fs%2FSyiNhFTvz)
    # endpoint
不為static,避免相關jscssicon失效
    if current_user.is_authenticated \
            and not current_user.confirmed \
            and request.endpoint \
            and request.blueprint != "auth" \
            and request.endpoint != "static":
        return redirect(url_for("auth.unconfirmed"))

@auth.route("/unconfirmed")
def unconfirmed():
    if current_user.is_anonymous or current_user.confirmed:
        return redirect(url_for("main.index"))
    return render_template("auth/unconfirmed.html")

#
寄出確認email
@auth.route("/register", methods=["GET", "POST"])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(email=form.email.data.lower(), username=form.username.data, password=form.password.data)
        db.session.add(user)
        db.session.commit()
        token = user.generate_confirmation_token()
        send_email(user.email, "Confirm Your Account", "auth/email/confirm", user=user, token=token)
        flash("A confirmation email has been sent to you by email.")
        return redirect(url_for("auth.login"))
    return render_template("auth/register.html", form=form)

#
確認使用者帳號
@auth.route("/confirm/<token>")
@login_required
def confirm(token):
    if current_user.confirmed:
        return redirect(url_for("main.index"))
    if current_user.confirm(token):
        db.session.commit()
        flash("You have confirmed your account. Thanks!")
    else:
        flash("The confirmation link is invalid or has expired.")
    return redirect(url_for("main.index"))

#
重寄帳號確認email
@auth.route("/confirm")
@login_required
def resend_confirmation():
    token = current_user.generate_confirmation_token()
    send_email(current_user.email, "Confirm Your Account", "auth/email/confirm", user=current_user, token=token)
    flash("A new confirmation email has been sent to you by email.")
    return redirect(url_for("main.index"))

l   app/templates/auth/unconfirmed.html
<!--
給未確認的使用者看的網頁 -->
<h1>Hello, {{ current_user.username }}!</h1>
<p>
    Need another confirmation email?
    <a href="{{ url_for('auth.resend_confirmation') }}">Click here</a>
</p>

l   app/templates/auth/email/confirm.txt
<!--
確認email的文字內文 -->
Dear {{ user.username }},
To confirm your account please click on the following link:
<!--
相對URL在網頁背景下可以良好地運作,因為瀏覽器會加上目前網頁的主機名稱與連接埠號碼來將它們轉換成絕對URL,但是用email寄出的URL沒有這種背景,所以我們在url_for()呼叫式傳入_external=True引數,來要求完整的URL -->
{{ url_for("auth.confirm", token=token, _external=True) }}

l   tests/test_user_model.py
# ...
class UserModelTestCase(unittest.TestCase):
    # ...
    def test_valid_confirmation_token(self):
        u = User(password="cat")
        db.session.add(u)
        db.session.commit()
        token = u.generate_confirmation_token()
        self.assertTrue(u.confirm(token))
   
    def test_invalid_confirmation_token(self):
        u1 = User(password="cat")
        u2 = User(password="dog")
        db.session.add(u1)
        db.session.add(u2)
        db.session.commit()
        token = u1.generate_confirmation_token()
        self.assertFalse(u2.confirm(token))
   
    def test_expired_confirmation_token(self):
        u = User(password="cat")
        db.session.add(u)
        db.session.commit()
        token = u.generate_confirmation_token(1)
        time.sleep(2)
        self.assertFalse(u.confirm(token))

第八章、使用者身分驗證-修改密碼(8f)

l   修改密碼:
git checkout 8f

l   app/auth/forms.py
# ...
class ChangePasswordForm(FlaskForm):
    old_password = PasswordField("Old password", validators=[DataRequired()])
    password = PasswordField("New password", validators=[DataRequired(), EqualTo("password2", message="Passwords must match.")])
    password2 = PasswordField("Confirm new password", validators=[DataRequired()])
    submit = SubmitField("Update Password")

l   app/auth/views.py
# ...
@auth.route("/change-password", methods=["GET", "POST"])
@login_required
def change_password():
    form = ChangePasswordForm()
    if form.validate_on_submit():
        if current_user.verify_password(form.old_password.data):
            current_user.password = form.password.data
            db.session.add(current_user)
            db.session.commit()
            flash("Your password has been updated.")
            return redirect(url_for("main.index"))
        else:
            flash("Invalid password.")
    return render_template("auth/change_password.html", form=form)

l   app/templates/base.html
<!-- ... -->
<ul class="dropdown-menu">
    <li><a href="{{ url_for('auth.change_password') }}">Change Password</a></li>
    <li><a href="{{ url_for('auth.logout') }}">Log Out</a></li>
</ul>
<!-- ... -->

l   app/templates/auth/change_password.html
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Change Password{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Change Your Password</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}

第八章、使用者身分驗證-重設密碼(8g)

l   重設密碼:
git checkout 8g

l   app/models.py
# ...
class User(UserMixin, db.Model):
    # ...
    @staticmethod
    def reset_password(token, new_password):
        s = Serializer(current_app.config["SECRET_KEY"])
        try:
            data = s.loads(token.encode("utf-8"))
        except:
            return False
        user = User.query.get(data.get("reset"))
        if user is None:
            return False
        user.password = new_password
        db.session.add(user)
        return True

l   app/auth/forms.py
# ...
class PasswordResetRequestForm(FlaskForm):
    email = StringField("Email", validators=[DataRequired(), Length(1, 64), Email()])
    submit = SubmitField("Reset Password")

class PasswordResetForm(FlaskForm):
    password = PasswordField("New Password", validators=[DataRequired(), EqualTo("password2", message="Passwords must match")])
    password2 = PasswordField("Confirm password", validators=[DataRequired()])
    submit = SubmitField("Reset Password")

l   app/auth/views.py
# ...
from .forms import PasswordResetRequestForm, PasswordResetForm
# ...
@auth.route("/reset", methods=["GET", "POST"])
def password_reset_request():
    if not current_user.is_anonymous:
        return redirect(url_for("main.index"))
    form = PasswordResetRequestForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data.lower()).first()
        if user:
            token = user.generate_reset_token()
            send_email(user.email, "Reset Your Password", "auth/email/reset_password", user=user, token=token)
        flash("An email with instructions to reset your password has been sent to you.")
        return redirect(url_for("auth.login"))
    return render_template("auth/reset_password.html", form=form)

@auth.route("/reset/<token>", methods=["GET", "POST"])
def password_reset(token):
    if not current_user.is_anonymous:
        return redirect(url_for("main.index"))
    form = PasswordResetForm()
    if form.validate_on_submit():
        if User.reset_password(token, form.password.data):
            db.session.commit()
            flash("Your password has been updated.")
            return redirect(url_for("auth.login"))
        else:
            return redirect(url_for("main.index"))
    return render_template("auth/reset_password.html", form=form)

l   app/templates/auth/login.html
<!-- ... -->
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
    <br>
    <p>Forgot your password? <a href="{{ url_for('auth.password_reset_request') }}">Click here to reset it</a>.</p>
    <p>New user? <a href="{{ url_for('auth.register') }}">Click here to register</a>.</p>
</div>
<!-- ... -->

l   app/templates/auth/reset_password.html
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Password Reset{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Reset Your Password</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}

l   app/templates/auth/email/reset_password.html
<p>Dear {{ user.username }},</p>
<p>To reset your password <a href="{{ url_for('auth.password_reset', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.password_reset', token=token, _external=True) }}</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>

l   tests/test_user_model.py
# ...
class UserModelTestCase(unittest.TestCase):
    # ...
    def test_valid_reset_token(self):
        u = User(password="cat")
        db.session.add(u)
        db.session.commit()
        token = u.generate_reset_token()
        self.assertTrue(User.reset_password(token, "dog"))
        self.assertTrue(u.verify_password("dog"))
   
    def test_invalid_reset_token(self):
        u = User(password="cat")
        db.session.add(u)
        db.session.commit()
        token = u.generate_reset_token()
        self.assertFalse(User.reset_password(token + "a", "horse"))
        self.assertTrue(u.verify_password("cat"))

第八章、使用者身分驗證-更改Email地址(8h)

l   更改Email地址:
git checkout 8h

l   app/models.py
# ...
class User(UserMixin, db.Model):
    # ...
    def generate_email_change_token(self, new_email, expiration=3600):
        s = Serializer(current_app.config["SECRET_KEY"], expiration)
        return s.dumps({"change_email": self.id, "new_email": new_email}).decode("utf-8")
   
    def change_email(self, token):
        s = Serializer(current_app.config["SECRET_KEY"])
        try:
            data = s.loads(token.encode("utf-8"))
        except:
            return False
        if data.get("change_email") != self.id:
            return False
        new_email = data.get("new_email")
        if new_email is None:
            return False
        if self.query.filter_by(email=new_email).first() is not None:
            return False
        self.email = new_email
        db.session.add(self)
        return True

l   app/auth/forms.py
# ...
class ChangeEmailForm(FlaskForm):
    email = StringField("New Email", validators=[DataRequired(), Length(1, 64), Email()])
    password = PasswordField("Password", validators=[DataRequired()])
    submit = SubmitField("Update Email Address")
   
    def validate_email(self, field):
        if User.query.filter_by(email=field.data.lower()).first():
            raise ValidationError("Email already registered.")

l   app/auth/views.py
# ...
from .forms import ChangeEmailForm
# ...
@auth.route("/change_email", methods=["GET", "POST"])
@login_required
def change_email_request():
    form = ChangeEmailForm()
    if form.validate_on_submit():
        if current_user.verify_password(form.password.data):
            new_email = form.email.data.lower()
            token = current_user.generate_email_change_token(new_email)
            send_email(new_email, "Confirm your email address", "auth/email/change_email", user=current_user, token=token)
            flash("An email with instructions to confirm your new email address has been sent to you.")
            return redirect(url_for("main.index"))
        else:
            flash("Invalid email or password.")
    return render_template("auth/change_email.html", form=form)

@auth.route("/change_email/<token>")
@login_required
def change_email(token):
    if current_user.change_email(token):
        db.session.commit()
        flash("Your email address has been updated.")
    else:
        flash("Invalid request.")
    return redirect(url_for("main.index"))

l   app/templates/base.html
<!-- ... -->
<ul class="dropdown-menu">
    <li><a href="{{ url_for('auth.change_password') }}">Change Password</a></li>
    <li><a href="{{ url_for('auth.change_email_request') }}">Change Email</a></li>
    <li><a href="{{ url_for('auth.logout') }}">Log Out</a></li>
</ul>
<!-- ... -->

l   app/templates/auth/change_email.html
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Change Email Address{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Change Your Email Address</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}

l   app/templates/auth/email/change_email.html
<p>Dear {{ user.username }},</p>
<p>To confirm your new email address <a href="{{ url_for('auth.change_email', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.change_email', token=token, _external=True) }}</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>

l   tests/test_user_model.py
# ...
class UserModelTestCase(unittest.TestCase):
    # ...
    def test_valid_email_change_token(self):
        u = User(email="john@example.com", password="cat")
        db.session.add(u)
        db.session.commit()
        token = u.generate_email_change_token("susan@example.org")
        self.assertTrue(u.change_email(token))
        self.assertTrue(u.email == "susan@example.org")
   
    def test_invalid_email_change_token(self):
        u1 = User(email="john@example.com", password="cat")
        u2 = User(email="susan@example.org", password="dog")
        db.session.add(u1)
        db.session.add(u2)
        db.session.commit()
        token = u1.generate_email_change_token("david@example.net")
        self.assertFalse(u2.change_email(token))
        self.assertTrue(u2.email == "susan@example.org")
   
    def test_duplicate_email_change_token(self):
        u1 = User(email="john@example.com", password="cat")
        u2 = User(email="susan@example.org", password="dog")
        db.session.add(u1)
        db.session.add(u2)
        db.session.commit()
        token = u2.generate_email_change_token("john@example.com")
        self.assertFalse(u2.change_email(token))
        self.assertTrue(u2.email == "susan@example.org")

沒有留言:

張貼留言