テスト

### いるもん

```ruby:
pip install pytest 

p/ に移動し PYTHONPATH=app pytest app/tests


# [ルートディレクトリで]テスト実行
pytest -s

# テスト実行されなかったら
pytest -v
```

### [test_auth.py]で
"with app.test_request_context():"を省略できる書き方

```ruby:[conftest.py]
def app():
    app = create_app()
    app.config.update({
        "TESTING": True,
        "WTF_CSRF_ENABLED": False,
        "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:",
        "SERVER_NAME": "localhost.localdomain"  # ここ！
    })

    
    2. test_auth.py
    def test_login_success(client, app, create_user):
        login_url = url_for('main.login')
        mypage_url = url_for('main.mypage')
```


### テスト勉強サイト

https://testdriven.io/blog/flask-pytest/

https://dev.to/pavanbelagatti/learn-how-to-test-flask-applications-with-pytest-o25

https://github.com/ka7eh/pytest-flask-tutorial

### viewエラー

```ruby:
'app.forms.LoginForm object' has no attribute 'email'

"forms.py"のLoginformにemail属性がない
```

### run.py動かそうとしたらエラー

```ruby:
1️⃣ ディレクトリ構成を、こうしてない
your_project/
├── run.py
└── app/
    ├── __init__.py
    ├── routes.py
    └── その他...

2.run.pyにcreate_appがない
from app import create_app, db

app = create_app()

if __name__ == "__main__":
    with app.app_context():
        db.create_all()  # DBのテーブルがなかったら作る
    app.run(debug=True, use_reloader=True)
```

### ディレクトリ構造

```ruby
 tests/
    └── conftest.py
    └── test_auth.py

  app
  ├──init__.py

  p
  ├──pytest.ini
```

### pytest.ini中身

```ruby:
[pytest]
pythonpath = .
```

### inite_py中身

```ruby:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_login import LoginManager

# 拡張機能の初期化（アプリとはまだ紐づけない）
db = SQLAlchemy()
bcrypt = Bcrypt()
login_manager = LoginManager()

def create_app():
    app = Flask(__name__)
    
    # 設定
    app.config['SECRET_KEY'] = 'your-secret-key'  # フォームなどで必要
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    # 拡張機能とアプリを紐づけ
    db.init_app(app)
    bcrypt.init_app(app)
    login_manager.init_app(app)

    # Blueprint やルーティングを後から登録するならここ
    # from .routes import main as main_blueprint
    # app.register_blueprint(main_blueprint)

    return app

```

```ruby:テストユーザーを作っておき、作ったtestユーザーは@pytest.fixtureで呼び出す

@pytest.fixture
def test_user():
```

```ruby:pytest.ini
    [pytest]
    testpaths = tests
    python_files = conftest.py,  test_auth.py 
    addopts = --maxfail=1 --disable-warnings -q
```

```ruby:conftest.py(お決まり文 テンプレ)
import pytest
from app import app, db, User
from app import bcrypt


bcrypt = Bcrypt(app)

import pytest
from app import app, db、User
from flask_bcrypt import Bcrypt

bcrypt = Bcrypt()

@pytest.fixture(scope='session')
def app():
    app.config['TESTING'] = True
    app.config['WTF_CSRF_ENABLED'] = False
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
    with app.app_context():
        db.create_all()
        yield app
        db.session.remove()
        db.drop_all()

@pytest.fixture(scope='function')
def client(app):
    return app.test_client()

@pytest.fixture(scope='function')
def db_session(app):
    # トランザクション制御用
    with app.app_context():
        db.session.begin(subtransactions=True)
        yield db.session
        db.session.rollback()

@pytest.fixture
def create_user(db_session):
    def _create_user(username, email, raw_password):
        hashed_pw = bcrypt.generate_password_hash(raw_password).decode('utf-8')
        user = User(username=username, email=email, password=hashed_pw)
        db_session.add(user)
        db_session.commit()
        return user
    return _create_user
```

```ruby:[ログインさせるテスト] "test_auth.py"

 def test_login_success(client, init_db):
    create_user('testuser', 'test@example.com', 'testpassword')
    
    login_data = {
        'username': 'testuser',
        'password': 'testpassword'
    }

    # ログインフォームにデータを送信
    response = client.post('/login', data=login_data)

    # レスポンスがリダイレクトされることを確認（成功時の挙動）
    assert response.status_code == 302
    assert '/mypage' in response.location
    
```


```ruby:ログインさせたら、文章表示
response = client.post('/login', data={
        'username': 'testuser',
        'password': 'testpassword'
    }, follow_redirects=True)

    # 成功したか確認（リダイレクトやメッセージ）
    assert 'ログイン成功' in response.data.decode('utf-8') 
```

### テスト考え方

```ruby:
def test_login_success(client, init_db):
    
    # なんのデータを？
　　　入れたいデータの配列名 
     
      = 入れたいデータ変数名 + 入れたいデータ 

    
    # どこのURLに？
    response = client.post('/行きたいURL', data=入れたいデータの配列名)

    # 処理の結果、どこに行きたい？
    assert response.status_code == 302
    assert '/行きたいページ' in response.location
```

