diff --git a/src/advanced-settings/data/api.js b/src/advanced-settings/data/api.js index f240357c9..92a8a9727 100644 --- a/src/advanced-settings/data/api.js +++ b/src/advanced-settings/data/api.js @@ -1,5 +1,10 @@ -import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +/* eslint-disable import/prefer-default-export */ +import { + camelCaseObject, + getConfig, +} from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { camelCase } from 'lodash'; import { convertObjectToSnakeCase } from '../../utils'; const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; @@ -14,7 +19,19 @@ const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/ export async function getCourseAdvancedSettings(courseId) { const { data } = await getAuthenticatedHttpClient() .get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`); - return camelCaseObject(data); + const keepValues = {}; + Object.keys(data).forEach((key) => { + keepValues[camelCase(key)] = { value: data[key].value }; + }); + const formattedData = {}; + const formattedCamelCaseData = camelCaseObject(data); + Object.keys(formattedCamelCaseData).forEach((key) => { + formattedData[key] = { + ...formattedCamelCaseData[key], + value: keepValues[key]?.value, + }; + }); + return formattedData; } /** @@ -26,7 +43,19 @@ export async function getCourseAdvancedSettings(courseId) { export async function updateCourseAdvancedSettings(courseId, settings) { const { data } = await getAuthenticatedHttpClient() .patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings)); - return camelCaseObject(data); + const keepValues = {}; + Object.keys(data).forEach((key) => { + keepValues[camelCase(key)] = { value: data[key].value }; + }); + const formattedData = {}; + const formattedCamelCaseData = camelCaseObject(data); + Object.keys(formattedCamelCaseData).forEach((key) => { + formattedData[key] = { + ...formattedCamelCaseData[key], + value: keepValues[key]?.value, + }; + }); + return formattedData; } /** @@ -36,5 +65,17 @@ export async function updateCourseAdvancedSettings(courseId, settings) { */ export async function getProctoringExamErrors(courseId) { const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`); - return camelCaseObject(data); + const keepValues = {}; + Object.keys(data).forEach((key) => { + keepValues[camelCase(key)] = { value: data[key].value }; + }); + const formattedData = {}; + const formattedCamelCaseData = camelCaseObject(data); + Object.keys(formattedCamelCaseData).forEach((key) => { + formattedData[key] = { + ...formattedCamelCaseData[key], + value: keepValues[key]?.value, + }; + }); + return formattedData; } diff --git a/src/advanced-settings/data/api.test.js b/src/advanced-settings/data/api.test.js new file mode 100644 index 000000000..5679d3ebf --- /dev/null +++ b/src/advanced-settings/data/api.test.js @@ -0,0 +1,236 @@ +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { + getCourseAdvancedSettings, + updateCourseAdvancedSettings, + getProctoringExamErrors, +} from './api'; + +jest.mock('@edx/frontend-platform/auth', () => ({ + getAuthenticatedHttpClient: jest.fn(), +})); + +describe('courseSettings API', () => { + const mockHttpClient = { + get: jest.fn(), + patch: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + getAuthenticatedHttpClient.mockReturnValue(mockHttpClient); + }); + + describe('getCourseAdvancedSettings', () => { + it('should fetch and unformat course advanced settings', async () => { + const fakeData = { + key_snake_case: { + display_name: 'To come camelCase', + testCamelCase: 'This key must not be formatted', + PascalCase: 'To come camelCase', + 'kebab-case': 'To come camelCase', + UPPER_CASE: 'To come camelCase', + lowercase: 'This key must not be formatted', + UPPERCASE: 'To come lowercase', + 'Title Case': 'To come camelCase', + 'dot.case': 'To come camelCase', + SCREAMING_SNAKE_CASE: 'To come camelCase', + MixedCase: 'To come camelCase', + 'Train-Case': 'To come camelCase', + nestedOption: { + anotherOption: 'To come camelCase', + }, + // value is an object with various cases + // this contain must not be formatted to camelCase + value: { + snake_case: 'snake_case', + camelCase: 'camelCase', + PascalCase: 'PascalCase', + 'kebab-case': 'kebab-case', + UPPER_CASE: 'UPPER_CASE', + lowercase: 'lowercase', + UPPERCASE: 'UPPERCASE', + 'Title Case': 'Title Case', + 'dot.case': 'dot.case', + SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE', + MixedCase: 'MixedCase', + 'Train-Case': 'Train-Case', + nestedOption: { + anotherOption: 'nestedContent', + }, + }, + }, + }; + const expected = { + keySnakeCase: { + displayName: 'To come camelCase', + testCamelCase: 'This key must not be formatted', + pascalCase: 'To come camelCase', + kebabCase: 'To come camelCase', + upperCase: 'To come camelCase', + lowercase: 'This key must not be formatted', + uppercase: 'To come lowercase', + titleCase: 'To come camelCase', + dotCase: 'To come camelCase', + screamingSnakeCase: 'To come camelCase', + mixedCase: 'To come camelCase', + trainCase: 'To come camelCase', + nestedOption: { + anotherOption: 'To come camelCase', + }, + value: fakeData.key_snake_case.value, + }, + }; + + mockHttpClient.get.mockResolvedValue({ data: fakeData }); + + const result = await getCourseAdvancedSettings('course-v1:Test+T101+2024'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024?fetch_all=0`, + ); + expect(result).toEqual(expected); + }); + }); + + describe('updateCourseAdvancedSettings', () => { + it('should update and unformat course advanced settings', async () => { + const fakeData = { + key_snake_case: { + display_name: 'To come camelCase', + testCamelCase: 'This key must not be formatted', // because already be camelCase + PascalCase: 'To come camelCase', + 'kebab-case': 'To come camelCase', + UPPER_CASE: 'To come camelCase', + lowercase: 'This key must not be formatted', // because camelCase in lowercase not formatted + UPPERCASE: 'To come lowercase', // because camelCase in UPPERCASE format to lowercase + 'Title Case': 'To come camelCase', + 'dot.case': 'To come camelCase', + SCREAMING_SNAKE_CASE: 'To come camelCase', + MixedCase: 'To come camelCase', + 'Train-Case': 'To come camelCase', + nestedOption: { + anotherOption: 'To come camelCase', + }, + // value is an object with various cases + // this contain must not be formatted to camelCase + value: { + snake_case: 'snake_case', + camelCase: 'camelCase', + PascalCase: 'PascalCase', + 'kebab-case': 'kebab-case', + UPPER_CASE: 'UPPER_CASE', + lowercase: 'lowercase', + UPPERCASE: 'UPPERCASE', + 'Title Case': 'Title Case', + 'dot.case': 'dot.case', + SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE', + MixedCase: 'MixedCase', + 'Train-Case': 'Train-Case', + nestedOption: { + anotherOption: 'nestedContent', + }, + }, + }, + }; + const expected = { + keySnakeCase: { + displayName: 'To come camelCase', + testCamelCase: 'This key must not be formatted', + pascalCase: 'To come camelCase', + kebabCase: 'To come camelCase', + upperCase: 'To come camelCase', + lowercase: 'This key must not be formatted', + uppercase: 'To come lowercase', + titleCase: 'To come camelCase', + dotCase: 'To come camelCase', + screamingSnakeCase: 'To come camelCase', + mixedCase: 'To come camelCase', + trainCase: 'To come camelCase', + nestedOption: { + anotherOption: 'To come camelCase', + }, + value: fakeData.key_snake_case.value, + }, + }; + + mockHttpClient.patch.mockResolvedValue({ data: fakeData }); + + const result = await updateCourseAdvancedSettings('course-v1:Test+T101+2024', {}); + expect(mockHttpClient.patch).toHaveBeenCalledWith( + `${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024`, + {}, + ); + expect(result).toEqual(expected); + }); + }); + + describe('getProctoringExamErrors', () => { + it('should fetch proctoring errors and return unformat object', async () => { + const fakeData = { + key_snake_case: { + display_name: 'To come camelCase', + testCamelCase: 'This key must not be formatted', + PascalCase: 'To come camelCase', + 'kebab-case': 'To come camelCase', + UPPER_CASE: 'To come camelCase', + lowercase: 'This key must not be formatted', + UPPERCASE: 'To come lowercase', + 'Title Case': 'To come camelCase', + 'dot.case': 'To come camelCase', + SCREAMING_SNAKE_CASE: 'To come camelCase', + MixedCase: 'To come camelCase', + 'Train-Case': 'To come camelCase', + nestedOption: { + anotherOption: 'To come camelCase', + }, + // value is an object with various cases + // this contain must not be formatted to camelCase + value: { + snake_case: 'snake_case', + camelCase: 'camelCase', + PascalCase: 'PascalCase', + 'kebab-case': 'kebab-case', + UPPER_CASE: 'UPPER_CASE', + lowercase: 'lowercase', + UPPERCASE: 'UPPERCASE', + 'Title Case': 'Title Case', + 'dot.case': 'dot.case', + SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE', + MixedCase: 'MixedCase', + 'Train-Case': 'Train-Case', + nestedOption: { + anotherOption: 'nestedContent', + }, + }, + }, + }; + const expected = { + keySnakeCase: { + displayName: 'To come camelCase', + testCamelCase: 'This key must not be formatted', + pascalCase: 'To come camelCase', + kebabCase: 'To come camelCase', + upperCase: 'To come camelCase', + lowercase: 'This key must not be formatted', + uppercase: 'To come lowercase', + titleCase: 'To come camelCase', + dotCase: 'To come camelCase', + screamingSnakeCase: 'To come camelCase', + mixedCase: 'To come camelCase', + trainCase: 'To come camelCase', + nestedOption: { + anotherOption: 'To come camelCase', + }, + value: fakeData.key_snake_case.value, + }, + }; + + mockHttpClient.get.mockResolvedValue({ data: fakeData }); + + const result = await getProctoringExamErrors('course-v1:Test+T101+2024'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `${process.env.STUDIO_BASE_URL}/api/contentstore/v1/proctoring_errors/course-v1:Test+T101+2024`, + ); + expect(result).toEqual(expected); + }); + }); +});