20200621のGitに関する記事は4件です。

Git, GitHubの利用方法

はじめに

投稿者の私自身、業務で未だにGit, GitHubを利用したことがないのですが、
個人でWebアプリを開発し始め、ソースのバージョン管理をGitHubで管理することにした。
今後、開発中のWebアプリ開発を自身の所属する会社のメンバーにも展開しようと思っています。
その時の為に、個人でissueを上げたり、issueに対応するためのbranchを作ったり、pull-request
をしてmargeしたりしたので記録として残しておきます。

前提

  • GitHubのアカウントは作成済み
  • GitHub上にプロジェクトのリポジトリは作成済み
  • 自分のマシンにGitがインストール済み

環境

  • Windows10 Home
  • Git操作(Git Bash)

参考サイト

操作方法(Git)

  • リモートリポジトリ(GitHub)からmasterブランチをローカルに取得する WS000000.JPG GitHubのリポジトリ画面の[Clone or download]を押下してリポジトリURLを取得し
    下記コマンドを実行する。
    予めローカルリポジトリを作成するディレクトリを作成し移動しておくこと。
    下記コマンドを実行すると、初回にGitHubのログインフォームが表示され、
    その後OpenSSH認証?の為、GitHubユーザー、パスワードが順番に聞かれる。
git clone <リポジトリURL>

cloneが正常終了すると所定のディレクトリにローカルリポジトリが作成されます。
さぁ、ここからチーム開発を実施しましょう。

  • ブランチを作成する
    issueに対応する時など、masterブランチで作業せず、対応するissue用のブランチを作成して作業をします。
git branch <ブランチ名>
  • ブランチを切り替える
    ブランチを作成しただけでは、まだ作業ディレクトリの内容は元のブランチままです。 以下のコマンドを実行し、作業ディレクトリの内容を指定のブランチの内容にしましょう。
git checkout <ブランチ名>

上記ブランチ作成とブランチ切り替えを同時に行うには以下のコマンドです。

git checkout -b <ブランチ名>
  • 作業ディレクトリがどのブランチなのかを確認する
    masterブランチとissueに対応するブランチなど、複数のブランチがある場合、現在の作業ディレクトリがどのブランチのものなのかを確認するコマンドです。
    ブランチ一覧が表示され、ブランチ名の横に*が表示されているものが現在のブランチになります。
git branch
  • ブランチを削除する
    issue対応で作成したブランチなどで、対応が完了しmasterへのmargeも行われたものは削除します。 margeが行われていないブランチを削除しようとすると警告が表示され、強制的に削除する方法が提案されます。
    但し、このブランチ削除はあくまでローカルリポジトリからの削除となります。
git branch -d <ブランチ名>
  • ローカルでのブランチ削除をサーバーリポジトリに反映する
    上記で記載したgit branch -d <ブランチ名>を行っただけでは、サーバーリポジトリに保持している同ブランチは削除されていません。
    それを削除するには以下のコマンドを実行します。
git push --delete origin <ブランチ名>
  • リモートリポジトリ(GitHub)から最新の情報を取得する
    他のメンバーが修正しリモートリポジトリに反映した情報は、自分のローカルリポジトリに自動では反映されません。
    下記コマンドで最新の情報を取得します。
git fetch
  • リモートリポジトリから取得した最新情報をローカルのブランチに反映する
    上記git fetchしただけでは、まだ自分のローカルリポジトリは変更されていません。 取得した最新情報をローカルリポジトリに反映するには、以下のコマンドを実行します。 この際、取得した最新の情報に含まれるソースファイルと、自分がローカルリポジトリで修正していたファイルが同じ場合、コンフリクト(競合)が発生します。 その場合はマージが必要となりますが、今回はコンフリクトが発生していないと仮定します。
git pull

ここから以下の操作については、リポジトリ内のファイルに対して行う操作となります。
その前にgitでのファイルステータスについて記載しておきます。
下記画像は、参考サイト:GitBookの【2.2 Git の基本 - 変更内容のリポジトリへの記録】から抜粋したものとなります。
画像を見るとファイルには4つの状態があることが分かります。
このファイル状態を理解した上で以降の操作説明を見たほうがイメージがつくと思います。

