From 292504413d99c76ae6a4dc548d2dff0cdff21091 Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Mon, 13 Jan 2025 14:44:16 +0100 Subject: [PATCH] Validate and only return first portfolio --- .../octab-get-portfolio.entity.spec.ts | 33 +++++++++++++++++ .../entities/octav-get-portfolio.entity.ts | 8 +++++ .../portfolio-api/octav-api.service.spec.ts | 7 ++-- .../portfolio-api/octav-api.service.ts | 36 +++++++++++-------- 4 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 src/datasources/portfolio-api/entities/octab-get-portfolio.entity.spec.ts create mode 100644 src/datasources/portfolio-api/entities/octav-get-portfolio.entity.ts diff --git a/src/datasources/portfolio-api/entities/octab-get-portfolio.entity.spec.ts b/src/datasources/portfolio-api/entities/octab-get-portfolio.entity.spec.ts new file mode 100644 index 0000000000..69b9f145f6 --- /dev/null +++ b/src/datasources/portfolio-api/entities/octab-get-portfolio.entity.spec.ts @@ -0,0 +1,33 @@ +import { ZodError } from 'zod'; +import { OctavGetPortfolioSchema } from '@/datasources/portfolio-api/entities/octav-get-portfolio.entity'; +import type { OctavGetPortfolio } from '@/datasources/portfolio-api/entities/octav-get-portfolio.entity'; + +describe('OctavGetPortfolioSchema', () => { + it('should validate a getPortfolio response', () => { + const portfolio = { example: 'payload' }; + const getPortfolio: OctavGetPortfolio = { getPortfolio: [portfolio] }; + + const result = OctavGetPortfolioSchema.safeParse(getPortfolio); + + expect(result.success).toBe(true); + }); + + it('should not validate an invalid getPortfolio response', () => { + const getPortfolio = { invalid: 'getPortfolio' }; + + const result = OctavGetPortfolioSchema.safeParse(getPortfolio); + + expect(result.success).toBe(false); + expect(!result.success && result.error).toStrictEqual( + new ZodError([ + { + code: 'invalid_type', + expected: 'array', + received: 'undefined', + path: ['getPortfolio'], + message: 'Required', + }, + ]), + ); + }); +}); diff --git a/src/datasources/portfolio-api/entities/octav-get-portfolio.entity.ts b/src/datasources/portfolio-api/entities/octav-get-portfolio.entity.ts new file mode 100644 index 0000000000..d40dcc68ea --- /dev/null +++ b/src/datasources/portfolio-api/entities/octav-get-portfolio.entity.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { PortfolioSchema } from '@/domain/portfolio/entities/portfolio.entity'; + +export const OctavGetPortfolioSchema = z.object({ + getPortfolio: z.array(PortfolioSchema), +}); + +export type OctavGetPortfolio = z.infer; diff --git a/src/datasources/portfolio-api/octav-api.service.spec.ts b/src/datasources/portfolio-api/octav-api.service.spec.ts index 737f8d73e9..ed27393c93 100644 --- a/src/datasources/portfolio-api/octav-api.service.spec.ts +++ b/src/datasources/portfolio-api/octav-api.service.spec.ts @@ -7,7 +7,7 @@ import { NetworkResponseError } from '@/datasources/network/entities/network.err import { OctavApi } from '@/datasources/portfolio-api/octav-api.service'; import { rawify } from '@/validation/entities/raw.entity'; import type { INetworkService } from '@/datasources/network/network.service.interface'; -import type { Portfolio } from '@/domain/portfolio/entities/portfolio.entity'; +import type { OctavGetPortfolio } from '@/datasources/portfolio-api/entities/octav-get-portfolio.entity'; const mockNetworkService = jest.mocked({ get: jest.fn(), @@ -70,10 +70,11 @@ describe('OctavApiService', () => { describe('getPortfolio', () => { it('should get portfolio', async () => { const safeAddress = getAddress(faker.finance.ethereumAddress()); - const portfolio: Portfolio = {}; + const portfolio = { example: 'payload' }; + const getPortfolio: OctavGetPortfolio = { getPortfolio: [portfolio] }; mockNetworkService.get.mockResolvedValueOnce({ status: 200, - data: rawify(portfolio), + data: rawify(getPortfolio), }); await target.getPortfolio(safeAddress); diff --git a/src/datasources/portfolio-api/octav-api.service.ts b/src/datasources/portfolio-api/octav-api.service.ts index bb8f02143c..d70f8cc7dc 100644 --- a/src/datasources/portfolio-api/octav-api.service.ts +++ b/src/datasources/portfolio-api/octav-api.service.ts @@ -5,9 +5,11 @@ import { NetworkService, INetworkService, } from '@/datasources/network/network.service.interface'; +import { OctavGetPortfolioSchema } from '@/datasources/portfolio-api/entities/octav-get-portfolio.entity'; import { IPortfolioApi } from '@/domain/interfaces/portfolio-api.interface'; -import { Portfolio } from '@/domain/portfolio/entities/portfolio.entity'; -import { Raw } from '@/validation/entities/raw.entity'; +import { rawify } from '@/validation/entities/raw.entity'; +import type { Raw } from '@/validation/entities/raw.entity'; +import type { Portfolio } from '@/domain/portfolio/entities/portfolio.entity'; @Injectable() export class OctavApi implements IPortfolioApi { @@ -30,19 +32,25 @@ export class OctavApi implements IPortfolioApi { async getPortfolio(safeAddress: `0x${string}`): Promise> { try { const url = `${this.baseUri}/api/rest/portfolio`; - const { data: portfolio } = await this.networkService.get({ - url, - networkRequest: { - headers: { - Authorization: `Bearer ${this.apiKey}`, + const portfolios = await this.networkService + .get>({ + url, + networkRequest: { + headers: { + Authorization: `Bearer ${this.apiKey}`, + }, + params: { + addresses: safeAddress, + includeImages: true, + }, }, - params: { - addresses: safeAddress, - includeImages: true, - }, - }, - }); - return portfolio; + }) + .then((res) => { + return OctavGetPortfolioSchema.parse(res.data).getPortfolio; + }); + + // As we are only fetching the portfolio of one Safe, there will only be one element + return rawify(portfolios[0]); } catch (error) { throw this.httpErrorFactory.from(error); }