@@ -89,6 +89,34 @@ public function render(): array|null
8989 }
9090}
9191
92+ class TestSecretChallenge extends Challenge
93+ {
94+ // only available once the user has set it up
95+ public static function isAvailable (User $ user , string $ mode ): bool
96+ {
97+ return $ user ->secret ('foo ' ) !== null ;
98+ }
99+
100+ public function create (): Pending |null
101+ {
102+ return null ;
103+ }
104+
105+ public function form (Pending $ pending ): Component
106+ {
107+ return new Component (
108+ component: 'k-login-test-secret-challenge-form ' ,
109+ submit: $ this ->submit (),
110+ user: $ this ->user ->email (),
111+ );
112+ }
113+
114+ public function verify (mixed $ input , Pending $ data ): bool
115+ {
116+ return true ;
117+ }
118+ }
119+
92120#[CoversClass(LoginViewController::class)]
93121class LoginViewControllerTest extends TestCase
94122{
@@ -100,6 +128,7 @@ public function setUp(): void
100128 Challenges::$ challenges ['test ' ] = TestChallenge::class;
101129 Challenges::$ challenges ['test2 ' ] = TestChallenge2::class;
102130 Challenges::$ challenges ['test-null-form ' ] = TestNullFormChallenge::class;
131+ Challenges::$ challenges ['test-secret ' ] = TestSecretChallenge::class;
103132 }
104133
105134 public function tearDown (): void
@@ -109,7 +138,8 @@ public function tearDown(): void
109138 unset(
110139 Challenges::$ challenges ['test ' ],
111140 Challenges::$ challenges ['test2 ' ],
112- Challenges::$ challenges ['test-null-form ' ]
141+ Challenges::$ challenges ['test-null-form ' ],
142+ Challenges::$ challenges ['test-secret ' ]
113143 );
114144 }
115145
@@ -329,6 +359,62 @@ public function testLoadWithChallenge(): void
329359 $ this ->assertFalse ($ props ['challenges ' ][1 ]['active ' ]);
330360 }
331361
362+ public function testLoadWithChallengeForNonExistentUser (): void
363+ {
364+ $ this ->app = $ this ->app ->clone ([
365+ 'options ' => [
366+ 'auth ' => [
367+ 'challenges ' => ['test ' , 'test2 ' ]
368+ ]
369+ ],
370+ 'users ' => [] // no users at all
371+ ]);
372+
373+ $ this ->app ->session ()->set ('kirby.challenge.email ' , 'ghost@example.com ' );
374+ $ this ->app ->session ()->set ('kirby.challenge.type ' , 'test ' );
375+ $ this ->app ->session ()->set ('kirby.challenge.mode ' , 'login ' );
376+
377+ $ props = (new LoginViewController ('challenge ' , 'test ' ))->load ()->props ();
378+
379+ $ this ->assertSame ('pending ' , $ props ['state ' ]);
380+
381+ // the form renders from a virtual user carrying the submitted email
382+ $ this ->assertSame ('k-login-test-challenge-form ' , $ props ['form ' ]['component ' ]);
383+ $ this ->assertSame ('ghost@example.com ' , $ props ['form ' ]['props ' ]['user ' ]);
384+
385+ $ this ->assertCount (2 , $ props ['challenges ' ]);
386+ $ this ->assertSame ('test ' , $ props ['challenges ' ][0 ]['type ' ]);
387+ $ this ->assertTrue ($ props ['challenges ' ][0 ]['active ' ]);
388+ $ this ->assertSame ('test2 ' , $ props ['challenges ' ][1 ]['type ' ]);
389+ $ this ->assertFalse ($ props ['challenges ' ][1 ]['active ' ]);
390+ }
391+
392+ public function testLoadExcludesUserSpecificChallengeForNonExistentUser (): void
393+ {
394+ $ this ->app = $ this ->app ->clone ([
395+ 'options ' => [
396+ 'auth ' => [
397+ 'challenges ' => ['test-secret ' , 'test2 ' ]
398+ ]
399+ ],
400+ 'users ' => []
401+ ]);
402+
403+ $ session = $ this ->app ->session ();
404+ $ session ->set ('kirby.challenge.email ' , 'ghost@example.com ' );
405+ $ session ->set ('kirby.challenge.mode ' , 'login ' );
406+ // note: no kirby.challenge.type, fallback to last enabled ('test2')
407+
408+ $ props = (new LoginViewController ('challenge ' , 'test2 ' ))->load ()->props ();
409+
410+ $ this ->assertSame ('k-login-test-challenge2-form ' , $ props ['form ' ]['component ' ]);
411+ $ this ->assertSame ('ghost@example.com ' , $ props ['form ' ]['props ' ]['user ' ]);
412+
413+ $ types = array_column ($ props ['challenges ' ], 'type ' );
414+ $ this ->assertSame (['test2 ' ], $ types );
415+ $ this ->assertNotContains ('test-secret ' , $ types );
416+ }
417+
332418 public function testLoadAppliesValueToForm (): void
333419 {
334420 // subclass that returns a value() so form props get prefilled
0 commit comments