Skip to content

Commit 03b95e3

Browse files
Merge pull request #122 from contentstack/CL-1888
fix: improve handling of Launch deployment file upload size errors
2 parents 9f3a6d1 + 50a836b commit 03b95e3

File tree

4 files changed

+365
-1
lines changed

4 files changed

+365
-1
lines changed

src/adapters/base-class.test.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import BaseClass from './base-class';
22
import { cliux as ux, ContentstackClient } from '@contentstack/cli-utilities';
33
import config from '../config';
4+
import { FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE } from '../util/deployment-errors';
45

56
jest.mock('@contentstack/cli-utilities', () => ({
67
cliux: {
@@ -546,4 +547,146 @@ describe('BaseClass', () => {
546547
]);
547548
});
548549
});
550+
551+
describe('createNewDeployment', () => {
552+
let mutateMock: jest.Mock;
553+
554+
beforeEach(() => {
555+
mutateMock = jest.fn();
556+
baseClass = new BaseClass({
557+
log: logMock,
558+
exit: exitMock,
559+
apolloClient: { mutate: mutateMock } as any,
560+
config: {
561+
currentConfig: { deployments: [] },
562+
},
563+
} as any);
564+
});
565+
566+
it('should log success and append deployment when mutate succeeds', async () => {
567+
const deployment = { uid: 'dep-1', status: 'PENDING' };
568+
mutateMock.mockResolvedValueOnce({ data: { deployment } });
569+
570+
await baseClass.createNewDeployment(false, 'env-uid-1');
571+
572+
expect(mutateMock).toHaveBeenCalled();
573+
expect(logMock).toHaveBeenCalledWith('Deployment process started.!', 'info');
574+
expect(baseClass.config.currentConfig.deployments).toEqual([deployment]);
575+
expect(exitMock).not.toHaveBeenCalled();
576+
});
577+
578+
it('should log file size limit message and exit when mutate fails with deployment file size error', async () => {
579+
const apolloError = {
580+
graphQLErrors: [
581+
{
582+
extensions: {
583+
exception: {
584+
messages: ['launch.DEPLOYMENT.INVALID_FILE_SIZE'],
585+
},
586+
},
587+
},
588+
],
589+
};
590+
mutateMock.mockRejectedValueOnce(apolloError);
591+
592+
await baseClass.createNewDeployment(true, 'env-uid-2', 'upload-uid-1');
593+
594+
expect(logMock).toHaveBeenCalledWith('Deployment process failed.!', 'error');
595+
expect(logMock).toHaveBeenCalledWith(apolloError, 'debug');
596+
expect(logMock).toHaveBeenCalledWith(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
597+
expect(exitMock).toHaveBeenCalledWith(1);
598+
expect(logMock).not.toHaveBeenCalledWith(apolloError, 'error');
599+
});
600+
601+
it('should log raw error and exit when mutate fails with a non file size error', async () => {
602+
const otherError = new Error('GraphQL failure');
603+
mutateMock.mockRejectedValueOnce(otherError);
604+
605+
await baseClass.createNewDeployment(false, 'env-uid-3');
606+
607+
expect(logMock).toHaveBeenCalledWith('Deployment process failed.!', 'error');
608+
expect(logMock).toHaveBeenCalledWith(otherError, 'error');
609+
expect(logMock).not.toHaveBeenCalledWith(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
610+
expect(exitMock).toHaveBeenCalledWith(1);
611+
});
612+
});
613+
614+
describe('handleNewProjectCreationError', () => {
615+
beforeEach(() => {
616+
baseClass = new BaseClass({
617+
log: logMock,
618+
exit: exitMock,
619+
config: {
620+
projectCreationRetryMaxCount: 3,
621+
},
622+
} as any);
623+
});
624+
625+
it('should log file size limit message and exit when error is launch.DEPLOYMENT.INVALID_FILE_SIZE', async () => {
626+
const apolloError = {
627+
graphQLErrors: [
628+
{
629+
extensions: {
630+
exception: {
631+
messages: ['launch.DEPLOYMENT.INVALID_FILE_SIZE'],
632+
},
633+
},
634+
},
635+
],
636+
};
637+
638+
await baseClass.handleNewProjectCreationError(apolloError);
639+
640+
expect(logMock).toHaveBeenCalledWith('New project creation failed!', 'error');
641+
expect(logMock).toHaveBeenCalledWith(apolloError, 'debug');
642+
expect(logMock).toHaveBeenCalledWith(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
643+
expect(exitMock).toHaveBeenCalledWith(1);
644+
expect(logMock).not.toHaveBeenCalledWith(apolloError, 'error');
645+
});
646+
647+
it('should log file size limit message and exit when error is launch.DEPLOYMENT.FILE_UPLOAD_FAILED in errorObject', async () => {
648+
const apolloError = {
649+
graphQLErrors: [
650+
{
651+
extensions: {
652+
exception: {
653+
errorObject: {
654+
uploadUid: [{ code: 'launch.DEPLOYMENT.FILE_UPLOAD_FAILED' }],
655+
},
656+
},
657+
},
658+
},
659+
],
660+
};
661+
662+
await baseClass.handleNewProjectCreationError(apolloError);
663+
664+
expect(logMock).toHaveBeenCalledWith('New project creation failed!', 'error');
665+
expect(logMock).toHaveBeenCalledWith(apolloError, 'debug');
666+
expect(logMock).toHaveBeenCalledWith(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
667+
expect(exitMock).toHaveBeenCalledWith(1);
668+
expect(logMock).not.toHaveBeenCalledWith(apolloError, 'error');
669+
});
670+
671+
it('should log raw error and exit when error is not a known handled case', async () => {
672+
const apolloError = {
673+
graphQLErrors: [
674+
{
675+
extensions: {
676+
exception: {
677+
messages: ['launch.PROJECTS.UPLOADED_FILE_NOT_FOUND_ERROR'],
678+
},
679+
},
680+
},
681+
],
682+
};
683+
684+
await baseClass.handleNewProjectCreationError(apolloError);
685+
686+
expect(logMock).toHaveBeenCalledWith('New project creation failed!', 'error');
687+
expect(logMock).toHaveBeenCalledWith(apolloError, 'error');
688+
expect(logMock).not.toHaveBeenCalledWith(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
689+
expect(exitMock).toHaveBeenCalledWith(1);
690+
});
691+
});
549692
});

src/adapters/base-class.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { writeFileSync, existsSync, readFileSync } from 'fs';
1919
import { cliux as ux, ContentstackClient } from '@contentstack/cli-utilities';
2020

2121
import { print, GraphqlApiClient, LogPolling, getOrganizations } from '../util';
22+
import { FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, isLaunchDeploymentFileSizeRelatedError } from '../util/deployment-errors';
2223
import {
2324
branchesQuery,
2425
frameworkQuery,
@@ -106,7 +107,12 @@ export default class BaseClass {
106107
})
107108
.catch((error) => {
108109
this.log('Deployment process failed.!', 'error');
109-
this.log(error, 'error');
110+
if (isLaunchDeploymentFileSizeRelatedError(error)) {
111+
this.log(error, 'debug');
112+
this.log(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
113+
} else {
114+
this.log(error, 'error');
115+
}
110116
this.exit(1);
111117
});
112118
}
@@ -748,6 +754,9 @@ export default class BaseClass {
748754
}
749755
} else if (includes(error?.graphQLErrors?.[0]?.extensions?.exception?.messages, 'launch.PROJECTS.LIMIT_REACHED')) {
750756
this.log('Launch project limit reached!', 'error');
757+
} else if (isLaunchDeploymentFileSizeRelatedError(error)) {
758+
this.log(error, 'debug');
759+
this.log(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE, 'error');
751760
} else {
752761
this.log(error, 'error');
753762
}

src/util/deployment-errors.test.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import {
2+
collectLaunchDeploymentErrorCodesFromGraphQLError,
3+
FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE,
4+
isLaunchDeploymentFileSizeRelatedError,
5+
} from './deployment-errors';
6+
7+
describe('deployment-errors', () => {
8+
it('should expose user message aligned with Launch file upload rules', () => {
9+
expect(FILE_UPLOAD_SIZE_LIMIT_USER_MESSAGE).toBe(
10+
'Please use a file over the size of 1KB and under the size of 100MB.',
11+
);
12+
});
13+
14+
it('should collect codes from messages, uploadUid errorObject, and cause graphQLErrors in one flow', () => {
15+
const error = {
16+
graphQLErrors: [
17+
{
18+
extensions: {
19+
exception: {
20+
messages: ['launch.DEPLOYMENT.INVALID_FILE_SIZE', 'other'],
21+
errorObject: {
22+
uploadUid: [{ code: 'launch.DEPLOYMENT.FILE_UPLOAD_FAILED' }],
23+
},
24+
},
25+
},
26+
},
27+
],
28+
cause: {
29+
graphQLErrors: [
30+
{
31+
extensions: {
32+
exception: {
33+
messages: ['launch.OTHER.CODE'],
34+
},
35+
},
36+
},
37+
],
38+
},
39+
};
40+
41+
const codes = collectLaunchDeploymentErrorCodesFromGraphQLError(error);
42+
43+
expect(codes).toEqual([
44+
'launch.DEPLOYMENT.INVALID_FILE_SIZE',
45+
'other',
46+
'launch.DEPLOYMENT.FILE_UPLOAD_FAILED',
47+
'launch.OTHER.CODE',
48+
]);
49+
expect(isLaunchDeploymentFileSizeRelatedError(error)).toBe(true);
50+
});
51+
52+
it('should return false when graphQL errors have no file size related codes', () => {
53+
const error = {
54+
graphQLErrors: [
55+
{
56+
extensions: {
57+
exception: {
58+
messages: ['launch.PROJECTS.DUPLICATE_NAME'],
59+
},
60+
},
61+
},
62+
],
63+
};
64+
65+
expect(collectLaunchDeploymentErrorCodesFromGraphQLError(error)).toEqual(['launch.PROJECTS.DUPLICATE_NAME']);
66+
expect(isLaunchDeploymentFileSizeRelatedError(error)).toBe(false);
67+
});
68+
69+
it('should return false for empty or malformed error input', () => {
70+
expect(collectLaunchDeploymentErrorCodesFromGraphQLError(undefined)).toEqual([]);
71+
expect(isLaunchDeploymentFileSizeRelatedError(undefined)).toBe(false);
72+
expect(collectLaunchDeploymentErrorCodesFromGraphQLError({})).toEqual([]);
73+
expect(isLaunchDeploymentFileSizeRelatedError({})).toBe(false);
74+
expect(
75+
collectLaunchDeploymentErrorCodesFromGraphQLError({
76+
graphQLErrors: [{ extensions: {} }],
77+
}),
78+
).toEqual([]);
79+
expect(
80+
isLaunchDeploymentFileSizeRelatedError({
81+
graphQLErrors: [{ extensions: {} }],
82+
}),
83+
).toBe(false);
84+
});
85+
86+
it('should skip non-string entries in messages and still collect string codes', () => {
87+
const error = {
88+
graphQLErrors: [
89+
{
90+
extensions: {
91+
exception: {
92+
messages: [
93+
null,
94+
42,
95+
{ nested: 'launch.DEPLOYMENT.INVALID_FILE_SIZE' },
96+
'launch.DEPLOYMENT.INVALID_FILE_SIZE',
97+
'',
98+
' ',
99+
'\t',
100+
],
101+
},
102+
},
103+
},
104+
],
105+
};
106+
107+
const codes = collectLaunchDeploymentErrorCodesFromGraphQLError(error);
108+
109+
expect(codes).toEqual(['launch.DEPLOYMENT.INVALID_FILE_SIZE']);
110+
expect(isLaunchDeploymentFileSizeRelatedError(error)).toBe(true);
111+
});
112+
113+
it('should skip uploadUid entries without a string code and still collect valid codes', () => {
114+
const error = {
115+
graphQLErrors: [
116+
{
117+
extensions: {
118+
exception: {
119+
errorObject: {
120+
uploadUid: [
121+
null,
122+
'not-an-object',
123+
{},
124+
{ code: 123 },
125+
{ notCode: 'launch.DEPLOYMENT.FILE_UPLOAD_FAILED' },
126+
{ code: 'launch.DEPLOYMENT.FILE_UPLOAD_FAILED' },
127+
],
128+
},
129+
},
130+
},
131+
},
132+
],
133+
};
134+
135+
const codes = collectLaunchDeploymentErrorCodesFromGraphQLError(error);
136+
137+
expect(codes).toEqual(['launch.DEPLOYMENT.FILE_UPLOAD_FAILED']);
138+
expect(isLaunchDeploymentFileSizeRelatedError(error)).toBe(true);
139+
});
140+
141+
it('should detect FILE_UPLOAD_FAILED only from uploadUid when messages are absent', () => {
142+
const error = {
143+
graphQLErrors: [
144+
{
145+
extensions: {
146+
exception: {
147+
errorObject: {
148+
uploadUid: [{ code: 'launch.DEPLOYMENT.FILE_UPLOAD_FAILED' }],
149+
},
150+
},
151+
},
152+
},
153+
],
154+
};
155+
156+
expect(isLaunchDeploymentFileSizeRelatedError(error)).toBe(true);
157+
});
158+
});

0 commit comments

Comments
 (0)