diff --git a/payment_sdk/src/__tests__/CardInputGroup.test.tsx b/payment_sdk/src/__tests__/CardInputGroup.test.tsx
new file mode 100644
index 0000000..868808f
--- /dev/null
+++ b/payment_sdk/src/__tests__/CardInputGroup.test.tsx
@@ -0,0 +1,133 @@
+import React, { ReactNode } from "react";
+import { render, fireEvent } from "@testing-library/react-native";
+
+import { formatCreditCardNumber, formatExpiry } from "../util/helpers";
+import { Actions, DispatchContext, StateContext } from "../state";
+import CardInputGroup from "../components/CardInputGroup";
+import { isCardNumberValid, validateCardExpiry } from "../util/validator";
+
+// Mock helper functions
+jest.mock("../util/helpers", () => ({
+ formatCreditCardNumber: jest.fn(),
+ formatExpiry: jest.fn(),
+}));
+
+// Mock validate function
+
+jest.mock("../util/validator", () => ({
+ isCardNumberValid: jest.fn(),
+ validateCardExpiry: jest.fn(),
+}));
+
+// Mocked context values
+const mockState = {
+ cardCVV: "",
+ cardNumber: "",
+ cardExpiredDate: "",
+};
+
+const mockDispatch = jest.fn();
+
+describe("CardInputGroup Component", () => {
+ const renderWithContext = (component: ReactNode) => {
+ return render(
+
+
+ {component}
+
+
+ );
+ };
+
+ beforeEach(() => {
+ // Clear all mock call history before each test
+ jest.clearAllMocks();
+ });
+
+ it("renders correctly with initial state", () => {
+ const { getByTestId } = renderWithContext();
+
+ // Check if the inputs render with the initial values from context
+ expect(getByTestId("cardNumberInput").props.value).toBe(
+ mockState.cardNumber
+ );
+ expect(getByTestId("cardExpiryInput").props.value).toBe(
+ mockState.cardExpiredDate
+ );
+ expect(getByTestId("cardCVVInput").props.value).toBe(mockState.cardCVV);
+ });
+
+ it("updates card number when valid and dispatches action", () => {
+ isCardNumberValid.mockReturnValue(true);
+ formatCreditCardNumber.mockReturnValue("1234 1234 1234 1234");
+
+ const { getByTestId } = renderWithContext();
+
+ const cardNumberInput = getByTestId("cardNumberInput");
+
+ // Simulate typing a valid card number
+ fireEvent.changeText(cardNumberInput, "1234123412341234");
+
+ // Check if the validation function was called
+ expect(isCardNumberValid).toHaveBeenCalledWith("1234123412341234");
+
+ // Check if the dispatch was called with the correct action
+ expect(mockDispatch).toHaveBeenCalledWith({
+ type: Actions.SET_CARD_NUMBER,
+ payload: "1234 1234 1234 1234",
+ });
+ });
+
+ it("does not update card number when invalid", () => {
+ isCardNumberValid.mockReturnValue(false);
+
+ const { getByTestId } = renderWithContext();
+
+ const cardNumberInput = getByTestId("cardNumberInput");
+
+ // Simulate typing an invalid card number
+ fireEvent.changeText(cardNumberInput, "1234");
+
+ // Check if the validation function was called
+ expect(isCardNumberValid).toHaveBeenCalledWith("1234");
+
+ // Check that dispatch was not called
+ expect(mockDispatch).not.toHaveBeenCalled();
+ });
+
+ it("updates card expiry date and dispatches action", () => {
+ validateCardExpiry.mockReturnValue(true);
+ formatExpiry.mockReturnValue("12 / 25");
+
+ const { getByTestId } = renderWithContext();
+
+ const cardExpiryInput = getByTestId("cardExpiryInput");
+
+ // Simulate typing a valid expiry date
+ fireEvent.changeText(cardExpiryInput, "12/25");
+
+ // Check if the validation function was called
+ expect(validateCardExpiry).toHaveBeenCalledWith("12/25");
+
+ // Check if the dispatch was called with the correct action
+ expect(mockDispatch).toHaveBeenCalledWith({
+ type: Actions.SET_CARD_EXPIRED_DATE,
+ payload: "12 / 25",
+ });
+ });
+
+ it("updates card CVV and dispatches action", () => {
+ const { getByTestId } = renderWithContext();
+
+ const cardCVVInput = getByTestId("cardCVVInput");
+
+ // Simulate typing a CVV
+ fireEvent.changeText(cardCVVInput, "123");
+
+ // Check if the dispatch was called with the correct action
+ expect(mockDispatch).toHaveBeenCalledWith({
+ type: Actions.SET_CARD_CVV,
+ payload: "123",
+ });
+ });
+});
diff --git a/payment_sdk/src/__tests__/CardSection.test.tsx b/payment_sdk/src/__tests__/CardSection.test.tsx
index 81e95ef..9b7cd6b 100644
--- a/payment_sdk/src/__tests__/CardSection.test.tsx
+++ b/payment_sdk/src/__tests__/CardSection.test.tsx
@@ -1,9 +1,74 @@
-import React from "react";
-import { render } from "@testing-library/react-native";
+import React, { ReactNode } from "react";
+import { fireEvent, render, waitFor } from "@testing-library/react-native";
import CardSection from "../components/sections/CardSection";
-test("Cardholder name should be initially rendered", () => {
- const { getByText } = render();
- expect(getByText("Cardholder name")).toBeTruthy();
+import StateProvider from "../components/paymentState/stateProvider";
+import { DispatchContext, StateContext } from "../state";
+import { PaymentType } from "../util/types";
+
+export const mockState = {
+ sessionPay: jest.fn(),
+ cardholderName: "John Doe",
+ cardCVV: "",
+ cardNumber: "",
+ cardExpiredDate: "",
+ amount: 1000,
+ currency: "USD",
+};
+
+export const mockDispatch = jest.fn();
+
+export const renderWithContext = (component: ReactNode) => {
+ return render(
+
+
+ {component}
+
+
+ );
+};
+
+describe("Card Section Test cases", () => {
+ test("Cardholder name should be initially rendered", async () => {
+ const { getByText, getByTestId } = render(
+
+
+
+ );
+ const cardHolderText = getByText("Cardholder name");
+ expect(cardHolderText).toBeTruthy();
+ });
+
+ test("When enter card holders name state context should capture the card holder name", async () => {
+ const { getByText, getByTestId } = render(
+
+
+
+ );
+ fireEvent.changeText(getByText("Cardholder name"), "Kasun Prabath");
+ await waitFor(() =>
+ expect(getByTestId("cardHolderName").props.value).toBe("Kasun Prabath")
+ );
+ });
+
+ it("calls sessionPay with correct parameters on button press", () => {
+ const { getByTestId } = renderWithContext();
+
+ const payButton = getByTestId("PayCTA");
+
+ // Simulate pressing the Pay button
+ fireEvent.press(payButton);
+
+ // Check if sessionPay was called with the correct arguments
+ expect(mockState.sessionPay).toHaveBeenCalledWith({
+ paymentType: PaymentType.CREDIT,
+ cardDetails: {
+ cardholderName: mockState.cardholderName,
+ cardCVV: mockState.cardCVV,
+ cardNumber: mockState.cardNumber,
+ cardExpiredDate: mockState.cardExpiredDate,
+ },
+ });
+ });
});
diff --git a/payment_sdk/src/__tests__/paymentService.test.ts b/payment_sdk/src/__tests__/paymentService.test.ts
new file mode 100644
index 0000000..d24c339
--- /dev/null
+++ b/payment_sdk/src/__tests__/paymentService.test.ts
@@ -0,0 +1,89 @@
+import payForSession from "../services/paymentService";
+import { PaymentType } from "../util/types";
+
+// Mock the fetch function
+global.fetch = jest.fn();
+
+const paymentDetails = {
+ publicKey: "samplepublickKey",
+ sessionId: "sessionID123",
+ paymentType: PaymentType.CREDIT,
+ cardDetails: {
+ cardholderName: "John Doe",
+ cardNumber: "1234 5678 9012 3456",
+ cardExpiredDate: "12/25",
+ cardCVV: "123",
+ },
+};
+
+describe("payForSession", () => {
+ test("should successfully process credit card payment", async () => {
+ // Mock successful response from the server
+
+ const successPaymentResponse = {
+ redirect_url: null,
+ status: "completed",
+ payment: {
+ id: "15xvkc1r4we8g4xjgcrj2ltzd",
+ resource: "payment",
+ status: "captured",
+ amount: 1000,
+ tax: 0,
+ customer: null,
+ payment_deadline: "2024-06-08T14:59:59Z",
+ payment_details: {
+ type: "credit_card",
+ email: null,
+ brand: "visa",
+ last_four_digits: "0100",
+ month: 8,
+ year: 2025,
+ },
+ payment_method_fee: 0,
+ total: 1000,
+ currency: "JPY",
+ description: null,
+ captured_at: "2024-06-06T12:56:14Z",
+ external_order_num: null,
+ metadata: {},
+ created_at: "2024-06-06T12:56:14Z",
+ amount_refunded: 0,
+ locale: "ja",
+ session: "6u1jrun2y7fmr8wwu7qskwgov",
+ customer_family_name: null,
+ customer_given_name: null,
+ mcc: null,
+ statement_descriptor: null,
+ refunds: [],
+ refund_requests: [],
+ },
+ };
+
+ fetch.mockResolvedValueOnce({
+ json: jest.fn().mockResolvedValueOnce(successPaymentResponse),
+ });
+
+ const paymentResponse = await payForSession(paymentDetails);
+ expect(paymentResponse).toBe(successPaymentResponse);
+ });
+
+ test("should handle network error gracefully", async () => {
+ const errorResponse = {
+ error: {
+ code: "unprocessable_entity",
+ message:
+ "Validation failed: Payment is invalid, Payment source is invalid, Payment source number is not a valid credit card number",
+ param: null,
+ details: {},
+ },
+ };
+ // Mock network error response from the server
+ fetch.mockResolvedValueOnce({
+ json: jest.fn().mockResolvedValueOnce(errorResponse),
+ });
+ const paymentResponse = await payForSession(paymentDetails);
+ expect(paymentResponse).toBe(errorResponse);
+ });
+
+ // Add more test cases for different scenarios
+});
diff --git a/payment_sdk/src/__tests__/validator.test.ts b/payment_sdk/src/__tests__/validator.test.ts
new file mode 100644
index 0000000..8c41437
--- /dev/null
+++ b/payment_sdk/src/__tests__/validator.test.ts
@@ -0,0 +1,102 @@
+// util/helpers.test.js
+
+import { PaymentStatuses } from "../util/types";
+import {
+ isCardNumberValid,
+ validateCardExpiry,
+ validateSessionResponse,
+} from "../util/validator";
+
+describe("isCardNumberValid", () => {
+ it("returns true for empty input", () => {
+ expect(isCardNumberValid("")).toBe(true);
+ });
+
+ it("returns false for input exceeding max card length", () => {
+ expect(isCardNumberValid("12345678901234567")).toBe(false);
+ });
+
+ it("returns false for input with non-numeric characters", () => {
+ expect(isCardNumberValid("1234abcd5678")).toBe(false);
+ });
+
+ it("returns true for valid card number input", () => {
+ expect(isCardNumberValid("1234 5678 1234 5678")).toBe(true);
+ });
+
+ it("returns false for card number with spaces but exceeding max length", () => {
+ expect(isCardNumberValid("1234 5678 1234 5678 90")).toBe(false);
+ });
+
+ it("returns false for input length not matching numeric conversion", () => {
+ expect(isCardNumberValid("000012341234")).toBe(false);
+ });
+});
+
+describe("validateCardExpiry", () => {
+ it("returns true for empty expiry date", () => {
+ expect(validateCardExpiry("")).toBe(true);
+ });
+
+ it("returns false for expiry date with invalid month", () => {
+ expect(validateCardExpiry("13 / 25")).toBe(false);
+ });
+
+ it("returns false for expiry date in the past", () => {
+ const pastDate = new Date();
+ pastDate.setMonth(pastDate.getMonth() - 1); // One month in the past
+ const pastMonth = String(pastDate.getMonth() + 1).padStart(2, "0");
+ const pastYear = String(pastDate.getFullYear() % 100).padStart(2, "0");
+ expect(validateCardExpiry(`${pastMonth} / ${pastYear}`)).toBe(false);
+ });
+
+ it("returns true for valid future expiry date", () => {
+ const futureDate = new Date();
+ futureDate.setMonth(futureDate.getMonth() + 1); // One month in the future
+ const futureMonth = String(futureDate.getMonth() + 1).padStart(2, "0");
+ const futureYear = String(futureDate.getFullYear() % 100).padStart(2, "0");
+ expect(validateCardExpiry(`${futureMonth} / ${futureYear}`)).toBe(true);
+ });
+
+ it("returns false for expiry date with year more than two digits", () => {
+ expect(validateCardExpiry("12 / 2025")).toBe(false);
+ });
+
+ it("returns false for expiry date with year exceeding 99", () => {
+ expect(validateCardExpiry("12 / 100")).toBe(false);
+ });
+});
+
+describe("validateSessionResponse", () => {
+ it("returns true for null session data", () => {
+ expect(validateSessionResponse(null)).toBe(true);
+ });
+
+ it("returns true for session data with an error", () => {
+ expect(validateSessionResponse({ error: "Some error" })).toBe("Some error");
+ });
+
+ it("returns true for expired session data", () => {
+ expect(validateSessionResponse({ expired: true })).toBe(true);
+ });
+
+ it("returns true for session data with SUCCESS status", () => {
+ expect(validateSessionResponse({ status: PaymentStatuses.SUCCESS })).toBe(
+ true
+ );
+ });
+
+ it("returns true for session data with ERROR status", () => {
+ expect(validateSessionResponse({ status: PaymentStatuses.ERROR })).toBe(
+ true
+ );
+ });
+
+ it("returns false for session data with any other status", () => {
+ expect(validateSessionResponse({ status: "PENDING" })).toBe(false);
+ });
+
+ it("returns false for empty object session data", () => {
+ expect(validateSessionResponse({})).toBe(false);
+ });
+});
diff --git a/payment_sdk/src/components/CardInputGroup.tsx b/payment_sdk/src/components/CardInputGroup.tsx
index e7738ca..7a39ccc 100644
--- a/payment_sdk/src/components/CardInputGroup.tsx
+++ b/payment_sdk/src/components/CardInputGroup.tsx
@@ -17,6 +17,7 @@ const CardInputGroup = memo(() => {
{
if (isCardNumberValid(text)) {
@@ -33,6 +34,7 @@ const CardInputGroup = memo(() => {
{
if (validateCardExpiry(text)) {
@@ -47,6 +49,7 @@ const CardInputGroup = memo(() => {
{
dispatch({ type: Actions.SET_CARD_CVV, payload: text });
diff --git a/payment_sdk/src/components/Input.tsx b/payment_sdk/src/components/Input.tsx
index fc29ef8..13950fe 100644
--- a/payment_sdk/src/components/Input.tsx
+++ b/payment_sdk/src/components/Input.tsx
@@ -1,5 +1,5 @@
-import React from 'react';
-import {View, TextInput, Text, StyleSheet, ViewStyle} from 'react-native';
+import React from "react";
+import { View, TextInput, Text, StyleSheet, ViewStyle } from "react-native";
interface InputProps {
value: string;
@@ -8,6 +8,7 @@ interface InputProps {
placeholder?: string;
hasBorder?: boolean;
inputStyle?: ViewStyle;
+ testID?: string;
}
const Input: React.FC = ({
@@ -17,6 +18,7 @@ const Input: React.FC = ({
inputStyle,
hasBorder,
placeholder,
+ testID,
}) => {
return (
@@ -26,6 +28,7 @@ const Input: React.FC = ({
onChangeText={onChangeText}
placeholder={placeholder}
style={[styles.input, inputStyle, hasBorder && styles.withBorder]}
+ testID={testID}
/>
);
@@ -35,15 +38,15 @@ const styles = StyleSheet.create({
label: {
fontSize: 16,
marginBottom: 8,
- color: '#172E44',
+ color: "#172E44",
},
input: {
- height: '100%',
+ height: "100%",
paddingLeft: 16,
fontSize: 16,
},
withBorder: {
- borderColor: '#CAD6E1',
+ borderColor: "#CAD6E1",
borderWidth: 1,
borderRadius: 8,
},
diff --git a/payment_sdk/src/components/SubmitButton.tsx b/payment_sdk/src/components/SubmitButton.tsx
index 5c2f400..92add96 100644
--- a/payment_sdk/src/components/SubmitButton.tsx
+++ b/payment_sdk/src/components/SubmitButton.tsx
@@ -1,14 +1,19 @@
-import {StyleSheet, Text, TouchableOpacity} from 'react-native';
-import React from 'react';
+import { StyleSheet, Text, TouchableOpacity } from "react-native";
+import React from "react";
type Props = {
onPress: () => void;
label: string;
+ testID?: string;
};
-const SubmitButton = ({label, onPress}: Props) => {
+const SubmitButton = ({ label, onPress, testID }: Props) => {
return (
-
+
{label}
);
@@ -18,17 +23,17 @@ export default SubmitButton;
const styles = StyleSheet.create({
buttonWrapper: {
- backgroundColor: '#0B82EE',
+ backgroundColor: "#0B82EE",
borderRadius: 8,
minHeight: 50,
marginHorizontal: 16,
flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
+ justifyContent: "center",
+ alignItems: "center",
},
label: {
- color: 'white',
+ color: "white",
fontSize: 16,
- fontWeight: 'bold',
+ fontWeight: "bold",
},
});
diff --git a/payment_sdk/src/components/sections/CardSection.tsx b/payment_sdk/src/components/sections/CardSection.tsx
index 03a81b4..cb3e31f 100644
--- a/payment_sdk/src/components/sections/CardSection.tsx
+++ b/payment_sdk/src/components/sections/CardSection.tsx
@@ -41,12 +41,14 @@ const CardSection = (props: Props) => {
}}
hasBorder
inputStyle={styles.inputStyle}
+ testID="cardHolderName"
/>
);
@@ -56,7 +58,7 @@ export default CardSection;
const styles = StyleSheet.create({
cardContainer: {
- position: 'relative',
+ position: "relative",
flex: 1,
},
cardNameContainer: {