Skip to content
Open
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
150 changes: 150 additions & 0 deletions src/actions/__tests__/payment-profile-actions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import configureStore from "redux-mock-store";
import thunk from "redux-thunk";
import flushPromises from "flush-promises";
import { getRequest } from "openstack-uicore-foundation/lib/utils/actions";
import { getPaymentProfiles } from "../ticket-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()
}));

describe("getPaymentProfiles", () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const SUMMIT_ID = 42;
let store;
let capturedParams;

beforeEach(() => {
jest.clearAllMocks();
capturedParams = null;
store = mockStore({
currentSummitState: { currentSummit: { id: SUMMIT_ID } }
});
jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN");
window.PURCHASES_API_URL = "https://purchases.example.com";

getRequest.mockImplementation(
(reqAC, recAC, url, errHandler, extraPayload) => (params) => {
capturedParams = params;
return (dispatch) => {
dispatch(reqAC(extraPayload || {}));
return Promise.resolve().then(() => {
dispatch(
recAC({
response: {
data: [],
total: 0,
current_page: 1,
last_page: 1
}
})
);
});
};
}
);
});

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

test("dispatches START_LOADING, REQUEST, RECEIVE, STOP_LOADING", async () => {
store.dispatch(getPaymentProfiles());
await flushPromises();

const types = store.getActions().map((a) => a.type);
expect(types).toEqual([
"START_LOADING",
"REQUEST_PAYMENT_PROFILES",
"RECEIVE_PAYMENT_PROFILES",
"STOP_LOADING"
]);
expect(getRequest).toHaveBeenCalledTimes(1);
});

test("builds correct summit URL", async () => {
store.dispatch(getPaymentProfiles());
await flushPromises();

const url = getRequest.mock.calls[0][2];
expect(url).toBe(
`https://purchases.example.com/api/v1/summits/${SUMMIT_ID}/payment-profiles`
);
});

test.each(["", undefined])("omits filter[] when term is %p", async (term) => {
store.dispatch(getPaymentProfiles(term));
await flushPromises();

expect(capturedParams).not.toHaveProperty("filter[]");
});

test("adds filter[] array with provider, id, application_type for non-empty term", async () => {
store.dispatch(getPaymentProfiles("stripe"));
await flushPromises();

expect(capturedParams["filter[]"]).toEqual([
"provider=@stripe,id=@stripe,application_type=@stripe"
]);
});

test.each([
[
"foo,bar",
"provider=@foo\\,bar,id=@foo\\,bar,application_type=@foo\\,bar"
],
["foo;bar", "provider=@foo\\;bar,id=@foo\\;bar,application_type=@foo\\;bar"]
])(
"escapeFilterValue escapes special chars in %p",
async (term, expected) => {
store.dispatch(getPaymentProfiles(term));
await flushPromises();

expect(capturedParams["filter[]"]).toEqual([expected]);
}
);

test("REQUEST_PAYMENT_PROFILES payload includes term, page, perPage, order, orderDir", async () => {
store.dispatch(getPaymentProfiles("stripe", 2, 25, "provider", 0));
await flushPromises();

const extraPayload = getRequest.mock.calls[0][4];
expect(extraPayload).toEqual({
term: "stripe",
page: 2,
perPage: 25,
order: "provider",
orderDir: 0
});
});

test("params include access_token, page, and per_page", async () => {
store.dispatch(getPaymentProfiles("", 3, 20));
await flushPromises();

expect(capturedParams).toMatchObject({
access_token: "TOKEN",
page: 3,
per_page: 20
});
});

test.each([
[1, "+provider"],
[0, "-provider"]
])(
"order param uses correct prefix for orderDir %i",
async (orderDir, expected) => {
store.dispatch(getPaymentProfiles("", 1, 10, "provider", orderDir));
await flushPromises();

expect(capturedParams.order).toBe(expected);
}
);
});
112 changes: 59 additions & 53 deletions src/actions/ticket-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
EXPORT_PAGE_SIZE_100,
TEN
} from "../utils/constants";
import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions";

export const REQUEST_TICKETS = "REQUEST_TICKETS";
export const RECEIVE_TICKETS = "RECEIVE_TICKETS";
Expand Down Expand Up @@ -1186,6 +1187,7 @@ export const deleteRefundPolicy =

export const getPaymentProfiles =
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 [MEDIUM] Missing test coverage for the new behavior

