diff --git a/app/Commands/LoginCommand.php b/app/Commands/LoginCommand.php index 7442ff3..7080354 100644 --- a/app/Commands/LoginCommand.php +++ b/app/Commands/LoginCommand.php @@ -89,14 +89,18 @@ private function loginWithPersonalAccessToken(CredentialStore $credentials, Flar } try { - $response = Http::withToken($token)->get("{$urlResolver->getApiBaseUrl()}/me"); + // Without acceptJson, an invalid token gets redirected to the HTML + // login page and reads as a successful response. + $response = Http::withToken($token)->acceptJson()->get("{$urlResolver->getApiBaseUrl()}/me"); } catch (ConnectionException) { $this->error('Could not connect to Flare. Please check your internet connection.'); return self::FAILURE; } - if (! $response->successful()) { + $email = $response->json('email'); + + if (! $response->successful() || ! is_string($email) || $email === '') { $this->error('Invalid API token.'); return self::FAILURE; @@ -104,7 +108,7 @@ private function loginWithPersonalAccessToken(CredentialStore $credentials, Flar $credentials->setToken($token); - return $this->reportSuccess($response->json('email', 'unknown'), $urlResolver); + return $this->reportSuccess($email, $urlResolver); } private function loginWithBrowser( @@ -137,7 +141,7 @@ private function loginWithBrowser( private function fetchEmail(TokenRecord $record, FlareUrlResolver $urlResolver): ?string { try { - $response = Http::withToken($record->accessToken)->get("{$urlResolver->getApiBaseUrl()}/me"); + $response = Http::withToken($record->accessToken)->acceptJson()->get("{$urlResolver->getApiBaseUrl()}/me"); } catch (ConnectionException) { return null; } diff --git a/tests/Feature/LoginCommandTest.php b/tests/Feature/LoginCommandTest.php index b12cea0..7de6d05 100644 --- a/tests/Feature/LoginCommandTest.php +++ b/tests/Feature/LoginCommandTest.php @@ -77,6 +77,38 @@ function loginRecord(array $overrides = []): TokenRecord expect($this->store->getToken())->toBe('valid-token-123'); }); +it('rejects a --token when /me responds with a non-JSON page', function () { + // An invalid token used to get redirected to the HTML login page, which the + // HTTP client follows to a 200 — and login reported success as "unknown". + Http::fake([ + 'flareapp.io/api/me' => Http::response('Log in', 200, [ + 'Content-Type' => 'text/html', + ]), + ]); + + $this->artisan('login --token') + ->expectsQuestion('Enter your Flare API token', 'invalid-token') + ->expectsOutput('Invalid API token.') + ->assertExitCode(1); + + expect($this->store->getToken())->toBeNull(); +}); + +it('sends an Accept json header when validating a --token', function () { + Http::fake([ + 'flareapp.io/api/me' => Http::response(['email' => 'alex@spatie.be']), + ]); + + $this->artisan('login --token') + ->expectsQuestion('Enter your Flare API token', 'valid-token-123') + ->assertExitCode(0); + + Http::assertSent(function ($request) { + return $request->url() === 'https://flareapp.io/api/me' + && $request->hasHeader('Accept', 'application/json'); + }); +}); + it('shows error and does not store token on invalid --token input', function () { Http::fake([ 'flareapp.io/api/me' => Http::response(['error' => 'Unauthorized'], 401),