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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
- For the happy path, can we have a single unit test where all the top level if conditions are executed? This might help with reducing the number of total unit tests created and still give same test coverage.
- For the tests for edge cases do not create separate describe blocks, keep the hierarchy flat.
- For the tests for edge cases, do not skip assertions, its still worth adding all assertions similar to the happy paths tests.
- Use only jest for writing test cases and refer existing unit test under the /src folder.
- Do not create code comments for any changes.

319 changes: 319 additions & 0 deletions src/adapters/file-upload.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
import FileUpload from './file-upload';
import BaseClass from './base-class';
import { cliux } from '@contentstack/cli-utilities';
import { DeploymentStatus } from '../types/launch';

jest.mock('@contentstack/cli-utilities', () => ({
...jest.requireActual('@contentstack/cli-utilities'),
cliux: {
inquire: jest.fn(),
loader: jest.fn(),
print: jest.fn(),
},
configHandler: {
get: jest.fn(),
},
HttpClient: jest.fn(),
}));

describe('FileUpload Adapter', () => {
let logMock: jest.Mock;
let exitMock: jest.Mock;

beforeEach(() => {
logMock = jest.fn();
exitMock = jest.fn().mockImplementation(() => {
throw new Error('1');
});
});

afterEach(() => {
jest.resetAllMocks();
});

describe('runFileUploadFlow', () => {
let getEnvironmentMock: jest.SpyInstance;
let handleExistingProjectMock: jest.SpyInstance;
let handleNewProjectMock: jest.SpyInstance;
let prepareLaunchConfigMock: jest.SpyInstance;
let showLogsMock: jest.SpyInstance;
let showDeploymentUrlMock: jest.SpyInstance;
let showSuggestionMock: jest.SpyInstance;

beforeEach(() => {
getEnvironmentMock = jest
.spyOn(BaseClass.prototype, 'getEnvironment')
.mockResolvedValue({ uid: 'env-123', name: 'Default', frameworkPreset: 'OTHER' });
handleExistingProjectMock = jest
.spyOn(FileUpload.prototype as any, 'handleExistingProject')
.mockResolvedValue(undefined);
handleNewProjectMock = jest
.spyOn(FileUpload.prototype as any, 'handleNewProject')
.mockResolvedValue(undefined);
prepareLaunchConfigMock = jest.spyOn(BaseClass.prototype, 'prepareLaunchConfig').mockImplementation(() => {});
showLogsMock = jest.spyOn(BaseClass.prototype, 'showLogs').mockResolvedValue(true);
showDeploymentUrlMock = jest.spyOn(BaseClass.prototype, 'showDeploymentUrl').mockImplementation(() => {});
showSuggestionMock = jest.spyOn(BaseClass.prototype, 'showSuggestion').mockImplementation(() => {});
});

afterEach(() => {
getEnvironmentMock.mockRestore();
handleExistingProjectMock.mockRestore();
handleNewProjectMock.mockRestore();
prepareLaunchConfigMock.mockRestore();
showLogsMock.mockRestore();
showDeploymentUrlMock.mockRestore();
showSuggestionMock.mockRestore();
});

it('should exit with code 1 when deployment status is FAILED for existing project', async () => {
const fileUploadInstance = new FileUpload({
config: {
isExistingProject: true,
currentDeploymentStatus: DeploymentStatus.FAILED,
},
log: logMock,
exit: exitMock,
} as any);

try {
await fileUploadInstance.run();
} catch (error: any) {
expect(error.message).toBe('1');
}

expect(getEnvironmentMock).toHaveBeenCalled();
expect(handleExistingProjectMock).toHaveBeenCalled();
expect(prepareLaunchConfigMock).toHaveBeenCalled();
expect(showLogsMock).toHaveBeenCalled();
expect(exitMock).toHaveBeenCalledWith(1);
expect(showDeploymentUrlMock).not.toHaveBeenCalled();
expect(showSuggestionMock).not.toHaveBeenCalled();
});

it('should exit with code 1 when deployment status is FAILED for new project', async () => {
const fileUploadInstance = new FileUpload({
config: {
isExistingProject: false,
currentDeploymentStatus: DeploymentStatus.FAILED,
},
log: logMock,
exit: exitMock,
} as any);

try {
await fileUploadInstance.run();
} catch (error: any) {
expect(error.message).toBe('1');
}

expect(handleNewProjectMock).toHaveBeenCalled();
expect(prepareLaunchConfigMock).toHaveBeenCalled();
expect(showLogsMock).toHaveBeenCalled();
expect(exitMock).toHaveBeenCalledWith(1);
expect(showDeploymentUrlMock).not.toHaveBeenCalled();
expect(showSuggestionMock).not.toHaveBeenCalled();
});

it('should continue normally when deployment status is not FAILED', async () => {
const fileUploadInstance = new FileUpload({
config: {
isExistingProject: true,
currentDeploymentStatus: DeploymentStatus.LIVE,
},
log: logMock,
exit: exitMock,
} as any);

await fileUploadInstance.run();

expect(getEnvironmentMock).toHaveBeenCalled();
expect(handleExistingProjectMock).toHaveBeenCalled();
expect(prepareLaunchConfigMock).toHaveBeenCalled();
expect(showLogsMock).toHaveBeenCalled();
expect(exitMock).not.toHaveBeenCalled();
expect(showDeploymentUrlMock).toHaveBeenCalled();
expect(showSuggestionMock).toHaveBeenCalled();
});
});

describe('prepareAndUploadNewProjectFile', () => {
let selectOrgMock: jest.SpyInstance;
let createSignedUploadUrlMock: jest.SpyInstance;
let archiveMock: jest.SpyInstance;
let uploadFileMock: jest.SpyInstance;
let detectFrameworkMock: jest.SpyInstance;
let handleEnvImportFlowMock: jest.SpyInstance;

beforeEach(() => {
selectOrgMock = jest.spyOn(BaseClass.prototype, 'selectOrg').mockResolvedValue();
createSignedUploadUrlMock = jest.spyOn(FileUpload.prototype, 'createSignedUploadUrl').mockResolvedValue({
uploadUid: 'upload-123',
uploadUrl: 'https://example.com/upload',
fields: [],
headers: [],
method: 'PUT',
});
archiveMock = jest.spyOn(FileUpload.prototype, 'archive').mockResolvedValue({
zipName: 'test.zip',
zipPath: '/path/to/test.zip',
projectName: 'test-project',
});
uploadFileMock = jest.spyOn(FileUpload.prototype, 'uploadFile').mockResolvedValue();
detectFrameworkMock = jest.spyOn(BaseClass.prototype, 'detectFramework').mockResolvedValue();
handleEnvImportFlowMock = jest.spyOn(BaseClass.prototype, 'handleEnvImportFlow').mockResolvedValue();
});

afterEach(() => {
selectOrgMock.mockRestore();
createSignedUploadUrlMock.mockRestore();
archiveMock.mockRestore();
uploadFileMock.mockRestore();
detectFrameworkMock.mockRestore();
handleEnvImportFlowMock.mockRestore();
});

it('should prompt for server command when framework supports it and serverCommand is not provided', async () => {
(cliux.inquire as jest.Mock).mockResolvedValueOnce('test-project'); // projectName
(cliux.inquire as jest.Mock).mockResolvedValueOnce('Default'); // environmentName
(cliux.inquire as jest.Mock).mockResolvedValueOnce('npm run build'); // buildCommand
(cliux.inquire as jest.Mock).mockResolvedValueOnce('./dist'); // outputDirectory
(cliux.inquire as jest.Mock).mockResolvedValueOnce('npm start'); // serverCommand

const fileUploadInstance = new FileUpload({
config: {
flags: {
'server-command': undefined,
},
framework: 'OTHER',
supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX', 'NUXT'],
outputDirectories: { OTHER: './dist' },
},
log: logMock,
exit: exitMock,
} as any);

await fileUploadInstance.prepareAndUploadNewProjectFile();

expect(cliux.inquire).toHaveBeenCalledWith({
type: 'input',
name: 'serverCommand',
message: 'Server Command',
});
expect(fileUploadInstance.config.serverCommand).toBe('npm start');
});

it('should not prompt for server command when framework does not support it', async () => {
(cliux.inquire as jest.Mock).mockResolvedValueOnce('test-project');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('Default');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('npm run build');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('./dist');

const fileUploadInstance = new FileUpload({
config: {
flags: {
'server-command': undefined,
},
framework: 'GATSBY',
supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX', 'NUXT'],
outputDirectories: { GATSBY: './public' },
},
log: logMock,
exit: exitMock,
} as any);

await fileUploadInstance.prepareAndUploadNewProjectFile();

const serverCommandCalls = (cliux.inquire as jest.Mock).mock.calls.filter(
(call) => call[0]?.name === 'serverCommand',
);
expect(serverCommandCalls.length).toBe(0);
});

it('should not prompt for server command when serverCommand is already provided in flags', async () => {
(cliux.inquire as jest.Mock).mockResolvedValueOnce('test-project');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('Default');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('npm run build');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('./dist');

const fileUploadInstance = new FileUpload({
config: {
flags: {
'server-command': 'npm run serve',
},
framework: 'OTHER',
supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX', 'NUXT'],
outputDirectories: { OTHER: './dist' },
},
log: logMock,
exit: exitMock,
} as any);

await fileUploadInstance.prepareAndUploadNewProjectFile();

const serverCommandCalls = (cliux.inquire as jest.Mock).mock.calls.filter(
(call) => call[0]?.name === 'serverCommand',
);
expect(serverCommandCalls.length).toBe(0);
expect(fileUploadInstance.config.flags['server-command']).toBe('npm run serve');
});

it('should not set serverCommand when user provides empty input', async () => {
(cliux.inquire as jest.Mock).mockResolvedValueOnce('test-project');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('Default');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('npm run build');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('./dist');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('');

const fileUploadInstance = new FileUpload({
config: {
flags: {
'server-command': undefined,
},
framework: 'OTHER',
supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX', 'NUXT'],
outputDirectories: { OTHER: './dist' },
},
log: logMock,
exit: exitMock,
} as any);

await fileUploadInstance.prepareAndUploadNewProjectFile();

expect(cliux.inquire).toHaveBeenCalledWith({
type: 'input',
name: 'serverCommand',
message: 'Server Command',
});
expect(fileUploadInstance.config.serverCommand).toBeUndefined();
});

it('should not prompt for server command when framework is not set', async () => {
(cliux.inquire as jest.Mock).mockResolvedValueOnce('test-project');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('Default');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('npm run build');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('./dist');

const fileUploadInstance = new FileUpload({
config: {
flags: {
'server-command': undefined,
},
framework: undefined,
supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX', 'NUXT'],
outputDirectories: { OTHER: './dist' },
},
log: logMock,
exit: exitMock,
} as any);

await fileUploadInstance.prepareAndUploadNewProjectFile();

const serverCommandCalls = (cliux.inquire as jest.Mock).mock.calls.filter(
(call) => call[0]?.name === 'serverCommand',
);
expect(serverCommandCalls.length).toBe(0);
});
});
});

