LaravelでSNS認証ログインのテストを書いてみた

2021.01.18

見出し画像

今回は前回実装したSNS認証ログイン機能のテストを書いていこうと思います!

バージョン

PHP 7.4.8
Laravel 7.x

実装する事

今回はgoogleとfacebookどちらも書きます。ただし、実装するテストの数は5個です。

グーグルの認証画面を表示出来る, 登録出来る、とかfacebookだとemailがない場合は登録出来ないとか、既にログインしている場合は~とか。登録されているユーザーはそのままログイン~みたいなテストをしていきます。

前提

SNS認証機能は実装されている前提で進めていきます。

大まかな流れ

テストファイル作成

Googleの認証画面を表示できる

Googleアカウントでユーザー登録できる

すでに登録されているユーザーはログインできる

すでにログインしている場合はリダイレクトされる

emailの情報が無いとFacebookアカウントでユーザー登録できない

上記のような流れで進めていきます。

テストファイル作成

下記の様な感じで、コマンドで作成していきます。

php artisan make:test SnsLogin

テスト実行コマンドは以下です

./vendor/bin/phpunit --testdox tests/Feature/ファイル名.php

Googleの認証画面を表示できる

ここではボタンをクリックして、グーグルの認証画面が正しく表示されるか?というテストを行なっています。

public function test_Googleの認証画面を表示できる()
{

   $response = $this->get('/login');      1

   $response->assertStatus(200);

   $response->assertSeeText('Google Login');      2
   // URLをコール
   $response = $this->get(route('google_login_before'));    3
   // リダイレクトだったので302で対応。
   $response->assertStatus(302);
   // parse_urlでurlの連想配列を返している
   $target = parse_url($response->headers->get('location'));    4
   // リダイレクト先ドメインが求めているものと一致しているか
   $this->assertEquals('accounts.google.com', $target['host']);     5
   // パラメータの検証
   // $target['query']の中に含まれている情報を&区切りで取り出します
   $query = explode('&', $target['query']);                6
   
7  $this->assertContains('redirect_uri=' . urlencode(config('services.google.redirect')), $query);
8  $this->assertContains('client_id=' . config('services.google.client_id'), $query);

}

1,  ログインページに移動します。

2,  グーグルログインの文字がページに表示されているかを確認します。

3,  グーグルの認証画面に飛ばします。

4,  urlの中に含まれるlocation部分をparse_url()メソッドを使用して取得します。

5,  4で取得した連想配列の中身でhostの値がgoogleのものになっているかassertEquals()で確認します。

6,  コメントアウトにある通りです。$target[‘query’]の中に含まれている情報を&区切りで取り出します。

7,  assertContains()メソッドで6で取得した$queryのなかに特定の値が含まれているかを確認します。redirect_uriの値ですね。

8,  こちらも7と同じ考え方です。値の方はclient_idを指定しています。

Googleアカウントでユーザー登録できる(wizard形式)

こちらは登録処理についてのテストです。driverを設定したりとコントローラーの記載を確認する必要も出てくるかと思います。https://note.com/embed/notes/nf49ee45acdaa

上記の記事のテストになりますね。wizard形式じゃないよ!って方はいらない部分を削除して進めるかと思います!

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\User;
use Socialite;
use Mockery;
use Session;

public function SocialUser($email = '')     1
{
   // $emailの値の有無によって処理を変更しています。
2  if(!$email) {
       $email = uniqid().'@test.com';
   }

   // 初期設定のようなもの
3  Mockery::getConfiguration()->allowMockingNonExistentMethods(false);

   // ユーザーを作成
4  $this->user = Mockery::mock('Laravel\Socialite\Two\User');
   // 中身の値を設定
5  $this->user->shouldReceive('getId')
   ->andReturn(uniqid())
   ->shouldReceive('getEmail')
   ->andReturn($email)
   ->shouldReceive('getName')
   ->andReturn('testman');

   // プロバイダーを通して、ユーザーが呼ばれた時にプロバイダーを通ったユーザーとして認識させている?
 6 $this->provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');
 7 $this->provider->shouldReceive('user')->andReturn($this->user);
}

