From c624e18a4347d9ee23545dc7fbda9826a33dcdd0 Mon Sep 17 00:00:00 2001 From: Anusha Dharmasena Date: Thu, 27 Jun 2024 21:23:42 +1000 Subject: [PATCH] Integrate Sentry and add some unit tests with zod validation --- package-lock.json | 211 ++++++++++++++++++++++++++ package.json | 1 + src/App.tsx | 5 +- src/config.ts | 1 + src/index.tsx | 19 ++- src/utils/fetchSymbols.ts | 14 +- src/utils/fetchUserWatchPairs.test.ts | 79 ++++++++++ src/utils/fetchUserWatchPairs.ts | 39 +++-- src/utils/isTokenExpiringSoon.ts | 15 +- src/utils/setUserWatchPairs.test.ts | 65 ++++++++ src/utils/setUserWatchPairs.ts | 17 ++- 11 files changed, 448 insertions(+), 18 deletions(-) create mode 100644 src/utils/fetchUserWatchPairs.test.ts create mode 100644 src/utils/setUserWatchPairs.test.ts diff --git a/package-lock.json b/package-lock.json index 611a43c..118f9d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@sentry/react": "^8.12.0", "@tanstack/react-query": "^5.8.7", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", @@ -3461,6 +3462,126 @@ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==" }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.12.0.tgz", + "integrity": "sha512-h7HRqED15Qa+DRt8iZGna24Z331nglgjPzdFn4+u+jvnZrehUjH0vjsfuj7qhwSUNZu8Rxi1ZlUYFURjLDTKCA==", + "dependencies": { + "@sentry/core": "8.12.0", + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.12.0.tgz", + "integrity": "sha512-PvQ14wVOPmzRdYdmXD791CqERZZC4jZa5hnyBKBuF6ZpifIQ4Uk7spPu6ZO+Ympx3GtRlpYjk4dbjHyNSfYTwA==", + "dependencies": { + "@sentry/core": "8.12.0", + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.12.0.tgz", + "integrity": "sha512-TJceMtzRnY3SCvt3nFDu9rlT00Le7SaV2RL3D7SyDuijvJbWvIw3DRk7yutpF8c9YKO9j6FMa4NlkCJ+YAnnKQ==", + "dependencies": { + "@sentry-internal/browser-utils": "8.12.0", + "@sentry/core": "8.12.0", + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.12.0.tgz", + "integrity": "sha512-0slfHZ3TD3MKeBu5NEGuKuecxStX23gts5L3mGFJd/zwsd04A31fhVmo6agIkxnZbOU4GPX/7HPWIeevkvy3ig==", + "dependencies": { + "@sentry-internal/replay": "8.12.0", + "@sentry/core": "8.12.0", + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/browser": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.12.0.tgz", + "integrity": "sha512-H82dmr7KQWoS2DQc5dJko5wNepltcEro1EM4mBeL2YmVbNRtoZzD3HQTpbxJJuFsTvEMZevvez5HFlpUgxmIwQ==", + "dependencies": { + "@sentry-internal/browser-utils": "8.12.0", + "@sentry-internal/feedback": "8.12.0", + "@sentry-internal/replay": "8.12.0", + "@sentry-internal/replay-canvas": "8.12.0", + "@sentry/core": "8.12.0", + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/core": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.12.0.tgz", + "integrity": "sha512-y+5Hlf/E45nj2adJy4aUCNBefQbyWIX66Z9bOM6JjnVB0hxCm5H0sYqrFKldYqaeZx6/Q2cgAcGs61krUxNerQ==", + "dependencies": { + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/react": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.12.0.tgz", + "integrity": "sha512-VZaLH35sqGD52s3tEWI243RsVbAhXwppRa7AcyFZTdPgNquOIBRZcVCTl3pSN9ad/NfrHTMngMTybwPMkavIJA==", + "dependencies": { + "@sentry/browser": "8.12.0", + "@sentry/core": "8.12.0", + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/types": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.12.0.tgz", + "integrity": "sha512-pKuW64IjgcklWAOHzPJ02Ej480hyL25TLnYCAfl2TDMrYc+N0bbbH1N7ZxqJpTSVK9IxZPY/t2TRxpQBiyPEcg==", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/utils": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.12.0.tgz", + "integrity": "sha512-pwYMoOmexz3vsNSOJGPvD2qwp/fsPcr8mkFk67wMM37Y+30KQ8pF4Aq1cc+HBRIn1tKmenzFDPTczSdVPFxm3Q==", + "dependencies": { + "@sentry/types": "8.12.0" + }, + "engines": { + "node": ">=14.18" + } + }, "node_modules/@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -21948,6 +22069,96 @@ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==" }, + "@sentry-internal/browser-utils": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.12.0.tgz", + "integrity": "sha512-h7HRqED15Qa+DRt8iZGna24Z331nglgjPzdFn4+u+jvnZrehUjH0vjsfuj7qhwSUNZu8Rxi1ZlUYFURjLDTKCA==", + "requires": { + "@sentry/core": "8.12.0", + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0" + } + }, + "@sentry-internal/feedback": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.12.0.tgz", + "integrity": "sha512-PvQ14wVOPmzRdYdmXD791CqERZZC4jZa5hnyBKBuF6ZpifIQ4Uk7spPu6ZO+Ympx3GtRlpYjk4dbjHyNSfYTwA==", + "requires": { + "@sentry/core": "8.12.0", + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0" + } + }, + "@sentry-internal/replay": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.12.0.tgz", + "integrity": "sha512-TJceMtzRnY3SCvt3nFDu9rlT00Le7SaV2RL3D7SyDuijvJbWvIw3DRk7yutpF8c9YKO9j6FMa4NlkCJ+YAnnKQ==", + "requires": { + "@sentry-internal/browser-utils": "8.12.0", + "@sentry/core": "8.12.0", + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0" + } + }, + "@sentry-internal/replay-canvas": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.12.0.tgz", + "integrity": "sha512-0slfHZ3TD3MKeBu5NEGuKuecxStX23gts5L3mGFJd/zwsd04A31fhVmo6agIkxnZbOU4GPX/7HPWIeevkvy3ig==", + "requires": { + "@sentry-internal/replay": "8.12.0", + "@sentry/core": "8.12.0", + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0" + } + }, + "@sentry/browser": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.12.0.tgz", + "integrity": "sha512-H82dmr7KQWoS2DQc5dJko5wNepltcEro1EM4mBeL2YmVbNRtoZzD3HQTpbxJJuFsTvEMZevvez5HFlpUgxmIwQ==", + "requires": { + "@sentry-internal/browser-utils": "8.12.0", + "@sentry-internal/feedback": "8.12.0", + "@sentry-internal/replay": "8.12.0", + "@sentry-internal/replay-canvas": "8.12.0", + "@sentry/core": "8.12.0", + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0" + } + }, + "@sentry/core": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.12.0.tgz", + "integrity": "sha512-y+5Hlf/E45nj2adJy4aUCNBefQbyWIX66Z9bOM6JjnVB0hxCm5H0sYqrFKldYqaeZx6/Q2cgAcGs61krUxNerQ==", + "requires": { + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0" + } + }, + "@sentry/react": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.12.0.tgz", + "integrity": "sha512-VZaLH35sqGD52s3tEWI243RsVbAhXwppRa7AcyFZTdPgNquOIBRZcVCTl3pSN9ad/NfrHTMngMTybwPMkavIJA==", + "requires": { + "@sentry/browser": "8.12.0", + "@sentry/core": "8.12.0", + "@sentry/types": "8.12.0", + "@sentry/utils": "8.12.0", + "hoist-non-react-statics": "^3.3.2" + } + }, + "@sentry/types": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.12.0.tgz", + "integrity": "sha512-pKuW64IjgcklWAOHzPJ02Ej480hyL25TLnYCAfl2TDMrYc+N0bbbH1N7ZxqJpTSVK9IxZPY/t2TRxpQBiyPEcg==" + }, + "@sentry/utils": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.12.0.tgz", + "integrity": "sha512-pwYMoOmexz3vsNSOJGPvD2qwp/fsPcr8mkFk67wMM37Y+30KQ8pF4Aq1cc+HBRIn1tKmenzFDPTczSdVPFxm3Q==", + "requires": { + "@sentry/types": "8.12.0" + } + }, "@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", diff --git a/package.json b/package.json index a3785b1..b79089d 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { + "@sentry/react": "^8.12.0", "@tanstack/react-query": "^5.8.7", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", diff --git a/src/App.tsx b/src/App.tsx index 8684281..87a3e88 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +import * as Sentry from '@sentry/react'; import { Router } from 'react-router-dom'; import { createBrowserHistory } from 'history'; import { ToastContainer } from 'react-toastify'; @@ -9,7 +10,7 @@ import { Footer } from './components/Footer'; const history = createBrowserHistory(); -export const App = () => { +const App = () => { useOneSignal(); return ( @@ -28,3 +29,5 @@ export const App = () => { ); }; + +export default Sentry.withProfiler(App); diff --git a/src/config.ts b/src/config.ts index 14b3cb1..af31c49 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,4 +5,5 @@ const buildNumber = isProduction ? process.env.REACT_APP_BUILD_NUMBER : 'dev'; export const config = { API_URI: 'https://crypto-stdev-express.vercel.app', BUILD_NUMBER: buildNumber, + SENTRY_DSN: process.env.REACT_APP_SENTRY_DSN, }; diff --git a/src/index.tsx b/src/index.tsx index b4e2f78..271936c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,14 +1,31 @@ +import * as Sentry from "@sentry/react"; import React from 'react'; import ReactDOM from 'react-dom/client'; import { Chart as ChartJS, registerables } from 'chart.js'; import 'react-toastify/dist/ReactToastify.css'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import './index.css'; -import { App } from './App'; +import App from './App'; // import * as serviceWorkerRegistration from './serviceWorkerRegistration'; import reportWebVitals from './reportWebVitals'; import { AppSettingsProvider } from './providers/AppSettingsProvider'; import UserProvider from './providers/UserProvider'; +import { config } from "./config"; + +Sentry.init({ + dsn: config.SENTRY_DSN, + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration(), + ], + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions + // Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled + tracePropagationTargets: ["localhost", config.API_URI], + // Session Replay + replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. + replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. +}); ChartJS.register(...registerables); diff --git a/src/utils/fetchSymbols.ts b/src/utils/fetchSymbols.ts index 8c8dfce..9120ab6 100644 --- a/src/utils/fetchSymbols.ts +++ b/src/utils/fetchSymbols.ts @@ -1,14 +1,24 @@ +import { z } from 'zod'; +import * as Sentry from '@sentry/react'; import { config } from '../config'; import { DEFAULT_SYMBOLS } from '../consts/DefaultSymbols'; +// Define the Zod schema for the response +const responseSchema = z.object({ + symbols: z.array(z.string()), +}); + export const fetchSymbols = async (): Promise> => { try { const response = await fetch(`${config.API_URI}/api/symbols`); + const jsonResponse = await response.json(); - const { symbols } = await response.json(); + // Validate the response + const parsedResponse = responseSchema.parse(jsonResponse); - return symbols; + return parsedResponse.symbols; } catch (error) { + Sentry.captureException(error); return DEFAULT_SYMBOLS; } }; diff --git a/src/utils/fetchUserWatchPairs.test.ts b/src/utils/fetchUserWatchPairs.test.ts new file mode 100644 index 0000000..b6aba8b --- /dev/null +++ b/src/utils/fetchUserWatchPairs.test.ts @@ -0,0 +1,79 @@ +import fetchUserWatchPairs from './fetchUserWatchPairs'; // Adjust the import path as necessary +import * as Sentry from '@sentry/react'; +import { z } from 'zod'; + +jest.mock('../config', () => ({ + config: { + API_URI: 'https://example.com', + }, +})); + +jest.mock('@sentry/react', () => ({ + captureException: jest.fn(), +})); + +describe('fetchUserWatchPairs', () => { + const mockFetch = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call fetch with correct parameters and return watch pairs', async () => { + mockFetch.mockResolvedValue({ + json: jest.fn().mockResolvedValue(['BTC-USD', 'ETH-USD']), + }); + + const result = await fetchUserWatchPairs(mockFetch); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/api/watch_pairs', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + ); + expect(result).toEqual(['BTC-USD', 'ETH-USD']); + }); + + it('should return an empty array and log a validation error to Sentry if response is invalid', async () => { + mockFetch.mockResolvedValue({ + json: jest.fn().mockResolvedValue({ invalid: 'response' }), // Invalid response + }); + + const result = await fetchUserWatchPairs(mockFetch); + + expect(Sentry.captureException).toHaveBeenCalled(); + expect(result).toEqual([]); + }); + + it('should return an empty array and log a fetch error to Sentry if fetch fails', async () => { + mockFetch.mockRejectedValue(new Error('Network error')); + + const result = await fetchUserWatchPairs(mockFetch); + + expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Network error')); + expect(result).toEqual([]); + }); + + it('should return an empty array if the response is empty', async () => { + mockFetch.mockResolvedValue({ + json: jest.fn().mockResolvedValue([]), + }); + + const result = await fetchUserWatchPairs(mockFetch); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/api/watch_pairs', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + ); + expect(result).toEqual([]); + }); +}); diff --git a/src/utils/fetchUserWatchPairs.ts b/src/utils/fetchUserWatchPairs.ts index f5f71a5..d07fa9a 100644 --- a/src/utils/fetchUserWatchPairs.ts +++ b/src/utils/fetchUserWatchPairs.ts @@ -1,21 +1,38 @@ +import { z } from 'zod'; +import * as Sentry from '@sentry/react'; import { config } from '../config'; +// Define the Zod schema for the response +const responseSchema = z.array(z.string()); + const fetchUserWatchPairs = async ( fetchFn: (url: RequestInfo | URL, options: RequestInit) => Promise, -) => { - const response = await fetchFn( - `${config.API_URI}/api/watch_pairs`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', +): Promise> => { + try { + const response = await fetchFn( + `${config.API_URI}/api/watch_pairs`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, }, - }, - ); + ); + + const watchPairs = await response.json(); - const watchPairs = await response.json(); + // Validate the response + const parsedResponse = responseSchema.parse(watchPairs); - return watchPairs as Array; + return parsedResponse; + } catch (error) { + if (error instanceof z.ZodError) { + Sentry.captureException(error); + } else { + Sentry.captureException(error); + } + return []; + } }; export default fetchUserWatchPairs; diff --git a/src/utils/isTokenExpiringSoon.ts b/src/utils/isTokenExpiringSoon.ts index 1a72d00..00227ca 100644 --- a/src/utils/isTokenExpiringSoon.ts +++ b/src/utils/isTokenExpiringSoon.ts @@ -1,11 +1,22 @@ import { jwtDecode, JwtPayload } from 'jwt-decode'; +import { z } from 'zod'; + +// Define the Zod schema for the JWT payload +const jwtPayloadSchema = z.object({ + exp: z.number().optional(), // 'exp' is optional since it may not always be present +}); export const isTokenExpiringSoon = ( jwt: string, threshold = 5 * 60 * 1000 /* 5 minutes */, -) => { +): boolean => { try { - const exp = (jwtDecode(jwt) as JwtPayload)?.exp; + const decodedPayload = jwtDecode(jwt); + + // Validate the decoded payload + const parsedPayload = jwtPayloadSchema.parse(decodedPayload); + + const exp = parsedPayload?.exp; if (exp === undefined) { return false; diff --git a/src/utils/setUserWatchPairs.test.ts b/src/utils/setUserWatchPairs.test.ts new file mode 100644 index 0000000..ef4c750 --- /dev/null +++ b/src/utils/setUserWatchPairs.test.ts @@ -0,0 +1,65 @@ +import { z } from 'zod'; +import setUserWatchPairs from './setUserWatchPairs'; + +jest.mock('../config', () => ({ + config: { + API_URI: 'https://example.com', + }, +})); + +describe('setUserWatchPairs', () => { + const mockFetch = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should validate input and call fetch with correct parameters', async () => { + mockFetch.mockResolvedValue({ + json: jest.fn().mockResolvedValue({ + watchPairs: ['BTC-USD', 'ETH-USD'], + }), + }); + + const watchPairs = ['BTC-USD', 'ETH-USD']; + const result = await setUserWatchPairs(mockFetch, watchPairs); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://example.com/api/watch_pairs', + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ watchPairs }), + } + ); + expect(result).toEqual(['BTC-USD', 'ETH-USD']); + }); + + it('should throw an error if input is invalid', async () => { + const watchPairs = [123, 'ETH-USD']; // Invalid input + + await expect(setUserWatchPairs(mockFetch, watchPairs as any)).rejects.toThrow(z.ZodError); + }); + + it('should throw an error if response is invalid', async () => { + mockFetch.mockResolvedValue({ + json: jest.fn().mockResolvedValue({ + watchPairs: 'invalid-response', + }), + }); + + const watchPairs = ['BTC-USD', 'ETH-USD']; + + await expect(setUserWatchPairs(mockFetch, watchPairs)).rejects.toThrow(z.ZodError); + }); + + it('should throw an error if fetch fails', async () => { + mockFetch.mockRejectedValue(new Error('Network error')); + + const watchPairs = ['BTC-USD', 'ETH-USD']; + + await expect(setUserWatchPairs(mockFetch, watchPairs)).rejects.toThrow('Network error'); + }); +}); diff --git a/src/utils/setUserWatchPairs.ts b/src/utils/setUserWatchPairs.ts index 7cbabd3..2678b01 100644 --- a/src/utils/setUserWatchPairs.ts +++ b/src/utils/setUserWatchPairs.ts @@ -1,9 +1,21 @@ +import { z } from 'zod'; import { config } from '../config'; +// Define the Zod schema for the input +const watchPairsSchema = z.array(z.string()); + +// Define the Zod schema for the response +const responseSchema = z.object({ + watchPairs: z.array(z.string()) +}); + const setUserWatchPairs = async ( fetchFn: (url: RequestInfo | URL, options: RequestInit) => Promise, watchPairs: string[], ): Promise> => { + // Validate the input + watchPairsSchema.parse(watchPairs); + const response = await fetchFn( `${config.API_URI}/api/watch_pairs`, { @@ -17,7 +29,10 @@ const setUserWatchPairs = async ( const updatedWatchPairs = await response.json(); - return updatedWatchPairs.watchPairs as Array; + // Validate the response + const parsedResponse = responseSchema.parse(updatedWatchPairs); + + return parsedResponse.watchPairs; }; export default setUserWatchPairs;