Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/commands/switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ netlify switch

**Flags**

- `email` (*string*) - Switch to the account matching this email address
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in

Expand Down
1 change: 1 addition & 0 deletions src/commands/switch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const createSwitchCommand = (program: BaseCommand) =>
program
.command('switch')
.description('Switch your active Netlify account')
.option('--email <email>', 'Switch to the account matching this email address')
.action(async (options: OptionValues, command: BaseCommand) => {
const { switchCommand } = await import('./switch.js')
await switchCommand(options, command)
Expand Down
23 changes: 18 additions & 5 deletions src/commands/switch/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,41 @@ import { login } from '../login/login.js'

const LOGIN_NEW = 'I would like to login to a new account'

export const switchCommand = async (_options: OptionValues, command: BaseCommand) => {
const availableUsersChoices = Object.values(command.netlify.globalConfig.get('users') || {}).reduce(
export const switchCommand = async (options: OptionValues, command: BaseCommand) => {
const users = (command.netlify.globalConfig.get('users') || {}) as Record<
string,
{ id: string; name?: string; email: string }
>
const availableUsersChoices = Object.values(users).reduce<Record<string, string>>(
(prev, current) =>
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
Object.assign(prev, { [current.id]: current.name ? `${current.name} (${current.email})` : current.email }),
{},
)

if (options.email) {
const matchedUser = Object.values(users).find((user) => user.email === options.email)
if (matchedUser) {
command.netlify.globalConfig.set('userId', matchedUser.id)
log('')
log(`You're now using ${chalk.bold(availableUsersChoices[matchedUser.id])}.`)
return
}
log(`No account found matching ${chalk.bold(options.email)}, showing all available accounts.`)
log('')
}

const { accountSwitchChoice } = await inquirer.prompt([
{
type: 'list',
name: 'accountSwitchChoice',
message: 'Please select the account you want to use:',
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
choices: [...Object.entries(availableUsersChoices).map(([, val]) => val), LOGIN_NEW],
},
])

if (accountSwitchChoice === LOGIN_NEW) {
await login({ new: true }, command)
} else {
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
const selectedAccount = Object.entries(availableUsersChoices).find(
([, availableUsersChoice]) => availableUsersChoice === accountSwitchChoice,
)
Expand Down
98 changes: 98 additions & 0 deletions tests/unit/commands/switch/switch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { beforeEach, describe, expect, test, vi } from 'vitest'

const { logMessages, mockPrompt, mockLogin } = vi.hoisted(() => ({
logMessages: [] as string[],
mockPrompt: vi.fn(),
mockLogin: vi.fn(),
}))

vi.mock('inquirer', () => ({
default: { prompt: mockPrompt },
}))

vi.mock('../../../../src/utils/command-helpers.js', async () => ({
...(await vi.importActual('../../../../src/utils/command-helpers.js')),
log: (...args: string[]) => {
logMessages.push(args.join(' '))
},
}))

vi.mock('../../../../src/commands/login/login.js', () => ({
login: mockLogin,
}))

import { switchCommand } from '../../../../src/commands/switch/switch.js'

const users = {
'user-1': { id: 'user-1', name: 'Alice', email: 'alice@example.com' },
'user-2': { id: 'user-2', name: 'Bob', email: 'bob@corp.com' },
}

const createCommand = (usersData = users) => {
const mockSet = vi.fn()
const command = {
netlify: {
globalConfig: {
get: vi.fn().mockReturnValue(usersData),
set: mockSet,
},
},
} as unknown as Parameters<typeof switchCommand>[1]
return { command, mockSet }
}

describe('switchCommand', () => {
beforeEach(() => {
logMessages.length = 0
vi.clearAllMocks()
})

test('--email auto-switches when a match is found', async () => {
const { command, mockSet } = createCommand()

await switchCommand({ email: 'alice@example.com' }, command)

expect(mockSet).toHaveBeenCalledWith('userId', 'user-1')
expect(logMessages.some((m) => m.includes('Alice'))).toBe(true)
expect(mockPrompt).not.toHaveBeenCalled()
})

test('--email falls through to prompt when no match is found', async () => {
const { command } = createCommand()
mockPrompt.mockResolvedValueOnce({ accountSwitchChoice: 'Bob (bob@corp.com)' })

await switchCommand({ email: 'nobody@example.com' }, command)

expect(logMessages.some((m) => m.includes('No account found matching'))).toBe(true)
expect(mockPrompt).toHaveBeenCalled()
})

test('--email does not match partial email strings', async () => {
const { command } = createCommand()
mockPrompt.mockResolvedValueOnce({ accountSwitchChoice: 'Bob (bob@corp.com)' })

await switchCommand({ email: 'bob@corp' }, command)

expect(logMessages.some((m) => m.includes('No account found matching'))).toBe(true)
expect(mockPrompt).toHaveBeenCalled()
})

test('without --email shows interactive prompt', async () => {
const { command, mockSet } = createCommand()
mockPrompt.mockResolvedValueOnce({ accountSwitchChoice: 'Alice (alice@example.com)' })

await switchCommand({}, command)

expect(mockPrompt).toHaveBeenCalled()
expect(mockSet).toHaveBeenCalledWith('userId', 'user-1')
})

test('selecting login new triggers login flow', async () => {
const { command } = createCommand()
mockPrompt.mockResolvedValueOnce({ accountSwitchChoice: 'I would like to login to a new account' })

await switchCommand({}, command)

expect(mockLogin).toHaveBeenCalledWith({ new: true }, command)
})
})
Loading