```ruby:リダイレクトしたい
response = client.post('/login', data={
       なんちゃらなんちゃら,
    }, follow_redirects=True)
```

```ruby:新規登録成功 → ログインページへ
def test_register_success(client, db):
    response = client.post('/register', data={
        'username': 'newuser',
        'email': 'new@example.com',
        'password': 'password',
        'confirm': 'password'
    }, follow_redirects=True)

    assert 'ログインしてください' in response.data.decode('utf-8')
```

```ruby:新規登録失敗
def test_register_fail_password_mismatch(client, db):
    response = client.post('/register', data={
        'username': 'user',
        'email': 'bad@example.com',
        'password': 'pass1',
        'confirm': 'pass2'
    }, follow_redirects=True)

    assert 'パスワードが一致しません' in response.data.decode('utf-8')
```

```ruby:ログイン成功　→ mypageへ
def test_login_success_redirect_to_mypage(client, db, create_user):
    create_user('testuser', 'test@example.com', 'testpassword')

    response = client.post('/login', data={
        'username': 'testuser',
        'password': 'testpassword'
    }, follow_redirects=True)

    assert 'ようこそ' in response.data.decode('utf-8')  # mypageに「ようこそ」などが書かれている前提
```

```ruby:ログイン失敗 [パスワエラー]
def test_login_fail_wrong_password(client, db, create_user):
    create_user('testuser', 'test@example.com', 'correctpass')

    response = client.post('/login', data={
        'username': 'testuser',
        'password': 'wrongpass'
    }, follow_redirects=True)

    assert 'ログインに失敗しました' in response.data.decode('utf-8')
```

### 大規模アプリ
```ruby:
アプリが大規模になったら、pytest.iniファイル作成
 p/
    └──pytest.ini
```

### データ編集

```ruby:
from pathlib import Path

# get the resources folder in the tests folder
resources = Path(__file__).parent / "resources"

def test_edit_user(client):
    response = client.post("/user/2/edit", data={
        "name": "Flask",
        "theme": "dark",
        "picture": (resources / "picture.png").open("rb"),
    })
    assert response.status_code == 200
```

### ログアウトリンクに行ったら、リダイレクトさせる

```ruby:
assert len(response.history) == 1
    # Check that the second request was to the index page.
    assert response.request.path == "/index"
```

### 文法間違い

```ruby:
SyntaxError: unterminated string literal (detected at line 13)

＃ 内容 assert response.location == ''http://127.0.0.1:5000/mypage'
```

```ruby:
SyntaxError: bytes can only contain ASCII literal characters

＃ 修正前
assert b'ログインに失敗しました' in response.data

＃ 修正後
assert 'ログインに失敗しました' in response.data.decode('utf-8')
```

### 初期設定エラー

```ruby:
tests/conftest.py:10: AttributeError
_ ERROR at setup of test_register_fail_password_mismatch _

    @pytest.fixture()
    def app():
>       app.config['TESTING'] = True
E       AttributeError: 'function' object has no attribute 'config'


# 解決策
 １.[conftest.pyにこれ追記]
  from app.app import create_app
  
 2.[app.pyにこれ追記]
 from app import create_app

 3.conftest.py app関数 変更
  flask_app = create_app()
  app.config['TESTING'] = Trueを全部、
  flask_appにする
```

### init.pyに初期設定 + app.pyはこう書く

```ruby:
[app.py]
from flask import Blueprint,

bcrypt = Bcrypt()

# Blueprint の作成
main = Blueprint('main', __name__)


[init.py]
# __init__.py の create_app 関数内に追加

from app.app import main  # Blueprint を import
app.register_blueprint(main)
```

### ログイン機能全部

