diff --git a/cli/commands/auth/status.ts b/cli/commands/auth/status.ts new file mode 100644 index 000000000..ea863c01c --- /dev/null +++ b/cli/commands/auth/status.ts @@ -0,0 +1,43 @@ +import { __, sprintf } from '@wordpress/i18n'; +import { AuthCommandLoggerAction as LoggerAction } from 'common/logger-actions'; +import { getUserInfo } from 'cli/lib/api'; +import { getAuthToken } from 'cli/lib/appdata'; +import { Logger, LoggerError } from 'cli/logger'; +import { StudioArgv } from 'cli/types'; + +export async function runCommand(): Promise< void > { + const logger = new Logger< LoggerAction >(); + + logger.reportStart( LoggerAction.STATUS_CHECK, __( 'Checking authentication status…' ) ); + let token: Awaited< ReturnType< typeof getAuthToken > >; + + try { + token = await getAuthToken(); + } catch ( error ) { + logger.reportError( new LoggerError( __( 'Authentication token is invalid or expired' ) ) ); + return; + } + + try { + const userData = await getUserInfo( token.accessToken ); + logger.reportSuccess( + sprintf( __( 'Successfully authenticated with WordPress.com as `%s`' ), userData.username ) + ); + } catch ( error ) { + if ( error instanceof LoggerError ) { + logger.reportError( error ); + } else { + logger.reportError( new LoggerError( __( 'Failed to check authentication status' ), error ) ); + } + } +} + +export const registerCommand = ( yargs: StudioArgv ) => { + return yargs.command( { + command: 'status', + describe: __( 'Check authentication status' ), + handler: async () => { + await runCommand(); + }, + } ); +}; diff --git a/cli/commands/auth/tests/status.test.ts b/cli/commands/auth/tests/status.test.ts new file mode 100644 index 000000000..9d2350e20 --- /dev/null +++ b/cli/commands/auth/tests/status.test.ts @@ -0,0 +1,87 @@ +import { getUserInfo } from 'cli/lib/api'; +import { getAuthToken } from 'cli/lib/appdata'; +import { Logger, LoggerError } from 'cli/logger'; + +jest.mock( 'cli/lib/api' ); +jest.mock( 'cli/lib/appdata' ); +jest.mock( 'cli/logger' ); + +describe( 'Auth Status Command', () => { + const mockToken = { + accessToken: 'existing-token', + id: 999, + email: 'existing@example.com', + displayName: 'Existing User', + expiresIn: 1209600, + expirationTime: Date.now() + 1209600000, + }; + const mockUserData = { + username: 'testuser', + }; + + let mockLogger: { + reportStart: jest.Mock; + reportSuccess: jest.Mock; + reportError: jest.Mock; + }; + + beforeEach( () => { + jest.clearAllMocks(); + + mockLogger = { + reportStart: jest.fn(), + reportSuccess: jest.fn(), + reportError: jest.fn(), + }; + + ( Logger as unknown as jest.Mock ).mockReturnValue( mockLogger ); + ( getAuthToken as jest.Mock ).mockResolvedValue( mockToken ); + ( getUserInfo as jest.Mock ).mockResolvedValue( mockUserData ); + } ); + + afterEach( () => { + jest.restoreAllMocks(); + } ); + + it( 'should report success when authenticated', async () => { + const { runCommand } = await import( '../status' ); + await runCommand(); + + expect( mockLogger.reportStart ).toHaveBeenCalled(); + expect( getAuthToken ).toHaveBeenCalled(); + expect( getUserInfo ).toHaveBeenCalledWith( mockToken.accessToken ); + expect( mockLogger.reportSuccess ).toHaveBeenCalledWith( + expect.stringContaining( 'Successfully authenticated with WordPress.com as `testuser`' ) + ); + } ); + + it( 'should report error when token is invalid', async () => { + ( getAuthToken as jest.Mock ).mockRejectedValue( new Error( 'Token error' ) ); + + const { runCommand } = await import( '../status' ); + await runCommand(); + + expect( mockLogger.reportError ).toHaveBeenCalled(); + expect( mockLogger.reportError ).toHaveBeenCalledWith( expect.any( LoggerError ) ); + expect( getUserInfo ).not.toHaveBeenCalled(); + } ); + + it( 'should forward LoggerError from getUserInfo', async () => { + const apiError = new LoggerError( 'API error' ); + ( getUserInfo as jest.Mock ).mockRejectedValue( apiError ); + + const { runCommand } = await import( '../status' ); + await runCommand(); + + expect( mockLogger.reportError ).toHaveBeenCalledWith( apiError ); + } ); + + it( 'should wrap unknown error when getUserInfo fails', async () => { + ( getUserInfo as jest.Mock ).mockRejectedValue( new Error( 'Unknown error' ) ); + + const { runCommand } = await import( '../status' ); + await runCommand(); + + expect( mockLogger.reportError ).toHaveBeenCalledWith( expect.any( LoggerError ) ); + } ); +} ); diff --git a/cli/index.ts b/cli/index.ts index f02605ea7..3fcf65f47 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -6,6 +6,7 @@ import { StatsGroup, StatsMetric } from 'common/types/stats'; import yargs from 'yargs'; import { registerCommand as registerAuthLoginCommand } from 'cli/commands/auth/login'; import { registerCommand as registerAuthLogoutCommand } from 'cli/commands/auth/logout'; +import { registerCommand as registerAuthStatusCommand } from 'cli/commands/auth/status'; import { registerCommand as registerCreateCommand } from 'cli/commands/preview/create'; import { registerCommand as registerDeleteCommand } from 'cli/commands/preview/delete'; import { registerCommand as registerListCommand } from 'cli/commands/preview/list'; @@ -50,6 +51,7 @@ async function main() { .command( 'auth', __( 'Manage authentication' ), ( authYargs ) => { registerAuthLoginCommand( authYargs ); registerAuthLogoutCommand( authYargs ); + registerAuthStatusCommand( authYargs ); authYargs.demandCommand( 1, __( 'You must provide a valid auth command' ) ); } ) .command( 'preview', __( 'Manage preview sites' ), ( previewYargs ) => { diff --git a/cli/lib/api.ts b/cli/lib/api.ts index ac709b185..7c8956922 100644 --- a/cli/lib/api.ts +++ b/cli/lib/api.ts @@ -149,6 +149,7 @@ const userResponseSchema = z.object( { ID: z.number(), email: z.string().email(), display_name: z.string(), + username: z.string(), } ); export async function getUserInfo( @@ -157,7 +158,7 @@ export async function getUserInfo( const wpcom = wpcomFactory( token, wpcomXhrRequest ); try { const rawResponse = await wpcom.req.get( '/me', { - fields: 'ID,login,email,display_name', + fields: 'ID,username,email,display_name', } ); return userResponseSchema.parse( rawResponse ); } catch ( error ) { diff --git a/common/logger-actions.ts b/common/logger-actions.ts index 0e07a70fb..b847b7d1e 100644 --- a/common/logger-actions.ts +++ b/common/logger-actions.ts @@ -4,6 +4,7 @@ export enum AuthCommandLoggerAction { LOGIN = 'login', LOGOUT = 'logout', + STATUS_CHECK = 'status_check', } export enum PreviewCommandLoggerAction {