diff --git a/src/commands/sites/sites-create.ts b/src/commands/sites/sites-create.ts index b595a6af04d..00c1dce675d 100644 --- a/src/commands/sites/sites-create.ts +++ b/src/commands/sites/sites-create.ts @@ -7,7 +7,7 @@ import { chalk, logAndThrowError, log, logJson, warn, type APIError } from '../. import getRepoData from '../../utils/get-repo-data.js' import { configureRepo } from '../../utils/init/config.js' import { isInteractive } from '../../utils/scripted-commands.js' -import { resolveTeamForNonInteractive } from '../../utils/team.js' +import { requireTeams, resolveTeamForNonInteractive } from '../../utils/team.js' import { track } from '../../utils/telemetry/index.js' import type { SiteInfo } from '../../utils/types.js' import { MAX_SITE_NAME_LENGTH } from '../../utils/validation.js' @@ -41,9 +41,11 @@ export const sitesCreate = async (options: OptionValues, command: BaseCommand) = accountSlug = team.slug log(`Using team: ${team.name}`) } else { + // select prompts crash when choices is empty + const teams = requireTeams(accounts) accountSlug = await select({ message: 'Team:', - choices: accounts.map((account) => ({ + choices: teams.map((account) => ({ value: account.slug, name: account.name, })), diff --git a/src/utils/team.ts b/src/utils/team.ts index 81f92ca5814..a61ace71f99 100644 --- a/src/utils/team.ts +++ b/src/utils/team.ts @@ -1,6 +1,14 @@ import { logAndThrowError } from './command-helpers.js' import type { MinimalAccount } from './types.js' +export const requireTeams = (accounts: MinimalAccount[] | undefined): MinimalAccount[] => { + const teams = accounts ?? [] + if (teams.length === 0) { + return logAndThrowError('No teams available. Please ensure you have access to at least one team.') + } + return teams +} + export const resolveTeam = (accounts: MinimalAccount[]): MinimalAccount | undefined => { if (accounts.length === 1) { return accounts[0] @@ -9,16 +17,14 @@ export const resolveTeam = (accounts: MinimalAccount[]): MinimalAccount | undefi } export const resolveTeamForNonInteractive = (accounts: MinimalAccount[], commandExample: string): MinimalAccount => { - if (accounts.length === 0) { - return logAndThrowError('No teams available. Please ensure you have access to at least one team.') - } + const teams = requireTeams(accounts) - const team = resolveTeam(accounts) + const team = resolveTeam(teams) if (team) { return team } - const availableTeams = accounts.map((t) => t.slug).join(', ') + const availableTeams = teams.map((t) => t.slug).join(', ') return logAndThrowError( `Multiple teams available. Please specify which team to use.\n` + `Available teams: ${availableTeams}\n\n` + diff --git a/tests/unit/commands/sites/sites-create.test.ts b/tests/unit/commands/sites/sites-create.test.ts new file mode 100644 index 00000000000..45a88b683d6 --- /dev/null +++ b/tests/unit/commands/sites/sites-create.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test, vi } from 'vitest' + +const { mockSelect } = vi.hoisted(() => ({ + mockSelect: vi.fn(), +})) + +vi.mock('@inquirer/prompts', () => ({ + input: vi.fn(), + select: mockSelect, +})) + +vi.mock('../../../../src/utils/scripted-commands.js', () => ({ + isInteractive: () => true, +})) + +vi.mock('../../../../src/utils/telemetry/report-error.js', () => ({ + reportError: vi.fn(), +})) + +vi.mock('../../../../src/utils/telemetry/index.js', () => ({ + track: vi.fn(), +})) + +vi.mock('../../../../src/commands/link/link.js', () => ({ + link: vi.fn(), +})) + +import { sitesCreate } from '../../../../src/commands/sites/sites-create.js' +import type BaseCommand from '../../../../src/commands/base-command.js' + +const commandWithAccounts = (accounts: unknown[]) => + ({ + authenticate: vi.fn().mockResolvedValue(undefined), + netlify: { + accounts, + api: {}, + }, + }) as unknown as BaseCommand + +describe('sitesCreate', () => { + test('throws a team error instead of opening an empty list prompt', async () => { + await expect(sitesCreate({}, commandWithAccounts([]))).rejects.toThrowError(/No teams available/) + expect(mockSelect).not.toHaveBeenCalled() + }) +}) diff --git a/tests/unit/utils/team.test.ts b/tests/unit/utils/team.test.ts new file mode 100644 index 00000000000..391a4b18589 --- /dev/null +++ b/tests/unit/utils/team.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test, vi } from 'vitest' + +vi.mock('../../../src/utils/telemetry/report-error.js', () => ({ + reportError: vi.fn(), +})) + +import { requireTeams, resolveTeam, resolveTeamForNonInteractive } from '../../../src/utils/team.js' +import type { MinimalAccount } from '../../../src/utils/types.js' + +const team = (overrides: Partial = {}): MinimalAccount => ({ + id: 'acc-1', + name: 'Acme', + slug: 'acme', + default: false, + team_logo_url: null, + on_pro_trial: false, + organization_id: null, + type_name: 'Pro', + type_slug: 'pro', + members_count: 1, + ...overrides, +}) + +describe('requireTeams', () => { + test('throws when the account list is empty', () => { + expect(() => requireTeams([])).toThrowError(/No teams available/) + }) + + test('throws when the account list is missing', () => { + expect(() => requireTeams(undefined)).toThrowError(/No teams available/) + }) + + test('returns the account list when at least one team exists', () => { + const accounts = [team()] + expect(requireTeams(accounts)).toBe(accounts) + }) +}) + +describe('resolveTeam', () => { + test('returns the only team', () => { + const only = team() + expect(resolveTeam([only])).toBe(only) + }) + + test('returns the default team when several exist', () => { + const fallback = team({ id: 'acc-2', slug: 'other', name: 'Other' }) + const preferred = team({ default: true }) + expect(resolveTeam([fallback, preferred])).toBe(preferred) + }) +}) + +describe('resolveTeamForNonInteractive', () => { + test('throws when no teams can be selected', () => { + expect(() => resolveTeamForNonInteractive([], 'netlify sites:create --account-slug ')).toThrowError( + /No teams available/, + ) + }) +})