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
146 changes: 146 additions & 0 deletions src/actions/__tests__/speaker-actions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* @jest-environment jsdom
*/
import configureStore from "redux-mock-store";
import thunk from "redux-thunk";
import {
deleteRequest,
putRequest
} from "openstack-uicore-foundation/lib/utils/actions";
import { removeAttachedPicture, saveSpeaker } from "../speaker-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"),
deleteRequest: jest.fn(),
putRequest: jest.fn()
}));

const SPEAKER_ID = 42;

describe("removeAttachedPicture", () => {
const mockStore = configureStore([thunk]);

beforeEach(() => {
jest.clearAllMocks();
window.API_BASE_URL = "https://api.test";
jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN");
deleteRequest.mockImplementation(
(_requestAction, receiveAction) => () => (dispatch) => {
dispatch(receiveAction({ response: {} }));
return Promise.resolve({ response: {} });
}
);
});

afterEach(() => {
jest.restoreAllMocks();
delete window.API_BASE_URL;
});

it("deletes the profile photo through the photo endpoint", async () => {
const store = mockStore({});

await store.dispatch(removeAttachedPicture(SPEAKER_ID, "profile"));

expect(deleteRequest).toHaveBeenCalledTimes(1);
expect(deleteRequest.mock.calls[0][2]).toBe(
`https://api.test/api/v1/speakers/${SPEAKER_ID}/photo?access_token=TOKEN`
);
expect(store.getActions().map((a) => a.type)).toContain("PIC_DELETED");
});

it("deletes the big photo through the big-photo endpoint", async () => {
const store = mockStore({});

await store.dispatch(removeAttachedPicture(SPEAKER_ID, "big"));

expect(deleteRequest.mock.calls[0][2]).toBe(
`https://api.test/api/v1/speakers/${SPEAKER_ID}/big-photo?access_token=TOKEN`
);
expect(store.getActions().map((a) => a.type)).toContain("BIG_PIC_DELETED");
});

it("does not emit a delete action when the request fails", async () => {
deleteRequest.mockImplementation(
() => () => () => Promise.reject(new Error("boom"))
);
const store = mockStore({});

await expect(
store.dispatch(removeAttachedPicture(SPEAKER_ID, "profile"))
).resolves.toBeUndefined();

const types = store.getActions().map((a) => a.type);
expect(types).not.toContain("PIC_DELETED");
expect(types).not.toContain("STOP_LOADING");
});
});

describe("saveSpeaker", () => {
const mockStore = configureStore([thunk]);

beforeEach(() => {
jest.clearAllMocks();
window.API_BASE_URL = "https://api.test";
jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN");
putRequest.mockImplementation(
(requestAction, receiveAction, _url, _body, _err, payload) =>
() =>
(dispatch) => {
dispatch(requestAction(payload));
dispatch(receiveAction({ response: {} }));
return Promise.resolve({ response: {} });
}
);
});

afterEach(() => {
jest.restoreAllMocks();
delete window.API_BASE_URL;
});

it("never deletes photos as a side effect of saving", async () => {
const store = mockStore({});

await store.dispatch(
saveSpeaker({ id: SPEAKER_ID, title: "Dev", pic: "", big_pic: "" })
);

expect(deleteRequest).not.toHaveBeenCalled();
});

it("resolves only after the update request settles", async () => {
let resolveRequest;
putRequest.mockImplementation(
(requestAction, receiveAction) => () => (dispatch) => {
dispatch(requestAction());
return new Promise((resolve) => {
resolveRequest = () => {
dispatch(receiveAction({ response: {} }));
resolve({ response: {} });
};
});
}
);
const store = mockStore({});

let settled = false;
const dispatched = store
.dispatch(
saveSpeaker({ id: SPEAKER_ID, title: "Dev", pic: "", big_pic: "" })
)
.then(() => {
settled = true;
});

await Promise.resolve();
await Promise.resolve();
expect(settled).toBe(false);

resolveRequest();
await dispatched;
expect(settled).toBe(true);
});
});
86 changes: 64 additions & 22 deletions src/actions/speaker-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ export const UPDATE_SPEAKER = "UPDATE_SPEAKER";
export const SPEAKER_UPDATED = "SPEAKER_UPDATED";
export const SPEAKER_ADDED = "SPEAKER_ADDED";
export const PIC_ATTACHED = "PIC_ATTACHED";
export const PIC_DELETED = "PIC_DELETED";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
export const BIG_PIC_ATTACHED = "BIG_PIC_ATTACHED";
export const BIG_PIC_DELETED = "BIG_PIC_DELETED";
export const MERGE_SPEAKERS = "MERGE_SPEAKERS";
export const SPEAKER_MERGED = "SPEAKER_MERGED";

