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
143 changes: 143 additions & 0 deletions src/adapters/base-class.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import BaseClass from './base-class';
import { cliux as ux, ContentstackClient } from '@contentstack/cli-utilities';
import config from '../config';
import { FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE } from '../util/deployment-errors';

jest.mock('@contentstack/cli-utilities', () => ({
cliux: {
Expand Down Expand Up @@ -546,4 +547,146 @@ describe('BaseClass', () => {
]);
});
});

describe('createNewDeployment', () => {
let mutateMock: jest.Mock;

beforeEach(() => {
mutateMock = jest.fn();
baseClass = new BaseClass({
log: logMock,
exit: exitMock,
apolloClient: { mutate: mutateMock } as any,
config: {
currentConfig: { deployments: [] },
},
} as any);
});

it('should log success and append deployment when mutate succeeds', async () => {
const deployment = { uid: 'dep-1', status: 'PENDING' };
mutateMock.mockResolvedValueOnce({ data: { deployment } });

await baseClass.createNewDeployment(false, 'env-uid-1');

expect(mutateMock).toHaveBeenCalled();
expect(logMock).toHaveBeenCalledWith('Deployment process started.!', 'info');
expect(baseClass.config.currentConfig.deployments).toEqual([deployment]);
expect(exitMock).not.toHaveBeenCalled();
});

it('should log file size limit message and exit when mutate fails with deployment file size error', async () => {
const apolloError = {
graphQLErrors: [
{
extensions: {
exception: {
messages: ['launch.DEPLOYMENT.INVALID_FILE_SIZE'],
},
},
},
],
};
mutateMock.mockRejectedValueOnce(apolloError);

await baseClass.createNewDeployment(true, 'env-uid-2', 'upload-uid-1');

expect(logMock).toHaveBeenCalledWith('Deployment process failed.!', 'error');
expect(logMock).toHaveBeenCalledWith(apolloError, 'debug');
expect(logMock).toHaveBeenCalledWith(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
expect(exitMock).toHaveBeenCalledWith(1);
expect(logMock).not.toHaveBeenCalledWith(apolloError, 'error');
});

it('should log raw error and exit when mutate fails with a non file size error', async () => {
const otherError = new Error('GraphQL failure');
mutateMock.mockRejectedValueOnce(otherError);

await baseClass.createNewDeployment(false, 'env-uid-3');

expect(logMock).toHaveBeenCalledWith('Deployment process failed.!', 'error');
expect(logMock).toHaveBeenCalledWith(otherError, 'error');
expect(logMock).not.toHaveBeenCalledWith(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
expect(exitMock).toHaveBeenCalledWith(1);
});
});

describe('handleNewProjectCreationError', () => {
beforeEach(() => {
baseClass = new BaseClass({
log: logMock,
exit: exitMock,
config: {
projectCreationRetryMaxCount: 3,
},
} as any);
});

it('should log file size limit message and exit when error is launch.DEPLOYMENT.INVALID_FILE_SIZE', async () => {
const apolloError = {
graphQLErrors: [
{
extensions: {
exception: {
messages: ['launch.DEPLOYMENT.INVALID_FILE_SIZE'],
},
},
},
],
};

await baseClass.handleNewProjectCreationError(apolloError);

expect(logMock).toHaveBeenCalledWith('New project creation failed!', 'error');
expect(logMock).toHaveBeenCalledWith(apolloError, 'debug');
expect(logMock).toHaveBeenCalledWith(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
expect(exitMock).toHaveBeenCalledWith(1);
expect(logMock).not.toHaveBeenCalledWith(apolloError, 'error');
});

it('should log file size limit message and exit when error is launch.DEPLOYMENT.FILE_UPLOAD_FAILED in errorObject', async () => {
const apolloError = {
graphQLErrors: [
{
extensions: {
exception: {
errorObject: {
uploadUid: [{ code: 'launch.DEPLOYMENT.FILE_UPLOAD_FAILED' }],
},
},
},
},
],
};

await baseClass.handleNewProjectCreationError(apolloError);

expect(logMock).toHaveBeenCalledWith('New project creation failed!', 'error');
expect(logMock).toHaveBeenCalledWith(apolloError, 'debug');
expect(logMock).toHaveBeenCalledWith(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
expect(exitMock).toHaveBeenCalledWith(1);
expect(logMock).not.toHaveBeenCalledWith(apolloError, 'error');
});

it('should log raw error and exit when error is not a known handled case', async () => {
const apolloError = {
graphQLErrors: [
{
extensions: {
exception: {
messages: ['launch.PROJECTS.UPLOADED_FILE_NOT_FOUND_ERROR'],
},
},
},
],
};

await baseClass.handleNewProjectCreationError(apolloError);

expect(logMock).toHaveBeenCalledWith('New project creation failed!', 'error');
expect(logMock).toHaveBeenCalledWith(apolloError, 'error');
expect(logMock).not.toHaveBeenCalledWith(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
expect(exitMock).toHaveBeenCalledWith(1);
});
});
});
11 changes: 10 additions & 1 deletion src/adapters/base-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { writeFileSync, existsSync, readFileSync } from 'fs';
import { cliux as ux, ContentstackClient } from '@contentstack/cli-utilities';
Comment thread
Harshi-Shah-CS marked this conversation as resolved.

import { print, GraphqlApiClient, LogPolling, getOrganizations } from '../util';
import { FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, isLaunchDeploymentFileSizeRelatedError } from '../util/deployment-errors';
import {
branchesQuery,
frameworkQuery,
Expand Down Expand Up @@ -106,7 +107,12 @@ export default class BaseClass {
})
.catch((error) => {
this.log('Deployment process failed.!', 'error');
this.log(error, 'error');
if (isLaunchDeploymentFileSizeRelatedError(error)) {
this.log(error, 'debug');
this.log(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
} else {
this.log(error, 'error');
Comment thread
Harshi-Shah-CS marked this conversation as resolved.
}
this.exit(1);
});
}
Expand Down Expand Up @@ -748,6 +754,9 @@ export default class BaseClass {
}
} else if (includes(error?.graphQLErrors?.[0]?.extensions?.exception?.messages, 'launch.PROJECTS.LIMIT_REACHED')) {
this.log('Launch project limit reached!', 'error');
} else if (isLaunchDeploymentFileSizeRelatedError(error)) {
this.log(error, 'debug');
this.log(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
} else {
this.log(error, 'error');
}
Expand Down
158 changes: 158 additions & 0 deletions src/util/deployment-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {
collectLaunchDeploymentErrorCodesFromGraphQLError,
FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE,
isLaunchDeploymentFileSizeRelatedError,
} from './deployment-errors';

describe('deployment-errors', () => {
it('should expose user message aligned with Launch file upload rules', () => {
expect(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE).toBe(
'Please use a file over the size of 1KB and under the size of 100MB.',
);
});

it('should collect codes from messages, uploadUid errorObject, and cause graphQLErrors in one flow', () => {
const error = {
graphQLErrors: [
{
extensions: {
exception: {
messages: ['launch.DEPLOYMENT.INVALID_FILE_SIZE', 'other'],
errorObject: {
uploadUid: [{ code: 'launch.DEPLOYMENT.FILE_UPLOAD_FAILED' }],
},
},
},
},
],
cause: {
graphQLErrors: [
{
extensions: {
exception: {
messages: ['launch.OTHER.CODE'],
},
},
},
],
},
};

const codes = collectLaunchDeploymentErrorCodesFromGraphQLError(error);

expect(codes).toEqual([
'launch.DEPLOYMENT.INVALID_FILE_SIZE',
'other',
'launch.DEPLOYMENT.FILE_UPLOAD_FAILED',
'launch.OTHER.CODE',
]);
expect(isLaunchDeploymentFileSizeRelatedError(error)).toBe(true);
});

it('should return false when graphQL errors have no file size related codes', () => {
const error = {
graphQLErrors: [
{
extensions: {
exception: {
messages: ['launch.PROJECTS.DUPLICATE_NAME'],
},
},
},
],
};

expect(collectLaunchDeploymentErrorCodesFromGraphQLError(error)).toEqual(['launch.PROJECTS.DUPLICATE_NAME']);
expect(isLaunchDeploymentFileSizeRelatedError(error)).toBe(false);
});

it('should return false for empty or malformed error input', () => {
expect(collectLaunchDeploymentErrorCodesFromGraphQLError(undefined)).toEqual([]);
expect(isLaunchDeploymentFileSizeRelatedError(undefined)).toBe(false);
expect(collectLaunchDeploymentErrorCodesFromGraphQLError({})).toEqual([]);
expect(isLaunchDeploymentFileSizeRelatedError({})).toBe(false);
expect(
collectLaunchDeploymentErrorCodesFromGraphQLError({
graphQLErrors: [{ extensions: {} }],
}),
).toEqual([]);
expect(
isLaunchDeploymentFileSizeRelatedError({
graphQLErrors: [{ extensions: {} }],
}),
).toBe(false);
});

it('should skip non-string entries in messages and still collect string codes', () => {
const error = {
graphQLErrors: [
{
extensions: {
exception: {
messages: [
null,
42,
{ nested: 'launch.DEPLOYMENT.INVALID_FILE_SIZE' },
'launch.DEPLOYMENT.INVALID_FILE_SIZE',
'',
' ',
'\t',
],
},
},
},
],
};

const codes = collectLaunchDeploymentErrorCodesFromGraphQLError(error);

expect(codes).toEqual(['launch.DEPLOYMENT.INVALID_FILE_SIZE']);
expect(isLaunchDeploymentFileSizeRelatedError(error)).toBe(true);
});

it('should skip uploadUid entries without a string code and still collect valid codes', () => {
const error = {
graphQLErrors: [
{
extensions: {
exception: {
errorObject: {
uploadUid: [
null,
'not-an-object',
{},
{ code: 123 },
{ notCode: 'launch.DEPLOYMENT.FILE_UPLOAD_FAILED' },
{ code: 'launch.DEPLOYMENT.FILE_UPLOAD_FAILED' },
],
},
},
},
},
],
};

const codes = collectLaunchDeploymentErrorCodesFromGraphQLError(error);

expect(codes).toEqual(['launch.DEPLOYMENT.FILE_UPLOAD_FAILED']);
expect(isLaunchDeploymentFileSizeRelatedError(error)).toBe(true);
});

it('should detect FILE_UPLOAD_FAILED only from uploadUid when messages are absent', () => {
const error = {
graphQLErrors: [
{
extensions: {
exception: {
errorObject: {
uploadUid: [{ code: 'launch.DEPLOYMENT.FILE_UPLOAD_FAILED' }],
},
},
},
},
],
};

expect(isLaunchDeploymentFileSizeRelatedError(error)).toBe(true);
});
});
Loading