From 702081909bf0141749a0f715364b70e02f5f67e8 Mon Sep 17 00:00:00 2001 From: Jose Ignacio Palma Date: Mon, 30 Dec 2024 19:28:55 -0400 Subject: [PATCH 1/7] fix: use modifyObjectKeys and snakeCaseObject on api data advanced settings to format value --- src/advanced-settings/data/api.js | 24 +++++- src/advanced-settings/data/api.test.js | 102 +++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 src/advanced-settings/data/api.test.js diff --git a/src/advanced-settings/data/api.js b/src/advanced-settings/data/api.js index f240357c9f..970b199856 100644 --- a/src/advanced-settings/data/api.js +++ b/src/advanced-settings/data/api.js @@ -1,4 +1,10 @@ -import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +/* eslint-disable import/prefer-default-export */ +import { + camelCaseObject, + getConfig, + modifyObjectKeys, + snakeCaseObject, +} from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { convertObjectToSnakeCase } from '../../utils'; @@ -14,7 +20,13 @@ 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 objectFormatted = camelCaseObject(data); + return modifyObjectKeys(objectFormatted, (key) => { + if (objectFormatted[key]?.value) { + objectFormatted[key].value = snakeCaseObject(objectFormatted[key].value); + } + return key; + }); } /** @@ -26,7 +38,13 @@ export async function getCourseAdvancedSettings(courseId) { export async function updateCourseAdvancedSettings(courseId, settings) { const { data } = await getAuthenticatedHttpClient() .patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings)); - return camelCaseObject(data); + const objectFormatted = camelCaseObject(data); + return modifyObjectKeys(objectFormatted, (key) => { + if (objectFormatted[key]?.value) { + objectFormatted[key].value = snakeCaseObject(objectFormatted[key].value); + } + return key; + }); } /** diff --git a/src/advanced-settings/data/api.test.js b/src/advanced-settings/data/api.test.js new file mode 100644 index 0000000000..2d351b0e1c --- /dev/null +++ b/src/advanced-settings/data/api.test.js @@ -0,0 +1,102 @@ +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { + getConfig, camelCaseObject, modifyObjectKeys, snakeCaseObject, +} from '@edx/frontend-platform'; +import { + getCourseAdvancedSettings, + updateCourseAdvancedSettings, + getProctoringExamErrors, +} from './api'; +import { convertObjectToSnakeCase } from '../../utils'; + +jest.mock('@edx/frontend-platform', () => ({ + getConfig: jest.fn(), + camelCaseObject: jest.fn(), + modifyObjectKeys: jest.fn(), + snakeCaseObject: jest.fn(), +})); + +jest.mock('@edx/frontend-platform/auth', () => ({ + getAuthenticatedHttpClient: jest.fn(), +})); + +jest.mock('../../utils', () => ({ + convertObjectToSnakeCase: jest.fn(), +})); + +describe('courseSettings API', () => { + const mockHttpClient = { + get: jest.fn(), + patch: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + getAuthenticatedHttpClient.mockReturnValue(mockHttpClient); + getConfig.mockReturnValue({ STUDIO_BASE_URL: 'http://studio.test' }); + }); + + describe('getCourseAdvancedSettings', () => { + it('should fetch and format course advanced settings', async () => { + const fakeData = { key: { value: 'some_value' } }; + const camelCased = { key: { value: 'some_value' } }; + const modified = { key: { value: 'some_value_snake' } }; + + mockHttpClient.get.mockResolvedValue({ data: fakeData }); + camelCaseObject.mockReturnValue(camelCased); + snakeCaseObject.mockReturnValue('some_value_snake'); + modifyObjectKeys.mockImplementation((obj, fn) => { + Object.keys(obj).forEach(fn); + return modified; + }); + + const result = await getCourseAdvancedSettings('course-v1:Test+T101+2024'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + 'http://studio.test/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024?fetch_all=0', + ); + expect(result).toEqual(modified); + }); + }); + + describe('updateCourseAdvancedSettings', () => { + it('should update and format course advanced settings', async () => { + const input = { key: 'value' }; + const snakeInput = { key: 'snake_value' }; + const serverData = { key: { value: 'server_value' } }; + const camelCased = { key: { value: 'server_value' } }; + const modified = { key: { value: 'formatted_value' } }; + + convertObjectToSnakeCase.mockReturnValue(snakeInput); + mockHttpClient.patch.mockResolvedValue({ data: serverData }); + camelCaseObject.mockReturnValue(camelCased); + snakeCaseObject.mockReturnValue('formatted_value'); + modifyObjectKeys.mockImplementation((obj, fn) => { + Object.keys(obj).forEach(fn); + return modified; + }); + + const result = await updateCourseAdvancedSettings('course-v1:Test+T101+2024', input); + expect(mockHttpClient.patch).toHaveBeenCalledWith( + 'http://studio.test/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024', + snakeInput, + ); + expect(result).toEqual(modified); + }); + }); + + describe('getProctoringExamErrors', () => { + it('should fetch proctoring errors and return camelCase object', async () => { + const fakeErrors = { errors: [] }; + const camelCased = { errors: [] }; + + mockHttpClient.get.mockResolvedValue({ data: fakeErrors }); + camelCaseObject.mockReturnValue(camelCased); + + const result = await getProctoringExamErrors('course-v1:Test+T101+2024'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + 'http://studio.test/api/contentstore/v1/proctoring_errors/course-v1:Test+T101+2024', + ); + expect(result).toEqual(camelCased); + }); + }); +}); From b68891161baf011df1bbedcdf093179ac242eddc Mon Sep 17 00:00:00 2001 From: Jose Ignacio Palma Date: Fri, 6 Jun 2025 13:51:34 -0400 Subject: [PATCH 2/7] fix: improvements from feedback review --- src/advanced-settings/data/api.js | 9 +- src/advanced-settings/data/api.test.js | 122 ++++++++++++++++--------- 2 files changed, 81 insertions(+), 50 deletions(-) diff --git a/src/advanced-settings/data/api.js b/src/advanced-settings/data/api.js index 970b199856..9ea99772ac 100644 --- a/src/advanced-settings/data/api.js +++ b/src/advanced-settings/data/api.js @@ -2,7 +2,6 @@ import { camelCaseObject, getConfig, - modifyObjectKeys, snakeCaseObject, } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; @@ -21,12 +20,12 @@ export async function getCourseAdvancedSettings(courseId) { const { data } = await getAuthenticatedHttpClient() .get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`); const objectFormatted = camelCaseObject(data); - return modifyObjectKeys(objectFormatted, (key) => { + Object.keys(objectFormatted).forEach((key) => { if (objectFormatted[key]?.value) { objectFormatted[key].value = snakeCaseObject(objectFormatted[key].value); } - return key; }); + return objectFormatted; } /** @@ -39,12 +38,12 @@ export async function updateCourseAdvancedSettings(courseId, settings) { const { data } = await getAuthenticatedHttpClient() .patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings)); const objectFormatted = camelCaseObject(data); - return modifyObjectKeys(objectFormatted, (key) => { + Object.keys(objectFormatted).forEach((key) => { if (objectFormatted[key]?.value) { objectFormatted[key].value = snakeCaseObject(objectFormatted[key].value); } - return key; }); + return objectFormatted; } /** diff --git a/src/advanced-settings/data/api.test.js b/src/advanced-settings/data/api.test.js index 2d351b0e1c..66c099136c 100644 --- a/src/advanced-settings/data/api.test.js +++ b/src/advanced-settings/data/api.test.js @@ -1,7 +1,4 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { - getConfig, camelCaseObject, modifyObjectKeys, snakeCaseObject, -} from '@edx/frontend-platform'; import { getCourseAdvancedSettings, updateCourseAdvancedSettings, @@ -9,13 +6,6 @@ import { } from './api'; import { convertObjectToSnakeCase } from '../../utils'; -jest.mock('@edx/frontend-platform', () => ({ - getConfig: jest.fn(), - camelCaseObject: jest.fn(), - modifyObjectKeys: jest.fn(), - snakeCaseObject: jest.fn(), -})); - jest.mock('@edx/frontend-platform/auth', () => ({ getAuthenticatedHttpClient: jest.fn(), })); @@ -33,70 +23,112 @@ describe('courseSettings API', () => { beforeEach(() => { jest.clearAllMocks(); getAuthenticatedHttpClient.mockReturnValue(mockHttpClient); - getConfig.mockReturnValue({ STUDIO_BASE_URL: 'http://studio.test' }); }); describe('getCourseAdvancedSettings', () => { it('should fetch and format course advanced settings', async () => { - const fakeData = { key: { value: 'some_value' } }; - const camelCased = { key: { value: 'some_value' } }; - const modified = { key: { value: 'some_value_snake' } }; + const fakeData = { + keyCamelCase: { + value: { + oneOption: 'content-1', + two_option: 'content-2', + threeOption: 'threeContent', + nestedOption: { + anotherOption: 'nestedContent', + }, + }, + }, + }; + const expected = { + keyCamelCase: { + value: { + one_option: 'content-1', + two_option: 'content-2', + three_option: 'threeContent', + nested_option: { + another_option: 'nestedContent', + }, + }, + }, + }; mockHttpClient.get.mockResolvedValue({ data: fakeData }); - camelCaseObject.mockReturnValue(camelCased); - snakeCaseObject.mockReturnValue('some_value_snake'); - modifyObjectKeys.mockImplementation((obj, fn) => { - Object.keys(obj).forEach(fn); - return modified; - }); const result = await getCourseAdvancedSettings('course-v1:Test+T101+2024'); expect(mockHttpClient.get).toHaveBeenCalledWith( - 'http://studio.test/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024?fetch_all=0', + `${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024?fetch_all=0`, ); - expect(result).toEqual(modified); + expect(result).toEqual(expected); }); }); describe('updateCourseAdvancedSettings', () => { it('should update and format course advanced settings', async () => { - const input = { key: 'value' }; - const snakeInput = { key: 'snake_value' }; - const serverData = { key: { value: 'server_value' } }; - const camelCased = { key: { value: 'server_value' } }; - const modified = { key: { value: 'formatted_value' } }; + const fakeData = { + keyCamelCase: { + value: { + oneOption: 'content-1', + two_option: 'content-2', + threeOption: 'threeContent', + nestedOption: { + anotherOption: 'nestedContent', + }, + }, + }, + }; + const expected = { + keyCamelCase: { + value: { + one_option: 'content-1', + two_option: 'content-2', + three_option: 'threeContent', + nested_option: { + another_option: 'nestedContent', + }, + }, + }, + }; - convertObjectToSnakeCase.mockReturnValue(snakeInput); - mockHttpClient.patch.mockResolvedValue({ data: serverData }); - camelCaseObject.mockReturnValue(camelCased); - snakeCaseObject.mockReturnValue('formatted_value'); - modifyObjectKeys.mockImplementation((obj, fn) => { - Object.keys(obj).forEach(fn); - return modified; - }); + convertObjectToSnakeCase.mockReturnValue({}); + mockHttpClient.patch.mockResolvedValue({ data: fakeData }); - const result = await updateCourseAdvancedSettings('course-v1:Test+T101+2024', input); + const result = await updateCourseAdvancedSettings('course-v1:Test+T101+2024', {}); expect(mockHttpClient.patch).toHaveBeenCalledWith( - 'http://studio.test/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024', - snakeInput, + `${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024`, + {}, ); - expect(result).toEqual(modified); + expect(result).toEqual(expected); }); }); describe('getProctoringExamErrors', () => { it('should fetch proctoring errors and return camelCase object', async () => { - const fakeErrors = { errors: [] }; - const camelCased = { errors: [] }; + const fakeData = { + keyCamelCase: { + value: { + oneOption: 'content-1', + two_option: 'content-2', + threeOption: 'threeContent', + }, + }, + }; + const expected = { + keyCamelCase: { + value: { + oneOption: 'content-1', + twoOption: 'content-2', + threeOption: 'threeContent', + }, + }, + }; - mockHttpClient.get.mockResolvedValue({ data: fakeErrors }); - camelCaseObject.mockReturnValue(camelCased); + mockHttpClient.get.mockResolvedValue({ data: fakeData }); const result = await getProctoringExamErrors('course-v1:Test+T101+2024'); expect(mockHttpClient.get).toHaveBeenCalledWith( - 'http://studio.test/api/contentstore/v1/proctoring_errors/course-v1:Test+T101+2024', + `${process.env.STUDIO_BASE_URL}/api/contentstore/v1/proctoring_errors/course-v1:Test+T101+2024`, ); - expect(result).toEqual(camelCased); + expect(result).toEqual(expected); }); }); }); From b440fc4bc07f0dd82fdf098abf4e23ed79ddcee1 Mon Sep 17 00:00:00 2001 From: Jose Ignacio Palma Date: Fri, 6 Jun 2025 13:58:30 -0400 Subject: [PATCH 3/7] fix: add comments --- src/advanced-settings/data/api.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/advanced-settings/data/api.js b/src/advanced-settings/data/api.js index 9ea99772ac..f58426d2b5 100644 --- a/src/advanced-settings/data/api.js +++ b/src/advanced-settings/data/api.js @@ -19,7 +19,9 @@ const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/ export async function getCourseAdvancedSettings(courseId) { const { data } = await getAuthenticatedHttpClient() .get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`); + // we start with: { camelCaseField: { value: { shouldBeSnakeCase: '123' } }, ... } const objectFormatted = camelCaseObject(data); + // we want: { camelCaseField: { value: { should_be_snake_case: '123' } }, ... } Object.keys(objectFormatted).forEach((key) => { if (objectFormatted[key]?.value) { objectFormatted[key].value = snakeCaseObject(objectFormatted[key].value); @@ -37,7 +39,9 @@ export async function getCourseAdvancedSettings(courseId) { export async function updateCourseAdvancedSettings(courseId, settings) { const { data } = await getAuthenticatedHttpClient() .patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings)); + // we start with: { camelCaseField: { value: { shouldBeSnakeCase: '123' } }, ... } const objectFormatted = camelCaseObject(data); + // we want: { camelCaseField: { value: { should_be_snake_case: '123' } }, ... } Object.keys(objectFormatted).forEach((key) => { if (objectFormatted[key]?.value) { objectFormatted[key].value = snakeCaseObject(objectFormatted[key].value); From 1ae37633d0ed34f6487b276820782d382b50aece Mon Sep 17 00:00:00 2001 From: Jose Ignacio Palma Date: Fri, 6 Jun 2025 16:16:13 -0400 Subject: [PATCH 4/7] fix: completly changed the logic to always return exactly what it receives --- src/advanced-settings/data/api.js | 24 +------ src/advanced-settings/data/api.test.js | 93 +++++++++++++------------- 2 files changed, 48 insertions(+), 69 deletions(-) diff --git a/src/advanced-settings/data/api.js b/src/advanced-settings/data/api.js index f58426d2b5..82d23ffd68 100644 --- a/src/advanced-settings/data/api.js +++ b/src/advanced-settings/data/api.js @@ -1,8 +1,6 @@ /* eslint-disable import/prefer-default-export */ import { - camelCaseObject, getConfig, - snakeCaseObject, } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { convertObjectToSnakeCase } from '../../utils'; @@ -19,15 +17,7 @@ const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/ export async function getCourseAdvancedSettings(courseId) { const { data } = await getAuthenticatedHttpClient() .get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`); - // we start with: { camelCaseField: { value: { shouldBeSnakeCase: '123' } }, ... } - const objectFormatted = camelCaseObject(data); - // we want: { camelCaseField: { value: { should_be_snake_case: '123' } }, ... } - Object.keys(objectFormatted).forEach((key) => { - if (objectFormatted[key]?.value) { - objectFormatted[key].value = snakeCaseObject(objectFormatted[key].value); - } - }); - return objectFormatted; + return data; } /** @@ -39,15 +29,7 @@ export async function getCourseAdvancedSettings(courseId) { export async function updateCourseAdvancedSettings(courseId, settings) { const { data } = await getAuthenticatedHttpClient() .patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings)); - // we start with: { camelCaseField: { value: { shouldBeSnakeCase: '123' } }, ... } - const objectFormatted = camelCaseObject(data); - // we want: { camelCaseField: { value: { should_be_snake_case: '123' } }, ... } - Object.keys(objectFormatted).forEach((key) => { - if (objectFormatted[key]?.value) { - objectFormatted[key].value = snakeCaseObject(objectFormatted[key].value); - } - }); - return objectFormatted; + return data; } /** @@ -57,5 +39,5 @@ export async function updateCourseAdvancedSettings(courseId, settings) { */ export async function getProctoringExamErrors(courseId) { const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`); - return camelCaseObject(data); + return data; } diff --git a/src/advanced-settings/data/api.test.js b/src/advanced-settings/data/api.test.js index 66c099136c..d403feca59 100644 --- a/src/advanced-settings/data/api.test.js +++ b/src/advanced-settings/data/api.test.js @@ -26,31 +26,28 @@ describe('courseSettings API', () => { }); describe('getCourseAdvancedSettings', () => { - it('should fetch and format course advanced settings', async () => { + it('should fetch and unformat course advanced settings', async () => { const fakeData = { keyCamelCase: { value: { - oneOption: 'content-1', - two_option: 'content-2', - threeOption: 'threeContent', + 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 = { - keyCamelCase: { - value: { - one_option: 'content-1', - two_option: 'content-2', - three_option: 'threeContent', - nested_option: { - another_option: 'nestedContent', - }, - }, - }, - }; mockHttpClient.get.mockResolvedValue({ data: fakeData }); @@ -58,36 +55,33 @@ describe('courseSettings API', () => { 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); + expect(result).toEqual(fakeData); }); }); describe('updateCourseAdvancedSettings', () => { - it('should update and format course advanced settings', async () => { + it('should update and unformat course advanced settings', async () => { const fakeData = { keyCamelCase: { value: { - oneOption: 'content-1', - two_option: 'content-2', - threeOption: 'threeContent', + 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 = { - keyCamelCase: { - value: { - one_option: 'content-1', - two_option: 'content-2', - three_option: 'threeContent', - nested_option: { - another_option: 'nestedContent', - }, - }, - }, - }; convertObjectToSnakeCase.mockReturnValue({}); mockHttpClient.patch.mockResolvedValue({ data: fakeData }); @@ -97,27 +91,30 @@ describe('courseSettings API', () => { `${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024`, {}, ); - expect(result).toEqual(expected); + expect(result).toEqual(fakeData); }); }); describe('getProctoringExamErrors', () => { - it('should fetch proctoring errors and return camelCase object', async () => { + it('should fetch proctoring errors and return unformat object', async () => { const fakeData = { keyCamelCase: { value: { - oneOption: 'content-1', - two_option: 'content-2', - threeOption: 'threeContent', - }, - }, - }; - const expected = { - keyCamelCase: { - value: { - oneOption: 'content-1', - twoOption: 'content-2', - threeOption: 'threeContent', + 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', + }, }, }, }; @@ -128,7 +125,7 @@ describe('courseSettings API', () => { expect(mockHttpClient.get).toHaveBeenCalledWith( `${process.env.STUDIO_BASE_URL}/api/contentstore/v1/proctoring_errors/course-v1:Test+T101+2024`, ); - expect(result).toEqual(expected); + expect(result).toEqual(fakeData); }); }); }); From 78467d420f2d2f5ccfaf345b7970632412297473 Mon Sep 17 00:00:00 2001 From: Jose Ignacio Palma Date: Mon, 9 Jun 2025 12:52:04 -0400 Subject: [PATCH 5/7] fix: unit test and advanced setting titles in blank --- src/advanced-settings/data/api.js | 44 ++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/advanced-settings/data/api.js b/src/advanced-settings/data/api.js index 82d23ffd68..92a8a97272 100644 --- a/src/advanced-settings/data/api.js +++ b/src/advanced-settings/data/api.js @@ -1,8 +1,10 @@ /* 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; @@ -17,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 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; } /** @@ -29,7 +43,19 @@ export async function getCourseAdvancedSettings(courseId) { export async function updateCourseAdvancedSettings(courseId, settings) { const { data } = await getAuthenticatedHttpClient() .patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings)); - return 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; } /** @@ -39,5 +65,17 @@ export async function updateCourseAdvancedSettings(courseId, settings) { */ export async function getProctoringExamErrors(courseId) { const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`); - return 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; } From 094a210b202504f9d182b6966ce2b30dfbaeb3cb Mon Sep 17 00:00:00 2001 From: Jose Ignacio Palma Date: Wed, 11 Jun 2025 12:01:10 -0400 Subject: [PATCH 6/7] fix: remove use convertObjectToSnakeCase --- src/advanced-settings/data/api.js | 3 +-- src/advanced-settings/data/api.test.js | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/advanced-settings/data/api.js b/src/advanced-settings/data/api.js index 92a8a97272..dae787a705 100644 --- a/src/advanced-settings/data/api.js +++ b/src/advanced-settings/data/api.js @@ -5,7 +5,6 @@ import { } 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; export const getCourseAdvancedSettingsApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v0/advanced_settings/${courseId}`; @@ -42,7 +41,7 @@ export async function getCourseAdvancedSettings(courseId) { */ export async function updateCourseAdvancedSettings(courseId, settings) { const { data } = await getAuthenticatedHttpClient() - .patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings)); + .patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, settings); const keepValues = {}; Object.keys(data).forEach((key) => { keepValues[camelCase(key)] = { value: data[key].value }; diff --git a/src/advanced-settings/data/api.test.js b/src/advanced-settings/data/api.test.js index d403feca59..f7581beb9f 100644 --- a/src/advanced-settings/data/api.test.js +++ b/src/advanced-settings/data/api.test.js @@ -4,16 +4,11 @@ import { updateCourseAdvancedSettings, getProctoringExamErrors, } from './api'; -import { convertObjectToSnakeCase } from '../../utils'; jest.mock('@edx/frontend-platform/auth', () => ({ getAuthenticatedHttpClient: jest.fn(), })); -jest.mock('../../utils', () => ({ - convertObjectToSnakeCase: jest.fn(), -})); - describe('courseSettings API', () => { const mockHttpClient = { get: jest.fn(), @@ -83,7 +78,6 @@ describe('courseSettings API', () => { }, }; - convertObjectToSnakeCase.mockReturnValue({}); mockHttpClient.patch.mockResolvedValue({ data: fakeData }); const result = await updateCourseAdvancedSettings('course-v1:Test+T101+2024', {}); From 90c69d3ec1b28060fd29afefbe12558f2c6f9752 Mon Sep 17 00:00:00 2001 From: Jose Ignacio Palma Date: Fri, 13 Jun 2025 14:26:07 -0400 Subject: [PATCH 7/7] fix: add more cases to expected --- src/advanced-settings/data/api.test.js | 123 +++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 6 deletions(-) diff --git a/src/advanced-settings/data/api.test.js b/src/advanced-settings/data/api.test.js index f7581beb9f..5679d3ebfa 100644 --- a/src/advanced-settings/data/api.test.js +++ b/src/advanced-settings/data/api.test.js @@ -23,7 +23,24 @@ describe('courseSettings API', () => { describe('getCourseAdvancedSettings', () => { it('should fetch and unformat course advanced settings', async () => { const fakeData = { - keyCamelCase: { + 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', @@ -43,6 +60,26 @@ describe('courseSettings API', () => { }, }, }; + 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 }); @@ -50,14 +87,31 @@ describe('courseSettings API', () => { 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(fakeData); + expect(result).toEqual(expected); }); }); describe('updateCourseAdvancedSettings', () => { it('should update and unformat course advanced settings', async () => { const fakeData = { - keyCamelCase: { + 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', @@ -77,6 +131,26 @@ describe('courseSettings API', () => { }, }, }; + 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 }); @@ -85,14 +159,31 @@ describe('courseSettings API', () => { `${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024`, {}, ); - expect(result).toEqual(fakeData); + expect(result).toEqual(expected); }); }); describe('getProctoringExamErrors', () => { it('should fetch proctoring errors and return unformat object', async () => { const fakeData = { - keyCamelCase: { + 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', @@ -112,6 +203,26 @@ describe('courseSettings API', () => { }, }, }; + 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 }); @@ -119,7 +230,7 @@ describe('courseSettings API', () => { expect(mockHttpClient.get).toHaveBeenCalledWith( `${process.env.STUDIO_BASE_URL}/api/contentstore/v1/proctoring_errors/course-v1:Test+T101+2024`, ); - expect(result).toEqual(fakeData); + expect(result).toEqual(expected); }); }); });