ステータス 状態説明
Untracked リポジトリで管理されていない
Unmodified 変更されていない
Modified 変更されている(ローカルリポジトリにコミットされているファイルと比較して)
Steged ローカルリポジトリへのコミット待ち

WS000001.JPG

  • ファイルの状態を確認する
    どのファイルが変更(modified)されているか、そもそもリポジトリで管理されていない(untracked)かなどがわかります
git status
  • ファイルを管理対象として登録する
    新規追加されたファイルはまだリポジトリで管理されていない状態(untracked)なので、 まずはリポジトリに登録しましょう
git add <ファイル名>

ステータス=untrackedのファイルを登録するために上記コマンドを行いましたが、それ以外にも作業ディレクトリでプログラムを修正したものをステージ状態にするためなどに使用します。
上記コマンドを行うと、ファイルの状態は"Staged"になります

  • 変更をローカルリポジトリにコミットする
    状態=Stagedのファイルをローカルリポジトリにコミットする
--ファイル名を指定してコミット
git commit -m "コミットコメント" <file1> <file2>

--Staged状態のファイル全てをコミット
git commit -m "コミットコメント" -a
  • 変更を取り消してリポジトリのファイルで上書きする
    状態=modifiedのファイルをリポジトリの状態に戻したい場合に使用します。
git checkout <ファイル名>

git add後(状態=staged)のファイルを取り消すには、下記コマンドで一旦staged → modifiedにした上で取り消します。

git reset <ファイル名>
  • リモートリポジトリ(GitHub)に反映する
    ローカルリポジトリにコミットされている情報をリモートリポジトリ(GitHub)に反映する。
git push origin <リポジトリ名>

GitHub画面での操作

GitHub画面でのissue作成、pull-request作成、レビュアーのレビュー&マージについて記載します。
Gitコマンドでも可能だと思いますが、現時点ではまだ実施していないので...

  • issueを作成する
    開発プロジェクトでのToDoリストのような使い方をしていることも多いのではないでしょうか。
    プロジェクトのissuesを選択し、New issueから新規issueを追加します。

WS000003.JPG

追加後
WS000004.JPG

  • プルリクエスト作成する
    issueに対応したブランチをGitHubにpush後、そのブランチの変更内容をmasterブランチにmargeしてもらうため、プルリクエストを作成します。 GitHub上でマージしてもらいたいブランチに切り替え、[New Pull request]を押下してプルリクエストを作成します。

WS000005.JPG

  • プルリクエストの内容をレビューしmasterブランチにマージする
    プルリクエストが作成されると[Pull requests]に表示される。 プルリクエスト画面のコミットコメント[下記画像だと"supports iss3"]を押下すると、 変更内容が表示されるので、ソースレビューを実施します。 問題なければマージを実施します。 今回のケースではマージ対象のソースファイルにコンフリクト(競合)が発生していないため、 ボタンぽちーでマージされます。 コンフリクト発生時の対応は後日更新予定とします。。。

WS000007.JPG

以上となります。
私自身、未だ業務でGit及びGitHubを利用していないため、自分なりの理解で記載しました。
もし間違いなどがあれば指摘いただけると幸いです。
なお、各種Git CLIですが、オプションも色々とありますが今回は記載していません。
今後そのへんも記載できたらいいなぁと思っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Github:Windowsで複数のアカウント情報をキャッシュ(HTTPS経由)

前提条件

1. アカウント情報のキャッシュ設定

  • Windows Credential Manager(資格情報マネージャ)にアカウント情報を保存するように設定。
git config --global credential.helper wincred
  • 同一ドメインで複数アカウントを保存できるように設定。
git config --global credential.useHttpPath true

2. リモートリポジトリの設定

GithubでHTTPSのリポジトリのURLをコピーして、以下のコマンドにてリモートリポジトリを設定する。

2-1. HTTPS経由でのclone(新規にリポジトリ取得)

git clone https://github.com/XXXX/YYYY
  • ログインダイアログが表示されるのでログイン情報を入力。

2-2. 既存のローカルリポジトリにpush先リモートリポジトリ登録

git remote add [任意のリモート名] https://github.com/XXXX/YYYY
  • リモートへのpushを実行すると、ログインダイアログが表示されるのでログイン情報を入力。

