-
Notifications
You must be signed in to change notification settings - Fork 425
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): update CLI to use new deploy endpoint (#7244)
* feat(cli): update CLI to use new deploy endpoint * feat: restore streaming support * refactor: use client to stream instead of fetch * fix: use `urlType` for new API spec * fix(cli): fixes issues with deploying to new endpoint * fix(cli): fixes dependency issue * fix(core): fixes issue with depedency in tests * feat(cli): handle error statusCode from API * fix(cli): don't create default apps on deploy * feat(cli): add list of deployments on deploy * test: fix and add tests for new changes * fix: error handling and update copy * chore(cli): update copy Co-authored-by: Carolina Gonzalez <[email protected]> * feat(cli): add message about adding studioHost to cliConfig on deploys (#7349) Co-authored-by: Rune Botten <[email protected]> --------- Co-authored-by: Binoy Patel <[email protected]> Co-authored-by: Carolina Gonzalez <[email protected]> Co-authored-by: Carolina Gonzalez <[email protected]> Co-authored-by: Rune Botten <[email protected]>
- Loading branch information
1 parent
a361063
commit 14ae5cb
Showing
9 changed files
with
1,273 additions
and
117 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
295 changes: 295 additions & 0 deletions
295
packages/sanity/src/_internal/cli/actions/deploy/__tests__/deployAction.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) | ||
}) |
Oops, something went wrong.