-
Notifications
You must be signed in to change notification settings - Fork 4
feat: Dropbox sync UI — toggle, Rebuild All, Resync Room #833
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
427a1da
7fe4baf
8a82836
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| /** | ||
| * @jest-environment jsdom | ||
| */ | ||
|
|
||
| import configureStore from "redux-mock-store"; | ||
| import thunk from "redux-thunk"; | ||
| import flushPromises from "flush-promises"; | ||
| import { | ||
| getRequest, | ||
| putRequest, | ||
| postRequest | ||
| } from "openstack-uicore-foundation/lib/utils/actions"; | ||
| import * as DropboxSyncActions from "../dropbox-sync-actions"; | ||
| import * as methods from "../../utils/methods"; | ||
|
|
||
| jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({ | ||
| __esModule: true, | ||
| ...jest.requireActual("openstack-uicore-foundation/lib/utils/actions"), | ||
| getRequest: jest.fn(), | ||
| putRequest: jest.fn(), | ||
| postRequest: jest.fn() | ||
| })); | ||
|
|
||
| const mockRequestImpl = | ||
| (requestActionCreator, receiveActionCreator) => () => (dispatch) => { | ||
| if (requestActionCreator && typeof requestActionCreator === "function") | ||
| dispatch(requestActionCreator({})); | ||
|
|
||
| return new Promise((resolve) => { | ||
| if (typeof receiveActionCreator === "function") { | ||
| dispatch(receiveActionCreator({ response: {} })); | ||
| resolve({ response: {} }); | ||
| return; | ||
| } | ||
| dispatch(receiveActionCreator); | ||
| resolve({ response: {} }); | ||
| }); | ||
| }; | ||
|
|
||
| const mockRequestImplReject = () => () => () => | ||
| Promise.reject(new Error("network error")); | ||
|
|
||
| describe("dropbox sync actions", () => { | ||
| const middlewares = [thunk]; | ||
| const mockStore = configureStore(middlewares); | ||
| let store; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| store = mockStore({ | ||
| currentSummitState: { currentSummit: { id: 1 } } | ||
| }); | ||
| jest.spyOn(methods, "getAccessTokenSafely").mockReturnValue("TOKEN"); | ||
| window.DROPBOX_MATERIALIZER_API_BASE_URL = "https://test-api.example.com"; | ||
|
|
||
| getRequest.mockImplementation(mockRequestImpl); | ||
| putRequest.mockImplementation( | ||
| (requestActionCreator, receiveActionCreator) => | ||
| mockRequestImpl(requestActionCreator, receiveActionCreator) | ||
| ); | ||
| postRequest.mockImplementation( | ||
| (requestActionCreator, receiveActionCreator) => | ||
| mockRequestImpl(requestActionCreator, receiveActionCreator) | ||
| ); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| jest.restoreAllMocks(); | ||
| delete window.DROPBOX_MATERIALIZER_API_BASE_URL; | ||
| }); | ||
|
|
||
| test("getSyncConfig dispatches REQUEST, RECEIVE, then STOP_LOADING", async () => { | ||
| store.dispatch(DropboxSyncActions.getSyncConfig()); | ||
| await flushPromises(); | ||
|
|
||
| const actions = store.getActions(); | ||
| expect(actions).toEqual([ | ||
| { payload: {}, type: "REQUEST_SYNC_CONFIG" }, | ||
| { payload: { response: {} }, type: "RECEIVE_SYNC_CONFIG" }, | ||
| { payload: undefined, type: "STOP_LOADING" } | ||
| ]); | ||
| expect(getRequest).toBeCalledTimes(1); | ||
| }); | ||
|
|
||
| test("getSyncConfig early returns when getBaseUrl() is falsy", async () => { | ||
| window.DROPBOX_MATERIALIZER_API_BASE_URL = ""; | ||
|
|
||
| store.dispatch(DropboxSyncActions.getSyncConfig()); | ||
| await flushPromises(); | ||
|
|
||
| expect(store.getActions()).toEqual([]); | ||
| expect(getRequest).not.toBeCalled(); | ||
| expect(methods.getAccessTokenSafely).not.toBeCalled(); | ||
| }); | ||
|
|
||
| test("getSyncConfig early returns when summitId is missing", async () => { | ||
| store = configureStore([thunk])({ | ||
| currentSummitState: { currentSummit: {} } | ||
| }); | ||
|
|
||
| store.dispatch(DropboxSyncActions.getSyncConfig()); | ||
| await flushPromises(); | ||
|
|
||
| expect(store.getActions()).toEqual([]); | ||
| expect(getRequest).not.toBeCalled(); | ||
| expect(methods.getAccessTokenSafely).not.toBeCalled(); | ||
| }); | ||
|
|
||
| test("updateSyncConfig dispatches START_LOADING, SYNC_CONFIG_UPDATED, STOP_LOADING", async () => { | ||
| store.dispatch( | ||
| DropboxSyncActions.updateSyncConfig({ dropbox_sync_enabled: true }) | ||
| ); | ||
| await flushPromises(); | ||
|
|
||
| const actions = store.getActions(); | ||
| expect(actions[0]).toEqual({ payload: undefined, type: "START_LOADING" }); | ||
| expect(actions[1]).toEqual({ | ||
| payload: { response: {} }, | ||
| type: "SYNC_CONFIG_UPDATED" | ||
| }); | ||
| expect(actions[2]).toEqual({ payload: undefined, type: "STOP_LOADING" }); | ||
| expect(putRequest).toBeCalledTimes(1); | ||
| }); | ||
|
|
||
| test("rebuildSync dispatches START_LOADING, REBUILD_SYNC_DISPATCHED, STOP_LOADING", async () => { | ||
| store.dispatch(DropboxSyncActions.rebuildSync()); | ||
| await flushPromises(); | ||
|
|
||
| const actions = store.getActions(); | ||
| expect(actions[0]).toEqual({ payload: undefined, type: "START_LOADING" }); | ||
| expect(actions[1]).toEqual({ | ||
| payload: { response: {} }, | ||
| type: "REBUILD_SYNC_DISPATCHED" | ||
| }); | ||
| expect(actions[2]).toEqual({ payload: undefined, type: "STOP_LOADING" }); | ||
| expect(postRequest).toBeCalledTimes(1); | ||
| }); | ||
|
|
||
| test("resyncRoom dispatches START_LOADING, RESYNC_ROOM_DISPATCHED, STOP_LOADING", async () => { | ||
| store.dispatch(DropboxSyncActions.resyncRoom("Main Venue", "Room A")); | ||
| await flushPromises(); | ||
|
|
||
| const actions = store.getActions(); | ||
| expect(actions[0]).toEqual({ payload: undefined, type: "START_LOADING" }); | ||
| expect(actions[1]).toEqual({ | ||
| payload: { response: {} }, | ||
| type: "RESYNC_ROOM_DISPATCHED" | ||
| }); | ||
| expect(actions[2]).toEqual({ payload: undefined, type: "STOP_LOADING" }); | ||
| expect(postRequest).toBeCalled(); | ||
| }); | ||
|
|
||
| test("getSyncConfig dispatches RECEIVE_SYNC_CONFIG with empty payload on failure", async () => { | ||
| getRequest.mockImplementation(mockRequestImplReject); | ||
|
|
||
| store.dispatch(DropboxSyncActions.getSyncConfig()); | ||
| await flushPromises(); | ||
|
|
||
| const actions = store.getActions(); | ||
| expect(actions).toEqual([{ payload: {}, type: "RECEIVE_SYNC_CONFIG" }]); | ||
| }); | ||
|
|
||
| test("updateSyncConfig dispatches STOP_LOADING on failure", async () => { | ||
| putRequest.mockImplementation(() => mockRequestImplReject()); | ||
|
|
||
| store.dispatch( | ||
| DropboxSyncActions.updateSyncConfig({ dropbox_sync_enabled: true }) | ||
| ); | ||
| await flushPromises(); | ||
|
|
||
| const actions = store.getActions(); | ||
| expect(actions[0]).toEqual({ payload: undefined, type: "START_LOADING" }); | ||
| expect(actions[1]).toEqual({ payload: undefined, type: "STOP_LOADING" }); | ||
| }); | ||
|
|
||
| test("rebuildSync dispatches STOP_LOADING on failure", async () => { | ||
| postRequest.mockImplementation(() => mockRequestImplReject()); | ||
|
|
||
| store.dispatch(DropboxSyncActions.rebuildSync()); | ||
| await flushPromises(); | ||
|
|
||
| const actions = store.getActions(); | ||
| expect(actions[0]).toEqual({ payload: undefined, type: "START_LOADING" }); | ||
| expect(actions[1]).toEqual({ payload: undefined, type: "STOP_LOADING" }); | ||
| }); | ||
|
|
||
| test("resyncRoom dispatches STOP_LOADING on failure", async () => { | ||
| postRequest.mockImplementation(() => mockRequestImplReject()); | ||
|
|
||
| store.dispatch(DropboxSyncActions.resyncRoom("Main Venue", "Room A")); | ||
| await flushPromises(); | ||
|
|
||
| const actions = store.getActions(); | ||
| expect(actions[0]).toEqual({ payload: undefined, type: "START_LOADING" }); | ||
| expect(actions[1]).toEqual({ payload: undefined, type: "STOP_LOADING" }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| /** | ||
| * Copyright 2026 OpenStack Foundation | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| * */ | ||
|
|
||
| import T from "i18n-react/dist/i18n-react"; | ||
| import { | ||
| getRequest, | ||
| putRequest, | ||
| postRequest, | ||
| createAction, | ||
| stopLoading, | ||
| startLoading, | ||
| showSuccessMessage, | ||
| authErrorHandler | ||
| } from "openstack-uicore-foundation/lib/utils/actions"; | ||
| import { getAccessTokenSafely } from "../utils/methods"; | ||
|
|
||
| export const REQUEST_SYNC_CONFIG = "REQUEST_SYNC_CONFIG"; | ||
| export const RECEIVE_SYNC_CONFIG = "RECEIVE_SYNC_CONFIG"; | ||
| export const SYNC_CONFIG_UPDATED = "SYNC_CONFIG_UPDATED"; | ||
| export const REBUILD_SYNC_DISPATCHED = "REBUILD_SYNC_DISPATCHED"; | ||
| export const RESYNC_ROOM_DISPATCHED = "RESYNC_ROOM_DISPATCHED"; | ||
|
|
||
| const getBaseUrl = () => window.DROPBOX_MATERIALIZER_API_BASE_URL; | ||
|
|
||
| export const getSyncConfig = () => async (dispatch, getState) => { | ||
| const { currentSummitState } = getState(); | ||
| const baseUrl = getBaseUrl(); | ||
| const summitId = currentSummitState?.currentSummit?.id; | ||
|
|
||
| if (!baseUrl || !summitId) return; | ||
|
|
||
| const accessToken = await getAccessTokenSafely(); | ||
|
|
||
| const params = { | ||
| access_token: accessToken | ||
| }; | ||
|
|
||
| return getRequest( | ||
| createAction(REQUEST_SYNC_CONFIG), | ||
| createAction(RECEIVE_SYNC_CONFIG), | ||
| `${baseUrl}/api/v1/sync/config/${summitId}/`, | ||
| authErrorHandler | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @caseylocker if the GET |
||
| )(params)(dispatch) | ||
| .then(() => { | ||
| dispatch(stopLoading()); | ||
| }) | ||
| .catch(() => { | ||
| dispatch(createAction(RECEIVE_SYNC_CONFIG)({})); | ||
| }); | ||
| }; | ||
|
|
||
| export const updateSyncConfig = (data) => async (dispatch, getState) => { | ||
| const { currentSummitState } = getState(); | ||
| const baseUrl = getBaseUrl(); | ||
| const summitId = currentSummitState?.currentSummit?.id; | ||
|
|
||
| if (!baseUrl || !summitId) return; | ||
|
|
||
| const accessToken = await getAccessTokenSafely(); | ||
|
|
||
| dispatch(startLoading()); | ||
|
|
||
| const params = { | ||
| access_token: accessToken | ||
| }; | ||
|
|
||
| return putRequest( | ||
| null, | ||
| createAction(SYNC_CONFIG_UPDATED), | ||
| `${baseUrl}/api/v1/sync/config/${summitId}/`, | ||
| data, | ||
| authErrorHandler | ||
| )(params)(dispatch) | ||
| .then(() => { | ||
| dispatch(stopLoading()); | ||
| dispatch(showSuccessMessage(T.translate("dropbox_sync.config_saved"))); | ||
| }) | ||
| .catch(() => { | ||
| dispatch(stopLoading()); | ||
| }); | ||
| }; | ||
|
|
||
| export const rebuildSync = () => async (dispatch, getState) => { | ||
| const { currentSummitState } = getState(); | ||
| const baseUrl = getBaseUrl(); | ||
| const summitId = currentSummitState?.currentSummit?.id; | ||
|
|
||
| if (!baseUrl || !summitId) return; | ||
|
|
||
| const accessToken = await getAccessTokenSafely(); | ||
|
|
||
| dispatch(startLoading()); | ||
|
|
||
| const params = { | ||
| access_token: accessToken | ||
| }; | ||
|
|
||
| return postRequest( | ||
| null, | ||
| createAction(REBUILD_SYNC_DISPATCHED), | ||
| `${baseUrl}/api/v1/sync/rebuild/${summitId}/`, | ||
| null, | ||
| authErrorHandler | ||
| )(params)(dispatch) | ||
| .then(() => { | ||
| dispatch(stopLoading()); | ||
| dispatch( | ||
| showSuccessMessage(T.translate("dropbox_sync.rebuild_dispatched")) | ||
| ); | ||
| }) | ||
| .catch(() => { | ||
| dispatch(stopLoading()); | ||
| }); | ||
| }; | ||
|
|
||
| export const resyncRoom = | ||
| (venueName, roomName) => async (dispatch, getState) => { | ||
| const { currentSummitState } = getState(); | ||
| const baseUrl = getBaseUrl(); | ||
| const summitId = currentSummitState?.currentSummit?.id; | ||
|
|
||
| if (!baseUrl || !summitId) return; | ||
|
|
||
| const accessToken = await getAccessTokenSafely(); | ||
|
|
||
| dispatch(startLoading()); | ||
|
|
||
| const params = { | ||
| access_token: accessToken | ||
| }; | ||
|
|
||
| return postRequest( | ||
| null, | ||
| createAction(RESYNC_ROOM_DISPATCHED), | ||
| `${baseUrl}/api/v1/sync/materialize/${summitId}/${encodeURIComponent( | ||
| venueName | ||
| )}/${encodeURIComponent(roomName)}/`, | ||
| null, | ||
| authErrorHandler | ||
| )(params)(dispatch) | ||
| .then(() => { | ||
| dispatch(stopLoading()); | ||
| dispatch( | ||
| showSuccessMessage(T.translate("dropbox_sync.resync_dispatched")) | ||
| ); | ||
| }) | ||
| .catch(() => { | ||
| dispatch(stopLoading()); | ||
| }); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@caseylocker
No test files added for src/actions/dropbox-sync-actions.js or src/reducers/locations/dropbox-sync-reducer.js
Add reducer tests (state transitions per action type, DEFAULT_STATE reset on summit change/logout) and action tests (correct dispatch sequences, early return when getBaseUrl()
is falsy).