Bläddra i källkod

[price-service/client] Add e2 test (#559)

Ali Behjati 2 år sedan
förälder
incheckning
3f7cffd5b2

+ 13 - 0
Tiltfile

@@ -338,3 +338,16 @@ k8s_resource(
     labels = ["solana"],
     trigger_mode = trigger_mode,
 )
+
+# Pyth Price Client JS e2e test
+docker_build(
+    ref = "pyth-price-client-js",
+    context = ".",
+    dockerfile = "price_service/client/js/Dockerfile",
+)
+k8s_yaml_with_ns("tilt_devnet/k8s/pyth-price-client-js.yaml")
+k8s_resource(
+    "pyth-price-client-js",
+    resource_deps = ["pyth-price-server"],
+    labels = ["pyth"]
+)

+ 17 - 0
price_service/client/js/Dockerfile

@@ -0,0 +1,17 @@
+# Defined in tilt_devnet/docker_images/Dockerfile.lerna
+FROM lerna
+
+USER root
+RUN apt-get update && apt-get install -y ncat
+
+WORKDIR /home/node/
+USER 1000
+
+COPY --chown=1000:1000 price_service/client/js price_service/client/js
+COPY --chown=1000:1000 price_service/sdk/js price_service/sdk/js
+
+RUN npx lerna run build --scope="@pythnetwork/price-service-client" --include-dependencies
+
+WORKDIR /home/node/price_service/client/js
+
+ENTRYPOINT ["npm"]

+ 2 - 1
price_service/client/js/package.json

@@ -13,7 +13,8 @@
   ],
   "repository": "https://github.com/pyth-network/pyth-crosschain",
   "scripts": {
-    "test": "jest --passWithNoTests",
+    "test": "jest --testPathIgnorePatterns=.*.e2e.test.ts --passWithNoTests",
+    "test:e2e": "jest --testPathPattern=.*.e2e.test.ts",
     "build": "tsc",
     "example": "npm run build && node lib/examples/PriceServiceClient.js",
     "format": "prettier --write \"src/**/*.ts\"",

+ 196 - 0
price_service/client/js/src/__tests__/connection.e2e.test.ts

@@ -0,0 +1,196 @@
+import {
+  DurationInMs,
+  Price,
+  PriceFeed,
+  PriceFeedMetadata,
+  PriceServiceConnection,
+} from "../index";
+
+async function sleep(duration: DurationInMs): Promise<void> {
+  return new Promise((res) => setTimeout(res, duration));
+}
+
+// The endpoint is set to the price service endpoint in Tilt.
+// Please note that if you change it to a mainnet/testnet endpoint
+// some tests might fail due to the huge response size of a request
+// , i.e. requesting latest price feeds or vaas of all price ids.
+const PRICE_SERVICE_ENDPOINT = "http://pyth-price-server:4200";
+
+describe("Test http endpoints", () => {
+  test("Get price feed (without verbose/binary) works", async () => {
+    const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT);
+    const ids = await connection.getPriceFeedIds();
+    expect(ids.length).toBeGreaterThan(0);
+
+    const priceFeeds = await connection.getLatestPriceFeeds(ids);
+    expect(priceFeeds).toBeDefined();
+    expect(priceFeeds!.length).toEqual(ids.length);
+
+    for (const priceFeed of priceFeeds!) {
+      expect(priceFeed.id.length).toBe(64); // 32 byte address has size 64 in hex
+      expect(priceFeed).toBeInstanceOf(PriceFeed);
+      expect(priceFeed.getPriceUnchecked()).toBeInstanceOf(Price);
+      expect(priceFeed.getEmaPriceUnchecked()).toBeInstanceOf(Price);
+      expect(priceFeed.getMetadata()).toBeUndefined();
+      expect(priceFeed.getVAA()).toBeUndefined();
+    }
+  });
+
+  test("Get price feed with verbose flag works", async () => {
+    const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT, {
+      priceFeedRequestConfig: { verbose: true },
+    });
+
+    const ids = await connection.getPriceFeedIds();
+    expect(ids.length).toBeGreaterThan(0);
+
+    const priceFeeds = await connection.getLatestPriceFeeds(ids);
+    expect(priceFeeds).toBeDefined();
+    expect(priceFeeds!.length).toEqual(ids.length);
+
+    for (const priceFeed of priceFeeds!) {
+      expect(priceFeed.getMetadata()).toBeInstanceOf(PriceFeedMetadata);
+      expect(priceFeed.getVAA()).toBeUndefined();
+    }
+  });
+
+  test("Get price feed with binary flag works", async () => {
+    const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT, {
+      priceFeedRequestConfig: { binary: true },
+    });
+
+    const ids = await connection.getPriceFeedIds();
+    expect(ids.length).toBeGreaterThan(0);
+
+    const priceFeeds = await connection.getLatestPriceFeeds(ids);
+    expect(priceFeeds).toBeDefined();
+    expect(priceFeeds!.length).toEqual(ids.length);
+
+    for (const priceFeed of priceFeeds!) {
+      expect(priceFeed.getMetadata()).toBeUndefined();
+      expect(priceFeed.getVAA()?.length).toBeGreaterThan(0);
+    }
+  });
+
+  test("Get latest vaa works", async () => {
+    const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT, {
+      priceFeedRequestConfig: { binary: true },
+    });
+
+    const ids = await connection.getPriceFeedIds();
+    expect(ids.length).toBeGreaterThan(0);
+
+    const vaas = await connection.getLatestVaas(ids);
+    expect(vaas.length).toBeGreaterThan(0);
+
+    for (const vaa of vaas) {
+      expect(vaa.length).toBeGreaterThan(0);
+    }
+  });
+
+  test("Get vaa works", async () => {
+    const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT, {
+      priceFeedRequestConfig: { binary: true },
+    });
+
+    const ids = await connection.getPriceFeedIds();
+    expect(ids.length).toBeGreaterThan(0);
+
+    const publishTime10SecAgo = Math.floor(new Date().getTime() / 1000) - 10;
+    const [vaa, vaaPublishTime] = await connection.getVaa(
+      ids[0],
+      publishTime10SecAgo
+    );
+
+    expect(vaa.length).toBeGreaterThan(0);
+    expect(vaaPublishTime).toBeGreaterThanOrEqual(publishTime10SecAgo);
+  });
+});
+
+describe("Test websocket endpoints", () => {
+  jest.setTimeout(60 * 1000);
+
+  test.concurrent(
+    "websocket subscription works without verbose and binary",
+    async () => {
+      const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT);
+
+      const ids = await connection.getPriceFeedIds();
+      expect(ids.length).toBeGreaterThan(0);
+
+      const counter: Map<string, number> = new Map();
+      let totalCounter = 0;
+
+      await connection.subscribePriceFeedUpdates(ids, (priceFeed) => {
+        expect(priceFeed.id.length).toBe(64); // 32 byte address has size 64 in hex
+        expect(priceFeed.getMetadata()).toBeUndefined();
+        expect(priceFeed.getVAA()).toBeUndefined();
+
+        counter.set(priceFeed.id, (counter.get(priceFeed.id) ?? 0) + 1);
+        totalCounter += 1;
+      });
+
+      // Wait for 30 seconds
+      await sleep(30000);
+      connection.closeWebSocket();
+
+      expect(totalCounter).toBeGreaterThan(30);
+
+      for (const id of ids) {
+        expect(counter.get(id)).toBeDefined();
+        // Make sure it receives more than 1 update
+        expect(counter.get(id)).toBeGreaterThan(1);
+      }
+    }
+  );
+
+  test.concurrent("websocket subscription works with verbose", async () => {
+    const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT, {
+      priceFeedRequestConfig: { verbose: true },
+    });
+
+    const ids = await connection.getPriceFeedIds();
+    expect(ids.length).toBeGreaterThan(0);
+
+    const observedFeeds: Set<string> = new Set();
+
+    await connection.subscribePriceFeedUpdates(ids, (priceFeed) => {
+      expect(priceFeed.getMetadata()).toBeInstanceOf(PriceFeedMetadata);
+      expect(priceFeed.getVAA()).toBeUndefined();
+      observedFeeds.add(priceFeed.id);
+    });
+
+    // Wait for 20 seconds
+    await sleep(20000);
+    await connection.unsubscribePriceFeedUpdates(ids);
+
+    for (const id of ids) {
+      expect(observedFeeds.has(id)).toBe(true);
+    }
+  });
+
+  test.concurrent("websocket subscription works with binary", async () => {
+    const connection = new PriceServiceConnection(PRICE_SERVICE_ENDPOINT, {
+      priceFeedRequestConfig: { binary: true },
+    });
+
+    const ids = await connection.getPriceFeedIds();
+    expect(ids.length).toBeGreaterThan(0);
+
+    const observedFeeds: Set<string> = new Set();
+
+    await connection.subscribePriceFeedUpdates(ids, (priceFeed) => {
+      expect(priceFeed.getMetadata()).toBeUndefined();
+      expect(priceFeed.getVAA()?.length).toBeGreaterThan(0);
+      observedFeeds.add(priceFeed.id);
+    });
+
+    // Wait for 20 seconds
+    await sleep(20000);
+    connection.closeWebSocket();
+
+    for (const id of ids) {
+      expect(observedFeeds.has(id)).toBe(true);
+    }
+  });
+});

