Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions cli/commands/auth/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { __ } from '@wordpress/i18n';
import { AuthCommandLoggerAction as LoggerAction } from 'common/logger-actions';
import { revokeAuthToken } from 'cli/lib/api';
import {
readAppdata,
saveAppdata,
lockAppdata,
unlockAppdata,
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.LOGOUT, __( 'Logging out…' ) );
let token: Awaited< ReturnType< typeof getAuthToken > >;

try {
token = await getAuthToken();
} catch ( error ) {
logger.reportSuccess( __( 'Already logged out' ) );
return;
}

try {
await revokeAuthToken( token.accessToken );

await lockAppdata();
const userData = await readAppdata();
delete userData.authToken;
await saveAppdata( userData );

logger.reportSuccess( __( 'Successfully logged out' ) );
} catch ( error ) {
if ( error instanceof LoggerError ) {
logger.reportError( error );
} else {
logger.reportError( new LoggerError( __( 'Failed to log out' ), error ) );
}
} finally {
await unlockAppdata();
}
}

export const registerCommand = ( yargs: StudioArgv ) => {
return yargs.command( {
command: 'logout',
describe: __( 'Log out and clear WordPress.com authentication' ),
handler: async () => {
await runCommand();
},
} );
};
109 changes: 109 additions & 0 deletions cli/commands/auth/tests/logout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { revokeAuthToken } from 'cli/lib/api';
import {
getAuthToken,
lockAppdata,
readAppdata,
saveAppdata,
unlockAppdata,
} from 'cli/lib/appdata';
import { Logger, LoggerError } from 'cli/logger';

jest.mock( 'cli/lib/appdata' );
jest.mock( 'cli/logger' );
jest.mock( 'cli/lib/api' );

describe( 'Auth Logout Command', () => {
const mockAppdata = {
authToken: {
accessToken: 'existing-token',
id: 999,
email: '[email protected]',
displayName: 'Existing User',
expiresIn: 1209600,
expirationTime: Date.now() + 1209600000,
},
};

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 jest.Mock ).mockReturnValue( mockLogger );
( getAuthToken as jest.Mock ).mockResolvedValue( mockAppdata.authToken );
( revokeAuthToken as jest.Mock ).mockResolvedValue( undefined );
( lockAppdata as jest.Mock ).mockResolvedValue( undefined );
( unlockAppdata as jest.Mock ).mockResolvedValue( undefined );
( readAppdata as jest.Mock ).mockResolvedValue( JSON.parse( JSON.stringify( mockAppdata ) ) );
( saveAppdata as jest.Mock ).mockResolvedValue( undefined );
} );

afterEach( () => {
jest.restoreAllMocks();
} );

it( 'should complete the logout process successfully', async () => {
const { runCommand } = await import( '../logout' );
await runCommand();

expect( getAuthToken ).toHaveBeenCalled();
expect( revokeAuthToken ).toHaveBeenCalled();
expect( lockAppdata ).toHaveBeenCalled();
expect( readAppdata ).toHaveBeenCalled();
expect( saveAppdata ).toHaveBeenCalledWith( {} );
expect( unlockAppdata ).toHaveBeenCalled();
expect( mockLogger.reportSuccess ).toHaveBeenCalledWith( 'Successfully logged out' );
} );

it( 'should report an error if revoking the token fails', async () => {
( revokeAuthToken as jest.Mock ).mockRejectedValue( new Error( 'Failed to revoke token' ) );

const { runCommand } = await import( '../logout' );
await runCommand();

expect( getAuthToken ).toHaveBeenCalled();
expect( lockAppdata ).not.toHaveBeenCalled();
expect( readAppdata ).not.toHaveBeenCalled();
expect( saveAppdata ).not.toHaveBeenCalledWith( {} );
expect( unlockAppdata ).toHaveBeenCalled();
expect( mockLogger.reportError ).toHaveBeenCalled();
expect( mockLogger.reportError ).toHaveBeenCalledWith( expect.any( LoggerError ) );
} );

it( 'should report already logged out if no auth token exists', async () => {
( getAuthToken as jest.Mock ).mockRejectedValue( new Error( 'No auth token' ) );

const { runCommand } = await import( '../logout' );
await runCommand();

expect( getAuthToken ).toHaveBeenCalled();
expect( revokeAuthToken ).not.toHaveBeenCalled();
expect( lockAppdata ).not.toHaveBeenCalled();
expect( readAppdata ).not.toHaveBeenCalled();
expect( saveAppdata ).not.toHaveBeenCalled();
expect( mockLogger.reportSuccess ).toHaveBeenCalledWith( 'Already logged out' );
} );

it( 'should unlock appdata even if save fails', async () => {
( saveAppdata as jest.Mock ).mockRejectedValue( new Error( 'Failed to save' ) );

const { runCommand } = await import( '../logout' );
await runCommand();

expect( revokeAuthToken ).toHaveBeenCalled();
expect( lockAppdata ).toHaveBeenCalled();
expect( unlockAppdata ).toHaveBeenCalled();
expect( mockLogger.reportError ).toHaveBeenCalled();
expect( mockLogger.reportError ).toHaveBeenCalledWith( expect.any( LoggerError ) );
} );
} );
2 changes: 2 additions & 0 deletions cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { suppressPunycodeWarning } from 'common/lib/suppress-punycode-warning';
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 registerCreateCommand } from 'cli/commands/preview/create';
import { registerCommand as registerDeleteCommand } from 'cli/commands/preview/delete';
import { registerCommand as registerListCommand } from 'cli/commands/preview/list';
Expand Down Expand Up @@ -48,6 +49,7 @@ async function main() {
} )
.command( 'auth', __( 'Manage authentication' ), ( authYargs ) => {
registerAuthLoginCommand( authYargs );
registerAuthLogoutCommand( authYargs );
authYargs.demandCommand( 1, __( 'You must provide a valid auth command' ) );
} )
.command( 'preview', __( 'Manage preview sites' ), ( previewYargs ) => {
Expand Down
13 changes: 13 additions & 0 deletions cli/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,16 @@ export async function getUserInfo(
throw new LoggerError( __( 'Failed to fetch user info' ), error );
}
}

export async function revokeAuthToken( token: string ): Promise< void > {
const wpcom = wpcomFactory( token, wpcomXhrRequest );
try {
await wpcom.req.del( {
apiNamespace: 'wpcom/v2',
path: '/studio-app/token',
method: 'DELETE',
} );
} catch ( error ) {
throw new LoggerError( __( 'Failed to revoke token' ), error );
}
}
1 change: 1 addition & 0 deletions common/logger-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

export enum AuthCommandLoggerAction {
LOGIN = 'login',
LOGOUT = 'logout',
}

export enum PreviewCommandLoggerAction {
Expand Down