3. 保存したアカウント情報の確認

  • 「コントロールパネル > ユーザーアカウント > 資格情報マネージャー」を開く。

image.png

  • Githubドメインの該当アカウントを開く。

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データサイエンス100本ノック(構造化データ加工編)の環境構築(Windows10)

はじめに

一般社団法人データサイエンティスト協会が、構造化データの加工を実践的に学べる無料の学習環境「データサイエンス100本ノック(構造化データ加工編)」GitHubに公開しました。
本記事は、初学者の方でも無料の学習環境を構築できるように、導入手順の詳細を記載しました。
(構築する実行環境は下図になります。)
dss_structure.png

前提条件(Windows10)

  1. Docker Desktop for Windows
    ※起動しない場合は、Hyper-Vが「無効」になっている可能性があるので「有効」に設定変更。
  2. Git for Windows
    ※インストール時のデフォルト設定である改行コード変更を「無効」に設定変更。
> git config --global core.autocrlf input

環境構築

学習環境用のディレクトリ(今回はdss)を作成し、100本ノックのリポジトリをクローンする。
その後、100本ノックのディレクトリ内に移動し、docker-composeコマンドを使ってコンテナを作成する。(10分前後の時間がかかる。)
※環境構築中にポップアップの警告が表示される場合、DockerのローカルPCに対するアクセス権限がない可能性があるため「Share it」を選択してアクセス権限を付与する。

> mkdir dss
> cd dss
> git clone https://github.com/The-Japan-DataScientist-Society/100knocks-preprocess.git
> cd 100knocks-preprocess
> docker-compose up -d --build

起動済みのコンテナを確認し、「dss-notebook」「dss-postgres」の出力を確認できれば環境構築が成功。

> docker ps

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
b35f99d4148a        dss-notebook        "tini -g -- start-no…"   23 seconds ago      Up 22 seconds       0.0.0.0:8888->8888/tcp   dss-notebook
3cb559c7f66d        dss-postgres        "docker-entrypoint.s…"   27 seconds ago      Up 26 seconds       0.0.0.0:5432->5432/tcp   dss-postgres

使い方

ブラウザで下記のURLにアクセスすると、構築したJupyterの環境にアクセスできる。

http://localhost:8888

workディレクトリ配下に、構造化データ加工の演習問題の.ipynbファイルがある。
必要ライブラリのインポートや加工前のデータ取得は、最初のセルに記述済み。
演習問題に適した処理を、空欄のセルに入力して実行し、学習を進める。
dss_jupyter_work.png
演習問題の解答は、work/answerディレクトリ内に.ipynbファイルがある。
そのため、演習問題のファイルで回答した処理の正否を確認しながら作業可能。
dss_jupyter_answer.png

学習環境の停止・起動

下記のコマンドで、構築した環境を停止可能。

> docker-compose stop

また、2回目以降に起動する場合は、下記のコマンドで起動可能。

> docker-compose start

補足事項

構築した環境のレスポンスが遅い場合

Docker Desktop for WindowsのSettingsでResourcesでMemoryの値を変更してください。
推奨は、4.00GB以上です。
docker_settings_resources.png

8888ポートが使用されている場合

もし、ローカルホストの8888ポートを他の開発環境(LAMPなど)で利用している場合は、docker-compose.ymlを下記のように変更(notebookのportsの値を変更)することで対応可能。

docker-compose.yml
  notebook:
    ports:
      - "888:8888"

上記の場合、下記のURLでアクセス可能になる。

http://localhost:888

まとめ

Windows10環境における、データサイエンス100本ノック(構造化データ加工編)の環境構築手順を記載いたしました。
上記の手順で不明点や疑問点等がありましたら、コメントいただけますと幸いです。

参考リンク

データサイエンス100本ノックのガイド

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails Tutorial 第6版 学習まとめ 第9章

概要

この記事は私の知識をより確実なものにするためにRailsチュートリアル解説記事を書くことで理解を深め
勉強の一環としています。稀にとんでもない内容や間違えた内容が書いてあるかもしれませんので
ご了承ください。
できればそれとなく教えてくれますと幸いです・・・

出典
Railsチュートリアル第6版

この章でやること

