فهرست منبع

Refactor Price Service + Add tests (#202)

* Add Rest Exception

* Add tests

* Update endpoint names and query params
Ali Behjati 3 سال پیش
والد
کامیت
6ce60e5ba6

+ 5 - 0
third_party/pyth/price-service/jest.config.js

@@ -0,0 +1,5 @@
+/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
+module.exports = {
+  preset: 'ts-jest',
+  testEnvironment: 'node',
+};

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1916 - 1282
third_party/pyth/price-service/package-lock.json


+ 8 - 1
third_party/pyth/price-service/package.json

@@ -4,15 +4,22 @@
   "description": "Pyth Price Service",
   "main": "index.js",
   "scripts": {
+    "format": "prettier --write \"src/**/*.ts\"",
     "build": "tsc",
-    "start": "node lib/index.js"
+    "start": "node lib/index.js",
+    "test": "jest"
   },
   "author": "",
   "license": "Apache-2.0",
   "devDependencies": {
+    "@types/jest": "^27.5.0",
     "@types/long": "^4.0.1",
     "@types/node": "^16.6.1",
+    "@types/supertest": "^2.0.12",
+    "jest": "^28.0.3",
     "prettier": "^2.3.2",
+    "supertest": "^6.2.3",
+    "ts-jest": "^28.0.1",
     "tslint": "^6.1.3",
     "tslint-config-prettier": "^1.18.0",
     "typescript": "^4.3.5"

+ 104 - 0
third_party/pyth/price-service/src/__tests__/rest.test.ts

@@ -0,0 +1,104 @@
+import { HexString, PriceFeed, PriceStatus } from "@pythnetwork/pyth-sdk-js";
+import { PriceFeedPriceInfo, PriceInfo } from "../listen";
+import {RestAPI} from "../rest"
+import { Express } from "express";
+import request from "supertest";
+import { StatusCodes } from "http-status-codes";
+
+let app: Express;
+let priceInfoMap: Map<string, PriceInfo>;
+
+function expandTo64Len(id: string): string {
+  return id.repeat(64).substring(0, 64);
+}
+
+function dummyPriceFeed(id: string): PriceFeed {
+  return new PriceFeed({
+    conf: "0",
+    emaConf: "1",
+    emaPrice: "2",
+    expo: 4,
+    id,
+    maxNumPublishers: 7,
+    numPublishers: 6,
+    prevConf: "8",
+    prevPrice: "9",
+    prevPublishTime: 10,
+    price: "11",
+    productId: "def456",
+    publishTime: 13,
+    status: PriceStatus.Trading
+  });
+}
+
+function dummyPriceInfoPair(id: HexString, seqNum: number, vaa: HexString): [HexString, PriceInfo] {
+  return [id, {
+    priceFeed: dummyPriceFeed(id),
+    receiveTime: 0,
+    seqNum,
+    vaaBytes: Buffer.from(vaa, 'hex').toString('binary')
+  }]
+}
+
+beforeAll(async () => {
+    priceInfoMap = new Map<string, PriceInfo>([
+        dummyPriceInfoPair(expandTo64Len('abcd'), 1, 'a1b2c3d4'),
+        dummyPriceInfoPair(expandTo64Len('ef01'), 1, 'a1b2c3d4'),
+        dummyPriceInfoPair(expandTo64Len('3456'), 2, 'bad01bad'),
+        dummyPriceInfoPair(expandTo64Len('10101'), 3, 'bidbidbid'),
+    ]);
+
+    let priceInfo: PriceFeedPriceInfo = {
+        getLatestPriceInfo: (priceFeedId: string) => {
+            return priceInfoMap.get(priceFeedId);
+        }
+    };
+
+    const api = new RestAPI(
+        {port: 8889},
+        priceInfo,
+        () => true
+    );
+
+    app = await api.createApp();
+})
+
+describe("Latest Price Feed Endpoint", () => {
+    test("When called with valid ids, returns correct price feed", async () => {
+      const ids = [expandTo64Len('abcd'), expandTo64Len('3456')];
+      const resp = await request(app).get('/latest_price_feeds').query({ids});
+      expect(resp.status).toBe(StatusCodes.OK);
+      expect(resp.body.length).toBe(2);
+      expect(resp.body).toContainEqual(dummyPriceFeed(ids[0]).toJson());
+      expect(resp.body).toContainEqual(dummyPriceFeed(ids[1]).toJson());
+    });
+
+    test("When called with some non-existant ids within ids, returns error mentioning non-existant ids", async () => {
+      const ids = [expandTo64Len('ab01'), expandTo64Len('3456'), expandTo64Len('effe')];
+      const resp = await request(app).get('/latest_price_feeds').query({ids});
+      expect(resp.status).toBe(StatusCodes.BAD_REQUEST);
+      expect(resp.body.message).toContain(ids[0]);
+      expect(resp.body.message).not.toContain(ids[1]);
+      expect(resp.body.message).toContain(ids[2]);
+    });
+});
+
+describe("Latest Vaa Bytes Endpoint", () => {
+  test("When called with valid ids, returns vaa bytes as array, merged if necessary", async () => {
+    const ids = [expandTo64Len('abcd'), expandTo64Len('ef01'), expandTo64Len('3456')];
+    const resp = await request(app).get('/latest_vaas').query({ids});
+    expect(resp.status).toBe(StatusCodes.OK);
+    expect(resp.body.length).toBe(2);
+    expect(resp.body).toContain(Buffer.from('a1b2c3d4', 'hex').toString('base64'));
+    expect(resp.body).toContain(Buffer.from('bad01bad', 'hex').toString('base64'));
+  });
+
+  test("When called with some non-existant ids within ids, returns error mentioning non-existant ids", async () => {
+    const ids = [expandTo64Len('ab01'), expandTo64Len('3456'), expandTo64Len('effe')];
+    const resp = await request(app).get('/latest_vaas').query({ids});
+    expect(resp.status).toBe(StatusCodes.BAD_REQUEST);
+    expect(resp.body.message).toContain(ids[0]);
+    expect(resp.body.message).not.toContain(ids[1]);
+    expect(resp.body.message).toContain(ids[2]);
+  });
+})

+ 1 - 1
third_party/pyth/price-service/src/logging.ts

@@ -1,6 +1,6 @@
 import * as winston from "winston";
 
-export let logger = winston.createLogger();
+export let logger = winston.createLogger({transports: [new winston.transports.Console()]});
 
 // Logger should be initialized before using logger
 export function initLogger(config?: {logLevel?: string}) {

+ 41 - 20
third_party/pyth/price-service/src/rest.ts

@@ -1,4 +1,4 @@
-import express from "express";
+import express, {Express} from "express";
 import cors from "cors";
 import morgan from "morgan";
 import responseTime from "response-time";
@@ -13,6 +13,20 @@ import { validate, ValidationError, Joi, schema } from "express-validation";
 const MORGAN_LOG_FORMAT = ':remote-addr - :remote-user ":method :url HTTP/:http-version"' +
   ' :status :res[content-length] :response-time ms ":referrer" ":user-agent"';
 
+export class RestException extends Error {
+  statusCode: number;
+  message: string;
+  constructor(statusCode: number, message: string) {
+    super(message);
+    this.statusCode = statusCode;
+    this.message = message;
+  }
+
+  static PriceFeedIdNotFound(notFoundIds: string[]): RestException {
+    return new RestException(StatusCodes.BAD_REQUEST, `Price Feeds with ids ${notFoundIds.join(', ')} not found`);
+  }
+}
+
 export class RestAPI {
   private port: number;
   private priceFeedVaaInfo: PriceFeedPriceInfo;
@@ -30,7 +44,7 @@ export class RestAPI {
   }
 
   // Run this function without blocking (`await`) if you want to run it async.
-  async run() {
+  async createApp() {
     const app = express();
     app.use(cors());
 
@@ -48,19 +62,15 @@ export class RestAPI {
       }
     }))
 
-    app.listen(this.port, () =>
-      logger.debug("listening on REST port " + this.port)
-    );
-
     let endpoints: string[] = [];
     
-    const latestVaaBytesInputSchema: schema = {
+    const latestVaasInputSchema: schema = {
       query: Joi.object({
-        id: Joi.array().items(Joi.string().regex(/^[a-f0-9]{64}$/))
+        ids: Joi.array().items(Joi.string().regex(/^[a-f0-9]{64}$/))
       })
     }
-    app.get("/latest_vaa_bytes", validate(latestVaaBytesInputSchema), (req: Request, res: Response) => {
-      let priceIds = req.query.id as string[];
+    app.get("/latest_vaas", validate(latestVaasInputSchema), (req: Request, res: Response) => {
+      let priceIds = req.query.ids as string[];
 
       // Multiple price ids might share same vaa, we use sequence number as
       // key of a vaa and deduplicate using a map of seqnum to vaa bytes.
@@ -83,8 +93,7 @@ export class RestAPI {
       }
 
       if (notFoundIds.length > 0) {
-        res.status(StatusCodes.BAD_REQUEST).send(`Price Feeds with ids ${notFoundIds.join(', ')} not found`);
-        return;
+        throw RestException.PriceFeedIdNotFound(notFoundIds);
       }
 
       const jsonResponse = Array.from(vaaMap.values(),
@@ -93,15 +102,15 @@ export class RestAPI {
 
       res.json(jsonResponse);
     });
-    endpoints.push("latest_vaa_bytes?id[]=<price_feed_id>&id[]=<price_feed_id_2>&..");
+    endpoints.push("latest_vaas?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..");
 
-    const latestPriceFeedInputSchema: schema = {
+    const latestPriceFeedsInputSchema: schema = {
       query: Joi.object({
-        id: Joi.array().items(Joi.string().regex(/^[a-f0-9]{64}$/))
+        ids: Joi.array().items(Joi.string().regex(/^[a-f0-9]{64}$/))
       })
     }
-    app.get("/latest_price_feed", validate(latestPriceFeedInputSchema), (req: Request, res: Response) => {
-      let priceIds = req.query.id as string[];
+    app.get("/latest_price_feeds", validate(latestPriceFeedsInputSchema), (req: Request, res: Response) => {
+      let priceIds = req.query.ids as string[];
 
       let responseJson = [];
 
@@ -122,13 +131,12 @@ export class RestAPI {
       }
 
       if (notFoundIds.length > 0) {
-        res.status(StatusCodes.BAD_REQUEST).send(`Price Feeds with ids ${notFoundIds.join(', ')} not found`);
-        return;
+        throw RestException.PriceFeedIdNotFound(notFoundIds);
       }
 
       res.json(responseJson);
     });
-    endpoints.push("latest_price_feed?id[]=<price_feed_id>&id[]=<price_feed_id_2>&..");
+    endpoints.push("latest_price_feeds?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..");
 
 
     app.get("/ready", (_, res: Response) => {
@@ -154,8 +162,21 @@ export class RestAPI {
       if (err instanceof ValidationError) {
         return res.status(err.statusCode).json(err);
       }
+
+      if (err instanceof RestException) {
+        return res.status(err.statusCode).json(err);
+      }
     
       return next(err);
     })
+
+    return app;
+  }
+
+  async run() {
+    let app = await this.createApp();
+    app.listen(this.port, () =>
+      logger.debug("listening on REST port " + this.port)
+    );
   }
 }

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است