LoginTest.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. <?php
  2. namespace Tests\Feature\Http\Auth;
  3. use App\Http\Controllers\Auth\LoginController;
  4. use App\Http\Middleware\RejectIfAuthenticated;
  5. use App\Http\Middleware\RejectIfDemoMode;
  6. use App\Http\Middleware\RejectIfReverseProxy;
  7. use App\Http\Middleware\SkipIfAuthenticated;
  8. use App\Listeners\Authentication\FailedLoginListener;
  9. use App\Listeners\Authentication\LoginListener;
  10. use App\Models\User;
  11. use App\Notifications\FailedLogin;
  12. use App\Notifications\SignedInWithNewDevice;
  13. use Illuminate\Support\Carbon;
  14. use Illuminate\Support\Facades\Config;
  15. use Illuminate\Support\Facades\Notification;
  16. use PHPUnit\Framework\Attributes\CoversClass;
  17. use Tests\FeatureTestCase;
  18. /**
  19. * LoginTest test class
  20. */
  21. #[CoversClass(LoginController::class)]
  22. #[CoversClass(RejectIfAuthenticated::class)]
  23. #[CoversClass(RejectIfReverseProxy::class)]
  24. #[CoversClass(RejectIfDemoMode::class)]
  25. #[CoversClass(SkipIfAuthenticated::class)]
  26. #[CoversClass(LoginListener::class)]
  27. #[CoversClass(FailedLoginListener::class)]
  28. class LoginTest extends FeatureTestCase
  29. {
  30. /**
  31. * @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
  32. */
  33. protected $user;
  34. /**
  35. * @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
  36. */
  37. protected $admin;
  38. private const PASSWORD = 'password';
  39. private const WRONG_PASSWORD = 'wrong_password';
  40. /**
  41. * @test
  42. */
  43. public function setUp() : void
  44. {
  45. parent::setUp();
  46. $this->user = User::factory()->create();
  47. $this->admin = User::factory()->administrator()->create();
  48. }
  49. /**
  50. * @test
  51. */
  52. public function test_user_login_returns_success()
  53. {
  54. $response = $this->json('POST', '/user/login', [
  55. 'email' => $this->user->email,
  56. 'password' => self::PASSWORD,
  57. ])
  58. ->assertOk()
  59. ->assertJsonFragment([
  60. 'message' => 'authenticated',
  61. 'id' => $this->user->id,
  62. 'name' => $this->user->name,
  63. 'email' => $this->user->email,
  64. 'is_admin' => false,
  65. ])
  66. ->assertJsonStructure([
  67. 'preferences',
  68. ]);
  69. }
  70. /**
  71. * @test
  72. */
  73. public function test_login_send_new_device_notification()
  74. {
  75. Notification::fake();
  76. $this->json('POST', '/user/login', [
  77. 'email' => $this->user->email,
  78. 'password' => self::PASSWORD,
  79. ])->assertOk();
  80. $this->actingAs($this->user, 'web-guard')
  81. ->json('GET', '/user/logout');
  82. $this->travel(1)->minute();
  83. $this->json('POST', '/user/login', [
  84. 'email' => $this->user->email,
  85. 'password' => self::PASSWORD,
  86. ], [
  87. 'HTTP_USER_AGENT' => 'NotSymfony',
  88. ])->assertOk();
  89. Notification::assertSentTo($this->user, SignedInWithNewDevice::class);
  90. }
  91. /**
  92. * @test
  93. */
  94. public function test_login_does_not_send_new_device_notification()
  95. {
  96. Notification::fake();
  97. $this->user['preferences->notifyOnNewAuthDevice'] = 0;
  98. $this->user->save();
  99. $this->json('POST', '/user/login', [
  100. 'email' => $this->user->email,
  101. 'password' => self::PASSWORD,
  102. ])->assertOk();
  103. $this->actingAs($this->user, 'web-guard')
  104. ->json('GET', '/user/logout');
  105. $this->travel(1)->minute();
  106. $this->json('POST', '/user/login', [
  107. 'email' => $this->user->email,
  108. 'password' => self::PASSWORD,
  109. ], [
  110. 'HTTP_USER_AGENT' => 'NotSymfony',
  111. ])->assertOk();
  112. Notification::assertNothingSentTo($this->user);
  113. }
  114. /**
  115. * @test
  116. */
  117. public function test_admin_login_returns_admin_role()
  118. {
  119. $response = $this->json('POST', '/user/login', [
  120. 'email' => $this->admin->email,
  121. 'password' => self::PASSWORD,
  122. ])
  123. ->assertOk()
  124. ->assertJsonFragment([
  125. 'is_admin' => true,
  126. ]);
  127. }
  128. /**
  129. * @test
  130. *
  131. * @covers \App\Rules\CaseInsensitiveEmailExists
  132. */
  133. public function test_user_login_with_uppercased_email_returns_success()
  134. {
  135. $response = $this->json('POST', '/user/login', [
  136. 'email' => strtoupper($this->user->email),
  137. 'password' => self::PASSWORD,
  138. ])
  139. ->assertOk()
  140. ->assertJsonFragment([
  141. 'message' => 'authenticated',
  142. 'name' => $this->user->name,
  143. ])
  144. ->assertJsonStructure([
  145. 'message',
  146. 'name',
  147. 'preferences',
  148. ]);
  149. }
  150. /**
  151. * @test
  152. *
  153. * @covers \App\Http\Middleware\SkipIfAuthenticated
  154. */
  155. public function test_user_login_already_authenticated_is_rejected()
  156. {
  157. $response = $this->json('POST', '/user/login', [
  158. 'email' => $this->user->email,
  159. 'password' => self::PASSWORD,
  160. ]);
  161. $response = $this->actingAs($this->user, 'web-guard')
  162. ->json('POST', '/user/login', [
  163. 'email' => $this->user->email,
  164. 'password' => self::PASSWORD,
  165. ])
  166. ->assertStatus(400)
  167. ->assertJsonStructure([
  168. 'message',
  169. ]);
  170. }
  171. /**
  172. * @test
  173. */
  174. public function test_user_login_with_missing_data_returns_validation_error()
  175. {
  176. $response = $this->json('POST', '/user/login', [
  177. 'email' => '',
  178. 'password' => '',
  179. ])
  180. ->assertStatus(422)
  181. ->assertJsonValidationErrors([
  182. 'email',
  183. 'password',
  184. ]);
  185. }
  186. /**
  187. * @test
  188. *
  189. * @covers \App\Exceptions\Handler
  190. */
  191. public function test_user_login_with_invalid_credentials_returns_unauthorized()
  192. {
  193. $response = $this->json('POST', '/user/login', [
  194. 'email' => $this->user->email,
  195. 'password' => self::WRONG_PASSWORD,
  196. ])
  197. ->assertStatus(401)
  198. ->assertJson([
  199. 'message' => 'unauthorized',
  200. ]);
  201. }
  202. /**
  203. * @test
  204. */
  205. public function test_login_with_invalid_credentials_send_failed_login_notification()
  206. {
  207. Notification::fake();
  208. $this->json('POST', '/user/login', [
  209. 'email' => $this->user->email,
  210. 'password' => self::WRONG_PASSWORD,
  211. ])->assertStatus(401);
  212. Notification::assertSentTo($this->user, FailedLogin::class);
  213. }
  214. /**
  215. * @test
  216. */
  217. public function test_login_with_invalid_credentials_does_not_send_new_device_notification()
  218. {
  219. Notification::fake();
  220. $this->user['preferences->notifyOnFailedLogin'] = 0;
  221. $this->user->save();
  222. $this->json('POST', '/user/login', [
  223. 'email' => $this->user->email,
  224. 'password' => self::WRONG_PASSWORD,
  225. ])->assertStatus(401);
  226. Notification::assertNothingSentTo($this->user);
  227. }
  228. /**
  229. * @test
  230. */
  231. public function test_too_many_login_attempts_with_invalid_credentials_returns_too_many_request_error()
  232. {
  233. $throttle = 8;
  234. Config::set('auth.throttle.login', $throttle);
  235. $post = [
  236. 'email' => $this->user->email,
  237. 'password' => self::WRONG_PASSWORD,
  238. ];
  239. for ($i = 0; $i < $throttle - 1; $i++) {
  240. $this->json('POST', '/user/login', $post);
  241. }
  242. $this->json('POST', '/user/login', $post)
  243. ->assertUnauthorized();
  244. $this->json('POST', '/user/login', $post)
  245. ->assertStatus(429);
  246. }
  247. /**
  248. * @test
  249. */
  250. public function test_user_logout_returns_validation_success()
  251. {
  252. $response = $this->json('POST', '/user/login', [
  253. 'email' => $this->user->email,
  254. 'password' => self::PASSWORD,
  255. ]);
  256. $response = $this->actingAs($this->user, 'web-guard')
  257. ->json('GET', '/user/logout')
  258. ->assertOk()
  259. ->assertExactJson([
  260. 'message' => 'signed out',
  261. ]);
  262. }
  263. /**
  264. * @test
  265. *
  266. * @covers \App\Http\Middleware\KickOutInactiveUser
  267. * @covers \App\Http\Middleware\LogUserLastSeen
  268. */
  269. public function test_user_logout_after_inactivity_returns_teapot()
  270. {
  271. // Set the autolock period to 1 minute
  272. $this->user['preferences->kickUserAfter'] = 1;
  273. $this->user->save();
  274. $response = $this->json('POST', '/user/login', [
  275. 'email' => $this->user->email,
  276. 'password' => self::PASSWORD,
  277. ]);
  278. // Ping a protected endpoint to log last_seen_at time
  279. $response = $this->actingAs($this->user, 'api-guard')
  280. ->json('GET', '/api/v1/twofaccounts');
  281. $this->travelTo(Carbon::now()->addMinutes(2));
  282. $response = $this->actingAs($this->user, 'api-guard')
  283. ->json('GET', '/api/v1/twofaccounts')
  284. ->assertStatus(418);
  285. }
  286. }