・ユーザーの任意でログイン情報を記憶しておき、ブラウザを再起動してもログインできる機能を追加する。

Remember me機能

↑でも述べた通りブラウザを閉じてもログインを保持する機能を実装する(Remember me)
トピックブランチを作成して作業を始める。

記憶トークンと暗号化

これからの作業や作成物がなかなか難しいので先回り知識を確認する。

・トークンとは
コンピュータが使うパスワードのようなもの。
パスワードは人間が作成して人間が管理するがトークンはコンピュータが作成してコンピュータが管理する。

・永続的cookiesと一時セッションについて
前章で作成した一時セッションはsessionメソッドを使って、cookiesにブラウザ終了時が有効期限のセッションを作成した。
今回はcookiesメソッドを使って期限が無限(正確には20年ほど)のセッションを作成する。
cookiesメソッドではsessionメソッドと違い情報が保護されないかつ、セッションハイジャックと呼ばれる攻撃の的になるため
ユーザーIDと記憶トークンをセットでcookiesに保存し、ハッシュ化したトークンをDBに保存することで
セキュリティを確保する。

・具体的にどういう処理で実装するのか
1. cookiesメソッドを使って暗号化したユーザーIDと記憶トークンをブラウザに保存
2. DBにはハッシュ化した記憶トークン(記憶ダイジェスト)を同時に保存しておく。
3. 次回アクセス時はブラウザに保存されている期限付きcookiesのトークンとDBに保存された記憶ダイジェストを比較して
ログイン処理を自動で行う。

大まかに内容を確認したので
さっそくDBに記憶ダイジェスト(remember_digest)を追加する。
rails g migration add_remember_digest_to_users remember_digest:string
以前説明した通りファイル名末尾にto_usersとつけることでusersテーブルにカラムを追加すると勝手に認識してくれる。

remember_digestはユーザーが読み出せる内容ではないのでインデックスを追加する必要もない。
そのため、このままマイグレートする。

記憶トークンを作成するにあたり、何を使うかだが
長くてランダムな文字列が好ましい。
SecureRandomモジュールのurlsafe_base64メソッドが用途的にマッチしているのでこれを使っていく。
このメソッドは64種の文字を用いて、長さ22のランダム文字列を返すメソッド。
記憶トークンはこのメソッドを使って自動生成することにする。

>> SecureRandom.urlsafe_base64
=> "Rr2i4cNWOwhtDeVA4bnT2g"
>> SecureRandom.urlsafe_base64
=> "pQ86_IsKILLv4AxAnx9iHA"

パスワードと同じくトークンはほかのユーザ⁻と重複しても問題ないが、一意なものを使うことで
ユーザーIDとトークンの両方が奪われでもしない限りはセッションハイジャックなどにもつながらない。

新規でトークンを作成する(生成する)メソッドをuserモデルに定義していく。

  def User.new_token
    SecureRandom.urlsafe_base64
  end

このメソッドもユーザオブジェクトは不要のためクラスメソッドとして定義する。

つぎにrememberメソッドを作成していく。
このメソッドではDBにトークンに対応した記憶ダイジェストを保存する。
DBにremember_digestは存在するがremember_tokenは存在しない。
DBに保存したいのはダイジェストのみだがユーザーオブジェクトに紐づいたトークンに対するダイジェストを保存したいので
トークン属性にもアクセスしたい。
つまりパスワードの時と同じく仮想の属性としてトークンが必要になる。
パスワード実装時はhas_secure_passwordが自動生成してくれたが、今回は
attr_accessorを使ってremember_tokenを作成する。

user.rb
class User < ApplicationRecord
  attr_accessor :remember_token

  # before_save { self.email.downcase! }
  # has_secure_password
  # VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  # validates :name, presence: true, length:{maximum: 50}
  # validates :email, presence: true, length:{maximum: 255},
  #                   format: {with: VALID_EMAIL_REGEX},uniqueness: true
  # validates :password, presence: true, length:{minimum: 6}

  # def User.digest(string)
  #   cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
  #                                                 BCrypt::Engine::cost
  #   BCrypt::Password.create(string, cost: cost)
  # end

  # def User.new_token
  #   SecureRandom.urlsafe_base64
  # end

  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest,User.digest(remember_token))
  end
