Skip to content

Commit 92c59cb

Browse files
fix: advanced-settings api should not camel-case return value (backport) (#2087)
* fix: advanced-settings api should not camel-case return value (#1581) * fix: update advanced module list not working (#2189) Backend was still expecting `{'advanced_modules', {'value': ['poll', 'problem-builder', 'h5pxblock']}}` but without this change, it was receiving `{'advancedModules', ['poll', 'problem-builder', 'h5pxblock']}` Follow up to #1581 --------- Co-authored-by: Muhammad Faraz Maqsood <[email protected]>
1 parent b6bd94c commit 92c59cb

File tree

2 files changed

+281
-4
lines changed

2 files changed

+281
-4
lines changed

src/advanced-settings/data/api.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
1+
/* eslint-disable import/prefer-default-export */
2+
import {
3+
camelCaseObject,
4+
getConfig,
5+
} from '@edx/frontend-platform';
26
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
7+
import { camelCase } from 'lodash';
38
import { convertObjectToSnakeCase } from '../../utils';
49

510
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
@@ -14,7 +19,19 @@ const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/
1419
export async function getCourseAdvancedSettings(courseId) {
1520
const { data } = await getAuthenticatedHttpClient()
1621
.get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`);
17-
return camelCaseObject(data);
22+
const keepValues = {};
23+
Object.keys(data).forEach((key) => {
24+
keepValues[camelCase(key)] = { value: data[key].value };
25+
});
26+
const formattedData = {};
27+
const formattedCamelCaseData = camelCaseObject(data);
28+
Object.keys(formattedCamelCaseData).forEach((key) => {
29+
formattedData[key] = {
30+
...formattedCamelCaseData[key],
31+
value: keepValues[key]?.value,
32+
};
33+
});
34+
return formattedData;
1835
}
1936

2037
/**
@@ -26,7 +43,19 @@ export async function getCourseAdvancedSettings(courseId) {
2643
export async function updateCourseAdvancedSettings(courseId, settings) {
2744
const { data } = await getAuthenticatedHttpClient()
2845
.patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings));
29-
return camelCaseObject(data);
46+
const keepValues = {};
47+
Object.keys(data).forEach((key) => {
48+
keepValues[camelCase(key)] = { value: data[key].value };
49+
});
50+
const formattedData = {};
51+
const formattedCamelCaseData = camelCaseObject(data);
52+
Object.keys(formattedCamelCaseData).forEach((key) => {
53+
formattedData[key] = {
54+
...formattedCamelCaseData[key],
55+
value: keepValues[key]?.value,
56+
};
57+
});
58+
return formattedData;
3059
}
3160

3261
/**
@@ -36,5 +65,17 @@ export async function updateCourseAdvancedSettings(courseId, settings) {
3665
*/
3766
export async function getProctoringExamErrors(courseId) {
3867
const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`);
39-
return camelCaseObject(data);
68+
const keepValues = {};
69+
Object.keys(data).forEach((key) => {
70+
keepValues[camelCase(key)] = { value: data[key].value };
71+
});
72+
const formattedData = {};
73+
const formattedCamelCaseData = camelCaseObject(data);
74+
Object.keys(formattedCamelCaseData).forEach((key) => {
75+
formattedData[key] = {
76+
...formattedCamelCaseData[key],
77+
value: keepValues[key]?.value,
78+
};
79+
});
80+
return formattedData;
4081
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
2+
import {
3+
getCourseAdvancedSettings,
4+
updateCourseAdvancedSettings,
5+
getProctoringExamErrors,
6+
} from './api';
7+
8+
jest.mock('@edx/frontend-platform/auth', () => ({
9+
getAuthenticatedHttpClient: jest.fn(),
10+
}));
11+
12+
describe('courseSettings API', () => {
13+
const mockHttpClient = {
14+
get: jest.fn(),
15+
patch: jest.fn(),
16+
};
17+
18+
beforeEach(() => {
19+
jest.clearAllMocks();
20+
getAuthenticatedHttpClient.mockReturnValue(mockHttpClient);
21+
});
22+
23+
describe('getCourseAdvancedSettings', () => {
24+
it('should fetch and unformat course advanced settings', async () => {
25+
const fakeData = {
26+
key_snake_case: {
27+
display_name: 'To come camelCase',
28+
testCamelCase: 'This key must not be formatted',
29+
PascalCase: 'To come camelCase',
30+
'kebab-case': 'To come camelCase',
31+
UPPER_CASE: 'To come camelCase',
32+
lowercase: 'This key must not be formatted',
33+
UPPERCASE: 'To come lowercase',
34+
'Title Case': 'To come camelCase',
35+
'dot.case': 'To come camelCase',
36+
SCREAMING_SNAKE_CASE: 'To come camelCase',
37+
MixedCase: 'To come camelCase',
38+
'Train-Case': 'To come camelCase',
39+
nestedOption: {
40+
anotherOption: 'To come camelCase',
41+
},
42+
// value is an object with various cases
43+
// this contain must not be formatted to camelCase
44+
value: {
45+
snake_case: 'snake_case',
46+
camelCase: 'camelCase',
47+
PascalCase: 'PascalCase',
48+
'kebab-case': 'kebab-case',
49+
UPPER_CASE: 'UPPER_CASE',
50+
lowercase: 'lowercase',
51+
UPPERCASE: 'UPPERCASE',
52+
'Title Case': 'Title Case',
53+
'dot.case': 'dot.case',
54+
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
55+
MixedCase: 'MixedCase',
56+
'Train-Case': 'Train-Case',
57+
nestedOption: {
58+
anotherOption: 'nestedContent',
59+
},
60+
},
61+
},
62+
};
63+
const expected = {
64+
keySnakeCase: {
65+
displayName: 'To come camelCase',
66+
testCamelCase: 'This key must not be formatted',
67+
pascalCase: 'To come camelCase',
68+
kebabCase: 'To come camelCase',
69+
upperCase: 'To come camelCase',
70+
lowercase: 'This key must not be formatted',
71+
uppercase: 'To come lowercase',
72+
titleCase: 'To come camelCase',
73+
dotCase: 'To come camelCase',
74+
screamingSnakeCase: 'To come camelCase',
75+
mixedCase: 'To come camelCase',
76+
trainCase: 'To come camelCase',
77+
nestedOption: {
78+
anotherOption: 'To come camelCase',
79+
},
80+
value: fakeData.key_snake_case.value,
81+
},
82+
};
83+
84+
mockHttpClient.get.mockResolvedValue({ data: fakeData });
85+
86+
const result = await getCourseAdvancedSettings('course-v1:Test+T101+2024');
87+
expect(mockHttpClient.get).toHaveBeenCalledWith(
88+
`${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024?fetch_all=0`,
89+
);
90+
expect(result).toEqual(expected);
91+
});
92+
});
93+
94+
describe('updateCourseAdvancedSettings', () => {
95+
it('should update and unformat course advanced settings', async () => {
96+
const fakeData = {
97+
key_snake_case: {
98+
display_name: 'To come camelCase',
99+
testCamelCase: 'This key must not be formatted', // because already be camelCase
100+
PascalCase: 'To come camelCase',
101+
'kebab-case': 'To come camelCase',
102+
UPPER_CASE: 'To come camelCase',
103+
lowercase: 'This key must not be formatted', // because camelCase in lowercase not formatted
104+
UPPERCASE: 'To come lowercase', // because camelCase in UPPERCASE format to lowercase
105+
'Title Case': 'To come camelCase',
106+
'dot.case': 'To come camelCase',
107+
SCREAMING_SNAKE_CASE: 'To come camelCase',
108+
MixedCase: 'To come camelCase',
109+
'Train-Case': 'To come camelCase',
110+
nestedOption: {
111+
anotherOption: 'To come camelCase',
112+
},
113+
// value is an object with various cases
114+
// this contain must not be formatted to camelCase
115+
value: {
116+
snake_case: 'snake_case',
117+
camelCase: 'camelCase',
118+
PascalCase: 'PascalCase',
119+
'kebab-case': 'kebab-case',
120+
UPPER_CASE: 'UPPER_CASE',
121+
lowercase: 'lowercase',
122+
UPPERCASE: 'UPPERCASE',
123+
'Title Case': 'Title Case',
124+
'dot.case': 'dot.case',
125+
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
126+
MixedCase: 'MixedCase',
127+
'Train-Case': 'Train-Case',
128+
nestedOption: {
129+
anotherOption: 'nestedContent',
130+
},
131+
},
132+
},
133+
};
134+
const expected = {
135+
keySnakeCase: {
136+
displayName: 'To come camelCase',
137+
testCamelCase: 'This key must not be formatted',
138+
pascalCase: 'To come camelCase',
139+
kebabCase: 'To come camelCase',
140+
upperCase: 'To come camelCase',
141+
lowercase: 'This key must not be formatted',
142+
uppercase: 'To come lowercase',
143+
titleCase: 'To come camelCase',
144+
dotCase: 'To come camelCase',
145+
screamingSnakeCase: 'To come camelCase',
146+
mixedCase: 'To come camelCase',
147+
trainCase: 'To come camelCase',
148+
nestedOption: {
149+
anotherOption: 'To come camelCase',
150+
},
151+
value: fakeData.key_snake_case.value,
152+
},
153+
};
154+
155+
mockHttpClient.patch.mockResolvedValue({ data: fakeData });
156+
157+
const result = await updateCourseAdvancedSettings('course-v1:Test+T101+2024', {});
158+
expect(mockHttpClient.patch).toHaveBeenCalledWith(
159+
`${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024`,
160+
{},
161+
);
162+
expect(result).toEqual(expected);
163+
});
164+
});
165+
166+
describe('getProctoringExamErrors', () => {
167+
it('should fetch proctoring errors and return unformat object', async () => {
168+
const fakeData = {
169+
key_snake_case: {
170+
display_name: 'To come camelCase',
171+
testCamelCase: 'This key must not be formatted',
172+
PascalCase: 'To come camelCase',
173+
'kebab-case': 'To come camelCase',
174+
UPPER_CASE: 'To come camelCase',
175+
lowercase: 'This key must not be formatted',
176+
UPPERCASE: 'To come lowercase',
177+
'Title Case': 'To come camelCase',
178+
'dot.case': 'To come camelCase',
179+
SCREAMING_SNAKE_CASE: 'To come camelCase',
180+
MixedCase: 'To come camelCase',
181+
'Train-Case': 'To come camelCase',
182+
nestedOption: {
183+
anotherOption: 'To come camelCase',
184+
},
185+
// value is an object with various cases
186+
// this contain must not be formatted to camelCase
187+
value: {
188+
snake_case: 'snake_case',
189+
camelCase: 'camelCase',
190+
PascalCase: 'PascalCase',
191+
'kebab-case': 'kebab-case',
192+
UPPER_CASE: 'UPPER_CASE',
193+
lowercase: 'lowercase',
194+
UPPERCASE: 'UPPERCASE',
195+
'Title Case': 'Title Case',
196+
'dot.case': 'dot.case',
197+
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
198+
MixedCase: 'MixedCase',
199+
'Train-Case': 'Train-Case',
200+
nestedOption: {
201+
anotherOption: 'nestedContent',
202+
},
203+
},
204+
},
205+
};
206+
const expected = {
207+
keySnakeCase: {
208+
displayName: 'To come camelCase',
209+
testCamelCase: 'This key must not be formatted',
210+
pascalCase: 'To come camelCase',
211+
kebabCase: 'To come camelCase',
212+
upperCase: 'To come camelCase',
213+
lowercase: 'This key must not be formatted',
214+
uppercase: 'To come lowercase',
215+
titleCase: 'To come camelCase',
216+
dotCase: 'To come camelCase',
217+
screamingSnakeCase: 'To come camelCase',
218+
mixedCase: 'To come camelCase',
219+
trainCase: 'To come camelCase',
220+
nestedOption: {
221+
anotherOption: 'To come camelCase',
222+
},
223+
value: fakeData.key_snake_case.value,
224+
},
225+
};
226+
227+
mockHttpClient.get.mockResolvedValue({ data: fakeData });
228+
229+
const result = await getProctoringExamErrors('course-v1:Test+T101+2024');
230+
expect(mockHttpClient.get).toHaveBeenCalledWith(
231+
`${process.env.STUDIO_BASE_URL}/api/contentstore/v1/proctoring_errors/course-v1:Test+T101+2024`,
232+
);
233+
expect(result).toEqual(expected);
234+
});
235+
});
236+
});

0 commit comments

Comments
 (0)