public function test_Googleアカウントでユーザー登録できる()
{
   // 細かいエラー文を表示してくれる
   // $this->withoutExceptionHandling();
   
   // コールバック状態表すモックを実行出来る設定がされているメソッドを呼び出す。
1  $this->SocialUser();             

   // driverが何を値として持っているかをwithで指定します。コントローラーの記述と合わせる。
   // $this->providerはモックで擬似的に作成したコールバックの中身になります。
2  Socialite::shouldReceive('driver')->with('google')->once()->andReturn($this->provider);

   // コールバックで戻って来た時のアクションに飛ばしている。
3  $response = $this->get(route('google_login_after')); 
   
   // viewページに戻っているのでステータスは200番を期待している。
   $response->assertStatus(200);

   //  ページ遷移している事
4  $response->assertViewIs('auth.address');

       // ポップアップ表示が表示されている事
5  $response->assertSeeText('ユーザーは入力完了です!次は住所を入力してください!');

       // セッションの値を取得
6  $all = Session::all();

   // $this->userで作成している情報が含まれているかを確認。
7  $check_user = [
       'user.name' => $this->user->getName(),
       'user.email' => $this->user->getEmail(),
       'user.password' => $all['user']->password
   ];


   // それぞれ入力した値が入っているか確認。
8  $response->assertSessionHasAll($check_user);


   // ここから登録処理()wizard形式のものと全く同じ考え方

   // 住所
   $address_data = [
       'state' => '北海道',
       'town' => 'ニセコ町',
       'street' => '富士見13-1',
       'postal_code' => '0481583',
   ];
   $second_path = route('create_tel');

   $response = $this->post($second_path, $address_data);

   // エラーメッセージがないこと
   $response->assertSessionHasNoErrors(); 

   $response->assertStatus(200);

   // ページ遷移している事
   $response->assertViewIs('auth.tel');

   // ポップアップ表示が表示されている事
   $response->assertSeeText('ユーザー、住所は入力完了です!次は電話番号を入力して登録を完了しましょう!');

   // セッションの値を取得
   $all = Session::all();

   // ユーザー情報は保存されていないので外部キーはまだありません。
   $check_address = [
       'address.state' => '北海道',
       'address.town' => 'ニセコ町',
       'address.street' => '富士見13-1',
       'address.postal_code' => '0481583',
   ];

   $response->assertSessionHasAll($check_address);


   // 電話番号
   $tel_data = [
       'number' => '566890754336',
   ];
9  $third_path = route('create_do');

10 $response = $this->post($third_path, $tel_data);

   $response->assertSessionHasNoErrors(); 

   $response->assertStatus(302);

   // リダイレクトでページ遷移している事
11 $response->assertRedirect(route('home'));

   // リダイレクトで飛んでいるので、sessionから見つけます。https://qiita.com/NakanishiTetsuhiro/items/96470e796251ba7188f5
12 $response->assertSessionHas('say','登録が完了しました!');

   // 登録したユーザーのレコードが存在するか
13 $this->assertDatabaseHas('users', ['name' => $this->user->getName()]);

   // 登録したユーザーの住所レコードが存在するか
   $this->assertDatabaseHas('addresses', ['state' => '北海道']);
 
   // 登録したユーザーの電話番号レコードが存在するか
   $this->assertDatabaseHas('tels', ['number' => '566890754336']);

}

SocialUser

ここではモック処理でグーグル認証が終わったユーザーというのを作り出しています。(自分の理解なので、誤りがある場合は是非教えていただきたいです)

1,  $email = ”の引数を与える事で引数が渡されなかった場合には$emailが空で始まります。

2,  1でも出てきた$emailが空なのかチェックし、空である場合は$emailに値を代入します。

3,   自分も完璧に理解出来てませんが、要約するとモデルのコードに変更があったりしたら教えてくれるようにしたいから(エラー)設定しますよ。みたいねイメージです。公式

4,  モックでソーシャルユーザーを作成。

5,  shouldReceive()で4で作ったユーザーがどんな情報を持っているかを決めています。

6,  モックでプロバイダーを作成している。7で使用するためにです。

7,  正直理解浅いです。6で作成したプロバイダーと5で定義したユーザーを使って返り値に5で定義したユーザーを呼び出してます。って理解してます。詳しい方居ましたら、教えていただきたいです。

Test

1,  先ほど説明してきたSocialUserメソッドを実行する。

2,  Socialiteでdriverはgoogleを指定して、モックで作成した$this->providerを返り値として指定します。

3,  登録処理なのでコールバックのアクションに飛ばします。

4,  次のページに遷移している事を確認します。assertViewIs()メソッド

5,   ポップアップ表示が表示されている事。ない人はいらないです。

6,  セッションに含まれている値を取得。

7,  $this->userで作成した値が6で取得したものの中に含まれているか確認するための配列を定義。

8,  assertSessionHasAll()を使って、6で取得したもののなかに7で定義した値が含まれているか確認する。

wizard形式ではそれ以降は同じ考え方なので省略します。後は基本的に普通のテストと同じです。

9,  登録アクションに飛ばすパスを作成。

10,  電話番号の情報を持って9で設定したパスとpostアクションでコントローラーに飛ばします。

11,  登録がうまくいき、ホームにリダイレクトで戻っている事を確認。

12,  assertSessionHas()メソッドで特定の文字が含まれているか確認。設定してない人はいらないです。リダイレクトなのでassertSee()でやってもうまくいかないことに注意

13,  データベースに$this->user->getName()と名前が一致するものがあるか確認している。

すでに登録されているユーザーはログインできる

1度登録されたユーザーはemailが一致すればそのままログイン出来る事を確認するためのテストです。

