From f826e1ab198f288e15d103d6d2a149aa29a945c6 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 8 Mar 2026 12:49:50 -0400 Subject: [PATCH] test(auth): add Chain orchestration coverage for ordering and short-circuiting Signed-off-by: Josh --- tests/lib/Authentication/Login/ChainTest.php | 185 +++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 tests/lib/Authentication/Login/ChainTest.php diff --git a/tests/lib/Authentication/Login/ChainTest.php b/tests/lib/Authentication/Login/ChainTest.php new file mode 100644 index 0000000000000..5805692407bb1 --- /dev/null +++ b/tests/lib/Authentication/Login/ChainTest.php @@ -0,0 +1,185 @@ +request = $this->createMock(IRequest::class); + } + + public function testProcessBuildsExpectedOrderAndTraversesChain(): void { + $links = []; + $processed = []; + + $preLogin = new RecordingCommand('preLogin', $links, $processed); + $userDisabled = new RecordingCommand('userDisabled', $links, $processed); + $uidLogin = new RecordingCommand('uidLogin', $links, $processed); + $loggedInCheck = new RecordingCommand('loggedInCheck', $links, $processed); + $completeLogin = new RecordingCommand('completeLogin', $links, $processed); + $createSessionToken = new RecordingCommand('createSessionToken', $links, $processed); + $clearLostPasswordTokens = new RecordingCommand('clearLostPasswordTokens', $links, $processed); + $updateLastPasswordConfirm = new RecordingCommand('updateLastPasswordConfirm', $links, $processed); + $setUserTimezone = new RecordingCommand('setUserTimezone', $links, $processed); + $twoFactor = new RecordingCommand('twoFactor', $links, $processed); + $finishRememberedLogin = new RecordingCommand('finishRememberedLogin', $links, $processed); + $flowV2EphemeralSessions = new RecordingCommand('flowV2EphemeralSessions', $links, $processed); + + $chain = new Chain( + $preLogin, + $userDisabled, + $uidLogin, + $loggedInCheck, + $completeLogin, + $createSessionToken, + $clearLostPasswordTokens, + $updateLastPasswordConfirm, + $setUserTimezone, + $twoFactor, + $finishRememberedLogin, + $flowV2EphemeralSessions, + ); + + $loginData = new LoginData($this->request, 'user123', 'secret'); + $result = $chain->process($loginData); + + $this->assertTrue($result->isSuccess()); + + $this->assertSame([ + ['preLogin', 'userDisabled'], + ['userDisabled', 'uidLogin'], + ['uidLogin', 'loggedInCheck'], + ['loggedInCheck', 'completeLogin'], + ['completeLogin', 'flowV2EphemeralSessions'], + ['flowV2EphemeralSessions', 'createSessionToken'], + ['createSessionToken', 'clearLostPasswordTokens'], + ['clearLostPasswordTokens', 'updateLastPasswordConfirm'], + ['updateLastPasswordConfirm', 'setUserTimezone'], + ['setUserTimezone', 'twoFactor'], + ['twoFactor', 'finishRememberedLogin'], + ], $links); + + $this->assertSame([ + 'preLogin', + 'userDisabled', + 'uidLogin', + 'loggedInCheck', + 'completeLogin', + 'flowV2EphemeralSessions', + 'createSessionToken', + 'clearLostPasswordTokens', + 'updateLastPasswordConfirm', + 'setUserTimezone', + 'twoFactor', + 'finishRememberedLogin', + ], $processed); + } + + public function testProcessReturnsHeadFailureResult(): void { + $links = []; + $processed = []; + + $preLogin = new RecordingCommand( + 'preLogin', + $links, + $processed, + LoginResult::failure('boom'), + false // stop chain here + ); + + // Remaining commands should never process when head short-circuits + $userDisabled = new RecordingCommand('userDisabled', $links, $processed); + $uidLogin = new RecordingCommand('uidLogin', $links, $processed); + $loggedInCheck = new RecordingCommand('loggedInCheck', $links, $processed); + $completeLogin = new RecordingCommand('completeLogin', $links, $processed); + $createSessionToken = new RecordingCommand('createSessionToken', $links, $processed); + $clearLostPasswordTokens = new RecordingCommand('clearLostPasswordTokens', $links, $processed); + $updateLastPasswordConfirm = new RecordingCommand('updateLastPasswordConfirm', $links, $processed); + $setUserTimezone = new RecordingCommand('setUserTimezone', $links, $processed); + $twoFactor = new RecordingCommand('twoFactor', $links, $processed); + $finishRememberedLogin = new RecordingCommand('finishRememberedLogin', $links, $processed); + $flowV2EphemeralSessions = new RecordingCommand('flowV2EphemeralSessions', $links, $processed); + + $chain = new Chain( + $preLogin, + $userDisabled, + $uidLogin, + $loggedInCheck, + $completeLogin, + $createSessionToken, + $clearLostPasswordTokens, + $updateLastPasswordConfirm, + $setUserTimezone, + $twoFactor, + $finishRememberedLogin, + $flowV2EphemeralSessions, + ); + + $loginData = new LoginData($this->request, 'user123', 'secret'); + $result = $chain->process($loginData); + + $this->assertFalse($result->isSuccess()); + $this->assertSame('boom', $result->getErrorMessage()); + $this->assertSame(['preLogin'], $processed); + } +} + +/** + * Small test double for chain orchestration tests. + */ +class RecordingCommand extends ALoginCommand { + /** @var array */ + private array &$links; + /** @var array */ + private array &$processed; + + public function __construct( + private string $name, + array &$links, + array &$processed, + private ?LoginResult $forcedResult = null, + private bool $continueChain = true, + ) { + $this->links = &$links; + $this->processed = &$processed; + } + + public function setNext(ALoginCommand $next): ALoginCommand { + if ($next instanceof self) { + $this->links[] = [$this->name, $next->name]; + } + return parent::setNext($next); + } + + public function process(LoginData $loginData): LoginResult { + $this->processed[] = $this->name; + + if ($this->forcedResult !== null) { + return $this->forcedResult; + } + + if ($this->continueChain) { + return $this->processNextOrFinishSuccessfully($loginData); + } + + return LoginResult::success(); + } +}