Expand Down Expand Up @@ -141,6 +143,18 @@ const uploadProfilePic = (entity, file) => async (dispatch) => {
});
};

const deleteProfilePic = (speakerId) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();

return deleteRequest(
null,
createAction(PIC_DELETED),
`${window.API_BASE_URL}/api/v1/speakers/${speakerId}/photo?access_token=${accessToken}`,
null,
authErrorHandler
)({})(dispatch);
};

const uploadBigPic = (entity, file) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();

Expand All @@ -157,6 +171,18 @@ const uploadBigPic = (entity, file) => async (dispatch) => {
});
};

const deleteBigPic = (speakerId) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();

return deleteRequest(
null,
createAction(BIG_PIC_DELETED),
`${window.API_BASE_URL}/api/v1/speakers/${speakerId}/big-photo?access_token=${accessToken}`,
null,
authErrorHandler
)({})(dispatch);
};

export const initSpeakersList = () => async (dispatch) => {
dispatch(createAction(INIT_SPEAKERS_LIST_PARAMS)());
};
Expand Down Expand Up @@ -351,7 +377,7 @@ export const saveSpeaker = (entity) => async (dispatch) => {
const normalizedEntity = normalizeEntity(entity);

if (entity.id) {
putRequest(
return putRequest(
createAction(UPDATE_SPEAKER),
createAction(SPEAKER_UPDATED),
`${window.API_BASE_URL}/api/v1/speakers/${entity.id}?access_token=${accessToken}`,
Expand All @@ -361,28 +387,28 @@ export const saveSpeaker = (entity) => async (dispatch) => {
)({})(dispatch).then(() => {
dispatch(showSuccessMessage(T.translate("edit_speaker.speaker_saved")));
});
} else {
const successMessage = {
title: T.translate("general.done"),
html: T.translate("edit_speaker.speaker_created"),
type: "success"
};

postRequest(
createAction(UPDATE_SPEAKER),
createAction(SPEAKER_ADDED),
`${window.API_BASE_URL}/api/v1/speakers?access_token=${accessToken}`,
normalizedEntity,
authErrorHandler,
entity
)({})(dispatch).then(() => {
dispatch(
showMessage(successMessage, () => {
history.push("/app/speakers");
})
);
});
}

const successMessage = {
title: T.translate("general.done"),
html: T.translate("edit_speaker.speaker_created"),
type: "success"
};

return postRequest(
createAction(UPDATE_SPEAKER),
createAction(SPEAKER_ADDED),
`${window.API_BASE_URL}/api/v1/speakers?access_token=${accessToken}`,
normalizedEntity,
authErrorHandler,
entity
)({})(dispatch).then(() => {
dispatch(
showMessage(successMessage, () => {
history.push("/app/speakers");
})
);
});
};

export const attachPicture = (entity, file, picAttr) => async (dispatch) => {
Expand All @@ -409,6 +435,22 @@ export const attachPicture = (entity, file, picAttr) => async (dispatch) => {
});
};

export const removeAttachedPicture =
(speakerId, picAttr) => async (dispatch) => {
const deleteFile = picAttr === "profile" ? deleteProfilePic : deleteBigPic;

dispatch(startLoading());

return dispatch(deleteFile(speakerId))
.then(() => {
dispatch(stopLoading());
})
.catch(() => {
// authErrorHandler already reported the failure and stopped loading;
// the reducer is left untouched so the picture stays visible.
});
};

/** ************************************************************************************************* */
/* SPEAKER ATTENDANCE */
/** ************************************************************************************************* */
Expand Down
Loading
Loading