15 changes: 10 additions & 5 deletions src/adapters/file-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export default class FileUpload extends BaseClass {
name: environmentName || 'Default',
environmentVariables: map(this.envVariables, ({ key, value }) => ({ key, value })),
buildCommand: buildCommand === undefined || buildCommand === null ? 'npm run build' : buildCommand,
serverCommand: serverCommand === undefined || serverCommand === null ? 'npm run start' : serverCommand,
...(serverCommand && serverCommand.trim() !== '' ? { serverCommand } : {}),
},
},
skipGitData: true,
Expand Down Expand Up @@ -226,13 +226,18 @@ export default class FileUpload extends BaseClass {
default: (this.config.outputDirectories as Record<string, string>)[this.config?.framework || 'OTHER'],
}));
if (this.config.framework && this.config.supportedFrameworksForServerCommands.includes(this.config.framework)) {
this.config.serverCommand =
serverCommand ||
(await cliux.inquire({
if (!serverCommand) {
const serverCommandInput = await cliux.inquire({
type: 'input',
name: 'serverCommand',
message: 'Server Command',
}));
});
if (serverCommandInput) {
this.config.serverCommand = serverCommandInput;
}
} else {
this.config.serverCommand = serverCommand;
}
}
this.config.variableType = variableType as unknown as string;
this.config.envVariables = envVariables;
Expand Down
Loading