Skip to content

Commit b8dea6a

Browse files
committed
tests: add test for magic login code
1 parent 80d3e6c commit b8dea6a

File tree

1 file changed

+228
-0
lines changed

1 file changed

+228
-0
lines changed

tests/Controllers/MagicLinkTest.php

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818
use CodeIgniter\I18n\Time;
1919
use CodeIgniter\Shield\Authentication\Actions\EmailActivator;
2020
use CodeIgniter\Shield\Authentication\Authenticators\Session;
21+
use CodeIgniter\Shield\Controllers\MagicLinkController;
2122
use CodeIgniter\Shield\Entities\User;
23+
use CodeIgniter\Shield\Exceptions\InvalidArgumentException;
2224
use CodeIgniter\Shield\Models\UserIdentityModel;
2325
use CodeIgniter\Shield\Models\UserModel;
2426
use CodeIgniter\Test\DatabaseTestTrait;
2527
use CodeIgniter\Test\FeatureTestTrait;
2628
use Config\Services;
29+
use ReflectionMethod;
2730
use Tests\Support\FakeUser;
2831
use Tests\Support\TestCase;
2932

@@ -210,4 +213,229 @@ public function testMagicLinkVerifyReturns404ForRobotUserAgent(): void
210213

211214
$this->get(route_to('verify-magic-link') . '?token=validtoken123');
212215
}
216+
217+
public function testMagicCodeShowLoginForm(): void
218+
{
219+
$config = config('Auth');
220+
$config->magicLoginMode = '6-numeric';
221+
Factories::injectMock('config', 'Auth', $config);
222+
223+
$this->user->createEmailIdentity([
224+
'email' => 'foo@example.com',
225+
'password' => 'secret123',
226+
]);
227+
228+
$result = $this->post('/login/magic-link', [
229+
'email' => 'foo@example.com',
230+
]);
231+
232+
// must contain a code input form
233+
$result->seeElement('#magicCode');
234+
$result->assertStatus(200);
235+
}
236+
237+
public function testMagicCodeShowsOneOFLoginForm(): void
238+
{
239+
$config = config('Auth');
240+
$config->magicLoginMode = '15-oneof';
241+
Factories::injectMock('config', 'Auth', $config);
242+
243+
$this->user->createEmailIdentity([
244+
'email' => 'foo@example.com',
245+
'password' => 'secret123',
246+
]);
247+
248+
$result = $this->post('/login/magic-link', [
249+
'email' => 'foo@example.com',
250+
]);
251+
252+
$result->assertSee('15-character code');
253+
// must contain a code input form
254+
$result->seeElement('#magicCode');
255+
$result->assertStatus(200);
256+
}
257+
258+
public function testMagicCodeEmailContainsSixDigitCode(): void
259+
{
260+
$config = config('Auth');
261+
$config->magicLoginMode = '6-numeric';
262+
Factories::injectMock('config', 'Auth', $config);
263+
264+
$this->user->createEmailIdentity([
265+
'email' => 'foo@example.com',
266+
'password' => 'secret123',
267+
]);
268+
269+
$this->post('/login/magic-link', [
270+
'email' => 'foo@example.com',
271+
]);
272+
273+
$email = service('email')->archive['body'];
274+
275+
// Should have sent an email with the link....
276+
$this->assertStringContainsString(
277+
lang('Auth.email2FAMailBody'),
278+
$email,
279+
);
280+
281+
$this->assertMatchesRegularExpression(
282+
'!<h1>[0-9]{6}</h1>!',
283+
$email,
284+
);
285+
}
286+
287+
public function testValidMagicCodeLogsUserIn(): void
288+
{
289+
$config = config('Auth');
290+
$config->magicLoginMode = '6-numeric';
291+
Factories::injectMock('config', 'Auth', $config);
292+
293+
$this->user->createEmailIdentity([
294+
'email' => 'foo@example.com',
295+
'password' => 'secret123',
296+
]);
297+
298+
$this->post('/login/magic-link', [
299+
'email' => 'foo@example.com',
300+
]);
301+
302+
// Extract sent email body & OTP code
303+
$email = service('email')->archive['body'];
304+
preg_match('/[0-9]{6}/', $email, $match);
305+
$code = $match[0];
306+
$result = $this->post('/login/verify-magic-link', [
307+
'magicCode' => $code,
308+
]);
309+
310+
$result->assertStatus(302);
311+
$result->assertRedirectTo(config('Auth')->loginRedirect());
312+
$this->assertTrue(auth()->loggedIn());
313+
}
314+
315+
public function testInvalidMagicCodeShowsError(): void
316+
{
317+
$config = config('Auth');
318+
$config->magicLoginMode = '6-numeric';
319+
Factories::injectMock('config', 'Auth', $config);
320+
321+
$this->user->createEmailIdentity([
322+
'email' => 'foo@example.com',
323+
'password' => 'secret123',
324+
]);
325+
326+
$this->post('/login/magic-link', [
327+
'email' => 'foo@example.com',
328+
]);
329+
330+
$result = $this->post('/login/verify-magic-link', [
331+
'magicCode' => '000000', // surely invalid
332+
]);
333+
334+
$result->assertStatus(302);
335+
$result->assertRedirectTo('/login/magic-link');
336+
$result->assertSessionHas('error', lang('Auth.magicTokenNotFound'));
337+
338+
$this->assertFalse(auth()->loggedIn());
339+
}
340+
341+
public function testClickableMode(): void
342+
{
343+
$result = $this->callPrivateMethod('clickable');
344+
345+
$this->assertSame('magic-link-message', $result['displayMessageView']);
346+
$this->assertSame('magic-link-email', $result['emailView']);
347+
$this->assertSame(lang('Auth.magicLinkSubject'), $result['emailSubject']);
348+
$this->assertSame(20, strlen($result['token']));
349+
}
350+
351+
public function testNumericMode(): void
352+
{
353+
$config = config('Auth');
354+
$config->magicLoginMode = '6-numeric';
355+
Factories::injectMock('config', 'Auth', $config);
356+
357+
$result = $this->callPrivateMethod();
358+
359+
$this->assertSame('magic-link-code', $result['displayMessageView']);
360+
$this->assertSame('magic-link-email-code', $result['emailView']);
361+
$this->assertSame(6, strlen($result['token']));
362+
$this->assertMatchesRegularExpression('/^[0-9]{6}$/', $result['token']);
363+
}
364+
365+
public function testAlnumMode(): void
366+
{
367+
$config = config('Auth');
368+
$config->magicLoginMode = '8-alnum';
369+
Factories::injectMock('config', 'Auth', $config);
370+
371+
$result = $this->callPrivateMethod();
372+
373+
$this->assertSame(8, strlen($result['token']));
374+
$this->assertMatchesRegularExpression('/^[A-Za-z0-9]{8}$/', $result['token']);
375+
}
376+
377+
public function testOneofMode(): void
378+
{
379+
$result = $this->callPrivateMethod('4-oneof');
380+
381+
$this->assertSame('magic-link-code', $result['displayMessageView']);
382+
$this->assertSame('magic-link-email-code', $result['emailView']);
383+
$this->assertSame(lang('Auth.magicCodeSubject'), $result['emailSubject']);
384+
385+
$token = $result['token'];
386+
387+
$this->assertSame(4, strlen($token));
388+
389+
$tokens = [];
390+
$uniqueCount = 0;
391+
392+
for ($i = 0; $i < 10; $i++) {
393+
$newToken = $this->callPrivateMethod('4-oneof')['token'];
394+
$tokens[] = $newToken;
395+
396+
if ($newToken !== $token) {
397+
$uniqueCount++;
398+
}
399+
}
400+
401+
$this->assertGreaterThan(
402+
0,
403+
$uniqueCount,
404+
'Tokens generated in loop should not all be identical.',
405+
);
406+
}
407+
408+
public function testInvalidFormatThrowsException(): void
409+
{
410+
$this->expectException(InvalidArgumentException::class);
411+
$this->callPrivateMethod('INVALID_FORMAT');
412+
}
413+
414+
public function testInvalidLengthThrowsException(): void
415+
{
416+
$this->expectException(InvalidArgumentException::class);
417+
$this->callPrivateMethod('x-numeric');
418+
}
419+
420+
public function testZeroLengthThrowsException(): void
421+
{
422+
$this->expectException(InvalidArgumentException::class);
423+
$this->callPrivateMethod('0-numeric');
424+
}
425+
426+
public function testInvalidTypeThrowsException(): void
427+
{
428+
$this->expectException(InvalidArgumentException::class);
429+
$this->callPrivateMethod('5-invalid');
430+
}
431+
432+
private function callPrivateMethod(?string $mode = null): array
433+
{
434+
$controller = new MagicLinkController();
435+
436+
$method = new ReflectionMethod($controller, 'resolveMode');
437+
$method->setAccessible(true);
438+
439+
return $method->invoke($controller, $mode);
440+
}
213441
}

0 commit comments

Comments
 (0)