public function test_すでに登録されているユーザーはログインできる()
{
   // ログインするユーザーを作成
1  $user = factory(User::class)->create();

   // 上記で作成したユーザーがデータベースに存在しているか確認
   $this->assertDatabaseHas('users', ['name' => $user->name]);

   // ログイン状態では無い事を確認
2  $this->assertGuest();

   // 作成したユーザーのemailの情報を持ってSocialUserを実行
3  $this->SocialUser($user->email);

   // driverはgoogleとしてコールバックして来ましたよを擬似的に作っている
4  Socialite::shouldReceive('driver')->with('google')->once()->andReturn($this->provider);

   // ログインアクションに飛ばします。
5  $response = $this->get(route('google_login_after'));

   // ログインするとリダイレクト(302ステータス)でホーム画面にいることを確認しています。
6  $response->assertStatus(302)->assertRedirect('/');

   // 作成したユーザーがログイン状態にあることを確認
7  $this->assertAuthenticatedAs($user);
}

1,  create()メソッドとfactoryを使用してユーザーを作成。

2,  assertGuest()でログインしていない事を確認。

3,  コメントアウトの記載通り、1で作成した情報を持ってSocialUser()メソッドを実行します。

4,  ここ理解薄いです。driverにgoogleを設定する事でgoogleの環境を通った形にしている。返り値はgoogleを通ってきたモックで作成したプロバイダーが入っている。

5,  ログイン処理があるコールバックアクションに飛ばします。3で設定した通りemailの情報がある。

6,  正しくログイン出来た場合はリダイレクトでホームにいる事を確認。

7,  1で作成したユーザーがログイン状態になっている事を確認。

すでにログインしている場合はリダイレクトされる

これは普通にログインしているのにログイン画面に遷移出来るのはおかしい挙動なのでテストです。

public function test_すでにログインしている場合はリダイレクトされる()
{
   $user = factory(User::class)->create();    1
   $response = $this->actingAs($user);         2
   $response = $this->get('/login');      3
   $response->assertStatus(302);
   $response->assertRedirect('/');     4
}

1,  ユーザーを作成。

2,  1で作成したユーザをログイン状態に変更。

3,  ログインページに遷移しようとしてみる。

4,  リダイレクトでホームに戻されている事を確認。

Facebookはemailの情報が無いとユーザー登録できない

emailはユーザー情報として必須にしておきたいです。自分がそうだったんですけど、facebookはemailの設定が必須ではないのでその場合に対応するテストを書きます。

SocialUserNoneEmail

public function SocialUserNoneEmail()
{
1  Mockery::getConfiguration()->allowMockingNonExistentMethods(false);

2  $this->user = Mockery::mock('Laravel\Socialite\Two\User');
3  $this->user->shouldReceive('getId')
   ->andReturn(uniqid())
   ->shouldReceive('getEmail')
   ->andReturn(null)          //getEmailは設定するが値はnullにする
   ->shouldReceive('getName')
   ->andReturn('testman');

 4 $this->provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');
 5 $this->provider->shouldReceive('user')->andReturn($this->user);
}

1,  前にも説明した初期設定のようなもの。誰か詳しい方教えてください。公式

2,  モックでソーシャルユーザーを作成します。

3,  2で定義したユーザーに情報を加えますが、emailはnullにします。

4,  モックでプロバイダー環境を作ります。

5,  理解薄いとこです。4で作成したプロバイダー環境に3で作成したユーザーを返り値として実行します。

public function test_emailの情報が無いとFacebookアカウントでユーザー登録できない()
{
   // emailがnullの状態を表すメソッドを呼び出し
1  $this->SocialUserNoneEmail();

   // driverはfacebookの値を指定します。$this->providerはSocialUserNoneEmailの中で設定したもの。
2  Socialite::shouldReceive('driver')->with('facebook')->once()->andReturn($this->provider);

   // emailが存在しなかったので、コールバックで戻って来た時のアクションに飛ばしている。
3  $response = $this->get(route('facebook_login_after')); 

   // viewページに戻っているのでステータスは302番を期待している。
4  $response->assertStatus(302);

   // エラーメッセージが表示されているか確認している
5  $response->assertSessionHas('say','FacebookでログインするにはEmailが設定されている必要があります。');

}

1,  emailがnullを返すようにSocialUserNoneEmailメソッドを呼び出し。

2,  driverはfacebookを指定してプロバイダーの値を返り値に指定。

3,  ログイン処理をしている、コールバックアクションに飛ばしている。

4,  リダイレクトでviewページに戻されている事を確認。

5,  assertSessionHas()でセッションの中にメッセージが含まれているか確認しています。リダイレクト処理なのでassertSee()では確認出来ないかと思います。

まとめ

モックさんに初めましてだったので、最初は時間もかかりましたがなんとか実装できました。w

ただ、まだまだ理解が浅い部分がたくさんあるので勉強必要です。モックさん使えるようになると便利っぽいそうなんで。w

ちょっとこのままテスト勉強していきます。

今回はこんな感じで!

以上!