Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ EMAIL_SCOPES="clients/read templates/read templates/write emails/read"
FILE_UPLOAD_SCOPES="files/upload"
SPONSOR_PAGES_API_URL=https://sponsor-pages-api.dev.fnopen.com
SPONSOR_PAGES_SCOPES="page-template/read page-template/write show-page/read show-page/write media-upload/read"
SCOPES="profile openid offline_access reports/all ${EMAIL_SCOPES} ${INVENTORY_API_SCOPES} ${FILE_UPLOAD_SCOPES} ${PURCHASES_API_SCOPES} ${SPONSOR_USERS_SCOPES} ${SPONSOR_PAGES_SCOPES} ${SCOPES_BASE_REALM}/summits/delete-event ${SCOPES_BASE_REALM}/companies/read ${SCOPES_BASE_REALM}/companies/write ${SCOPES_BASE_REALM}/summits/write ${SCOPES_BASE_REALM}/summits/write-event ${SCOPES_BASE_REALM}/summits/read/all ${SCOPES_BASE_REALM}/summits/read ${SCOPES_BASE_REALM}/summits/publish-event ${SCOPES_BASE_REALM}/members/read ${SCOPES_BASE_REALM}/members/read/me ${SCOPES_BASE_REALM}/speakers/write ${SCOPES_BASE_REALM}/attendees/write ${SCOPES_BASE_REALM}/members/write ${SCOPES_BASE_REALM}/organizations/write ${SCOPES_BASE_REALM}/organizations/read ${SCOPES_BASE_REALM}/summits/write-presentation-materials ${SCOPES_BASE_REALM}/summits/registration-orders/update ${SCOPES_BASE_REALM}/summits/registration-orders/delete ${SCOPES_BASE_REALM}/summits/registration-orders/create/offline ${SCOPES_BASE_REALM}/summits/badge-scans/read config-values/write ${SCOPES_BASE_REALM}/summit-administrator-groups/read ${SCOPES_BASE_REALM}/summit-administrator-groups/write ${SCOPES_BASE_REALM}/summit-media-file-types/read ${SCOPES_BASE_REALM}/summit-media-file-types/write user-roles/write entity-updates/publish ${SCOPES_BASE_REALM}/audit-logs/read filter-criteria/read filter-criteria/write"
SCOPES="profile openid offline_access reports/all ${EMAIL_SCOPES} ${INVENTORY_API_SCOPES} ${FILE_UPLOAD_SCOPES} ${PURCHASES_API_SCOPES} ${SPONSOR_USERS_SCOPES} ${SPONSOR_PAGES_SCOPES} ${DROPBOX_MATERIALIZER_API_SCOPES} ${SCOPES_BASE_REALM}/summits/delete-event ${SCOPES_BASE_REALM}/companies/read ${SCOPES_BASE_REALM}/companies/write ${SCOPES_BASE_REALM}/summits/write ${SCOPES_BASE_REALM}/summits/write-event ${SCOPES_BASE_REALM}/summits/read/all ${SCOPES_BASE_REALM}/summits/read ${SCOPES_BASE_REALM}/summits/publish-event ${SCOPES_BASE_REALM}/members/read ${SCOPES_BASE_REALM}/members/read/me ${SCOPES_BASE_REALM}/speakers/write ${SCOPES_BASE_REALM}/attendees/write ${SCOPES_BASE_REALM}/members/write ${SCOPES_BASE_REALM}/organizations/write ${SCOPES_BASE_REALM}/organizations/read ${SCOPES_BASE_REALM}/summits/write-presentation-materials ${SCOPES_BASE_REALM}/summits/registration-orders/update ${SCOPES_BASE_REALM}/summits/registration-orders/delete ${SCOPES_BASE_REALM}/summits/registration-orders/create/offline ${SCOPES_BASE_REALM}/summits/badge-scans/read config-values/write ${SCOPES_BASE_REALM}/summit-administrator-groups/read ${SCOPES_BASE_REALM}/summit-administrator-groups/write ${SCOPES_BASE_REALM}/summit-media-file-types/read ${SCOPES_BASE_REALM}/summit-media-file-types/write user-roles/write entity-updates/publish ${SCOPES_BASE_REALM}/audit-logs/read filter-criteria/read filter-criteria/write"
GOOGLE_API_KEY=
ALLOWED_USER_GROUPS="super-admins administrators summit-front-end-administrators summit-room-administrators track-chairs-admins sponsors"
APP_CLIENT_NAME="openstack"
Expand All @@ -37,5 +37,7 @@ SENTRY_PROJECT=
SENTRY_TRACE_SAMPLE_RATE=
SENTRY_TRACE_PROPAGATION_TARGETS=
CFP_APP_BASE_URL=
DROPBOX_MATERIALIZER_API_BASE_URL=
DROPBOX_MATERIALIZER_API_SCOPES="dropbox-materializer/read dropbox-materializer/write"
S3_MEDIA_UPLOADS_ENDPOINT_URL=https://fntech.sfo2.digitaloceanspaces.com
S3_MEDIA_UPLOADS_BUCKET_NAME=PresentationMediaUploads_DEV
197 changes: 197 additions & 0 deletions src/actions/__tests__/dropbox-sync-actions.test.js
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" });
});
});
160 changes: 160 additions & 0 deletions src/actions/dropbox-sync-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**

Copy link
Copy Markdown

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).

* 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@caseylocker if the GET ${getBaseUrl()}/api/sync/config/${currentSummit.id}/ fails the loading flag https://github.com/fntechgit/summit-admin/pull/833/changes#diff-8cbab85249345248d52cc346a2db42c03a194eeb4dfd6598654dc7d1b9d2da48R36 stuck true forever. i do think that we need to add a catch .catch(() => { dispatch(createAction(RECEIVE_SYNC_CONFIG)({})); });`

)(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());
});
};
2 changes: 2 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ window.SENTRY_TRACE_SAMPLE_RATE = process.env.SENTRY_TRACE_SAMPLE_RATE;
window.SENTRY_TRACE_PROPAGATION_TARGETS =
process.env.SENTRY_TRACE_PROPAGATION_TARGETS;
window.CFP_APP_BASE_URL = process.env.CFP_APP_BASE_URL;
window.DROPBOX_MATERIALIZER_API_BASE_URL =
process.env.DROPBOX_MATERIALIZER_API_BASE_URL;

if (exclusiveSections.hasOwnProperty(process.env.APP_CLIENT_NAME)) {
window.EXCLUSIVE_SECTIONS = exclusiveSections[process.env.APP_CLIENT_NAME];
Expand Down
Loading
Loading