end

rememberメソッドの1行目の
self.remember_token = User.new_token
selfを書かないとremember_tokenというローカル変数が作成されてしまうためここでは必須。

ここではパスワードにアクセスできないためupdate_attributeはバリデーションを素通りさせるために使っている。

演習

1.しっかり動く。
remember_tokenは22文字のランダム生成文字列
remember_digestはそれらのハッシュ化文字列になっていることが見てわかる。

>> user.remember
   (0.1ms)  begin transaction
  User Update (2.4ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2020-06-17 14:30:27.202627"], ["remember_digest", "$2a$12$u8cnBDCQX85e9gBHbQWWNeJq.mxKq8hhpt/FSYMtm2rI2ljWIhRLi"], ["id", 1]]
   (6.1ms)  commit transaction
=> true
>> user.remember_token
=> "lZaXgeF42y5XeP-EEPzstw"
>> user.remember_digest
=> "$2a$12$u8cnBDCQX85e9gBHbQWWNeJq.mxKq8hhpt/FSYMtm2rI2ljWIhRLi"
>> 

2.どちらも動作は同じ
class << selfを使うとendの間まではすべてクラスメソッドとして定義される。
ここでのselfキーワードはインスタンスオブジェクトではなくUserクラスそのものを表しているため認識違いに注意。

ログイン状態の保持

永続cookiesに保存するためにはcookiesメソッドを使う。
sessionとおなじくハッシュとして使える。

cookiesはvalue(値)とexpires(有効期限)を持っていて

cookies[:remember_token] =  { value: remember_token, expires: 20.years.from_now.utc }

とすることでcookies[:remember_token]に有効期限が20年のremember_tokenの値を保存できる。
また有効期限が20年というのはよく使われるのでRailsには専用メソッドが追加されていて

cookies.permanent[:remember_token] = remember_token

としても同じ効果になる。

またユーザーIDも永続cookiesに保存するがそのまま保存するとIDがそのまま保存されてしまい、
cookiesがどのような形式で保存されているのか、攻撃者にバレバレになってしまうため、
暗号化する。
暗号化には署名付きcookieを使う。

cookies.signed[:user_id] = user.id
これで安全に暗号化して保存できる。

もちろんユーザーIDも永続cookiesとして保存する必要があるのでpermanentメソッドをつないで使う。
cookies.permanent.signed[:user_id] = user.id

このようにユーザーIDと記憶トークンをセットでcookiesに入れることで
ユーザーがログアウトするとログインできなくなる(DBのダイジェストが削除されるため)

最後にブラウザに保存されたトークンとDBのダイジェストを比較する方法だが
secure_passwordのソースコードを一部パクり

BCrypt::Password.new(remember_digest) == remember_token
このようなコードを使う。
このコードだとremember_digestとremember_tokenを直接比較している。
実は、Bcryptで==演算子が再定義されており、このコードは
BCrypt::Password.new(remember_digest).is_password?(remember_token)
という動作をしている。
これを利用して記憶ダイジェストと記憶トークンを比較するauthenticated?メソッドを定義する。

  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

ここでのremember_digestはself.remember_digestと同じである。
DBの記憶ダイジェストと引数に渡した記憶トークンを比較して正しければtrueを返す

さっそくsessions_controllerのログイン処理部にremember処理を追加する。

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user&.authenticate(params[:session][:password])
      log_in(user)
      remember user
      redirect_to user
    else
      flash.now[:danger] = "Invalid email/password combination"
      render 'new'
    end
  end

ここではrememberヘルパーメソッドを使う。(まだ定義していない)

↓rememberヘルパーメソッド
rb:sessions_helper.rb
def remember(user)
user.remember
cookies.signed.permanent[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end

わかりづらいので補足。
Userモデルに定義したrememberメソッドで
user.rb
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest,User.digest(remember_token))
end

ユーザーオブジェクトに対して記憶トークンと記憶ダイジェストを生成する。

sessions_helperに定義したrememberメソッドで
1.Userモデルのrememberメソッドを呼び出し、トークンとダイジェストを生成。
2.cookiesにユーザーIDを暗号化して保存
3.cookiesに1で生成したトークンを保存

の流れ。
メソッド名が被っているので注意。

これでユーザー情報をcookiesに安全に保存できるようになったがログイン状態を見て
動的にレイアウトを変更するために使っていたcurrent_userメソッドが一時セッションにしか
対応していないため修正する。

  def current_user #現在ログイン中のユーザ⁻オブジェクトを返す
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user &. authenticated?(cookies[remember_token])
        log_in user
        @current_user = user
      end
    end
  end

・user_idというローカル変数を使うことでコードの重複を減らしている。
・ブラウザを開いた初回実行時は永続cookiesの処理が行われ、同時にログイン処理も行われるため
ブラウザを閉じるまでは@current_userにユーザーが保存されている。

現時点だとログアウト処理(永続cookies)を削除する方法がないため
ログアウトできない。
(すでにあるログアウトアクションだと一時セッションを削除するだけなので、永続cookiesから情報を取り出して
自動でログインしてしまうためログアウトができない。)

演習

1.ある。
image.png

2.動く。

>> user = User.first
   (1.1ms)  SELECT sqlite_version(*)
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "take", email: "take.webengineer@gmail.com", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-18 15:18:53", password_digest: [FILTERED], remember_digest: "$2a$12$tAZFCVr39lkPONLS4/7zneYgOE5pcYDM2kX6F1yKew2...">
>> user.remember
   (0.1ms)  begin transaction
  User Update (2.8ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2020-06-18 15:23:21.357804"], ["remember_digest", "$2a$12$h3K3aZSBmXB7wGkNdsBrS.2/UaawMQ199DGMvTDU8upvvOKCzbeba"], ["id", 1]]
   (10.3ms)  commit transaction
=> true
>> user.authenticated?(user.remember_token)
=> true

ユーザーを忘れる

現在、永続cookiesを削除していないため、ログアウトできない。
この問題を解決するためにforgetメソッドを定義する。
このメソッドで記憶ダイジェストをnilにする。
さらにsessions_helperにもforgetメソッドを定義することで
こちらではcookiesに保存されたユーザーIDと記憶トークンも削除する。

  def forget(user) #永続セッションを削除・記憶ダイジェストもリセット
    user.forget
    cookies.delete[:user_id]
    cookies.delete[:remember_token]
  end
  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end

一応ログアウト処理の流れをサラッとおさらい。
1. ユーザオブジェクトに保存された記憶ダイジェストをnilにする(Userモデルのforgetメソッド)
2. cookiesのユーザーIDと記憶トークンを削除(sessions_helperのforgetメソッド)
3. 一時セッションのユーザIDを削除
4. カレントユーザ(現在ログイン中のユーザー)をnilにする。

演習

1.削除されている。(実行画面は省略)なおChromeだと以前と同じく一時セッションが残ってしまっているがアプリの動作上は
問題ない。

2つの目立たないバグ

現時点で二つのバグが残っている。かなり面倒なので詳細に説明していく。

1つ目のバグ
複数のタブでログインしていて、タブ1でログアウトした後、タブ2でもログアウトした時。
タブ1内でlog_outメソッドを使ってログアウトした後だとcurrent_userがnilになっている。
この状態でもう一度ログアウトしようとすると削除するcookieが見つからないため失敗する。

2つ目のバグ
別ブラウザで(Chrome、Firefoxなど)ログインしている時。
1. Firefoxでログアウトするとremember_digestがnilになる。
2. Chromeを閉じると一時セッションは削除されるがcookiesは残るため、ユーザーIDからユーザーを見つけることができてしまう。
3. user.authenticated?メソッドで比較するremember_digestがFirefox側で既に削除されているため
比較対象がなくなりエラーが発生する。

このバグを修正するためにまずはバグをキャッチするテストを書き
それを修正するコードを書く。

delete logout_path
これをログインテストのログアウト処理後にもう一度挿入することで2回ログアウトを再現する。

このテストをパスさせるためには
ログイン中だけログアウト処理を行うようにすればいい。

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end

2つ目のバグについて、テストで異なるブラウザ環境を再現するのは難しいため、
Userモデルのremember_digestに関してのテストにとどめる。
具体的にはremember_digestがnilの時にはfalseを返すことをテストする。

  test "authenticated? should return false for a user with nil digest" do 
    assert_not @user.authenticated?('')
  end

テストをパスさせるためにauthenticated?メソッドを改良する
rb
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

remember_digestがnilの場合には即座にreturnキーワードでfalseを返し処理を終了させる。

これで2つのバグが修正される。

演習

1.エラーが発生する。(実行画面は省略。)
2.これもエラーが発生する(EdgeとChrome)
3.確認済み。

[Remember me]チェックボックス

次はRememberme機能には欠かせない、チェックボックスを実装する(チェックした時だけ記憶する機能)

      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

ラベルの内側に配置する理由に関してはhttps://html-coding.co.jp/annex/dictionary/html/label/
このサイトがわかりやすい
つまりラベルに指定されている者のどこをクリックしてもチェックボックスを押したのと同じ動作にできる。

CSSで形を整えたら準備完了。
チェックボックスでparams[:session][:remember_me]に1 or 0が入るようになったので
1の時に記憶するようにすればいい。

三項演算子を使って実装すると

  params[:session][:remember_me] == '1' ? remember(user) : forget(user)

remember userの行をこれに差し替えるだけ
ちなみに三項演算子は

  条件文 ? trueの時の処理 : falseの時の処理

という形式で書ける。
ちなみにparamsの数値はすべて文字列で記録されているため条件文の1は''で囲わないと
必ずfalse分が実行されてrememberできなくなるので注意

演習

1.↑でも注意書きを書いたがparamsの条件は'1'としないとうまくいかない。うまくいけばcookiesに値が保存されて
うまく動く。

2.

>> hungry = true
=> true
>> hungry ? puts("I'm hungry now") : puts("I'm not hungry now")
I'm hungry now
=> nil

[Remember me]のテスト

Remembermeが実装できたのでテストも作成していく。

[Remember me]ボックスをテストする

直前の三項演算子で実装したparams[:session][:remember_me] == '1' ? remember(user) : forget(user)
という部分はプログラムを触っている人だと1(真)0(偽)ということで
params[:session][:remember_me] ? remember(user) : forget(user)
と書きたくなるが、チェックボックスはあくまで1と0を返す。
Rubyでは1と0は真偽値ではなくどちらもtrueとして扱われるためこのように書くのは間違いになる。
このようなミスをキャッチできるテストを書かなければならない。

ユーザーを記憶するためにはログインが必要になる。今までは逐一postメソッドを使ってparamsハッシュを送っていたが
毎回やるのはさすがに手間なのでログイン用のメソッドを定義する。
log_inメソッドとの混乱を防ぐためにlog_in_asメソッドとして定義する。

test_helper
class ActiveSupport::TestCase
  # Run tests in parallel with specified workers
  parallelize(workers: :number_of_processors)

  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all
  include ApplicationHelper
  # Add more helper methods to be used by all tests here...
  def is_logged_in?
    !session[:user_id].nil?
  end

  def log_in_as(user)
    session[:user_id] = user.id
  end


end

class ActionDispatch::IntegrationTest
  def log_in_as(user, password: 'password', remember_me: '1')
    post login_path, params:{ session: { email: user.email,
                                                password: password,
                                                remember_me: remember_me}}
  end
end

log_in_asメソッドをActionDispatch::IntegrationTestとActiveSupport::TestCaseで2回別々に定義しているのは
統合テストではsessionメソッドを使えないから。
そのため統合テストでは代わりにpostリクエストを使ってログインしている。

どちらのテストも同じ名前にすることで統合テストでも単体テストでもログインしたい時には何も気にせずlog_in_asメソッド
を呼べばいい。

log_in_asメソッドを定義したのでRemember_meのテストを実装する。

  test "login with remembering" do 
    log_in_as(@user, remember_me: '1')
    assert_not_empty cookies[:remember_token]
  end

  test "login without remembering" do
    log_in_as(@user, remember_me: '1')
    delete logout_path
    log_in_as(@user, remember_me: '0')
    assert_empty cookies[:remember_token]
  end

log_in_as(@user, remember_me:'1')
デフォルト値を設定しているため本来不要だが比較しやすいようremember_me属性も入力している。

演習

1.↑の統合テストでは仮想属性remember_tokenにアクセスできないためcookiesが空でないことだけをテストしていたが
assignsメソッドを使うことで直前にアクセスしたアクションのインスタンス変数を取得できる。
上のテストの例ではlog_in_asメソッド内でsessions_controllerのcreateアクションにアクセスしているため
createアクションで定義されたインスタンス変数の値をシンボルを使って読みだすことができる。
具体的には
現在createアクションで使われているのはuserというローカル変数なのでこれに@をつけて@userという
インスタンス変数に変えてしまうことでassignsメソッドが読み出せるようになる。
あとはテストでassigns(:user)とすることで@userを読み出せる。

users_login_test.rb
  test "login with remembering" do 
    log_in_as(@user, remember_me: '1')
    assert_equal cookies[:remember_token] , assigns(:user).remember_token
  end
sessions_controller.rb
  def create
    @user = User.find_by(email: params[:session][:email].downcase)
    if @user&.authenticate(params[:session][:password])
      log_in(@user)
      params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
      redirect_to @user
    else
      flash.now[:danger] = "Invalid email/password combination"
      render 'new'
    end
  end

[Remember me]をテストする

sessions_helperにログイン処理やセッション関連のヘルパーメソッドを実装してきたが
current_userメソッドの分岐処理に関してテストが行われていない。
その証拠に何も関連性のない適当な文字列を代入してもテストがパスしてしまう。

GREENのテスト↓

sessions_helper.rb
  def current_user #現在ログイン中のユーザ⁻オブジェクトを返す
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      テストしてないから日本語も許される。
      user = User.find_by(id: user_id)
      if user &.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end

これはまずいのでsessions_helperようのテストファイルを作成する。

sessions_helper_test.rb
require 'test_helper'
class SessionsHelperTest < ActionView::TestCase
  def setup
    @user = users(:michael)
    remember(@user)
  end

  test "current_user returns right user when session is nil" do
    assert_equal @user, current_user
    assert is_logged_in?
  end

  test "current_user returns nil when remember digest is wrong" do
    @user.update_attribute(:remember_digest, User.digest(User.new_token))
    assert_nil current_user
  end
end

1つ目のテストでは記憶したユーザとcurrent_userが同じかどうか確かめ、ログインしているかも確かめている。
こうすることでテストがcookiesにユーザーIDが存在した際に中身の処理が動いているか確認できる。

2つ目のテストではremember_digestを書き換えることでrememberメソッドで記録したremember_tokenと対応させない
ようにした際にcurrent_userが期待通りnilを返す、つまりauthenticated?メソッドが
正しく動作しているかテストしている。

また、補足としてassert_equalメソッドは第1引数と第2引数を入れ替えても動作するが
書き方は第1引数に期待値、第2引数に実際の値と書かなければならないことに注意
このように書かなければエラーが発生した際にログの表示がかみ合わなくなってしまう。

そしてこの段階ではもちろんテストは通らない。

入れておいた全く関係のない文を削除することでテストがパスする。
これでcurrent_userのどの分岐もテストできるようになったため回帰バグもキャッチできる。

演習

1.記憶トークンと記憶ダイジェストが正しく対応していなくともuserが存在するだけでif文を通過してしまうため
返り値がnilでなくなってしまう。つまりテストも失敗する。

 FAIL["test_current_user_returns_nil_when_remember_digest_is_wrong", #<Minitest::Reporters::Suite:0x000055b13fd67928 @name="SessionsHelperTest">, 1.4066297989993473]
 test_current_user_returns_nil_when_remember_digest_is_wrong#SessionsHelperTest (1.41s)
        Expected #<User id: 762146111, name: "Michael Example", email: "michael@example.com", created_at: "2020-06-20 15:38:57", updated_at: "2020-06-20 15:38:58", password_digest: [FILTERED], remember_digest: "$2a$04$uoeG1eJEySynSb.wI.vyOewe9s9TJsSoI9vtXNYJxrv..."> to be nil.
        test/helpers/sessions_helper_test.rb:15:in `block in <class:SessionsHelperTest>'

↑current_userの返り値がnilになることが期待されているのに対し、userオブジェクトが返ってしまっていることを
エラーとして出力している。

前の章へ

次の章へ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む