44
55use Illuminate \Foundation \Testing \RefreshDatabase ;
66use Illuminate \Support \Facades \Mail ;
7- use Illuminate \Support \Facades \URL ;
87use Ssntpl \Neev \Mail \LoginUsingLink ;
98use Ssntpl \Neev \Models \User ;
9+ use Ssntpl \Neev \Services \MagicLink \MagicLinkManager ;
1010use Ssntpl \Neev \Tests \TestCase ;
1111use Ssntpl \Neev \Tests \Traits \WithNeevConfig ;
1212
@@ -20,6 +20,14 @@ private function createUser(array $state = []): User
2020 return User::factory ()->create ($ state );
2121 }
2222
23+ /**
24+ * Generate a magic link for the user and return its plain token.
25+ */
26+ private function magicLinkToken (User $ user ): array
27+ {
28+ return app (MagicLinkManager::class)->forWeb ($ user );
29+ }
30+
2331 // -----------------------------------------------------------------
2432 // POST /neev/sendLoginLink
2533 // -----------------------------------------------------------------
@@ -64,17 +72,13 @@ public function test_send_login_link_returns_401_for_non_existent_email(): void
6472 // GET /neev/loginUsingLink
6573 // -----------------------------------------------------------------
6674
67- public function test_login_using_link_with_valid_signature_returns_token (): void
75+ public function test_login_using_link_with_valid_token_returns_token (): void
6876 {
6977 $ user = $ this ->createUser ();
7078
71- $ signedUrl = URL ::temporarySignedRoute (
72- 'loginUsingLink ' ,
73- now ()->addMinutes (60 ),
74- ['id ' => $ user ->id ]
75- );
79+ $ link = $ this ->magicLinkToken ($ user );
7680
77- $ response = $ this ->getJson ($ signedUrl );
81+ $ response = $ this ->getJson (' /neev/loginUsingLink?token= ' . $ link [ ' token ' ] );
7882
7983 $ response ->assertOk ();
8084 $ response ->assertJson ([
@@ -90,13 +94,9 @@ public function test_login_using_link_returns_email_verified_status(): void
9094 $ user = $ this ->createUser ();
9195
9296 // Email is verified by default from factory
93- $ signedUrl = URL ::temporarySignedRoute (
94- 'loginUsingLink ' ,
95- now ()->addMinutes (60 ),
96- ['id ' => $ user ->id ]
97- );
97+ $ link = $ this ->magicLinkToken ($ user );
9898
99- $ response = $ this ->getJson ($ signedUrl );
99+ $ response = $ this ->getJson (' /neev/loginUsingLink?token= ' . $ link [ ' token ' ] );
100100
101101 $ response ->assertOk ();
102102 $ response ->assertJson ([
@@ -105,50 +105,63 @@ public function test_login_using_link_returns_email_verified_status(): void
105105 ]);
106106 }
107107
108- public function test_login_using_link_with_invalid_signature_returns_403 (): void
108+ public function test_login_using_link_with_invalid_token_returns_403 (): void
109109 {
110- $ user = $ this ->createUser ();
111-
112- // Build a URL without a valid signature
113- $ url = route ('loginUsingLink ' , ['id ' => $ user ->id ]) . '?signature=invalidsignature ' ;
110+ $ this ->createUser ();
114111
115- $ response = $ this ->getJson ($ url );
112+ $ response = $ this ->getJson (' /neev/loginUsingLink?token=not-a-real-token ' );
116113
117114 $ response ->assertStatus (403 );
118115 $ response ->assertJson ([
119116 'message ' => 'Invalid or expired verification link. ' ,
120117 ]);
121118 }
122119
123- public function test_login_using_link_with_expired_signature_returns_403 (): void
120+ public function test_login_using_link_with_expired_token_returns_403 (): void
124121 {
125122 $ user = $ this ->createUser ();
126123
127- $ signedUrl = URL ::temporarySignedRoute (
128- 'loginUsingLink ' ,
129- now ()->subMinutes (1 ), // Already expired
130- ['id ' => $ user ->id ]
131- );
124+ $ link = $ this ->magicLinkToken ($ user );
125+ $ link ['model ' ]->forceFill (['expires_at ' => now ()->subMinute ()])->save ();
132126
133- $ response = $ this ->getJson ($ signedUrl );
127+ $ response = $ this ->getJson (' /neev/loginUsingLink?token= ' . $ link [ ' token ' ] );
134128
135129 $ response ->assertStatus (403 );
136130 $ response ->assertJson ([
137131 'message ' => 'Invalid or expired verification link. ' ,
138132 ]);
139133 }
140134
135+ public function test_login_using_link_is_single_use (): void
136+ {
137+ $ user = $ this ->createUser ();
138+
139+ $ link = $ this ->magicLinkToken ($ user );
140+
141+ $ this ->getJson ('/neev/loginUsingLink?token= ' . $ link ['token ' ])->assertOk ();
142+
143+ // Replay: the token row was deleted on consumption.
144+ $ this ->getJson ('/neev/loginUsingLink?token= ' . $ link ['token ' ])->assertStatus (403 );
145+ }
146+
147+ public function test_generating_a_new_link_invalidates_the_previous_one (): void
148+ {
149+ $ user = $ this ->createUser ();
150+
151+ $ old = $ this ->magicLinkToken ($ user );
152+ $ new = $ this ->magicLinkToken ($ user );
153+
154+ $ this ->getJson ('/neev/loginUsingLink?token= ' . $ old ['token ' ])->assertStatus (403 );
155+ $ this ->getJson ('/neev/loginUsingLink?token= ' . $ new ['token ' ])->assertOk ();
156+ }
157+
141158 public function test_login_using_link_creates_login_attempt (): void
142159 {
143160 $ user = $ this ->createUser ();
144161
145- $ signedUrl = URL ::temporarySignedRoute (
146- 'loginUsingLink ' ,
147- now ()->addMinutes (60 ),
148- ['id ' => $ user ->id ]
149- );
162+ $ link = $ this ->magicLinkToken ($ user );
150163
151- $ this ->getJson ($ signedUrl );
164+ $ this ->getJson (' /neev/loginUsingLink?token= ' . $ link [ ' token ' ] );
152165
153166 $ this ->assertDatabaseHas ('login_attempts ' , [
154167 'user_id ' => $ user ->id ,
@@ -161,13 +174,9 @@ public function test_login_using_link_creates_access_token(): void
161174 {
162175 $ user = $ this ->createUser ();
163176
164- $ signedUrl = URL ::temporarySignedRoute (
165- 'loginUsingLink ' ,
166- now ()->addMinutes (60 ),
167- ['id ' => $ user ->id ]
168- );
177+ $ link = $ this ->magicLinkToken ($ user );
169178
170- $ this ->getJson ($ signedUrl );
179+ $ this ->getJson (' /neev/loginUsingLink?token= ' . $ link [ ' token ' ] );
171180
172181 $ this ->assertDatabaseHas ('access_tokens ' , [
173182 'user_id ' => $ user ->id ,
@@ -179,13 +188,9 @@ public function test_login_using_link_for_inactive_user_returns_validation_error
179188 {
180189 $ user = $ this ->createUser (['active ' => false ]);
181190
182- $ signedUrl = URL ::temporarySignedRoute (
183- 'loginUsingLink ' ,
184- now ()->addMinutes (60 ),
185- ['id ' => $ user ->id ]
186- );
191+ $ link = $ this ->magicLinkToken ($ user );
187192
188- $ response = $ this ->getJson ($ signedUrl );
193+ $ response = $ this ->getJson (' /neev/loginUsingLink?token= ' . $ link [ ' token ' ] );
189194
190195 $ response ->assertUnprocessable ();
191196 $ response ->assertJsonValidationErrors (['email ' ]);
0 commit comments