From 8068eec7c78c5c81edf7a1198b6f0ad6843fba64 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Mon, 7 Oct 2024 12:41:31 +0200 Subject: [PATCH 1/7] feat: user command --- .vscode/launch.json | 11 ++++ src/commands/login/actions.ts | 2 +- src/commands/user/actions.test.ts | 46 +++++++++++++ src/commands/user/actions.ts | 26 ++++++++ src/commands/user/index.test.ts | 104 ++++++++++++++++++++++++++++++ src/commands/user/index.ts | 32 +++++++++ src/constants.ts | 1 + src/index.ts | 1 + 8 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 src/commands/user/actions.test.ts create mode 100644 src/commands/user/actions.ts create mode 100644 src/commands/user/index.test.ts create mode 100644 src/commands/user/index.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index fe0403e8..17e0d8ed 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -34,6 +34,17 @@ "console": "integratedTerminal", "sourceMaps": true, "outFiles": ["${workspaceFolder}/dist/**/*.js"] + }, + { + "type": "node", + "request": "launch", + "name": "Debug user", + "program": "${workspaceFolder}/dist/index.mjs", + "args": ["user"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/dist/**/*.js"] } ] } diff --git a/src/commands/login/actions.ts b/src/commands/login/actions.ts index 1c5ea952..844cb510 100644 --- a/src/commands/login/actions.ts +++ b/src/commands/login/actions.ts @@ -21,7 +21,7 @@ export const loginWithToken = async (token: string, region: string) => { if (!(error as FetchError).response) { throw new Error('No response from server, please check if you are correctly connected to internet', error as Error) } - throw new Error('Error logging with token', error as Error) + throw new Error('There was an error logging with token', error as Error) } } diff --git a/src/commands/user/actions.test.ts b/src/commands/user/actions.test.ts new file mode 100644 index 00000000..9370f800 --- /dev/null +++ b/src/commands/user/actions.test.ts @@ -0,0 +1,46 @@ +import { ofetch } from 'ofetch' +import chalk from 'chalk' +import { getUser } from './actions' + +vi.mock('ofetch', () => ({ + ofetch: vi.fn(), +})) + +describe('user actions', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('getUser', () => { + it('should get user successfully with a valid token', async () => { + const mockResponse = { data: 'user data' } + ofetch.mockResolvedValue(mockResponse) + + const result = await getUser('valid-token', 'eu') + expect(result).toEqual(mockResponse) + }) + }) + + it('should throw an masked error for invalid token', async () => { + const mockError = { + response: { status: 401 }, + data: { error: 'Unauthorized' }, + } + ofetch.mockRejectedValue(mockError) + + await expect(getUser('invalid-token', 'eu')).rejects.toThrow( + new Error(`The token provided ${chalk.bold('inva*********')} is invalid: ${chalk.bold('401 Unauthorized')} + + Please make sure you are using the correct token and try again.`), + ) + }) + + it('should throw a network error if response is empty (network)', async () => { + const mockError = new Error('Network error') + ofetch.mockRejectedValue(mockError) + + await expect(getUser('any-token', 'eu')).rejects.toThrow( + 'No response from server, please check if you are correctly connected to internet', + ) + }) +}) diff --git a/src/commands/user/actions.ts b/src/commands/user/actions.ts new file mode 100644 index 00000000..f9618b19 --- /dev/null +++ b/src/commands/user/actions.ts @@ -0,0 +1,26 @@ +import type { FetchError } from 'ofetch' +import { ofetch } from 'ofetch' +import { regionsDomain } from '../../constants' +import chalk from 'chalk' +import { maskToken } from '../../utils' + +export const getUser = async (token: string, region: string) => { + try { + return await ofetch(`https://${regionsDomain[region]}/v1/users/me`, { + headers: { + Authorization: token, + }, + }) + } + catch (error) { + if ((error as FetchError).response?.status === 401) { + throw new Error(`The token provided ${chalk.bold(maskToken(token))} is invalid: ${chalk.bold(`401 ${(error as FetchError).data.error}`)} + + Please make sure you are using the correct token and try again.`) + } + if (!(error as FetchError).response) { + throw new Error('No response from server, please check if you are correctly connected to internet', error as Error) + } + throw new Error('There was an error logging with token ', error as Error) + } +} diff --git a/src/commands/user/index.test.ts b/src/commands/user/index.test.ts new file mode 100644 index 00000000..83ec9e9f --- /dev/null +++ b/src/commands/user/index.test.ts @@ -0,0 +1,104 @@ +import { userCommand } from './' +import { getUser } from './actions' +import { konsola } from '../../utils' +import { session } from '../../session' +import chalk from 'chalk' + +vi.mock('./actions', () => ({ + getUser: vi.fn(), +})) + +vi.mock('../../creds', () => ({ + isAuthorized: vi.fn(), +})) + +// Mocking the session module +vi.mock('../../session', () => { + let _cache + const session = () => { + if (!_cache) { + _cache = { + state: { + isLoggedIn: false, + }, + updateSession: vi.fn(), + persistCredentials: vi.fn(), + initializeSession: vi.fn(), + } + } + return _cache + } + + return { + session, + } +}) + +vi.mock('../../utils', async () => { + const actualUtils = await vi.importActual('../../utils') + return { + ...actualUtils, + konsola: { + ok: vi.fn(), + error: vi.fn(), + }, + handleError: (error: Error, header = false) => { + konsola.error(error, header) + // Optionally, prevent process.exit during tests + }, + } +}) + +describe('userCommand', () => { + beforeEach(() => { + vi.resetAllMocks() + vi.clearAllMocks() + }) + + it('should show the user information', async () => { + const mockResponse = { + user: { + friendly_name: 'John Doe', + email: 'john.doe@storyblok.com', + }, + } + session().state = { + isLoggedIn: true, + password: 'valid-token', + region: 'eu', + } + getUser.mockResolvedValue(mockResponse) + await userCommand.parseAsync(['node', 'test']) + + expect(getUser).toHaveBeenCalledWith('valid-token', 'eu') + expect(konsola.ok).toHaveBeenCalledWith( + `Hi: ${chalk.bold('John Doe')}, you current logged in with: ${chalk.hex('#45bfb9')(mockResponse.user.email)} on region: ${chalk.bold('eu')}`, + ) + }) + + it('should show an error if the user is not logged in', async () => { + session().state = { + isLoggedIn: false, + } + await userCommand.parseAsync(['node', 'test']) + + expect(konsola.error).toHaveBeenCalledWith(new Error(`You are not logged in. Please login first. + `)) + }) + + it('should show an error if the user information cannot be fetched', async () => { + session().state = { + isLoggedIn: true, + password: 'valid-token', + region: 'eu', + } + + const mockError = new Error('Network error') + + getUser.mockRejectedValue(mockError) + + await userCommand.parseAsync(['node', 'test']) + + expect(konsola.error).toHaveBeenCalledWith(mockError, true) + }) +}) diff --git a/src/commands/user/index.ts b/src/commands/user/index.ts new file mode 100644 index 00000000..36bc3664 --- /dev/null +++ b/src/commands/user/index.ts @@ -0,0 +1,32 @@ +import chalk from 'chalk' +import type { NetrcMachine } from '../../creds' +import { commands } from '../../constants' +import { getProgram } from '../../program' +import { formatHeader, handleError, konsola } from '../../utils' +import { getUser } from './actions' +import { session } from '../../session' + +const program = getProgram() // Get the shared singleton instance + +export const userCommand = program + .command(commands.USER) + .description('Get the current user') + .action(async () => { + console.log(formatHeader(chalk.bgHex('#8556D3').bold.white(` ${commands.USER} `))) + const { state, initializeSession } = session() + await initializeSession() + + if (!state.isLoggedIn) { + konsola.error(new Error(`You are not logged in. Please login first. + `)) + return + } + try { + const { password, region } = state as NetrcMachine + const { user } = await getUser(password, region) + konsola.ok(`Hi: ${chalk.bold(user.friendly_name)}, you current logged in with: ${chalk.hex('#45bfb9')(user.email)} on region: ${chalk.bold(region)}`) + } + catch (error) { + handleError(error as Error, true) + } + }) diff --git a/src/constants.ts b/src/constants.ts index baacee34..cd16b9d0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,7 @@ export const commands: Record = { LOGIN: 'login', LOGOUT: 'logout', + USER: 'user', } export const regions: Record = { diff --git a/src/index.ts b/src/index.ts index 36a2d904..c00096c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { formatHeader, handleError } from './utils' import { getProgram } from './program' import './commands/login' import './commands/logout' +import './commands/user' import { session } from './session' dotenv.config() // This will load variables from .env into process.env From 55e8efdefe9b00ee86c24f3cafcecce9c38db5ca Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Mon, 7 Oct 2024 17:23:01 +0200 Subject: [PATCH 2/7] feat: improve user message --- src/commands/user/index.test.ts | 2 +- src/commands/user/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/user/index.test.ts b/src/commands/user/index.test.ts index 83ec9e9f..99be24de 100644 --- a/src/commands/user/index.test.ts +++ b/src/commands/user/index.test.ts @@ -72,7 +72,7 @@ describe('userCommand', () => { expect(getUser).toHaveBeenCalledWith('valid-token', 'eu') expect(konsola.ok).toHaveBeenCalledWith( - `Hi: ${chalk.bold('John Doe')}, you current logged in with: ${chalk.hex('#45bfb9')(mockResponse.user.email)} on region: ${chalk.bold('eu')}`, + `Hi ${chalk.bold('John Doe')}, you are currently logged in with ${chalk.hex('#45bfb9')(mockResponse.user.email)} on ${chalk.bold('eu')} region`, ) }) diff --git a/src/commands/user/index.ts b/src/commands/user/index.ts index 36bc3664..fce06f56 100644 --- a/src/commands/user/index.ts +++ b/src/commands/user/index.ts @@ -24,7 +24,7 @@ export const userCommand = program try { const { password, region } = state as NetrcMachine const { user } = await getUser(password, region) - konsola.ok(`Hi: ${chalk.bold(user.friendly_name)}, you current logged in with: ${chalk.hex('#45bfb9')(user.email)} on region: ${chalk.bold(region)}`) + konsola.ok(`Hi ${chalk.bold(user.friendly_name)}, you are currently logged in with ${chalk.hex('#45bfb9')(user.email)} on ${chalk.bold(region)} region`) } catch (error) { handleError(error as Error, true) From 087636fe978592d3e266ab197c0d4c5554c0ed14 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Tue, 29 Oct 2024 17:07:08 +0100 Subject: [PATCH 3/7] feat: adapt to newest error handling strategy --- src/commands/user/actions.test.ts | 43 +++++++++++++++++++------------ src/commands/user/actions.ts | 22 +++++++++------- src/utils/error/api-error.ts | 1 + 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/commands/user/actions.test.ts b/src/commands/user/actions.test.ts index 9370f800..233a2702 100644 --- a/src/commands/user/actions.test.ts +++ b/src/commands/user/actions.test.ts @@ -1,10 +1,26 @@ import { ofetch } from 'ofetch' import chalk from 'chalk' import { getUser } from './actions' +import { http, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const handlers = [ + http.get('https://api.storyblok.com/v1/users/me', async ({ request }) => { + const token = request.headers.get('Authorization') + if (token === 'valid-token') { + return HttpResponse.json({ data: 'user data' }) + } + return new HttpResponse('Unauthorized', { status: 401 }) + }), +] + +const server = setupServer(...handlers) + +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) -vi.mock('ofetch', () => ({ - ofetch: vi.fn(), -})) +afterEach(() => server.resetHandlers()) +afterAll(() => server.close()) describe('user actions', () => { beforeEach(() => { @@ -14,31 +30,24 @@ describe('user actions', () => { describe('getUser', () => { it('should get user successfully with a valid token', async () => { const mockResponse = { data: 'user data' } - ofetch.mockResolvedValue(mockResponse) - const result = await getUser('valid-token', 'eu') expect(result).toEqual(mockResponse) }) }) it('should throw an masked error for invalid token', async () => { - const mockError = { - response: { status: 401 }, - data: { error: 'Unauthorized' }, - } - ofetch.mockRejectedValue(mockError) - await expect(getUser('invalid-token', 'eu')).rejects.toThrow( - new Error(`The token provided ${chalk.bold('inva*********')} is invalid: ${chalk.bold('401 Unauthorized')} - - Please make sure you are using the correct token and try again.`), + new Error(`The token provided ${chalk.bold('inva*********')} is invalid. + Please make sure you are using the correct token and try again.`), ) }) it('should throw a network error if response is empty (network)', async () => { - const mockError = new Error('Network error') - ofetch.mockRejectedValue(mockError) - + server.use( + http.get('https://api.storyblok.com/v1/users/me', () => { + return new HttpResponse(null, { status: 500 }) + }), + ) await expect(getUser('any-token', 'eu')).rejects.toThrow( 'No response from server, please check if you are correctly connected to internet', ) diff --git a/src/commands/user/actions.ts b/src/commands/user/actions.ts index f9618b19..12656393 100644 --- a/src/commands/user/actions.ts +++ b/src/commands/user/actions.ts @@ -1,8 +1,7 @@ -import type { FetchError } from 'ofetch' -import { ofetch } from 'ofetch' +import { FetchError, ofetch } from 'ofetch' import { regionsDomain } from '../../constants' import chalk from 'chalk' -import { maskToken } from '../../utils' +import { APIError, maskToken } from '../../utils' export const getUser = async (token: string, region: string) => { try { @@ -13,14 +12,19 @@ export const getUser = async (token: string, region: string) => { }) } catch (error) { - if ((error as FetchError).response?.status === 401) { - throw new Error(`The token provided ${chalk.bold(maskToken(token))} is invalid: ${chalk.bold(`401 ${(error as FetchError).data.error}`)} + if (error instanceof FetchError) { + const status = error.response?.status - Please make sure you are using the correct token and try again.`) + switch (status) { + case 401: + throw new APIError('unauthorized', 'get_user', error, `The token provided ${chalk.bold(maskToken(token))} is invalid. + Please make sure you are using the correct token and try again.`) + default: + throw new APIError('network_error', 'get_user', error) + } } - if (!(error as FetchError).response) { - throw new Error('No response from server, please check if you are correctly connected to internet', error as Error) + else { + throw new APIError('generic', 'get_user', error as Error) } - throw new Error('There was an error logging with token ', error as Error) } } diff --git a/src/utils/error/api-error.ts b/src/utils/error/api-error.ts index dda384f0..0a859ee3 100644 --- a/src/utils/error/api-error.ts +++ b/src/utils/error/api-error.ts @@ -5,6 +5,7 @@ export const API_ACTIONS = { login_with_token: 'Failed to log in with token', login_with_otp: 'Failed to log in with email, password and otp', login_email_password: 'Failed to log in with email and password', + get_user: 'Failed to get user', } as const export const API_ERRORS = { From 754d10b12ae91e19cdf8003ff49be4e14ac5f018 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Tue, 29 Oct 2024 17:32:28 +0100 Subject: [PATCH 4/7] feat: colorPalette constant for command titles --- src/commands/login/index.ts | 6 +++--- src/commands/user/actions.test.ts | 1 - src/commands/user/index.ts | 11 +++++------ src/constants.ts | 6 ++++++ src/creds.ts | 6 +++--- src/index.ts | 1 - 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/commands/login/index.ts b/src/commands/login/index.ts index 044d53ae..fd082318 100644 --- a/src/commands/login/index.ts +++ b/src/commands/login/index.ts @@ -1,7 +1,7 @@ import chalk from 'chalk' import { input, password, select } from '@inquirer/prompts' import type { RegionCode } from '../../constants' -import { commands, regionNames, regions, regionsDomain } from '../../constants' +import { colorPalette, commands, regionNames, regions, regionsDomain } from '../../constants' import { getProgram } from '../../program' import { CommandError, handleError, isRegion, konsola } from '../../utils' import { loginWithEmailAndPassword, loginWithOtp, loginWithToken } from './actions' @@ -40,7 +40,7 @@ export const loginCommand = program token: string region: RegionCode }) => { - konsola.title(` ${commands.LOGIN} `, '#8556D3') + konsola.title(` ${commands.LOGIN} `, colorPalette.LOGIN) const verbose = program.opts().verbose const { token, region } = options if (!isRegion(region)) { @@ -122,7 +122,7 @@ export const loginCommand = program updateSession(userEmail, response.access_token, userRegion) } await persistCredentials(regionsDomain[userRegion]) - konsola.ok(`Successfully logged in with email ${chalk.hex('#45bfb9')(userEmail)}`) + konsola.ok(`Successfully logged in with email ${chalk.hex(colorPalette.PRIMARY)(userEmail)}`) } } catch (error) { diff --git a/src/commands/user/actions.test.ts b/src/commands/user/actions.test.ts index 233a2702..d6f44f44 100644 --- a/src/commands/user/actions.test.ts +++ b/src/commands/user/actions.test.ts @@ -1,4 +1,3 @@ -import { ofetch } from 'ofetch' import chalk from 'chalk' import { getUser } from './actions' import { http, HttpResponse } from 'msw' diff --git a/src/commands/user/index.ts b/src/commands/user/index.ts index fce06f56..a2dacda9 100644 --- a/src/commands/user/index.ts +++ b/src/commands/user/index.ts @@ -1,8 +1,8 @@ import chalk from 'chalk' import type { NetrcMachine } from '../../creds' -import { commands } from '../../constants' +import { colorPalette, commands } from '../../constants' import { getProgram } from '../../program' -import { formatHeader, handleError, konsola } from '../../utils' +import { CommandError, handleError, konsola } from '../../utils' import { getUser } from './actions' import { session } from '../../session' @@ -12,19 +12,18 @@ export const userCommand = program .command(commands.USER) .description('Get the current user') .action(async () => { - console.log(formatHeader(chalk.bgHex('#8556D3').bold.white(` ${commands.USER} `))) + konsola.title(` ${commands.USER} `, colorPalette.USER) const { state, initializeSession } = session() await initializeSession() if (!state.isLoggedIn) { - konsola.error(new Error(`You are not logged in. Please login first. - `)) + handleError(new CommandError(`You are currently not logged in. Please login first to get your user info.`)) return } try { const { password, region } = state as NetrcMachine const { user } = await getUser(password, region) - konsola.ok(`Hi ${chalk.bold(user.friendly_name)}, you are currently logged in with ${chalk.hex('#45bfb9')(user.email)} on ${chalk.bold(region)} region`) + konsola.ok(`Hi ${chalk.bold(user.friendly_name)}, you are currently logged in with ${chalk.hex(colorPalette.PRIMARY)(user.email)} on ${chalk.bold(region)} region`) } catch (error) { handleError(error as Error, true) diff --git a/src/constants.ts b/src/constants.ts index ac12733d..cc527b28 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,6 +4,12 @@ export const commands = { USER: 'user', } as const +export const colorPalette = { + PRIMARY: '#45bfb9', + LOGIN: '#8556D3', + USER: '#8BC34A', // Changed to a less saturated green color +} as const + export interface ReadonlyArray { includes: (searchElement: any, fromIndex?: number) => searchElement is T } diff --git a/src/creds.ts b/src/creds.ts index 29f2a116..ecdae14e 100644 --- a/src/creds.ts +++ b/src/creds.ts @@ -2,7 +2,7 @@ import { access, readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { FileSystemError, handleFileSystemError, konsola } from './utils' import chalk from 'chalk' -import { regionCodes } from './constants' +import { colorPalette, regionCodes } from './constants' export interface NetrcMachine { login: string @@ -186,7 +186,7 @@ export const addNetrcEntry = async ({ mode: 0o600, // Set file permissions }) - konsola.ok(`Successfully added/updated entry for machine ${machineName} in ${chalk.hex('#45bfb9')(filePath)}`, true) + konsola.ok(`Successfully added/updated entry for machine ${machineName} in ${chalk.hex(colorPalette.PRIMARY)(filePath)}`, true) } catch (error) { throw new FileSystemError('invalid_argument', 'write', error as NodeJS.ErrnoException, `Error adding/updating entry for machine ${machineName} in .netrc file`) @@ -223,7 +223,7 @@ export const removeNetrcEntry = async ( mode: 0o600, // Set file permissions }) - konsola.ok(`Successfully removed entry from ${chalk.hex('#45bfb9')(filePath)}`, true) + konsola.ok(`Successfully removed entry from ${chalk.hex(colorPalette.PRIMARY)(filePath)}`, true) } } catch (error: unknown) { diff --git a/src/index.ts b/src/index.ts index c47fca6b..c378fe7a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,7 +48,6 @@ ${chalk.hex('#45bfb9')(' |/ ')} try { program.parse(process.argv) - konsola.br() // Add a line break } catch (error) { handleError(error as Error) From bdc9c5d471aea033e58f389a634b6b27bdc92336 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Tue, 29 Oct 2024 17:33:14 +0100 Subject: [PATCH 5/7] ci: correct action title --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c846da2..af7e52a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Run linters +name: Run Tests on: [push] env: From 0415d33847b16776ada5035ebaa4cc4f95e51ae6 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Tue, 29 Oct 2024 17:35:08 +0100 Subject: [PATCH 6/7] chore: fixed tests --- src/commands/user/index.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/user/index.test.ts b/src/commands/user/index.test.ts index 99be24de..fe3f8a7d 100644 --- a/src/commands/user/index.test.ts +++ b/src/commands/user/index.test.ts @@ -3,6 +3,7 @@ import { getUser } from './actions' import { konsola } from '../../utils' import { session } from '../../session' import chalk from 'chalk' +import { title } from 'node:process' vi.mock('./actions', () => ({ getUser: vi.fn(), @@ -40,6 +41,7 @@ vi.mock('../../utils', async () => { ...actualUtils, konsola: { ok: vi.fn(), + title: vi.fn(), error: vi.fn(), }, handleError: (error: Error, header = false) => { @@ -82,8 +84,7 @@ describe('userCommand', () => { } await userCommand.parseAsync(['node', 'test']) - expect(konsola.error).toHaveBeenCalledWith(new Error(`You are not logged in. Please login first. - `)) + expect(konsola.error).toHaveBeenCalledWith(new Error(`You are currently not logged in. Please login first to get your user info.`), false) }) it('should show an error if the user information cannot be fetched', async () => { From b97f81940802370fc2529c903bf5f73344f3a75a Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Tue, 29 Oct 2024 17:37:32 +0100 Subject: [PATCH 7/7] chore: remove unused import --- src/commands/user/index.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/user/index.test.ts b/src/commands/user/index.test.ts index fe3f8a7d..f869e5cf 100644 --- a/src/commands/user/index.test.ts +++ b/src/commands/user/index.test.ts @@ -3,7 +3,6 @@ import { getUser } from './actions' import { konsola } from '../../utils' import { session } from '../../session' import chalk from 'chalk' -import { title } from 'node:process' vi.mock('./actions', () => ({ getUser: vi.fn(),