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
1 change: 1 addition & 0 deletions packages/@sanity/cli/oclif.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default {
mcp: {description: 'Configure Sanity MCP server for AI editors'},
media: {description: 'Manage media assets and aspect definitions'},
openapi: {description: 'Manage OpenAPI specifications'},
organizations: {description: 'Manage your organizations'},
projects: {description: 'Manage Sanity projects'},
schemas: {description: 'Manage and validate schemas'},
telemetry: {description: 'Manage telemetry consent'},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {describe, expect, test} from 'vitest'

import {validateOrganizationSlug} from '../validateOrganizationSlug.js'

describe('validateOrganizationSlug', () => {
test.each([['acme'], ['acme-corp'], ['my-org-123'], ['a'], ['abc123']])(
'returns true for valid slug: "%s"',
(slug) => {
expect(validateOrganizationSlug(slug)).toBe(true)
},
)

test.each([
['', 'Organization slug cannot be empty'],
[' ', 'Organization slug cannot be empty'],
])('returns error for empty or whitespace: "%s"', (slug, expected) => {
expect(validateOrganizationSlug(slug)).toBe(expected)
})

test.each([
['Acme', 'Organization slug must be lowercase'],
['ACME', 'Organization slug must be lowercase'],
])('returns error for uppercase: "%s"', (slug, expected) => {
expect(validateOrganizationSlug(slug)).toBe(expected)
})

test.each([
['acme corp', 'Organization slug cannot contain spaces'],
['acme\tcorp', 'Organization slug cannot contain spaces'],
])('returns error for spaces: "%s"', (slug, expected) => {
expect(validateOrganizationSlug(slug)).toBe(expected)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function validateOrganizationSlug(input: string): string | true {
if (!input || input.trim() === '') {
return 'Organization slug cannot be empty'
}
if (input !== input.toLowerCase()) {
return 'Organization slug must be lowercase'
}
if (/\s/.test(input)) {
return 'Organization slug cannot contain spaces'
}
return true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {testCommand} from '@sanity/cli-test'
import {afterEach, describe, expect, test, vi} from 'vitest'

import {CreateOrganizationCommand} from '../create.js'

const mockRequest = vi.hoisted(() => vi.fn())
const mockInput = vi.hoisted(() => vi.fn())

vi.mock('@sanity/cli-core', async (importOriginal) => {
const actual = await importOriginal<typeof import('@sanity/cli-core')>()
return {
...actual,
getGlobalCliClient: vi.fn().mockResolvedValue({
request: mockRequest,
}),
}
})

vi.mock('@sanity/cli-core/ux', async () => {
const actual = await vi.importActual<typeof import('@sanity/cli-core/ux')>('@sanity/cli-core/ux')
return {
...actual,
input: mockInput,
spinner: vi.fn().mockReturnValue({
fail: vi.fn(),
start: vi.fn().mockReturnThis(),
succeed: vi.fn(),
}),
}
})

const createdOrg = {
createdAt: '2026-01-01T00:00:00Z',
createdByUserId: 'user-123',
defaultRoleName: null,
features: [],
id: 'org-new',
members: [],
name: 'My Org',
slug: null,
telemetryConsentStatus: 'allowed',
updatedAt: '2026-01-01T00:00:00Z',
}

describe('organizations create', () => {
afterEach(() => {
vi.clearAllMocks()
})

test('creates organization with --name flag', async () => {
mockRequest.mockResolvedValue(createdOrg)

const {error, stdout} = await testCommand(CreateOrganizationCommand, ['--name', 'My Org'])

if (error) throw error
expect(stdout).toContain('org-new')
expect(stdout).toContain('My Org')
})

test('creates organization with --name flag and --default-role flag', async () => {
mockRequest.mockResolvedValue({...createdOrg, defaultRoleName: 'viewer'})

const {error, stdout} = await testCommand(CreateOrganizationCommand, [
'--name',
'My Org',
'--default-role',
'viewer',
])

if (error) throw error
expect(stdout).toContain('org-new')
})

test('prompts for name when arg is not provided', async () => {
mockInput.mockResolvedValue('Prompted Org')
mockRequest.mockResolvedValue({...createdOrg, name: 'Prompted Org'})

const {error, stdout} = await testCommand(CreateOrganizationCommand, [])

if (error) throw error
expect(mockInput).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Organization name:',
validate: expect.any(Function),
}),
)
expect(stdout).toContain('org-new')
})

test('errors when --name flag is empty', async () => {
const {error} = await testCommand(CreateOrganizationCommand, ['--name', ''])

expect(error).toBeInstanceOf(Error)
expect(error?.message).toContain('Organization name cannot be empty')
expect(error?.oclif?.exit).toBe(1)
})

test('errors when --name flag exceeds 100 characters', async () => {
const longName = 'a'.repeat(101)
const {error} = await testCommand(CreateOrganizationCommand, ['--name', longName])

expect(error).toBeInstanceOf(Error)
expect(error?.message).toContain('Organization name cannot be longer than 100 characters')
expect(error?.oclif?.exit).toBe(1)
})

test('errors when API call fails', async () => {
mockRequest.mockRejectedValue(new Error('Server error'))

const {error} = await testCommand(CreateOrganizationCommand, ['--name', 'My Org'])

expect(error).toBeInstanceOf(Error)
expect(error?.message).toContain('Failed to create organization')
expect(error?.oclif?.exit).toBe(1)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {input} from '@sanity/cli-core/ux'
import {testCommand} from '@sanity/cli-test'
import {afterEach, describe, expect, test, vi} from 'vitest'

import {DeleteOrganizationCommand} from '../delete.js'

const mockRequest = vi.hoisted(() => vi.fn())

vi.mock('@sanity/cli-core', async (importOriginal) => {
const actual = await importOriginal<typeof import('@sanity/cli-core')>()
return {
...actual,
getGlobalCliClient: vi.fn().mockResolvedValue({
request: mockRequest,
}),
}
})

vi.mock('@sanity/cli-core/ux', async () => {
const actual = await vi.importActual<typeof import('@sanity/cli-core/ux')>('@sanity/cli-core/ux')
return {
...actual,
input: vi.fn(),
spinner: vi
.fn()
.mockReturnValue({fail: vi.fn(), start: vi.fn().mockReturnThis(), succeed: vi.fn()}),
}
})

const mockInput = vi.mocked(input)

const org = {
createdAt: '2024-01-01T00:00:00Z',
defaultRoleName: null,
id: 'org-aaa',
name: 'Acme Corp',
slug: 'acme-corp',
updatedAt: '2026-03-18T00:00:00Z',
}

describe('organizations delete', () => {
afterEach(() => {
vi.clearAllMocks()
})

test('deletes organization after typing org name', async () => {
mockRequest.mockResolvedValueOnce(org).mockResolvedValueOnce({deleted: true})
mockInput.mockResolvedValue(org.name)

const {error, stdout} = await testCommand(DeleteOrganizationCommand, ['org-aaa'])

if (error) throw error
expect(mockInput).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Type the name of the organization'),
validate: expect.any(Function),
}),
)
expect(stdout).toContain('Organization deleted')
})

test('skips confirmation with --force flag', async () => {
mockRequest.mockResolvedValue({deleted: true})

const {error, stderr} = await testCommand(DeleteOrganizationCommand, ['org-aaa', '--force'])

if (error) throw error
expect(mockInput).not.toHaveBeenCalled()
expect(stderr).toContain(`--force' used: skipping confirmation`)
})

test('errors when user cancels the input prompt', async () => {
mockRequest.mockResolvedValueOnce(org)
mockInput.mockRejectedValue(new Error('User cancelled'))

const {error} = await testCommand(DeleteOrganizationCommand, ['org-aaa'])

expect(error).toBeInstanceOf(Error)
expect(error?.message).toBe('User cancelled')
expect(error?.oclif?.exit).toBe(1)
})

test('requires organizationId argument', async () => {
const {error} = await testCommand(DeleteOrganizationCommand, [])

expect(error).toBeInstanceOf(Error)
})

test('shows user-friendly error when org is not found during fetch', async () => {
const apiError = Object.assign(new Error('Not found'), {statusCode: 404})
mockRequest.mockRejectedValue(apiError)

const {error} = await testCommand(DeleteOrganizationCommand, ['org-aaa'])

expect(error).toBeInstanceOf(Error)
expect(error?.message).toContain('Organization "org-aaa" not found')
expect(error?.oclif?.exit).toBe(1)
})

test('errors when org retrieval fails', async () => {
mockRequest.mockRejectedValue(new Error('Network error'))

const {error} = await testCommand(DeleteOrganizationCommand, ['org-aaa'])

expect(error).toBeInstanceOf(Error)
expect(error?.message).toContain('Organization retrieval failed')
expect(error?.oclif?.exit).toBe(1)
})

test('shows user-friendly error on 404 during delete', async () => {
mockRequest.mockResolvedValueOnce(org)
mockInput.mockResolvedValue(org.name)
const apiError = Object.assign(new Error('Not found'), {statusCode: 404})
mockRequest.mockRejectedValueOnce(apiError)

const {error} = await testCommand(DeleteOrganizationCommand, ['org-aaa'])

expect(mockInput).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Type the name of the organization'),
validate: expect.any(Function),
}),
)
expect(error).toBeInstanceOf(Error)
expect(error?.message).toContain('Organization "org-aaa" not found')
expect(error?.oclif?.exit).toBe(1)
})

test('errors when delete API call fails', async () => {
mockRequest.mockResolvedValueOnce(org)
mockInput.mockResolvedValue(org.name)
const apiError = Object.assign(new Error('Organization has projects'), {statusCode: 409})
mockRequest.mockRejectedValueOnce(apiError)

const {error} = await testCommand(DeleteOrganizationCommand, ['org-aaa'])

expect(mockInput).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Type the name of the organization'),
validate: expect.any(Function),
}),
)
expect(error).toBeInstanceOf(Error)
expect(error?.message).toContain('Failed to delete organization')
expect(error?.oclif?.exit).toBe(1)
})