```ruby:
from app import db, bcrypt, login_manager
from flask import Flask, render_template, redirect, url_for, flash, request, send_from_directory, Blueprint
from flask_sqlalchemy import SQLAlchemy
from flask_mail import Mail, Message
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import InputRequired, Length, Email, EqualTo, ValidationError, Optional
from flask_login import login_user, login_required, logout_user, UserMixin, current_user
from flask_bcrypt import Bcrypt
from itsdangerous import URLSafeTimedSerializer, SignatureExpired
from pathlib import Path
from werkzeug.utils import secure_filename


main = Blueprint('main', __name__)

# ユーザーモデル
class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), nullable=False, unique=True)
    email = db.Column(db.String(120), unique=True, nullable=True)
    password = db.Column(db.String(150), nullable=False)
    email_verified = db.Column(db.Boolean, default=False)

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

class RegisterForm(FlaskForm):
    username = StringField('ユーザー名', validators=[
        InputRequired(message="ユーザー名は必須です。"),
        Length(min=4, max=150, message="ユーザー名は4文字以上150文字以下にしてください。")
    ])
    email = StringField('Email', validators=[Optional(), Email()])
    password = PasswordField('パスワード', validators=[
        InputRequired(message="パスワードは必須です。"),
        Length(min=4, max=150)
    ])
    submit = SubmitField('登録')

    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user:
            raise ValidationError('このユーザー名は既に使用されています。')

    def validate_email(self, email):
        if not email.data:
            return  # メールが空なら重複チェックしない
        user = User.query.filter_by(email=email.data).first()
        if user:
            raise ValidationError('このメールアドレスは既に登録されています')


class LoginForm(FlaskForm):
    username = StringField('ユーザー名', validators=[
        InputRequired(message="ユーザー名は必須です。")
    ])
    password = PasswordField('パスワード', validators=[
        InputRequired(message="パスワードは必須です。")
    ])
    email = StringField('ユーザー名', validators=[
        InputRequired(message="ユーザー名は必須です。")
    ])
    remember = BooleanField('ログイン状態を保持する')
    submit = SubmitField('ログイン')

class PasswordResetRequestForm(FlaskForm):
    email = StringField('メールアドレス', validators=[
        InputRequired(message="メールアドレスは必須です。"),
        Email()
    ])
    submit = SubmitField('パスワードリセットリンク送信')


# ログイン
@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user and bcrypt.check_password_hash(user.password, form.password.data):
            login_user(user, remember=form.remember.data)
            return redirect(url_for('mypage'))
        else:
            flash('ユーザー名 / パスワ違ってるよん。', 'error')
    return render_template('login.html', form=form)

# マイページ
@app.route("/mypage")
@login_required
def mypage():
    print(f"ようこそ、{current_user.username}さん！")
    return render_template('mypage.html', username=current_user.username, images=[p.relative_to(BASE_DIR) for p in list(UPLOAD_FOLDER.glob('*.*'))])

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def upload():
    if request.method == 'POST':
    
        if 'file' not in request.files:
        # ファイルが選択されていない場合
            flash('ファイルがありません')
        return redirect(url_for('mypage'))

    file = request.files['image']
    # 画像として読み込み

    if file.filename == "":
        # ファイル名がついていない場合
        flash("ファイル not found")
        return redirect(url_for('mypage'))

    if file and allowed_file(img.filename):
        filename = secure_filename(file.filename)

        os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(file_path)  # 保存処理
    return redirect(url_for('uploaded_file', filename=filename))
    # GET時はアップロードフォームを表示
    return render_template('mypage.html')

@app.route('/mypage/<filename>')
@login_required
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)



# ログアウト
@app.route("/logout")
@login_required
def logout():
    logout_user()
    return redirect(url_for("login"))

# 新規登録
@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm()
    if form.validate_on_submit():
        hashed_pw = bcrypt.generate_password_hash(form.password.data).decode('utf-8')

        # メールアドレスが入力されていたら「メール認証あり」
        if form.email.data:
            user = User(username=form.username.data, email=form.email.data, password=hashed_pw)
            db.session.add(user)
            db.session.commit()
            s = URLSafeTimedSerializer(app.config['SECRET_KEY'])
            token = os.dumps(user.email, salt='email-confirm')
            link = url_for('confirm_email', token=token, _external=True)
            print(link)
            msg = Message('メール認証を完了してください', sender='あなたのメールアドレス', recipients=[user.email])
            msg.body = f'こちらのリンクをクリックして認証を完了してください)： {link}'
            mail.send(msg)

            flash('認証メールを送りました。メールを確認してください！', 'success')

        # メールアドレスなし → 通常登録
        else:
            new_user = User(username=form.username.data, password=hashed_pw)
            db.session.add(new_user)
            db.session.commit()

            flash('登録が完了しました！ログインしてください。', 'success')
        return redirect(url_for('register_complete'))

    return render_template('register.html', form=form)

@app.route('/confirm_email/<token>')
def confirm_email(token):
    if user.email_verified:
        flash('すでに認証済みです。', 'info')
    else:
        user.email_verified = True
        db.session.commit()
        flash('メール認証が完了しました！', 'success')
    return redirect(url_for('login'))

@app.route('/register_complete')
def register_complete():
    return render_template('register_complete.html')

@app.route('/reset_password', methods=['GET', 'POST'])
def reset_request():
    form = PasswordResetRequestForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user:
            token = s.dumps(user.email, salt='password-reset')
            link = url_for('reset_token', token=token, _external=True)
            msg = Message('パスワードリセット', sender='あなたのメールアドレス', recipients=[user.email])
            msg.body = f'パスワードリセットはこちら： {link}'
            mail.send(msg)
        flash('リセットリンクを送信しました。メールを確認してください。', 'success')
        return redirect(url_for('login'))
    return render_template('reset_request.html', form=form)

@app.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_token(token):

    user = User.query.filter_by(email=email).first_or_404()

    form = RegisterForm()
    if form.validate_on_submit():
        hashed_pw = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
        user.password = hashed_pw
        db.session.commit()
        flash('パスワードをリセットしました。', 'success')
    return redirect(url_for('login'))
```

### [Loginform]

```ruby:
class LoginForm(FlaskForm):
    username = StringField('ユーザー名', validators=[
        InputRequired(message="ユーザー名は必須です。")
    ])
    password = PasswordField('パスワード', validators=[
        InputRequired(message="パスワードは必須です。")
    ])
    remember = BooleanField('ログイン状態を保持する')
    submit = SubmitField('ログイン')
```