<?php namespace Tests; use App\Events; use App\Mail\ForgotPassword; use App\Models\Player; use App\Models\User; use App\Services\Facades\Option; use Cache; use Event; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\URL; use Illuminate\Support\Str; use Laravel\Socialite\AbstractUser; use Laravel\Socialite\Facades\Socialite; use Vectorface\Whip\Whip; class AuthControllerTest extends TestCase { use DatabaseTransactions; protected function setUp(): void { parent::setUp(); app()->instance(\App\Rules\Captcha::class, new class() extends \App\Rules\Captcha { public function __construct(\GuzzleHttp\Client $client = null) { } public function passes($attribute, $value) { return true; } }); } public function testLogin() { $this->get('/auth/login')->assertSee('Log in'); option(['recaptcha_sitekey' => 'key']); $this->get('/auth/login')->assertSee('recaptcha.net'); } public function testHandleLogin() { Event::fake(); $user = factory(User::class)->create(); $user->changePassword('12345678'); $player = factory(Player::class)->create(['uid' => $user->uid]); // Should return a warning if `identification` is empty $this->postJson('/auth/login')->assertJsonValidationErrors('identification'); // Should return a warning if `password` is empty $this->postJson( '/auth/login', ['identification' => $user->email] )->assertJsonValidationErrors('password'); // Should return a warning if length of `password` is lower than 6 $this->postJson( '/auth/login', [ 'identification' => $user->email, 'password' => '123', ])->assertJsonValidationErrors('password'); // Should return a warning if length of `password` is greater than 32 $this->postJson( '/auth/login', [ 'identification' => $user->email, 'password' => Str::random(80), ])->assertJsonValidationErrors('password'); $this->flushSession(); // Should return a warning if user isn't existed $this->postJson( '/auth/login', [ 'identification' => 'nope@nope.net', 'password' => '12345678', ])->assertJson([ 'code' => 2, 'message' => trans('auth.validation.user'), ]); Event::assertDispatched('auth.login.attempt', function ($event, $payload) use ($user) { $this->assertEquals('nope@nope.net', $payload[0]); $this->assertEquals('12345678', $payload[1]); $this->assertEquals('email', $payload[2]); return true; }); Event::assertNotDispatched('auth.login.ready'); Event::assertNotDispatched('auth.login.succeeded'); Event::assertNotDispatched('auth.login.failed'); $this->flushSession(); Event::fake(); $whip = new Whip(); $ip = $whip->getValidIpAddress(); $loginFailsCacheKey = sha1('login_fails_'.$ip); // Logging in should be failed if password is wrong $this->postJson( '/auth/login', [ 'identification' => $user->email, 'password' => 'wrong-password', ])->assertJson( [ 'code' => 1, 'message' => trans('auth.validation.password'), 'data' => ['login_fails' => 1], ] ); $this->assertTrue(Cache::has($loginFailsCacheKey)); Event::assertDispatched('auth.login.attempt', function ($event, $payload) use ($user) { $this->assertEquals($user->email, $payload[0]); $this->assertEquals('wrong-password', $payload[1]); $this->assertEquals('email', $payload[2]); return true; }); Event::assertDispatched('auth.login.ready', function ($event, $payload) use ($user) { $this->assertEquals($user->uid, $payload[0]->uid); return true; }); Event::assertDispatched('auth.login.failed', function ($event, $payload) use ($user) { $this->assertEquals($user->uid, $payload[0]->uid); $this->assertEquals(1, $payload[1]); return true; }); $this->flushSession(); // Should check captcha if there are too many fails Cache::put($loginFailsCacheKey, 4); $this->postJson( '/auth/login', [ 'identification' => $user->email, 'password' => '12345678', ])->assertJsonValidationErrors('captcha'); Cache::flush(); $this->flushSession(); // Should clean the `login_fails` session if logged in successfully Cache::put($loginFailsCacheKey, 1); $this->postJson('/auth/login', [ 'identification' => $user->email, 'password' => '12345678', ])->assertJson( [ 'code' => 0, 'message' => trans('auth.login.success'), ] ); $this->assertFalse(Cache::has($loginFailsCacheKey)); Event::assertDispatched('auth.login.ready', function ($event, $payload) use ($user) { $this->assertEquals($user->uid, $payload[0]->uid); return true; }); Event::assertDispatched('auth.login.succeeded', function ($event, $payload) use ($user) { $this->assertEquals($user->uid, $payload[0]->uid); return true; }); Event::assertDispatched(Events\UserTryToLogin::class); Event::assertDispatched(Events\UserLoggedIn::class); Cache::flush(); $this->flushSession(); // Logged in should be in success if logged in with player name auth()->logout(); $this->postJson( '/auth/login', [ 'identification' => $player->name, 'password' => '12345678', ] )->assertJson( [ 'code' => 0, 'message' => trans('auth.login.success'), ] ); $this->assertAuthenticated(); } public function testLogout() { Event::fake(); $user = factory(User::class)->create(); $this->actingAs($user)->postJson('/auth/logout')->assertJson( [ 'code' => 0, 'message' => trans('auth.logout.success'), ] ); $this->assertGuest(); Event::assertDispatched('auth.logout.before', function ($event, $payload) use ($user) { $this->assertEquals($user->uid, $payload[0]->uid); return true; }); Event::assertDispatched('auth.logout.after', function ($event, $payload) use ($user) { $this->assertEquals($user->uid, $payload[0]->uid); return true; }); } public function testRegister() { $this->get('/auth/register')->assertSee('Register'); option(['user_can_register' => false]); $this->get('/auth/register')->assertSee(e(trans('auth.register.close'))); } public function testHandleRegister() { Event::fake(); $whip = new Whip(); $ip = $whip->getValidIpAddress(); // Should return a warning if `email` is empty $this->postJson('/auth/register')->assertJsonValidationErrors('email'); // Should return a warning if `email` is invalid $this->postJson( '/auth/register', ['email' => 'not_an_email'] )->assertJsonValidationErrors('email'); // An existed user $existedUser = factory(User::class)->create(); $this->postJson( '/auth/register', ['email' => $existedUser->email] )->assertJsonValidationErrors('email'); // Should return a warning if `password` is empty $this->postJson( '/auth/register', ['email' => 'a@b.c'] )->assertJsonValidationErrors('password'); // Should return a warning if length of `password` is lower than 8 $this->postJson( '/auth/register', [ 'email' => 'a@b.c', 'password' => '1', ] )->assertJsonValidationErrors('password'); // Should return a warning if length of `password` is greater than 32 $this->postJson( '/auth/register', [ 'email' => 'a@b.c', 'password' => Str::random(33), ] )->assertJsonValidationErrors('password'); // The register_with_player_name option is set to true by default. // Should return a warning if `player_name` is empty $this->postJson( '/auth/register', [ 'email' => 'a@b.c', 'password' => '12345678', 'captcha' => 'a', ] )->assertJsonValidationErrors('player_name'); // Should return a warning if `player_name` is invalid option(['player_name_rule' => 'official']); $this->postJson( '/auth/register', [ 'email' => 'a@b.c', 'password' => '12345678', 'player_name' => '角色名', 'captcha' => 'a', ] )->assertJsonValidationErrors('player_name'); // Should return a warning if `player_name` is too long $this->postJson( '/auth/register', [ 'email' => 'a@b.c', 'password' => '12345678', 'player_name' => Str::random(option('player_name_length_max') + 10), 'captcha' => 'a', ] )->assertJsonValidationErrors('player_name'); // Existed player $player = factory(Player::class)->create(); $this->postJson( '/auth/register', [ 'email' => 'a@b.c', 'password' => '12345678', 'player_name' => $player->name, 'captcha' => 'a', ] )->assertJson([ 'code' => 2, 'message' => trans('user.player.add.repeated'), ]); $this->assertNull(User::where('email', 'a@b.c')->first()); Event::assertDispatched('auth.registration.attempt', function ($event, $payload) { [$data] = $payload; $this->assertEquals('a@b.c', $data['email']); $this->assertEquals('12345678', $data['password']); return true; }); Event::assertNotDispatched('auth.registration.ready'); Event::assertNotDispatched('auth.registration.completed'); option(['register_with_player_name' => false]); // Should return a warning if `nickname` is empty $this->postJson( '/auth/register', [ 'email' => 'a@b.c', 'password' => '12345678', 'captcha' => 'a', ] )->assertJsonValidationErrors('nickname'); // Should return a warning if `nickname` is too long $this->postJson( '/auth/register', [ 'email' => 'a@b.c', 'password' => '12345678', 'nickname' => Str::random(256), 'captcha' => 'a', ] )->assertJsonValidationErrors('nickname'); // Should return a warning if `captcha` is empty $this->postJson( '/auth/register', [ 'email' => 'a@b.c', 'password' => '12345678', 'nickname' => 'nickname', ] )->assertJsonValidationErrors('captcha'); // Should be forbidden if registering is closed Option::set('user_can_register', false); $this->postJson( '/auth/register', [ 'email' => 'a@b.c', 'password' => '12345678', 'nickname' => 'nickname', 'captcha' => 'a', ] )->assertJson([ 'code' => 7, 'message' => trans('auth.register.close'), ]); option(['user_can_register' => true, 'regs_per_ip' => -1]); $this->postJson( '/auth/register', [ 'email' => 'a@b.c', 'password' => '12345678', 'nickname' => 'nickname', 'captcha' => 'a', ] )->assertJson([ 'code' => 7, 'message' => trans('auth.register.max', ['regs' => option('regs_per_ip')]), ]); Option::set('regs_per_ip', 100); // Database should be updated if succeeded $response = $this->postJson( '/auth/register', [ 'email' => 'a@b.c', 'password' => '12345678', 'nickname' => 'nickname', 'captcha' => 'a', ] ); $newUser = User::where('email', 'a@b.c')->first(); $response->assertJson([ 'code' => 0, 'message' => trans('auth.register.success'), ]); $this->assertTrue($newUser->verifyPassword('12345678')); $this->assertDatabaseHas('users', [ 'email' => 'a@b.c', 'nickname' => 'nickname', 'score' => option('user_initial_score'), 'ip' => $ip, 'permission' => User::NORMAL, ]); $this->assertAuthenticated(); Event::assertDispatched('auth.registration.attempt', function ($event, $payload) { [$data] = $payload; $this->assertEquals('a@b.c', $data['email']); $this->assertEquals('12345678', $data['password']); return true; }); Event::assertDispatched('auth.registration.ready', function ($event, $payload) { [$data] = $payload; $this->assertEquals('a@b.c', $data['email']); $this->assertEquals('12345678', $data['password']); return true; }); Event::assertDispatched('auth.registration.completed', function ($event, $payload) { [$user] = $payload; $this->assertEquals('a@b.c', $user->email); $this->assertGreaterThan(0, $user->uid); return true; }); Event::assertDispatched(Events\UserRegistered::class); Event::assertDispatched('auth.login.ready', function ($event, $payload) { [$user] = $payload; $this->assertEquals('a@b.c', $user->email); return true; }); Event::assertDispatched('auth.login.succeeded', function ($event, $payload) { [$user] = $payload; $this->assertEquals('a@b.c', $user->email); return true; }); // Require player name option(['register_with_player_name' => true]); auth()->logout(); $this->postJson( '/auth/register', [ 'email' => 'abc@test.org', 'password' => '12345678', 'player_name' => 'name', 'captcha' => 'a', ] )->assertJson(['code' => 0]); $this->assertNotNull(Player::where('player', 'name')); } public function testForgot() { $this->get('/auth/forgot')->assertSee('Forgot Password'); config(['mail.driver' => '']); $this->get('/auth/forgot')->assertSee(trans('auth.forgot.disabled')); } public function testHandleForgot() { Event::fake(); Mail::fake(); // Should be forbidden if "forgot password" is closed config(['mail.driver' => '']); $this->postJson('/auth/forgot', [ 'email' => 'nope@nope.net', 'captcha' => 'a', ])->assertJson([ 'code' => 1, 'message' => trans('auth.forgot.disabled'), ]); config(['mail.driver' => 'smtp']); $whip = new Whip(); $ip = $whip->getValidIpAddress(); $lastMailCacheKey = sha1('last_mail_'.$ip); // Should be forbidden if sending email frequently Cache::put($lastMailCacheKey, time()); $this->postJson('/auth/forgot', [ 'email' => 'nope@nope.net', 'captcha' => 'a', ])->assertJson([ 'code' => 2, 'message' => trans('auth.forgot.frequent-mail'), ]); Event::assertDispatched('auth.forgot.attempt', function ($event, $payload) { $this->assertEquals('nope@nope.net', $payload[0]); return true; }); Event::assertNotDispatched('auth.forgot.ready'); Event::assertNotDispatched('auth.forgot.sent'); Event::assertNotDispatched('auth.forgot.sent'); Cache::flush(); $this->flushSession(); // Should return a warning if user is not existed $user = factory(User::class)->create(); $this->withSession(['phrase' => 'a'])->postJson('/auth/forgot', [ 'email' => 'nope@nope.net', 'captcha' => 'a', ])->assertJson([ 'code' => 1, 'message' => trans('auth.forgot.unregistered'), ]); Event::fake(); $this->postJson('/auth/forgot', [ 'email' => $user->email, 'captcha' => 'a', ])->assertJson([ 'code' => 0, 'message' => trans('auth.forgot.success'), ]); $this->assertTrue(Cache::has($lastMailCacheKey)); Cache::flush(); Event::assertDispatched('auth.forgot.attempt', function ($event, $payload) use ($user) { $this->assertEquals($user->email, $payload[0]); return true; }); Event::assertDispatched('auth.forgot.ready', function ($event, $payload) use ($user) { $this->assertEquals($user->email, $payload[0]->email); return true; }); Mail::assertSent(ForgotPassword::class, function ($mail) use ($user) { return $mail->hasTo($user->email); }); Event::assertDispatched('auth.forgot.sent', function ($event, $payload) use ($user) { $this->assertEquals($user->email, $payload[0]->email); $this->assertStringContainsString('auth/reset/'.$user->uid, $payload[1]); return true; }); // Should handle exception when sending email Event::fake(); Mail::shouldReceive('to') ->once() ->andThrow(new \Mockery\Exception('A fake exception.')); $this->flushSession(); $this->withSession(['phrase' => 'a']) ->postJson('/auth/forgot', [ 'email' => $user->email, 'captcha' => 'a', ])->assertJson([ 'code' => 2, 'message' => trans('auth.forgot.failed', ['msg' => 'A fake exception.']), ]); Event::assertNotDispatched('auth.forgot.sent'); Event::assertDispatched('auth.forgot.failed', function ($event, $payload) use ($user) { $this->assertEquals($user->email, $payload[0]->email); $this->assertStringContainsString('auth/reset/'.$user->uid, $payload[1]); return true; }); // Addition: Mailable test $site_name = option_localized('site_name'); $mailable = new ForgotPassword('url'); $mailable->build(); $this->assertTrue($mailable->hasFrom(config('mail.username'), $site_name)); $this->assertEquals(trans('auth.forgot.mail.title', ['sitename' => $site_name]), $mailable->subject); $this->assertEquals('mails.password-reset', $mailable->view); } public function testReset() { $user = factory(User::class)->create(); $this->get( URL::temporarySignedRoute('auth.reset', now()->addHour(), ['uid' => $user->uid]) )->assertSuccessful(); } public function testHandleReset() { Event::fake(); $user = factory(User::class)->create(); $url = URL::temporarySignedRoute('auth.reset', now()->addHour(), ['uid' => $user->uid]); // Should return a warning if `password` is empty $this->postJson($url)->assertJsonValidationErrors('password'); // Should return a warning if `password` is too short $this->postJson($url, ['password' => '123']) ->assertJsonValidationErrors('password'); // Should return a warning if `password` is too long $this->postJson($url, ['password' => Str::random(33)]) ->assertJsonValidationErrors('password'); // Success $this->postJson($url, ['password' => '12345678'])->assertJson([ 'code' => 0, 'message' => trans('auth.reset.success'), ]); $user->refresh(); $this->assertTrue($user->verifyPassword('12345678')); Event::assertDispatched('auth.reset.before', function ($event, $payload) use ($user) { $this->assertEquals($user->uid, $payload[0]->uid); $this->assertEquals('12345678', $payload[1]); return true; }); Event::assertDispatched('auth.reset.after', function ($event, $payload) use ($user) { $this->assertEquals($user->uid, $payload[0]->uid); $this->assertEquals('12345678', $payload[1]); return true; }); } public function testCaptcha() { $this->mock(\Gregwar\Captcha\CaptchaBuilder::class, function ($mock) { $mock->shouldReceive('build')->with(100, 34)->once(); $mock->shouldReceive('getPhrase')->once()->andReturn('くみこ'); $mock->shouldReceive('output')->once()->andReturn(''); }); $this->get('/auth/captcha') ->assertSuccessful() ->assertHeader('Content-Type', 'image/jpeg') ->assertHeader('Cache-Control', 'no-store, private') ->assertSessionHas('captcha', 'くみこ'); } public function testFillEmail() { $user = factory(User::class)->create(['email' => '']); $other = factory(User::class)->create(); $this->actingAs($user)->post('/auth/bind')->assertRedirect('/'); $this->actingAs($user)->post('/auth/bind', ['email' => 'a'])->assertRedirect('/'); $this->actingAs($user)->post('/auth/bind', ['email' => $other->email])->assertRedirect('/'); $this->actingAs($user)->post('/auth/bind', ['email' => 'a@b.c'])->assertRedirect('/user'); $user->refresh(); $this->assertEquals('a@b.c', $user->email); } public function testVerify() { $url = URL::signedRoute('auth.verify', ['uid' => 1]); // Should be forbidden if account verification is disabled option(['require_verification' => false]); $this->get($url)->assertSee(trans('user.verification.disabled')); option(['require_verification' => true]); $this->get($url)->assertSee(trans('auth.verify.invalid')); $user = factory(User::class)->create(); $url = URL::signedRoute('auth.verify', ['uid' => $user->uid]); $this->get($url)->assertSee(trans('auth.verify.invalid')); $user = factory(User::class)->create(['verified' => false]); $url = URL::signedRoute('auth.verify', ['uid' => $user->uid]); $this->get($url)->assertViewIs('auth.verify'); $this->assertEquals(1, User::find($user->uid)->verified); } public function testApiLogin() { $user = factory(User::class)->create(); $user->changePassword('12345678'); $this->postJson('/api/auth/login')->assertJson(['token' => false]); $token = $this->postJson('/api/auth/login', [ 'email' => $user->email, 'password' => '12345678', ])->decodeResponseJson('token'); $this->assertTrue(is_string($token)); $this->postJson('/api/auth/login', [ 'email' => $user->email, 'password' => '123456789', ])->assertJson(['token' => '']); } public function testApiLogout() { $user = factory(User::class)->create(); $user->changePassword('12345678'); $token = $this->postJson('/api/auth/login', [ 'email' => $user->email, 'password' => '12345678', ])->decodeResponseJson('token'); $this->post('/api/auth/logout', [], [ 'Authorization' => "Bearer $token", ])->assertNoContent(); } public function testApiRefresh() { $user = factory(User::class)->create(); $user->changePassword('12345678'); $token = $this->postJson('/api/auth/login', [ 'email' => $user->email, 'password' => '12345678', ])->decodeResponseJson('token'); $token = $this->postJson('/api/auth/refresh', [], [ 'Authorization' => "Bearer $token", ])->decodeResponseJson('token'); $this->assertTrue(is_string($token)); } public function testOAuthLogin() { Socialite::shouldReceive('driver') ->with('github') ->once() ->andReturn(new class() { public function redirect() { return redirect('/'); } }); $this->get('/auth/login/github')->assertRedirect(); } public function testOAuthCallback() { Event::fake(); $whip = new Whip(); $ip = $whip->getValidIpAddress(); Socialite::shouldReceive('driver') ->with('github') ->times(3) ->andReturn( new class() { public function user() { return new class() extends AbstractUser { }; } }, new class() { public function user() { return new class() extends AbstractUser { public $email = 'a@b.c'; public $nickname = 'abc'; }; } }, new class() { public function user() { return new class() extends AbstractUser { public $email = 'a@b.c'; public $nickname = 'abc'; }; } } ); $this->get('/auth/login/github/callback') ->assertStatus(500) ->assertSee('Unsupported'); $this->get('/auth/login/github/callback')->assertRedirect('/user'); $this->assertDatabaseHas('users', [ 'email' => 'a@b.c', 'nickname' => 'abc', 'score' => option('user_initial_score'), 'avatar' => 0, 'ip' => $ip, 'permission' => User::NORMAL, 'verified' => true, ]); $this->assertAuthenticated(); Event::assertDispatched('auth.registration.completed', function ($event, $payload) { [$user] = $payload; $this->assertEquals('a@b.c', $user->email); $this->assertEquals(1, $user->uid); return true; }); Event::assertDispatched('auth.login.ready', function ($event, $payload) { [$user] = $payload; $this->assertEquals('a@b.c', $user->email); return true; }); Event::assertDispatched('auth.login.succeeded', function ($event, $payload) { [$user] = $payload; $this->assertEquals('a@b.c', $user->email); return true; }); auth()->logout(); $this->assertGuest(); Event::fake(); $this->get('/auth/login/github/callback')->assertRedirect('/user'); $this->assertAuthenticated(); Event::assertNotDispatched('auth.registration.completed'); Event::assertDispatched('auth.login.ready', function ($event, $payload) { [$user] = $payload; $this->assertEquals('a@b.c', $user->email); return true; }); Event::assertDispatched('auth.login.succeeded', function ($event, $payload) { [$user] = $payload; $this->assertEquals('a@b.c', $user->email); return true; }); } }