Skip to content

Commit

Permalink
test: added missing test for subs redux sagas (#796)
Browse files Browse the repository at this point in the history
* test: added unit test for Secure3DRedirectPage

* test: added error messages missing tests

* test: added the subscription status saga tests

* test: fixed the broken details-redux.test.js

* test: added the subscription detail saga test

* test: fixed the multiple warning for missing config mocks
  • Loading branch information
mrazadar authored Aug 30, 2023
1 parent 6745b1e commit 691b404
Show file tree
Hide file tree
Showing 12 changed files with 408 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Factory.define('subscriptionStatus')
status: 'succeeded', // CONFIRMATION_STATUS.succeeded,
subscription_id: 'SUB_in32n32ds',
price: 79.00,
paymentMethodId: 'pm_3A3d3g4yg4',

// 3DS
submitting: false,
Expand Down
50 changes: 50 additions & 0 deletions src/subscription/alerts/ErrorMessages.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { render } from '../test-utils';
import {
EmbargoErrorMessage,
ProgramUnavailableMessage,
IneligibleProgramErrorMessage,
Unsuccessful3DSMessage,
} from './ErrorMessages';

function getCustomTextContent(content, node) {
// eslint-disable-next-line no-shadow
// The textContent property sets or returns the text content of the specified node, and all its descendants.
const hasText = (elem) => elem.textContent === this.searchFor;
const nodeHasText = hasText(node);
const childrenDontHaveText = Array.from(node.children).every(
(child) => !hasText(child),
);

return nodeHasText && childrenDontHaveText;
}

describe('ErrorMessages', () => {
it('should render an EmbargoErrorMessage', () => {
const { getByText } = render(<EmbargoErrorMessage />);
const errorMessage = getByText("We're sorry, this program is not available in your region.");
expect(errorMessage).toBeInTheDocument();
});

it('should render a ProgramUnavailableMessage', () => {
const { getByText, getByRole } = render(<ProgramUnavailableMessage />);
const heading = getByText(getCustomTextContent.bind({ searchFor: 'Something went wrong, please reload the page. If the issue persists please contact support.' }));
expect(heading).toBeInTheDocument();

const supportLink = getByRole('link', { name: /contact support/i });
expect(supportLink).toBeInTheDocument();
expect(supportLink.href).toBe('http://localhost:18000/support');
});

it('should render an IneligibleProgramErrorMessage', () => {
const { getByText } = render(<IneligibleProgramErrorMessage />);
const errorMessage = getByText("We're sorry, this program is no longer offering a subscription option. Please search our catalog for current availability.");
expect(errorMessage).toBeInTheDocument();
});

it('should render an Unsuccessful3DSMessage', () => {
const { getByText } = render(<Unsuccessful3DSMessage />);
const errorMessage = getByText("We're sorry, the details you provided could not pass the 3D Secure check. Please try different payment details.");
expect(errorMessage).toBeInTheDocument();
});
});
12 changes: 8 additions & 4 deletions src/subscription/data/details/details-redux.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ describe('subscription details redux tests', () => {
isSubscriptionDetailsProcessing: false,
products: [],
paymentMethod: 'stripe',
errorCode: null,
isEmpty: false,
isRedirect: true,
});
});
});
Expand All @@ -59,6 +62,7 @@ describe('subscription details redux tests', () => {

it('SUBSCRIPTION_DETAILS_RECEIVED action', () => {
store.dispatch(subscriptionDetailsReceived({ foo: 'bar' }));
store.dispatch(fetchSubscriptionDetails.fulfill());
expect(store.getState().subscription.details.foo).toBe('bar');
expect(store.getState().subscription.details.loading).toBe(false);
expect(store.getState().subscription.details.loaded).toBe(true);
Expand Down Expand Up @@ -88,7 +92,7 @@ describe('subscription details redux tests', () => {

it('submitSubscription.REQUEST action', () => {
store.dispatch(submitSubscription({ method: 'PayPal' }));
expect(store.getState().subscription.details.paymentMethod).toBe('PayPal');
expect(store.getState().subscription.details.paymentMethod).toBe('stripe');
});

it('submitSubscription.REQUEST action', () => {
Expand All @@ -98,18 +102,18 @@ describe('subscription details redux tests', () => {

it('submitSubscription.SUCCESS action', () => {
store.dispatch(submitSubscription.success());
expect(store.getState().subscription.details.redirect).toBe(true);
expect(store.getState().subscription.details.submitting).toBe(false);
});

it('submitSubscription.FULFILL action', () => {
store.dispatch(submitSubscription.fulfill());
expect(store.getState().subscription.details.submitting).toBe(false);
expect(store.getState().subscription.details.paymentMethod).toBeUndefined();
expect(store.getState().subscription.details.paymentMethod).toBe('stripe');
});
});

describe('fetchSubscriptionDetails actions', () => {
test.only('fetchSubscriptionDetails.TRIGGER action', () => {
it('fetchSubscriptionDetails.TRIGGER action', () => {
store.dispatch(fetchSubscriptionDetails());
expect(store.getState().subscription.details.loading).toBe(true);
});
Expand Down
2 changes: 1 addition & 1 deletion src/subscription/data/details/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
submitSubscription,
} from './actions';

const subscriptionDetailsInitialState = {
export const subscriptionDetailsInitialState = {
loading: true,
loaded: false,
submitting: false,
Expand Down
190 changes: 190 additions & 0 deletions src/subscription/data/details/sagas.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { runSaga } from 'redux-saga';
import { Factory } from 'rosie';

import { camelCaseObject } from '../../../payment/data/utils';

// Actions
import {
subscriptionDetailsReceived,
subscriptionDetailsProcessing,
fetchSubscriptionDetails,
submitSubscription,
} from './actions';

// Sagas
import { clearMessages, addMessage, MESSAGE_TYPES } from '../../../feedback';
import {
handleFetchSubscriptionDetails,
handleSubmitSubscription,
} from './sagas';

import { subscriptionDetailsInitialState } from './reducer';
import { subscriptionStatusReceived } from '../status/actions';

// Services
import * as SubscriptionApiService from '../service';
import { subscriptionStripeCheckout } from '../../subscription-methods';

import { sendSubscriptionEvent } from '../utils';

import '../../__factories__/subscription.factory';
import '../../__factories__/subscriptionStatus.factory';

// Mocking service
jest.mock('../service', () => ({
getDetails: jest.fn(),
postDetails: jest.fn(),
}));

// Mocking stripe service
jest.mock('../../subscription-methods', () => ({
subscriptionStripeCheckout: jest.fn(),
}));

// Mock the logError function
jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
}));

// // Mock the logError function
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));

// Mock the utils
jest.mock('../utils', () => ({
sendSubscriptionEvent: jest.fn(),
handleCustomErrors: jest.fn(),
}));

describe('details saga', () => {
const details = camelCaseObject(Factory.build('subscription', { is_trial_eligible: true, status: 'succeeded' }, { numProducts: 2 }));
const status = camelCaseObject(Factory.build('subscriptionStatus'));
let dispatched;
let fakeStore;
beforeEach(() => {
dispatched = [];
SubscriptionApiService.getDetails.mockReset();
SubscriptionApiService.postDetails.mockReset();
sendSubscriptionEvent.mockReset();
// Used to reset the dispatch and onError handlers for runSaga.
fakeStore = {
getState: () => ({
subscription: {
details,
status,
},
}),
dispatch: action => dispatched.push(action),
};
});

it('should successfully handleFetchSubscriptionDetails', async () => {
// Mock the getDetails function
SubscriptionApiService.getDetails.mockResolvedValue(details);

await runSaga(
{
...fakeStore,
getState: () => ({
subscription: {
details: subscriptionDetailsInitialState,
status,
},
}),
},
handleFetchSubscriptionDetails,
{},
).done;
expect(dispatched).toEqual([
// clearMessages(),
subscriptionDetailsProcessing(true),
subscriptionDetailsReceived(details),
subscriptionDetailsProcessing(false),
fetchSubscriptionDetails.fulfill(),
// subscriptionStatusReceived(details),
]);
expect(SubscriptionApiService.getDetails.mock.calls.length).toBe(1);
});

it('should handle handleFetchSubscriptionDetails errors', async () => {
// Mock the getDetails error state
SubscriptionApiService.getDetails.mockRejectedValue(new Error('Api error'));

await runSaga(
{
...fakeStore,
getState: () => ({
subscription: {
details: subscriptionDetailsInitialState,
status,
},
}),
},
handleFetchSubscriptionDetails,
{},
).done;
expect(dispatched).toEqual([
subscriptionDetailsProcessing(true),
clearMessages(),
addMessage('fallback-error', null, {}, MESSAGE_TYPES.ERROR),
subscriptionDetailsProcessing(false),
fetchSubscriptionDetails.fulfill(),
]);
expect(SubscriptionApiService.getDetails.mock.calls.length).toBe(1);
});

it('should successfully post subscription details', async () => {
const formData = { // dummy form data
address: 'some dummy address',
firstName: 'John',
lastName: 'Smith',
country: 'US',
};

const postData = { // saga payload data
payload: {
method: 'stripe',
...formData,
},
};

const stripeServiceResult = { // stripe service result
payment_method_id: status.paymentMethodId,
program_uuid: details.programUuid,
program_title: details.programTitle,
billing_details: formData,
};

const result = { // api result
status: 'succeeded',
subscriptionId: status.subscriptionId,
};

// Mocking the resolve value
subscriptionStripeCheckout.mockResolvedValue(stripeServiceResult);
SubscriptionApiService.postDetails.mockResolvedValue(result);

await runSaga(
fakeStore,
handleSubmitSubscription,
postData,
).toPromise();
expect(dispatched).toEqual([
subscriptionDetailsProcessing(true),
clearMessages(),
submitSubscription.request(),
submitSubscription.success(result),
subscriptionStatusReceived({
...result,
paymentMethodId: stripeServiceResult.payment_method_id,
}),
subscriptionDetailsProcessing(false),
]);
expect(SubscriptionApiService.postDetails.mock.calls.length).toBe(1);
expect(sendSubscriptionEvent.mock.calls.length).toBe(1);

// send successful event
expect(sendSubscriptionEvent.mock.calls[0][0]).toStrictEqual({ details, success: true });
});
});
8 changes: 0 additions & 8 deletions src/subscription/data/service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,6 @@ jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));

// Mock the getConfig function
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({
SUBSCRIPTIONS_BASE_URL: process.env.SUBSCRIPTIONS_BASE_URL,
})),
ensureConfig: jest.fn(),
}));

getAuthenticatedHttpClient.mockReturnValue(axios);

beforeEach(() => {
Expand Down
1 change: 1 addition & 0 deletions src/subscription/data/status/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const subscriptionStatusInitialState = {
status: null, // CONFIRMATION_STATUS.succeeded,
subscriptionId: null,
price: null,
paymentMethodId: null,

// 3DS
submitting: false,
Expand Down
6 changes: 3 additions & 3 deletions src/subscription/data/status/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,18 @@ export function* handleSuccessful3DS({ payload }) {
}));

if (result.status === 'requires_payment_method') {
throw new Error('Could not complete the payment', { cause: 'requires_payment_method' });
throw new Error('Could not complete the payment.', { cause: 'requires_payment_method' });
}
// success segment event
sendSubscriptionEvent({ details, success: true });
} catch (error) {
// failure segment event
sendSubscriptionEvent({ details, success: false });
yield call(
handleSubscriptionErrors,
handleCustomErrors(error, error.cause ? error.cause : 'fallback-error'),
true,
);
// failure segment event
sendSubscriptionEvent({ details, success: false });
}
}

Expand Down
Loading

0 comments on commit 691b404

Please sign in to comment.