Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): update CLI to use new deploy endpoint #7244

Merged
merged 14 commits into from
Aug 14, 2024
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
2 changes: 2 additions & 0 deletions packages/@sanity/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ export interface CliConfig {
vite?: UserViteConfig

autoUpdates?: boolean

studioHost?: string
}

export type UserViteConfig =
Expand Down
1 change: 1 addition & 0 deletions packages/sanity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@
"esbuild-register": "^3.5.0",
"execa": "^2.0.0",
"exif-component": "^1.0.1",
"form-data": "^4.0.0",
"framer-motion": "11.0.8",
"get-it": "^8.6.4",
"get-random-values-esm": "1.0.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
import zlib from 'node:zlib'

import {beforeEach, describe, expect, it, jest} from '@jest/globals'
import {type CliCommandArguments, type CliCommandContext} from '@sanity/cli'
import tar from 'tar-fs'

import buildSanityStudio from '../../build/buildAction'
import deployStudioAction, {type DeployStudioActionFlags} from '../deployAction'
import * as _helpers from '../helpers'
import {type UserApplication} from '../helpers'

// Mock dependencies
jest.mock('tar-fs')
jest.mock('node:zlib')
jest.mock('../helpers')
jest.mock('../../build/buildAction')

type Helpers = typeof _helpers
const helpers = _helpers as unknown as {[K in keyof Helpers]: jest.Mock<Helpers[K]>}
const buildSanityStudioMock = buildSanityStudio as jest.Mock<typeof buildSanityStudio>
const tarPackMock = tar.pack as jest.Mock
const zlibCreateGzipMock = zlib.createGzip as jest.Mock
type SpinnerInstance = {
start: jest.Mock<() => SpinnerInstance>
succeed: jest.Mock<() => SpinnerInstance>
fail: jest.Mock<() => SpinnerInstance>
}

