From fd82210e1dba20e0f85c7efcd3a295083d643332 Mon Sep 17 00:00:00 2001 From: Taimoor Ahmed Date: Mon, 20 Jul 2026 00:47:34 +0500 Subject: [PATCH 1/5] refactor: migrate Xblock callers to standardized v1 REST endpoint (FC-0118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FC-0118 (ADR 0028/0037): point Studio's xblock CRUD callers at the standardized `/api/contentstore/v1/xblock/` REST endpoint instead of the legacy `/xblock/` Studio route. The v1 XblockViewSet uses strict REST routing, so: - create/duplicate/paste stay POST on the collection URL; - retrieve stays GET, delete stays DELETE (now on the detail URL, trailing slash); - legacy POST-as-update becomes PATCH (partial_update) on the detail URL; - the "move" op (previously PATCH on the collection) becomes PATCH on the source block's detail URL — `update_xblock_response` still routes by `move_source_locator`; - custom-pages "add page" (previously PUT on the collection) becomes POST (create). Trailing slashes are required by the DRF DefaultRouter that serves the v1 viewset. Fields the v1 strict serializer does not accept and the backend does not read are dropped from request bodies: `type` (create; redundant with `category`) and `courseKey` (editor save). `library_content_key` is kept (library import) and is accepted by the v1 serializer via openedx/edx-platform#38908. The `/xblock/outline/...`, `/xblock/{id}/studio_view`, and `/xblock/{id}/handler/...` sub-routes are NOT part of the v1 REST viewset and remain on the legacy route (editors `block()` helper kept; new `blockV1()` helper added for CRUD). Source-only commit; test mocks updated in follow-up commits. Refs openedx/frontend-app-authoring#3127 Depends-on openedx/edx-platform#38908 Co-Authored-By: Claude Opus 4.8 --- src/course-outline/data/api.ts | 35 ++++++++++++++++---------- src/course-unit/data/api.ts | 19 +++++++++++--- src/course-updates/data/api.js | 5 +++- src/custom-pages/data/api.ts | 11 +++++--- src/editors/data/services/cms/api.ts | 17 +++++-------- src/editors/data/services/cms/urls.ts | 15 ++++++++++- src/editors/data/services/cms/utils.ts | 8 ++++++ 7 files changed, 77 insertions(+), 33 deletions(-) diff --git a/src/course-outline/data/api.ts b/src/course-outline/data/api.ts index 4a95b4d19b..40ecacd0c5 100644 --- a/src/course-outline/data/api.ts +++ b/src/course-outline/data/api.ts @@ -46,13 +46,19 @@ export const getCourseLaunchApiUrl = ({ export const getCourseBlockApiUrl = (courseId: string) => { const formattedCourseId = courseIDtoBlockID(courseId); - return `${getApiBaseUrl()}/xblock/${formattedCourseId}`; + // FC-0118: v1 xblock REST detail URL for the course's root block. + return `${getApiBaseUrl()}/api/contentstore/v1/xblock/${formattedCourseId}/`; }; export const getCourseReindexApiUrl = (reindexLink: string) => `${getApiBaseUrl()}${reindexLink}`; -export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`; -export const getCourseItemApiUrl = (itemId: string) => `${getXBlockBaseApiUrl()}${itemId}`; -export const getXBlockApiUrl = (blockId: string) => `${getXBlockBaseApiUrl()}outline/${blockId}`; +// FC-0118 (ADR 0028/0037): standardized v1 xblock REST endpoint. Collection URL +// (POST here creates a block); detail URLs carry the trailing slash the DRF +// DefaultRouter requires. +export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/xblock/`; +export const getCourseItemApiUrl = (itemId: string) => `${getXBlockBaseApiUrl()}${itemId}/`; +// The outline handler (`/xblock/outline/{id}`) is a separate legacy Studio route, +// NOT part of the v1 REST viewset, so it keeps the legacy `/xblock/` base. +export const getXBlockApiUrl = (blockId: string) => `${getApiBaseUrl()}/xblock/outline/${blockId}`; export const exportTags = (courseId: string) => `${getApiBaseUrl()}/api/content_tagging/v1/object_tags/${courseId}/export/`; export const createDiscussionsTopicsUrl = (courseId: string) => @@ -163,7 +169,7 @@ export async function getCourseLaunch({ */ export async function enableCourseHighlightsEmails(courseId: string): Promise { const { data } = await getAuthenticatedHttpClient() - .post(getCourseBlockApiUrl(courseId), { + .patch(getCourseBlockApiUrl(courseId), { publish: 'republish', metadata: { highlights_enabled_for_messaging: true, @@ -205,7 +211,7 @@ export async function updateCourseSectionHighlights( highlights: Array, ): Promise { const { data } = await getAuthenticatedHttpClient() - .post(getCourseItemApiUrl(sectionId), { + .patch(getCourseItemApiUrl(sectionId), { publish: 'republish', metadata: { highlights, @@ -220,7 +226,7 @@ export async function updateCourseSectionHighlights( */ export async function publishCourseItem(itemId: string): Promise { const { data } = await getAuthenticatedHttpClient() - .post(getCourseItemApiUrl(itemId), { + .patch(getCourseItemApiUrl(itemId), { publish: 'make_public', }); @@ -232,7 +238,7 @@ export async function publishCourseItem(itemId: string): Promise { const { data } = await getAuthenticatedHttpClient() - .post(getCourseItemApiUrl(variables.sectionId), { + .patch(getCourseItemApiUrl(variables.sectionId), { publish: 'republish', metadata: { // The backend expects metadata.visible_to_staff_only to either true or null @@ -304,7 +310,7 @@ export async function configureCourseSubsection( }); const { data } = await getAuthenticatedHttpClient() - .post(getCourseItemApiUrl(itemId), body); + .patch(getCourseItemApiUrl(itemId), body); return data; } @@ -329,7 +335,8 @@ export async function configureCourseUnit(variables: ConfigureUnitData): Promise }; const url = getCourseItemApiUrl(variables.unitId); const { data } = await getAuthenticatedHttpClient() - .post(url, body); + // FC-0118: legacy POST-as-update becomes PATCH (partial_update) on v1. + .patch(url, body); return camelCaseObject(data); } @@ -342,7 +349,7 @@ export async function editItemDisplayName({ itemId, displayName }: { displayName: string; }): Promise { const { data } = await getAuthenticatedHttpClient() - .post(getCourseItemApiUrl(itemId), { + .patch(getCourseItemApiUrl(itemId), { metadata: { display_name: displayName, }, @@ -405,7 +412,9 @@ export async function createCourseXblock({ libraryContentKey, }: CreateCourseXBlockType) { const body = { - type, + // FC-0118: `type` is intentionally not sent — the v1 strict serializer does + // not accept it and the backend only reads `category` (which falls back to + // `type` here). `library_content_key` is accepted by the v1 serializer. boilerplate, category: category || type, parent_locator: parentLocator, @@ -461,7 +470,7 @@ export async function setVideoSharingOption( videoSharingOption: string, ): Promise { const { data } = await getAuthenticatedHttpClient() - .post(getCourseBlockApiUrl(courseId), { + .patch(getCourseBlockApiUrl(courseId), { metadata: { video_sharing_options: videoSharingOption, }, diff --git a/src/course-unit/data/api.ts b/src/course-unit/data/api.ts index 609c4715a1..aa50547035 100644 --- a/src/course-unit/data/api.ts +++ b/src/course-unit/data/api.ts @@ -6,13 +6,18 @@ import { isUnitImportedFromLib, normalizeCourseSectionVerticalData, updateXBlock const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL; -export const getXBlockBaseApiUrl = (itemId: string) => `${getStudioBaseUrl()}/xblock/${itemId}`; +// FC-0118 (ADR 0028/0037): standardized v1 xblock REST endpoint. The trailing +// slash is required by the DRF DefaultRouter that serves the v1 XblockViewSet +// (the legacy `/xblock/{id}` route had none). Detail actions: GET (retrieve), +// PUT/PATCH (update), DELETE (destroy). +export const getXBlockBaseApiUrl = (itemId: string) => `${getStudioBaseUrl()}/api/contentstore/v1/xblock/${itemId}/`; export const getCourseSectionVerticalApiUrl = (itemId: string) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`; export const getCourseVerticalChildrenApiUrl = (itemId: string, getUpstreamInfo: boolean = false) => `${getStudioBaseUrl()}/api/contentstore/v1/container/${itemId}/children?get_upstream_info=${getUpstreamInfo}`; export const getCourseOutlineInfoUrl = (courseId: string) => `${getStudioBaseUrl()}/course/${courseId}?format=concise`; -export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`; +// FC-0118: v1 xblock collection endpoint — POST here creates a block. +export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/api/contentstore/v1/xblock/`; export const libraryBlockChangesUrl = (blockId: string) => `${getStudioBaseUrl()}/api/contentstore/v2/downstreams/${blockId}/sync`; @@ -21,7 +26,8 @@ export const libraryBlockChangesUrl = (blockId: string) => */ export async function editUnitDisplayName(unitId: string, displayName: string): Promise { const { data } = await getAuthenticatedHttpClient() - .post(getXBlockBaseApiUrl(unitId), { + // FC-0118: legacy POST-as-update becomes PATCH (partial_update) on v1. + .patch(getXBlockBaseApiUrl(unitId), { metadata: { display_name: displayName, }, @@ -94,8 +100,13 @@ export async function getCourseOutlineInfo(courseId: string): Promise { + // FC-0118: the move used to PATCH the legacy collection URL (`/xblock/`); the + // v1 REST viewset only exposes PATCH on the detail route, so we PATCH the + // source block's detail URL. `update_xblock_response` still routes to the + // move-item path off `move_source_locator` in the body (the URL key is used + // only to derive the course for the permission check). const { data } = await getAuthenticatedHttpClient() - .patch(postXBlockBaseApiUrl(), { + .patch(getXBlockBaseApiUrl(sourceLocator), { parent_locator: targetParentLocator, move_source_locator: sourceLocator, }); diff --git a/src/course-updates/data/api.js b/src/course-updates/data/api.js index 4a528a506d..20ae08811e 100644 --- a/src/course-updates/data/api.js +++ b/src/course-updates/data/api.js @@ -7,7 +7,10 @@ export const updateCourseUpdatesApiUrl = (courseId, updateId) => `${getApiBaseUrl()}/course_info_update/${courseId}/${updateId}`; export const getCourseHandoutApiUrl = (courseId) => { const formattedCourseId = courseId.split('course-v1:')[1]; - return `${getApiBaseUrl()}/xblock/block-v1:${formattedCourseId}+type@course_info+block@handouts`; + // FC-0118 (ADR 0028/0037): standardized v1 xblock REST detail endpoint for the + // course handouts block. Trailing slash required by the DRF DefaultRouter; + // used by GET (retrieve) and PUT (full update). + return `${getApiBaseUrl()}/api/contentstore/v1/xblock/block-v1:${formattedCourseId}+type@course_info+block@handouts/`; }; /** diff --git a/src/custom-pages/data/api.ts b/src/custom-pages/data/api.ts index 2381e90912..431ad65109 100644 --- a/src/custom-pages/data/api.ts +++ b/src/custom-pages/data/api.ts @@ -29,8 +29,10 @@ export async function getCustomPages(courseId: string): Promise { * Delete custom page for provided block. */ export async function deleteCustomPage(blockId: string): Promise { + // FC-0118 (ADR 0028/0037): standardized v1 xblock REST endpoint (trailing + // slash required by the DRF DefaultRouter). await getAuthenticatedHttpClient() - .delete(`${getApiBaseUrl()}/xblock/${blockId}`); + .delete(`${getApiBaseUrl()}/api/contentstore/v1/xblock/${blockId}/`); } /** @@ -40,7 +42,9 @@ export async function addCustomPage(courseId: string): Promise; }): Promise> { const { data } = await getAuthenticatedHttpClient() - .put(`${getApiBaseUrl()}/xblock/${blockId}`, { + // FC-0118: PUT (full update) on the v1 detail endpoint (trailing slash). + .put(`${getApiBaseUrl()}/api/contentstore/v1/xblock/${blockId}/`, { id: blockId, data: htmlString, metadata, diff --git a/src/editors/data/services/cms/api.ts b/src/editors/data/services/cms/api.ts index dc33a8b9ec..dd9485f11b 100644 --- a/src/editors/data/services/cms/api.ts +++ b/src/editors/data/services/cms/api.ts @@ -6,6 +6,7 @@ import { get, post, put, + patch, deleteObject, } from './utils'; import { durationStringFromValue } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks'; @@ -113,7 +114,7 @@ export const processLicense = (licenseType, licenseDetails) => { export const apiMethods = { fetchBlockById: ({ blockId, studioEndpointUrl }): Promise<{ data: FieldsResponse; }> => get( - urls.block({ blockId, studioEndpointUrl }), + urls.blockV1({ blockId, studioEndpointUrl }), ), /** A better name for this would be 'get ancestors of block' */ fetchByUnitId: ({ blockId, studioEndpointUrl }): Promise<{ data: AncestorsResponse; }> => @@ -301,20 +302,17 @@ export const apiMethods = { blockId, blockType, content, - learningContextId, title, }: { blockId: string; blockType: string; content: any; // string for 'html' blocks, otherwise Record - learningContextId: string; title: string; }) => { let response = {}; if (blockType === 'html') { response = { category: blockType, - courseKey: learningContextId, data: content, has_changes: true, id: blockId, @@ -324,7 +322,6 @@ export const apiMethods = { response = { data: content.olx, category: blockType, - courseKey: learningContextId, has_changes: true, id: blockId, metadata: { display_name: title, ...content.settings }, @@ -341,7 +338,6 @@ export const apiMethods = { }); response = { category: blockType, - courseKey: learningContextId, display_name: title, id: blockId, metadata: { @@ -364,7 +360,6 @@ export const apiMethods = { } else if (blockType === 'pdf') { response = { category: blockType, - courseKey: learningContextId, has_changes: true, id: blockId, metadata: { ...snakeCaseKeys(content), display_name: title }, @@ -378,17 +373,17 @@ export const apiMethods = { blockId, blockType, content, - learningContextId, studioEndpointUrl, title, }) => - post( - urls.block({ studioEndpointUrl, blockId }), + // FC-0118: legacy POST-as-update becomes PATCH (partial_update) on the v1 + // xblock REST detail endpoint. + patch( + urls.blockV1({ studioEndpointUrl, blockId }), apiMethods.normalizeContent({ blockType, content, blockId, - learningContextId, title, }), ), diff --git a/src/editors/data/services/cms/urls.ts b/src/editors/data/services/cms/urls.ts index d24bd6bcc2..9ef361adf9 100644 --- a/src/editors/data/services/cms/urls.ts +++ b/src/editors/data/services/cms/urls.ts @@ -42,15 +42,28 @@ export const returnUrl = ({ return ''; }; +// Legacy Studio xblock detail base. Kept for the handler/view sub-routes +// (`/studio_view`, `/handler/...`) that are NOT served by the v1 REST viewset +// and are appended onto this URL by `blockStudioView` / `videoTranscripts`. export const block = (({ studioEndpointUrl, blockId }) => ( blockId.startsWith('lb:') ? `${studioEndpointUrl}/api/xblock/v2/xblocks/${blockId}/fields/` : `${studioEndpointUrl}/xblock/${blockId}` )) satisfies UrlFunction; +// FC-0118 (ADR 0028/0037): standardized v1 xblock REST detail URL, for CRUD +// (retrieve/update/delete). The trailing slash is required by the DRF +// DefaultRouter. `lb:` library blocks keep the v2 library xblock API. +export const blockV1 = (({ studioEndpointUrl, blockId }) => ( + blockId.startsWith('lb:') + ? `${studioEndpointUrl}/api/xblock/v2/xblocks/${blockId}/fields/` + : `${studioEndpointUrl}/api/contentstore/v1/xblock/${blockId}/` +)) satisfies UrlFunction; + export const blockAncestor = (({ studioEndpointUrl, blockId }) => { if (blockId.includes('block-v1')) { - return `${block({ studioEndpointUrl, blockId })}?fields=ancestorInfo`; + // v1 retrieve honours the legacy `?fields=ancestorInfo` pass-through. + return `${blockV1({ studioEndpointUrl, blockId })}?fields=ancestorInfo`; } throw new Error('Block ancestor not available (and not needed) for V2 blocks'); }) satisfies UrlFunction; diff --git a/src/editors/data/services/cms/utils.ts b/src/editors/data/services/cms/utils.ts index 2e77435cd5..e06d37105a 100644 --- a/src/editors/data/services/cms/utils.ts +++ b/src/editors/data/services/cms/utils.ts @@ -23,6 +23,14 @@ export const post: Axios['post'] = (...args) => client().post(...args); */ export const put: Axios['put'] = (...args) => client().put(...args); +/** + * patch(url, data) + * simple wrapper providing an authenticated Http client patch action + * @param {string} url - target url + * @param {object|string} data - patch payload + */ +export const patch: Axios['patch'] = (...args) => client().patch(...args); + /** * delete(url, data) * simple wrapper providing an authenticated Http client delete action From 8d47172dbefccaa32dbc6f07bd232960b375a9ad Mon Sep 17 00:00:00 2001 From: Taimoor Ahmed Date: Mon, 20 Jul 2026 00:50:59 +0500 Subject: [PATCH 2/5] test(custom-pages): update xblock mocks to v1 REST (add=POST, detail trailing slash) Refs openedx/frontend-app-authoring#3127 Co-Authored-By: Claude Opus 4.8 --- src/custom-pages/CustomPages.test.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/custom-pages/CustomPages.test.tsx b/src/custom-pages/CustomPages.test.tsx index 3fcd850422..57d8e2e488 100644 --- a/src/custom-pages/CustomPages.test.tsx +++ b/src/custom-pages/CustomPages.test.tsx @@ -110,9 +110,9 @@ describe('CustomPages', () => { }); it('should add new page when "add a new page button" is clicked', async () => { - const xblockAddUrl = `${getApiBaseUrl()}/xblock/`; + const xblockAddUrl = `${getApiBaseUrl()}/api/contentstore/v1/xblock/`; axiosMock.onGet(getTabHandlerUrl(courseId)).reply(200, generateFetchPageApiResponse()); - axiosMock.onPut(xblockAddUrl).reply(200, generateNewPageApiResponse()); + axiosMock.onPost(xblockAddUrl).reply(200, generateNewPageApiResponse()); renderComponent(); const addButton = await screen.findByTestId('body-add-button'); @@ -121,7 +121,7 @@ describe('CustomPages', () => { fireEvent.click(addButton); await waitFor(() => { - expect(axiosMock.history.put.length).toBeGreaterThanOrEqual(1); + expect(axiosMock.history.post.length).toBeGreaterThanOrEqual(1); }); }); @@ -166,7 +166,7 @@ describe('CustomPages', () => { it('should delete page on successful delete', async () => { const pageId = 'mOckID1'; - const xblockEditUrl = `${getApiBaseUrl()}/xblock/${pageId}`; + const xblockEditUrl = `${getApiBaseUrl()}/api/contentstore/v1/xblock/${pageId}/`; axiosMock.onGet(getTabHandlerUrl(courseId)).reply(200, generateFetchPageApiResponse()); axiosMock.onDelete(xblockEditUrl).reply(204); @@ -182,7 +182,7 @@ describe('CustomPages', () => { it('should show error alert on delete failure', async () => { const pageId = 'mOckID1'; - const xblockEditUrl = `${getApiBaseUrl()}/xblock/${pageId}`; + const xblockEditUrl = `${getApiBaseUrl()}/api/contentstore/v1/xblock/${pageId}/`; axiosMock.onGet(getTabHandlerUrl(courseId)).reply(200, generateFetchPageApiResponse()); axiosMock.onDelete(xblockEditUrl).reply(500); @@ -250,7 +250,7 @@ describe('CustomPages', () => { it('should save visibility on successful toggle', async () => { const pageId = 'mOckID1'; - const xblockEditUrl = `${getApiBaseUrl()}/xblock/${pageId}`; + const xblockEditUrl = `${getApiBaseUrl()}/api/contentstore/v1/xblock/${pageId}/`; axiosMock.onGet(getTabHandlerUrl(courseId)).reply(200, generateFetchPageApiResponse()); axiosMock.onPut(xblockEditUrl).reply(200, {}); @@ -265,7 +265,7 @@ describe('CustomPages', () => { it('should show error alert on visibility save failure', async () => { const pageId = 'mOckID1'; - const xblockEditUrl = `${getApiBaseUrl()}/xblock/${pageId}`; + const xblockEditUrl = `${getApiBaseUrl()}/api/contentstore/v1/xblock/${pageId}/`; axiosMock.onGet(getTabHandlerUrl(courseId)).reply(200, generateFetchPageApiResponse()); axiosMock.onPut(xblockEditUrl).reply(500); From 50f948ff167c3215efd629fc6d1b8a17a6d1071f Mon Sep 17 00:00:00 2001 From: Taimoor Ahmed Date: Mon, 20 Jul 2026 01:05:46 +0500 Subject: [PATCH 3/5] test(course-outline): update xblock mocks to v1 REST verbs Detail-update mocks POST->PATCH; history assertions for updates read history.patch; create expected bodies drop the dropped `type` key. Refs openedx/frontend-app-authoring#3127 Co-Authored-By: Claude Opus 4.8 --- src/course-outline/CourseOutline.test.tsx | 45 +++++++++---------- .../outline-sidebar/AddSidebar.test.tsx | 11 ----- 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/src/course-outline/CourseOutline.test.tsx b/src/course-outline/CourseOutline.test.tsx index bc97222ff7..80d161ae81 100644 --- a/src/course-outline/CourseOutline.test.tsx +++ b/src/course-outline/CourseOutline.test.tsx @@ -708,7 +708,7 @@ describe('', () => { const { findByLabelText } = renderComponent(); axiosMock - .onPost(getCourseBlockApiUrl(courseId), { + .onPatch(getCourseBlockApiUrl(courseId), { metadata: { video_sharing_options: VIDEO_SHARING_OPTIONS.allOff, }, @@ -721,7 +721,7 @@ describe('', () => { // The video sharing POST is the one with the expected data. // Find it by data content rather than assuming a fixed index. - const videoSharingPost = axiosMock.history.post.find( + const videoSharingPost = axiosMock.history.patch.find( (entry: any) => entry.data && entry.data.includes('video_sharing_options'), ); expect(videoSharingPost).toBeDefined(); @@ -738,7 +738,7 @@ describe('', () => { renderComponent(); axiosMock - .onPost(getCourseBlockApiUrl(courseId), { + .onPatch(getCourseBlockApiUrl(courseId), { metadata: { video_sharing_options: VIDEO_SHARING_OPTIONS.allOff, }, @@ -749,7 +749,7 @@ describe('', () => { async () => fireEvent.change(optionDropdown, { target: { value: VIDEO_SHARING_OPTIONS.allOff } }), ); - const videoSharingPost = axiosMock.history.post.find( + const videoSharingPost = axiosMock.history.patch.find( (entry: any) => entry.data && entry.data.includes('video_sharing_options'), ); expect(videoSharingPost).toBeDefined(); @@ -955,7 +955,6 @@ describe('', () => { ); expect(newUnitPost).toBeDefined(); expect(JSON.parse(newUnitPost!.data)).toEqual({ - type: COURSE_BLOCK_NAMES.vertical.id, category: COURSE_BLOCK_NAMES.vertical.id, parent_locator: subsection.id, display_name: COURSE_BLOCK_NAMES.vertical.name, @@ -1065,7 +1064,7 @@ describe('', () => { axiosMock.reset(); axiosMock - .onPost(getCourseBlockApiUrl(courseId), { + .onPatch(getCourseBlockApiUrl(courseId), { publish: 'republish', metadata: { highlights_enabled_for_messaging: true, @@ -1178,7 +1177,7 @@ describe('', () => { .reply(200, courseLaunchMock); // Rename-specific handlers axiosMock - .onPost(getCourseItemApiUrl(item.id)) + .onPatch(getCourseItemApiUrl(item.id)) .reply(200, { dummy: 'value' }); // mock section, subsection and unit name and check within the elements. @@ -1213,7 +1212,7 @@ describe('', () => { fireEvent.change(editField, { target: { value: newName } }); await user.keyboard('{enter}'); expect( - axiosMock.history.post[axiosMock.history.post.length - 1].data, + axiosMock.history.patch[axiosMock.history.patch.length - 1].data, ).toBe(JSON.stringify({ metadata: { display_name: newName, @@ -1432,7 +1431,7 @@ describe('', () => { ).toHaveTextContent(cardHeaderMessages.statusBadgeDraft.defaultMessage); axiosMock - .onPost(getCourseItemApiUrl(item.id), { + .onPatch(getCourseItemApiUrl(item.id), { publish: 'make_public', }) .reply(200, { dummy: 'value' }); @@ -1500,7 +1499,7 @@ describe('', () => { expect(releaseDatePicker).toHaveValue('08/10/2023'); axiosMock - .onPost(getCourseItemApiUrl(section.id), { + .onPatch(getCourseItemApiUrl(section.id), { publish: 'republish', metadata: { visible_to_staff_only: true, @@ -1521,7 +1520,7 @@ describe('', () => { const saveButton = await findByTestId('configure-save-button'); await act(async () => fireEvent.click(saveButton)); - const sectionCfgPost = axiosMock.history.post.find( + const sectionCfgPost = axiosMock.history.patch.find( (entry: any) => entry.data && entry.data.includes('visible_to_staff_only'), ); expect(sectionCfgPost).toBeDefined(); @@ -1571,7 +1570,7 @@ describe('', () => { }; axiosMock - .onPost(getCourseItemApiUrl(subsection.id), expectedRequestData) + .onPatch(getCourseItemApiUrl(subsection.id), expectedRequestData) .reply(200, { dummy: 'value' }); const [currentSection] = await findAllByTestId('section-card'); @@ -1630,7 +1629,7 @@ describe('', () => { await user.click(saveButton); // verify request - const subCfgPost = axiosMock.history.post.find( + const subCfgPost = axiosMock.history.patch.find( (entry: any) => entry.data && entry.data.includes(expectedRequestData.graderType), ); expect(subCfgPost).toBeDefined(); @@ -1698,7 +1697,7 @@ describe('', () => { }; axiosMock - .onPost(getCourseItemApiUrl(subsection.id), expectedRequestData) + .onPatch(getCourseItemApiUrl(subsection.id), expectedRequestData) .reply(200, { dummy: 'value' }); const [currentSection] = await findAllByTestId('section-card'); @@ -1775,7 +1774,7 @@ describe('', () => { await user.click(saveButton); // verify request - const cfgPosta = axiosMock.history.post.find( + const cfgPosta = axiosMock.history.patch.find( (entry: any) => entry.data && entry.data.includes(expectedRequestData.graderType), ); expect(cfgPosta).toBeDefined(); @@ -1849,7 +1848,7 @@ describe('', () => { }; axiosMock - .onPost(getCourseItemApiUrl(subsection.id), expectedRequestData) + .onPatch(getCourseItemApiUrl(subsection.id), expectedRequestData) .reply(200, { dummy: 'value' }); const [currentSection] = await findAllByTestId('section-card'); @@ -1904,7 +1903,7 @@ describe('', () => { await user.click(saveButton); // verify request - const cfgPostb = axiosMock.history.post.find( + const cfgPostb = axiosMock.history.patch.find( (entry: any) => entry.data && entry.data.includes(expectedRequestData.graderType), ); expect(cfgPostb).toBeDefined(); @@ -1960,7 +1959,7 @@ describe('', () => { }; axiosMock - .onPost(getCourseItemApiUrl(subsection.id), expectedRequestData) + .onPatch(getCourseItemApiUrl(subsection.id), expectedRequestData) .reply(200, { dummy: 'value' }); const [currentSection] = await findAllByTestId('section-card'); @@ -2016,7 +2015,7 @@ describe('', () => { await user.click(saveButton); // verify request - const cfgPostc = axiosMock.history.post.find( + const cfgPostc = axiosMock.history.patch.find( (entry: any) => entry.data && entry.data.includes(expectedRequestData.graderType), ); expect(cfgPostc).toBeDefined(); @@ -2071,7 +2070,7 @@ describe('', () => { }; axiosMock - .onPost(getCourseItemApiUrl(subsection.id), expectedRequestData) + .onPatch(getCourseItemApiUrl(subsection.id), expectedRequestData) .reply(200, { dummy: 'value' }); const [, currentSection] = await findAllByTestId('section-card'); @@ -2125,7 +2124,7 @@ describe('', () => { await user.click(saveButton); // verify request - const noSpecialPost = axiosMock.history.post.find( + const noSpecialPost = axiosMock.history.patch.find( (entry: any) => entry.data && entry.data.includes(expectedRequestData.graderType), ); expect(noSpecialPost).toBeDefined(); @@ -2169,7 +2168,7 @@ describe('', () => { const isVisibleToStaffOnly = true; axiosMock - .onPost(getCourseItemApiUrl(unit.id), { + .onPatch(getCourseItemApiUrl(unit.id), { publish: 'republish', metadata: { visible_to_staff_only: isVisibleToStaffOnly, @@ -2297,7 +2296,7 @@ describe('', () => { ]; axiosMock - .onPost(getCourseItemApiUrl(section.id), { + .onPatch(getCourseItemApiUrl(section.id), { publish: 'republish', metadata: { highlights, diff --git a/src/course-outline/outline-sidebar/AddSidebar.test.tsx b/src/course-outline/outline-sidebar/AddSidebar.test.tsx index 7d988cedc1..c2635a9240 100644 --- a/src/course-outline/outline-sidebar/AddSidebar.test.tsx +++ b/src/course-outline/outline-sidebar/AddSidebar.test.tsx @@ -298,21 +298,18 @@ describe('AddSidebar', () => { const unit = await screen.findByRole('button', { name: 'Unit' }); await user.click(section); expect(axiosMock.history.post[0].data).toEqual(JSON.stringify(snakeCaseKeys({ - type: 'chapter', category: 'chapter', parentLocator: 'block-v1:UNIX+UX1+2025_T3+type@course+block@course', displayName: 'Section', }))); await user.click(subsection); expect(axiosMock.history.post[1].data).toEqual(JSON.stringify(snakeCaseKeys({ - type: 'sequential', category: 'sequential', parentLocator: lastSection.id, displayName: 'Subsection', }))); await user.click(unit); expect(axiosMock.history.post[2].data).toEqual(JSON.stringify(snakeCaseKeys({ - type: 'vertical', category: 'vertical', parentLocator: lastSubsection.id, displayName: 'Unit', @@ -337,14 +334,12 @@ describe('AddSidebar', () => { await waitFor(() => expect(axiosMock.history.post.length).toBeGreaterThan(1)); // should add a section first expect(axiosMock.history.post[0].data).toEqual(JSON.stringify(snakeCaseKeys({ - type: 'chapter', category: 'chapter', parentLocator: 'block-v1:UNIX+UX1+2025_T3+type@course+block@course', displayName: 'Section', }))); // then subsection expect(axiosMock.history.post[1].data).toEqual(JSON.stringify(snakeCaseKeys({ - type: 'sequential', category: 'sequential', parentLocator: sectionId, displayName: 'Subsection', @@ -365,19 +360,16 @@ describe('AddSidebar', () => { const subsectionId = 'block-v1:UNIX+UX1+2025_T3+type@sequential+block@sequential234'; const unitId = 'block-v1:UNIX+UX1+2025_T3+type@vertical+block@vertical2133'; const sectionBody = snakeCaseKeys({ - type: 'chapter', category: 'chapter', parentLocator: 'block-v1:UNIX+UX1+2025_T3+type@course+block@course', displayName: 'Section', }); const subsectionBody = snakeCaseKeys({ - type: 'sequential', category: 'sequential', parentLocator: sectionId, displayName: 'Subsection', }); const unitBody = snakeCaseKeys({ - type: 'vertical', category: 'vertical', parentLocator: subsectionId, displayName: 'Unit', @@ -421,7 +413,6 @@ describe('AddSidebar', () => { // first one is unit as per mock await user.click(addBtns[0]); expect(axiosMock.history.post[0].data).toEqual(JSON.stringify(snakeCaseKeys({ - type: 'library_v2', category: 'vertical', parentLocator: lastSubsection.id, libraryContentKey: searchResult.results[0].hits[0].usage_key, @@ -429,7 +420,6 @@ describe('AddSidebar', () => { // second one is subsection as per mock await user.click(addBtns[1]); expect(axiosMock.history.post[1].data).toEqual(JSON.stringify(snakeCaseKeys({ - type: 'library_v2', category: 'sequential', parentLocator: lastSection.id, libraryContentKey: searchResult.results[0].hits[1].usage_key, @@ -437,7 +427,6 @@ describe('AddSidebar', () => { // third one is section as per mock await user.click(addBtns[2]); expect(axiosMock.history.post[2].data).toEqual(JSON.stringify(snakeCaseKeys({ - type: 'library_v2', category: 'chapter', parentLocator: 'block-v1:UNIX+UX1+2025_T3+type@course+block@course', libraryContentKey: searchResult.results[0].hits[2].usage_key, From 242dd5cc53a2935aefc10d9f035dfa7369bde785 Mon Sep 17 00:00:00 2001 From: Taimoor Ahmed Date: Mon, 20 Jul 2026 01:15:02 +0500 Subject: [PATCH 4/5] test(course-unit): update xblock mocks to v1 REST verbs Detail-update mocks POST->PATCH; move mocks PATCH the source detail URL; create body-matchers/expected bodies drop the dropped `type` key; update history assertions read history.patch. Refs openedx/frontend-app-authoring#3127 Co-Authored-By: Claude Opus 4.8 --- src/course-unit/CourseUnit.test.tsx | 62 ++++++++++++++--------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/src/course-unit/CourseUnit.test.tsx b/src/course-unit/CourseUnit.test.tsx index c3073affe7..1453b5610d 100644 --- a/src/course-unit/CourseUnit.test.tsx +++ b/src/course-unit/CourseUnit.test.tsx @@ -391,7 +391,7 @@ describe('', () => { await user.click(deleteButton); axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { publish: PUBLISH_TYPES.makePublic, }) .reply(200, { dummy: 'value' }); @@ -553,7 +553,7 @@ describe('', () => { .replyOnce(200, { locator: '1234567890' }); axiosMock - .onPost(getCourseItemApiUrl(blockId), { + .onPatch(getCourseItemApiUrl(blockId), { publish: PUBLISH_TYPES.makePublic, }) .reply(200, { dummy: 'value' }); @@ -677,7 +677,7 @@ describe('', () => { const newDisplayName = `${unitDisplayName} new`; axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { metadata: { display_name: newDisplayName, }, @@ -734,7 +734,7 @@ describe('', () => { const user = userEvent.setup(); const { courseKey, locator } = courseCreateXblockMock; axiosMock - .onPost(postXBlockBaseApiUrl(), { type: 'video', category: 'video', parent_locator: blockId }) + .onPost(postXBlockBaseApiUrl(), { category: 'video', parent_locator: blockId }) .reply(500, {}); render(); @@ -749,12 +749,12 @@ describe('', () => { it('handle creating Problem xblock and showing editor modal', async () => { const user = userEvent.setup(); axiosMock - .onPost(postXBlockBaseApiUrl(), { type: 'problem', category: 'problem', parent_locator: blockId }) + .onPost(postXBlockBaseApiUrl(), { category: 'problem', parent_locator: blockId }) .reply(200, courseCreateXblockMock); render(); axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { publish: PUBLISH_TYPES.makePublic, }) .reply(200, { dummy: 'value' }); @@ -914,7 +914,7 @@ describe('', () => { const newDisplayName = `${unitDisplayName} new`; axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { metadata: { display_name: newDisplayName, }, @@ -963,7 +963,7 @@ describe('', () => { const waffleSpy = mockWaffleFlags({ useVideoGalleryFlow: true }); axiosMock - .onPost(postXBlockBaseApiUrl(), { type: 'video', category: 'video', parent_locator: blockId }) + .onPost(postXBlockBaseApiUrl(), { category: 'video', parent_locator: blockId }) .reply(200, courseCreateXblockMock); render(); @@ -973,7 +973,7 @@ describe('', () => { await user.click(publishButton); axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { publish: PUBLISH_TYPES.makePublic, }) .reply(200, { dummy: 'value' }); @@ -1054,7 +1054,7 @@ describe('', () => { it('handles creating Video xblock and showing editor modal', async () => { axiosMock - .onPost(postXBlockBaseApiUrl(), { type: 'video', category: 'video', parent_locator: blockId }) + .onPost(postXBlockBaseApiUrl(), { category: 'video', parent_locator: blockId }) .reply(200, courseCreateXblockMock); const user = userEvent.setup(); render(); @@ -1064,7 +1064,7 @@ describe('', () => { ); axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { publish: PUBLISH_TYPES.makePublic, }) .reply(200, { dummy: 'value' }); @@ -1247,7 +1247,7 @@ describe('', () => { expect(visibilityCheckbox).not.toBeChecked(); axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { publish: PUBLISH_TYPES.republish, metadata: { visible_to_staff_only: true }, }) @@ -1302,7 +1302,7 @@ describe('', () => { await user.click(makeVisibilityBtn); axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { publish: PUBLISH_TYPES.republish, metadata: { visible_to_staff_only: null }, }) @@ -1346,7 +1346,7 @@ describe('', () => { }); axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { publish: PUBLISH_TYPES.makePublic, }) .reply(200, { dummy: 'value' }); @@ -1428,7 +1428,7 @@ describe('', () => { }); axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { publish: PUBLISH_TYPES.discardChanges, }) .reply(200, { dummy: 'value' }); @@ -1505,7 +1505,7 @@ describe('', () => { expect(group1Checkbox).toBeChecked(); axiosMock - .onPost(getXBlockBaseApiUrl(courseSectionVerticalMock.xblock_info.id), { + .onPatch(getXBlockBaseApiUrl(courseSectionVerticalMock.xblock_info.id), { publish: 'republish', metadata: { visible_to_staff_only: true, group_access: { 50: [2] }, discussion_enabled: true }, }) @@ -1996,7 +1996,7 @@ describe('', () => { render(); axiosMock - .onPatch(postXBlockBaseApiUrl()) + .onPatch(getXBlockBaseApiUrl(blockId)) .reply(200, {}); axiosMock @@ -2060,7 +2060,7 @@ describe('', () => { render(); axiosMock - .onPatch(postXBlockBaseApiUrl()) + .onPatch(getXBlockBaseApiUrl(blockId)) .reply(200, {}); await executeThunk( @@ -2114,7 +2114,7 @@ describe('', () => { render(); axiosMock - .onPatch(postXBlockBaseApiUrl()) + .onPatch(getXBlockBaseApiUrl(blockId)) .reply(200, {}); await executeThunk( @@ -2197,7 +2197,7 @@ describe('', () => { it('handles submit xblock restrict access data when save button is clicked', async () => { const user = userEvent.setup(); axiosMock - .onPost(getXBlockBaseApiUrl(id), { + .onPatch(getXBlockBaseApiUrl(id), { publish: PUBLISH_TYPES.republish, metadata: { visible_to_staff_only: false, group_access: { 970807507: [1959537066] } }, }) @@ -2251,8 +2251,8 @@ describe('', () => { await user.click(saveModalBtnText); await waitFor(() => { - expect(axiosMock.history.post.length).toBeGreaterThan(0); - expect(axiosMock.history.post[0].url).toBe(getXBlockBaseApiUrl(id)); + expect(axiosMock.history.patch.length).toBeGreaterThan(0); + expect(axiosMock.history.patch[0].url).toBe(getXBlockBaseApiUrl(id)); }); expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument(); @@ -2698,7 +2698,7 @@ describe('', () => { await user.click(settingsTab); axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { publish: PUBLISH_TYPES.republish, metadata: { visible_to_staff_only: true, discussion_enabled: true }, }) @@ -2730,7 +2730,7 @@ describe('', () => { expect(await screen.findByRole('heading', { name: /visible to staff only/i })).toBeInTheDocument(); axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { publish: PUBLISH_TYPES.republish, metadata: { visible_to_staff_only: null, discussion_enabled: true }, }) @@ -2798,7 +2798,7 @@ describe('', () => { await user.click(settingsTab); axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { + .onPatch(getXBlockBaseApiUrl(blockId), { publish: PUBLISH_TYPES.republish, metadata: { visible_to_staff_only: null, @@ -2839,7 +2839,7 @@ describe('', () => { ...courseSectionVerticalMock, }); axiosMock - .onPost(getXBlockBaseApiUrl(courseSectionVerticalMock.xblock_info.id)) + .onPatch(getXBlockBaseApiUrl(courseSectionVerticalMock.xblock_info.id)) .reply(200, { ...courseSectionVerticalMock, }); @@ -2898,11 +2898,11 @@ describe('', () => { // Check that the group access is being updated await waitFor(() => { - expect(axiosMock.history.post.length).toBeGreaterThan(0); + expect(axiosMock.history.patch.length).toBeGreaterThan(0); }); - expect(axiosMock.history.post[0].url).toBe(getXBlockBaseApiUrl(courseSectionVerticalMock.xblock_info.id)); - expect(axiosMock.history.post[0].data).toMatch(/"group_access":\{"10":\[1,2\]\}/); + expect(axiosMock.history.patch[0].url).toBe(getXBlockBaseApiUrl(courseSectionVerticalMock.xblock_info.id)); + expect(axiosMock.history.patch[0].data).toMatch(/"group_access":\{"10":\[1,2\]\}/); }); it('should one group in the visibility field in the unit sidebar', async () => { @@ -3360,7 +3360,6 @@ describe('', () => { expect(JSON.parse(axiosMock.history.post[0].data)).toMatchObject({ category: blockType, parent_locator: blockId, - type: blockType, }); }); }); @@ -3432,7 +3431,6 @@ describe('', () => { category: blockType, parent_locator: blockId, boilerplate: template.boilerplate, - ...(blockType !== 'openassessment' ? { type: blockType } : {}), }); }); }); @@ -3464,7 +3462,6 @@ describe('', () => { expect(JSON.parse(axiosMock.history.post[0].data)).toMatchObject({ category: blockType, parent_locator: blockId, - type: blockType, }); }); }); @@ -3489,7 +3486,6 @@ describe('', () => { category: 'html', parent_locator: blockId, library_content_key: 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd', - type: 'library_v2', }); }); }); From 13c334b39f5a058457a4576b24894ed7a5fa3f66 Mon Sep 17 00:00:00 2001 From: Taimoor Ahmed Date: Mon, 20 Jul 2026 01:32:01 +0500 Subject: [PATCH 5/5] test(editors): update xblock mocks to v1 REST (blockV1, PATCH save) fetchBlockById/saveBlock use blockV1; saveBlock asserts PATCH; normalizeContent tests drop learningContextId/courseKey; add blockV1 url test; PdfEditor save mock targets the v1 detail PATCH URL. Refs openedx/frontend-app-authoring#3127 Co-Authored-By: Claude Opus 4.8 --- src/editors/containers/PdfEditor/index.test.tsx | 8 +++++--- src/editors/data/services/cms/api.test.ts | 17 +++++++---------- src/editors/data/services/cms/urls.test.ts | 13 ++++++++++++- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/editors/containers/PdfEditor/index.test.tsx b/src/editors/containers/PdfEditor/index.test.tsx index 5e26a8dc70..bb1fdf064f 100644 --- a/src/editors/containers/PdfEditor/index.test.tsx +++ b/src/editors/containers/PdfEditor/index.test.tsx @@ -115,7 +115,7 @@ describe('PdfEditor', () => { expect(screen.queryAllByLabelText(downloadMessages.allowDownloadLabel.defaultMessage)).toEqual([]); }); it('submits changes to the cms', async () => { - axiosMock.onPost('https://studio.local/xblock/pdf-block-id').reply(200, { + axiosMock.onPatch('https://studio.local/api/contentstore/v1/xblock/pdf-block-id/').reply(200, { data: { id: 'pdf-block-id', data: null, @@ -136,8 +136,10 @@ describe('PdfEditor', () => { fireEvent.change(field, { target: { value: 'https://somewhere.com/stuff.pdf' } }); const saveButton = screen.getByLabelText(editorMessages.saveButtonAriaLabel.defaultMessage); await user.click(saveButton); - await waitFor(() => expect(axiosMock.history.post[0].url).toEqual('https://studio.local/xblock/pdf-block-id')); - const request = axiosMock.history.post[0]; + await waitFor(() => + expect(axiosMock.history.patch[0].url).toEqual('https://studio.local/api/contentstore/v1/xblock/pdf-block-id/') + ); + const request = axiosMock.history.patch[0]; expect(JSON.parse(request.data).metadata.url).toEqual('https://somewhere.com/stuff.pdf'); }); }); diff --git a/src/editors/data/services/cms/api.test.ts b/src/editors/data/services/cms/api.test.ts index 45a4bb4806..4f9c1bdbb8 100644 --- a/src/editors/data/services/cms/api.test.ts +++ b/src/editors/data/services/cms/api.test.ts @@ -4,11 +4,13 @@ import { get, post, put, + patch, deleteObject, } from './utils'; jest.mock('./urls', () => ({ block: jest.fn().mockReturnValue('urls.block'), + blockV1: jest.fn().mockReturnValue('urls.blockV1'), blockAncestor: jest.fn().mockReturnValue('urls.blockAncestor'), blockStudioView: jest.fn().mockReturnValue('urls.StudioView'), courseAssets: jest.fn().mockReturnValue('urls.courseAssets'), @@ -34,6 +36,7 @@ jest.mock('./utils', () => ({ get: jest.fn().mockName('get'), post: jest.fn().mockName('post'), put: jest.fn().mockName('put'), + patch: jest.fn().mockName('patch'), deleteObject: jest.fn().mockName('deleteObject'), })); @@ -53,7 +56,7 @@ describe('cms api', () => { describe('fetchBlockId', () => { it('should call get with url.blocks', async () => { await apiMethods.fetchBlockById({ blockId, studioEndpointUrl }); - expect(get).toHaveBeenCalledWith(urls.block({ blockId, studioEndpointUrl })); + expect(get).toHaveBeenCalledWith(urls.blockV1({ blockId, studioEndpointUrl })); }); }); @@ -175,11 +178,9 @@ describe('cms api', () => { blockId, blockType: 'html', content, - learningContextId, title, })).toEqual({ category: 'html', - courseKey: learningContextId, data: content, has_changes: true, id: blockId, @@ -221,11 +222,9 @@ describe('cms api', () => { blockId, blockType: 'video', content, - learningContextId, title, })).toEqual({ category: 'video', - courseKey: learningContextId, display_name: title, id: blockId, metadata: { @@ -259,22 +258,20 @@ describe('cms api', () => { describe('saveBlock', () => { const content = 'Im baby palo santo ugh celiac fashion axe. La croix lo-fi venmo whatever. Beard man braid migas single-origin coffee forage ramps.'; - it('should call post with urls.block and normalizeContent', async () => { + it('should call patch with urls.blockV1 and normalizeContent', async () => { await apiMethods.saveBlock({ blockId, blockType: 'html', content, - learningContextId, studioEndpointUrl, title, }); - expect(post).toHaveBeenCalledWith( - urls.block({ studioEndpointUrl }), + expect(patch).toHaveBeenCalledWith( + urls.blockV1({ studioEndpointUrl, blockId }), apiMethods.normalizeContent({ blockType: 'html', content, blockId, - learningContextId, title, }), ); diff --git a/src/editors/data/services/cms/urls.test.ts b/src/editors/data/services/cms/urls.test.ts index 3409e83c5b..caf5754f38 100644 --- a/src/editors/data/services/cms/urls.test.ts +++ b/src/editors/data/services/cms/urls.test.ts @@ -3,6 +3,7 @@ import { unit, libraryV1, block, + blockV1, blockAncestor, blockStudioView, courseAssets, @@ -115,10 +116,20 @@ describe('cms url methods', () => { .toEqual(`${studioEndpointUrl}/api/xblock/v2/xblocks/${v2BlockId}/fields/`); }); }); + describe('blockV1', () => { + it('returns v1 REST url with studioEndpointUrl and blockId', () => { + expect(blockV1({ studioEndpointUrl, blockId })) + .toEqual(`${studioEndpointUrl}/api/contentstore/v1/xblock/${blockId}/`); + }); + it('returns v2 url with studioEndpointUrl and v2BlockId', () => { + expect(blockV1({ studioEndpointUrl, blockId: v2BlockId })) + .toEqual(`${studioEndpointUrl}/api/xblock/v2/xblocks/${v2BlockId}/fields/`); + }); + }); describe('blockAncestor', () => { it('returns url with studioEndpointUrl, blockId and ancestor query', () => { expect(blockAncestor({ studioEndpointUrl, blockId })) - .toEqual(`${block({ studioEndpointUrl, blockId })}?fields=ancestorInfo`); + .toEqual(`${blockV1({ studioEndpointUrl, blockId })}?fields=ancestorInfo`); }); it('throws error with studioEndpointUrl, v2 blockId and ancestor query', () => { expect(() => {