+ 1 - 0
price_service/client/js/src/index.ts

@@ -6,6 +6,7 @@ export {
 
 export {
   HexString,
+  PriceFeedMetadata,
   PriceFeed,
   Price,
   UnixTimestamp,

+ 1 - 1
third_party/pyth/p2w_autoattest.py

@@ -114,7 +114,7 @@ mapping_reload_interval_mins: 1 # Very fast for testing purposes
 min_rpc_interval_ms: 0 # RIP RPC
 max_batch_jobs: 1000 # Where we're going there's no oomkiller
 default_attestation_conditions:
-  min_interval_secs: 60
+  min_interval_secs: 10
 symbol_groups:
   - group_name: fast_interval_only
     conditions:

+ 32 - 0
tilt_devnet/k8s/pyth-price-client-js.yaml

@@ -0,0 +1,32 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: pyth-price-client-js
+spec:
+  selector:
+    matchLabels:
+      app: pyth-price-client-js
+  serviceName: pyth-price-client-js
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        app: pyth-price-client-js
+    spec:
+      terminationGracePeriodSeconds: 0
+      containers:
+        - name: tests
+          image: pyth-price-client-js
+          command:
+            - /bin/sh
+            - -c
+            - "npm run test:e2e && nc -lk 0.0.0.0 2000"
+          readinessProbe:
+            periodSeconds: 5
+            failureThreshold: 300
+            tcpSocket:
+              port: 2000
+          resources:
+            limits:
+              cpu: "2"
+              memory: 1Gi