describe('deployStudioAction', () => {
let mockContext: CliCommandContext
let spinnerInstance: SpinnerInstance

const mockApplication: UserApplication = {
id: 'app-id',
appHost: 'app-host',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
urlType: 'internal',
projectId: 'example',
title: null,
type: 'studio',
}

beforeEach(() => {
jest.clearAllMocks()

spinnerInstance = {
start: jest.fn(() => spinnerInstance),
succeed: jest.fn(() => spinnerInstance),
fail: jest.fn(() => spinnerInstance),
}

mockContext = {
apiClient: jest.fn().mockReturnValue({
withConfig: jest.fn().mockReturnThis(),
}),
workDir: '/fake/work/dir',
chalk: {cyan: jest.fn((str) => str), red: jest.fn((str) => str)},
output: {
error: jest.fn((str) => str),
print: jest.fn(),
spinner: jest.fn().mockReturnValue(spinnerInstance),
},
prompt: {single: jest.fn()},
cliConfig: {},
} as unknown as CliCommandContext
})

it('builds and deploys the studio if the directory is empty', async () => {
const mockSpinner = mockContext.output.spinner('')

// Mock utility functions
helpers.dirIsEmptyOrNonExistent.mockResolvedValueOnce(true)
helpers.getInstalledSanityVersion.mockResolvedValueOnce('vX')
helpers.getOrCreateUserApplication.mockResolvedValueOnce(mockApplication)
helpers.createDeployment.mockResolvedValueOnce({location: 'https://app-host.sanity.studio'})
buildSanityStudioMock.mockResolvedValueOnce({didCompile: true})
tarPackMock.mockReturnValueOnce({pipe: jest.fn().mockReturnValue('tarball')})
zlibCreateGzipMock.mockReturnValue('gzipped')

await deployStudioAction(
{
argsWithoutOptions: ['customSourceDir'],
extOptions: {},
} as CliCommandArguments<DeployStudioActionFlags>,
mockContext,
)

// Check that buildSanityStudio was called
expect(buildSanityStudioMock).toHaveBeenCalledWith(
expect.objectContaining({
extOptions: {build: true},
argsWithoutOptions: ['customSourceDir'],
}),
mockContext,
{basePath: '/'},
)
expect(helpers.dirIsEmptyOrNonExistent).toHaveBeenCalledWith(
expect.stringContaining('customSourceDir'),
)
expect(helpers.getOrCreateUserApplication).toHaveBeenCalledWith(
expect.objectContaining({
client: expect.anything(),
context: expect.anything(),
}),
)
expect(helpers.createDeployment).toHaveBeenCalledWith({
client: expect.anything(),
applicationId: 'app-id',
version: 'vX',
isAutoUpdating: false,
tarball: 'tarball',
})

expect(mockContext.output.print).toHaveBeenCalledWith(
'\nSuccess! Studio deployed to https://app-host.sanity.studio',
)
expect(mockSpinner.succeed).toHaveBeenCalled()
})

it('builds and deploys the studio if the directory is empty and hostname in config', async () => {
const mockSpinner = mockContext.output.spinner('')
mockContext.cliConfig = {studioHost: 'app-host'}

// Mock utility functions
helpers.dirIsEmptyOrNonExistent.mockResolvedValueOnce(true)
helpers.getInstalledSanityVersion.mockResolvedValueOnce('vX')
helpers.getOrCreateUserApplicationFromConfig.mockResolvedValueOnce(mockApplication)
helpers.createDeployment.mockResolvedValueOnce({location: 'https://app-host.sanity.studio'})
buildSanityStudioMock.mockResolvedValueOnce({didCompile: true})
tarPackMock.mockReturnValueOnce({pipe: jest.fn().mockReturnValue('tarball')})
zlibCreateGzipMock.mockReturnValue('gzipped')

await deployStudioAction(
{
argsWithoutOptions: ['customSourceDir'],
extOptions: {},
} as CliCommandArguments<DeployStudioActionFlags>,
mockContext,
)

// Check that buildSanityStudio was called
expect(buildSanityStudioMock).toHaveBeenCalledWith(
expect.objectContaining({
extOptions: {build: true},
argsWithoutOptions: ['customSourceDir'],
}),
mockContext,
{basePath: '/'},
)
expect(helpers.dirIsEmptyOrNonExistent).toHaveBeenCalledWith(
expect.stringContaining('customSourceDir'),
)
expect(helpers.getOrCreateUserApplicationFromConfig).toHaveBeenCalledWith(
expect.objectContaining({
client: expect.anything(),
context: expect.anything(),
appHost: 'app-host',
}),
)
expect(helpers.createDeployment).toHaveBeenCalledWith({
client: expect.anything(),
applicationId: 'app-id',
version: 'vX',
isAutoUpdating: false,
tarball: 'tarball',
})

expect(mockContext.output.print).toHaveBeenCalledWith(
'\nSuccess! Studio deployed to https://app-host.sanity.studio',
)
expect(mockSpinner.succeed).toHaveBeenCalled()
})

it('prompts the user if the directory is not empty', async () => {
const mockSpinner = mockContext.output.spinner('')

helpers.dirIsEmptyOrNonExistent.mockResolvedValueOnce(false)
;(
mockContext.prompt.single as jest.Mock<typeof mockContext.prompt.single>
).mockResolvedValueOnce(true) // User confirms to proceed
helpers.getInstalledSanityVersion.mockResolvedValueOnce('vX')
helpers.getOrCreateUserApplication.mockResolvedValueOnce(mockApplication)
helpers.createDeployment.mockResolvedValueOnce({location: 'https://app-host.sanity.studio'})
buildSanityStudioMock.mockResolvedValueOnce({didCompile: true})
tarPackMock.mockReturnValueOnce({pipe: jest.fn().mockReturnValue('tarball')})
zlibCreateGzipMock.mockReturnValue('gzipped')

await deployStudioAction(
{
argsWithoutOptions: ['customSourceDir'],
extOptions: {},
} as CliCommandArguments<DeployStudioActionFlags>,
mockContext,
)

expect(helpers.dirIsEmptyOrNonExistent).toHaveBeenCalledWith(
expect.stringContaining('customSourceDir'),
)
expect(mockContext.prompt.single).toHaveBeenCalledWith({
type: 'confirm',
message: expect.stringContaining('is not empty, do you want to proceed?'),
default: false,
})
expect(buildSanityStudioMock).toHaveBeenCalled()
expect(mockSpinner.start).toHaveBeenCalled()
expect(mockSpinner.succeed).toHaveBeenCalled()
})

it('does not proceed if build fails', async () => {
const mockSpinner = mockContext.output.spinner('')

helpers.dirIsEmptyOrNonExistent.mockResolvedValueOnce(true)
buildSanityStudioMock.mockResolvedValueOnce({didCompile: false})

await deployStudioAction(
{
argsWithoutOptions: ['customSourceDir'],
extOptions: {},
} as CliCommandArguments<DeployStudioActionFlags>,
mockContext,
)

expect(buildSanityStudioMock).toHaveBeenCalled()
expect(helpers.createDeployment).not.toHaveBeenCalled()
expect(mockSpinner.fail).not.toHaveBeenCalled()
})

it('fails if the directory does not exist', async () => {
const mockSpinner = mockContext.output.spinner('')

helpers.checkDir.mockRejectedValueOnce(new Error('Example error'))
helpers.dirIsEmptyOrNonExistent.mockResolvedValue(true)
buildSanityStudioMock.mockResolvedValueOnce({didCompile: true})

await expect(
deployStudioAction(
{
argsWithoutOptions: ['nonexistentDir'],
extOptions: {},
} as CliCommandArguments<DeployStudioActionFlags>,
mockContext,
),
).rejects.toThrow('Example error')

expect(mockSpinner.fail).toHaveBeenCalled()
})

it('throws an error if "graphql" is passed as a source directory', async () => {
await expect(
deployStudioAction(
{
argsWithoutOptions: ['graphql'],
extOptions: {},
} as CliCommandArguments<DeployStudioActionFlags>,
mockContext,
),
).rejects.toThrow('Did you mean `sanity graphql deploy`?')
})

it('returns an error if API responds with 402', async () => {
// Mock utility functions
helpers.dirIsEmptyOrNonExistent.mockResolvedValueOnce(true)
helpers.getInstalledSanityVersion.mockResolvedValueOnce('vX')
helpers.getOrCreateUserApplication.mockRejectedValueOnce({
statusCode: 402,
message: 'Application limit reached',
error: 'Payment Required',
})
buildSanityStudioMock.mockResolvedValueOnce({didCompile: true})
tarPackMock.mockReturnValueOnce({pipe: jest.fn().mockReturnValue('tarball')})
zlibCreateGzipMock.mockReturnValue('gzipped')

await deployStudioAction(
{
argsWithoutOptions: ['customSourceDir'],
extOptions: {},
} as CliCommandArguments<DeployStudioActionFlags>,
mockContext,
)

expect(helpers.dirIsEmptyOrNonExistent).toHaveBeenCalledWith(
expect.stringContaining('customSourceDir'),
)
expect(helpers.getOrCreateUserApplication).toHaveBeenCalledWith(
expect.objectContaining({
client: expect.anything(),
context: expect.anything(),
spinner: expect.anything(),
}),
)

expect(mockContext.output.error).toHaveBeenCalledWith('Application limit reached')
})
})
Loading
Loading