test('errors when --force is used without organizationId', async () => {
const {error} = await testCommand(DeleteOrganizationCommand, ['--force'])

expect(error).toBeInstanceOf(Error)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {testCommand} from '@sanity/cli-test'
import {afterEach, describe, expect, test, vi} from 'vitest'

import {GetOrganizationCommand} from '../get.js'

const mockRequest = vi.hoisted(() => vi.fn())

vi.mock('@sanity/cli-core', async (importOriginal) => {
const actual = await importOriginal<typeof import('@sanity/cli-core')>()
return {
...actual,
getGlobalCliClient: vi.fn().mockResolvedValue({
request: mockRequest,
}),
}
})

const organization = {
createdAt: '2024-01-15T10:00:00Z',
defaultRoleName: 'viewer',
id: 'org-aaa',
name: 'Acme Corp',
slug: 'acme',
updatedAt: '2024-06-01T12:00:00Z',
}

describe('organizations get', () => {
afterEach(() => {
vi.clearAllMocks()
})

test('displays organization details', async () => {
mockRequest.mockResolvedValue(organization)

const {error, stdout} = await testCommand(GetOrganizationCommand, ['org-aaa'])

if (error) throw error
expect(stdout).toContain('org-aaa')
expect(stdout).toContain('Acme Corp')
expect(stdout).toContain('acme')
expect(stdout).toContain('viewer')
})

test('requires organizationId argument', async () => {
const {error} = await testCommand(GetOrganizationCommand, [])

expect(error).toBeInstanceOf(Error)
})

test('errors when organization not found', async () => {
const notFound = Object.assign(new Error('Not found'), {statusCode: 404})
mockRequest.mockRejectedValue(notFound)

const {error} = await testCommand(GetOrganizationCommand, ['org-missing'])

expect(error).toBeInstanceOf(Error)
expect(error?.message).toContain('org-missing')
expect(error?.oclif?.exit).toBe(1)
})

test('errors on generic API failure', async () => {
mockRequest.mockRejectedValue(new Error('Network error'))

const {error} = await testCommand(GetOrganizationCommand, ['org-aaa'])

expect(error).toBeInstanceOf(Error)
expect(error?.message).toContain('Failed to get organization')
expect(error?.oclif?.exit).toBe(1)
})
})
Loading
Loading