No __tests__ updates ship with this PR for any of the new surfaces. The following should be covered before merge:

  • getPaymentProfiles with term — URL/params construction with non-empty term (verify escapeFilterValue, the filter[] array shape, and the new REQUEST_PAYMENT_PROFILES payload).
  • Reducer REQUEST_PAYMENT_PROFILES — asserts that term, currentPage, perPage land in state (catches the bug flagged in payment-profile-list-reducer.js#L46).
  • PaymentProfileDialog flow
    • parent stays open after a fee-type save (catches the bug flagged in payment-profile-list-page.js#L110),
    • dialog stays open if save rejects (.catch behaviour),
    • validation prevents submit when required fields are empty (once the schema is re-enabled).

This is a UX-critical surface (payment configuration). At minimum the action + reducer should be covered.

(
term = "",
page = DEFAULT_CURRENT_PAGE,
perPage = DEFAULT_PER_PAGE,
order = "id",
Expand All @@ -1195,6 +1197,7 @@ export const getPaymentProfiles =
const { currentSummitState } = getState();
const accessToken = await getAccessTokenSafely();
const { currentSummit } = currentSummitState;
const filter = [];

dispatch(startLoading());

Expand All @@ -1204,19 +1207,30 @@ export const getPaymentProfiles =
access_token: accessToken
};

if (term) {
const escapedTerm = escapeFilterValue(term);
filter.push(
`provider=@${escapedTerm},id=@${escapedTerm},application_type=@${escapedTerm}`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [LOW] Search filter uses =@ (contains) on enum & id fields

provider=@x,id=@x,application_type=@x does a substring match on enum-like fields (Registration, BookableRooms, SponsorServices) and on a numeric id — typing 1 matches ids 1, 10, 11, 12…, and Sponsor matches SponsorServices even when the user meant something narrower.

Fix: Use == for id and application_type; keep =@ only on provider if free-text matching is actually intended.

);
}

// order
if (order != null && orderDir != null) {
const orderDirSign = orderDir === DEFAULT_ORDER_DIR ? "+" : "-";
params.order = `${orderDirSign}${order}`;
}

if (filter.length > 0) {
params["filter[]"] = filter;
}

return getRequest(
createAction(REQUEST_PAYMENT_PROFILES),
createAction(RECEIVE_PAYMENT_PROFILES),

`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles`,
authErrorHandler,
{ page, perPage, order, orderDir }
snackbarErrorHandler,
{ term, page, perPage, order, orderDir }
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
Expand All @@ -1232,41 +1246,34 @@ export const savePaymentProfile = (entity) => async (dispatch, getState) => {
};

if (entity.id) {
putRequest(
return putRequest(
createAction(UPDATE_PAYMENT_PROFILE),
createAction(PAYMENT_PROFILE_UPDATED),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${entity.id}`,
entity,
authErrorHandler,
snackbarErrorHandler,
entity
)(params)(dispatch).then(() => {
dispatch(
showSuccessMessage(
T.translate("edit_payment_profile.payment_profile_saved")
)
snackbarSuccessHandler({
title: T.translate("general.success"),
html: T.translate("edit_payment_profile.payment_profile_saved")
})
);
});
return;
}

const success_message = {
title: T.translate("general.done"),
html: T.translate("edit_payment_profile.payment_profile_created"),
type: "success"
};

postRequest(
return postRequest(
createAction(UPDATE_PAYMENT_PROFILE),
createAction(PAYMENT_PROFILE_ADDED),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles`,
entity,
authErrorHandler
)(params)(dispatch).then((payload) => {
snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(
showMessage(success_message, () => {
history.push(
`/app/summits/${currentSummit.id}/payment-profiles/${payload.response.id}`
);
snackbarSuccessHandler({
title: T.translate("general.success"),
html: T.translate("edit_payment_profile.payment_profile_created")
})
);
});
Expand All @@ -1287,7 +1294,7 @@ export const deletePaymentProfile =
createAction(PAYMENT_PROFILE_DELETED)({ paymentProfileId }),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}`,
null,
authErrorHandler
snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
Expand All @@ -1309,7 +1316,7 @@ export const getPaymentProfile =
null,
createAction(RECEIVE_PAYMENT_PROFILE),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}`,
authErrorHandler
snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
Expand Down Expand Up @@ -1347,7 +1354,7 @@ export const getPaymentFeeTypes =
createAction(RECEIVE_PAYMENT_FEE_TYPES),

`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}/fee-types`,
authErrorHandler,
snackbarErrorHandler,
{ page, perPage, order, orderDir }
)(params)(dispatch).then(() => {
dispatch(stopLoading());
Expand All @@ -1366,45 +1373,44 @@ export const savePaymentFeeType = (entity) => async (dispatch, getState) => {
access_token: accessToken
};

dispatch(startLoading());

if (entity.id) {
putRequest(
return putRequest(
createAction(UPDATE_PAYMENT_FEE_TYPE),
createAction(PAYMENT_FEE_TYPE_UPDATED),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}/fee-types/${entity.id}`,
entity,
authErrorHandler,
snackbarErrorHandler,
entity
)(params)(dispatch).then(() => {
dispatch(
showSuccessMessage(
T.translate("edit_payment_fee_type.payment_fee_type_saved")
)
);
});
return;
)(params)(dispatch)
.then(() => {
dispatch(
snackbarSuccessHandler({
title: T.translate("general.success"),
html: T.translate("edit_payment_fee_type.payment_fee_type_saved")
})
);
})
.finally(() => dispatch(stopLoading()));
}

const success_message = {
title: T.translate("general.done"),
html: T.translate("edit_payment_fee_type.payment_fee_type_created"),
type: "success"
};

postRequest(
return postRequest(
createAction(UPDATE_PAYMENT_FEE_TYPE),
createAction(PAYMENT_FEE_TYPE_ADDED),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}/fee-types`,
entity,
authErrorHandler
)(params)(dispatch).then(() => {
dispatch(
showMessage(success_message, () => {
history.push(
`/app/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}`
);
})
);
});
snackbarErrorHandler
)(params)(dispatch)
.then(() => {
dispatch(
snackbarSuccessHandler({
title: T.translate("general.success"),
html: T.translate("edit_payment_fee_type.payment_fee_type_created")
})
);
})
.finally(() => dispatch(stopLoading()));
};

export const deletePaymentFeeType =
Expand All @@ -1425,7 +1431,7 @@ export const deletePaymentFeeType =
createAction(PAYMENT_FEE_TYPE_DELETED)({ paymentFeeTypeId }),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}/fee-types/${paymentFeeTypeId}`,
null,
authErrorHandler
snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
Expand All @@ -1450,7 +1456,7 @@ export const getPaymentFeeType =
null,
createAction(RECEIVE_PAYMENT_FEE_TYPE),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/payment-profiles/${paymentProfileId}/fee-types/${paymentFeeTypeId}`,
authErrorHandler
snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
Expand Down
Loading
Loading