Skip to content

Commit 0114907

Browse files
committed
test: add initial set of tests and new workflow that would run them on each branch
1 parent 63c5418 commit 0114907

File tree

7 files changed

+608
-2
lines changed

7 files changed

+608
-2
lines changed

.eslintignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
*.js
2-
.dist/**/*.js
2+
.dist/**/*.js
3+
jest.config.cjs

.github/workflows/branch.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: branch-push
2+
3+
on:
4+
push:
5+
branches:
6+
- "**" # Match all branches
7+
paths-ignore:
8+
- "**.md"
9+
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
# Setup
15+
- uses: actions/checkout@v2
16+
- uses: actions/setup-node@v2
17+
with:
18+
node-version: "18"
19+
registry-url: "https://registry.npmjs.org"
20+
21+
- name: Reconfigure git
22+
run: |
23+
git config user.email "[email protected]"
24+
git config user.name "Barchart Builder"
25+
git config --global url.https://${{ secrets.API_TOKEN_GITHUB }}@github.com/.insteadOf ssh://[email protected]/
26+
27+
- name: Install dependencies
28+
run: yarn
29+
30+
- name: Run tests
31+
run: yarn test

jest.config.cjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/** @type {import('jest').Config} */
2+
const config = {
3+
moduleNameMapper: {
4+
"^@src/(.*)$": "<rootDir>/src/$1",
5+
"^@gen/(.*)$": "<rootDir>/generated/$1",
6+
},
7+
transform: {
8+
"^.+\\.tsx?$": "ts-jest",
9+
},
10+
testMatch: ["<rootDir>/test/**/*.test.ts"],
11+
};
12+
13+
module.exports = config;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"build:test-release": "standard-version --dry-run",
2424
"build:prepare-release": "standard-version -t ''",
2525
"run:browser": "yarn generate:version && vite dev",
26-
"run:node": "yarn generate:version && tsx ./src/test.ts"
26+
"run:node": "yarn generate:version && tsx ./src/test.ts",
27+
"test": "jest"
2728
},
2829
"devDependencies": {
2930
"@commitlint/cli": "^18.4.3",

test/client.test.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import WebSocket from "isomorphic-ws";
2+
import { OpenFeedClient } from "@src/connection/client";
3+
import { OpenFeedListeners } from "@src/connection/listeners";
4+
import { version } from "@gen/version";
5+
import { Result, SubscriptionType } from "@gen/openfeed_api";
6+
import { Service } from "@gen/openfeed";
7+
import { TIME } from "@src/utilities/constants";
8+
9+
jest.mock("@src/utilities/communication", () => ({
10+
send: jest.fn(),
11+
receive: jest.fn(),
12+
}));
13+
jest.mock("isomorphic-ws");
14+
15+
const testVersion = "mocked-os-version";
16+
const testRelease = "mocked-os-release";
17+
const testArch = "mocked-os-arch";
18+
19+
// Mock os module because it usees async import (a bit more complicated, but tests our string building)
20+
jest.mock("os", () => ({
21+
version: jest.fn().mockReturnValue(testVersion),
22+
release: jest.fn().mockReturnValue(testRelease),
23+
arch: jest.fn().mockReturnValue(testArch),
24+
}));
25+
26+
const emptyMessageEvent = {} as WebSocket.MessageEvent;
27+
const emptyCloseEvent = {} as WebSocket.CloseEvent;
28+
29+
const testUsername = "test-username";
30+
const testPassword = "test-password";
31+
32+
// Mock the import as the following:
33+
// connectionMock = jest.spyOn(jest.requireActual("@src/connection/connection"), "OpenFeedConnection");
34+
// doesn't seem to work to spy on the constructor
35+
jest.mock("@src/connection/connection", () => {
36+
const originalModule = jest.requireActual("@src/connection/connection");
37+
return {
38+
...originalModule,
39+
OpenFeedConnection: jest.fn().mockImplementation((...args: any[]) => {
40+
return new originalModule.OpenFeedConnection(...args);
41+
}),
42+
};
43+
});
44+
45+
// Flush all active promises to advance the code stuck on awaits
46+
const yieldToEventLoop = () => new Promise(setImmediate);
47+
48+
describe("OpenFeedClient", () => {
49+
let client: OpenFeedClient;
50+
let mockSocket: WebSocket;
51+
let mockSocketInstances: WebSocket[];
52+
let listeners: OpenFeedListeners;
53+
let communication: { send: jest.Mock; receive: jest.Mock };
54+
let connectionMock: jest.SpyInstance;
55+
56+
beforeEach(() => {
57+
listeners = new OpenFeedListeners();
58+
client = new OpenFeedClient("ws://test-url", testUsername, testPassword, listeners);
59+
communication = jest.requireMock("@src/utilities/communication");
60+
const MockSocket = jest.requireMock("isomorphic-ws");
61+
mockSocketInstances = MockSocket.mock.instances;
62+
mockSocket = mockSocketInstances[0];
63+
connectionMock = jest.requireMock("@src/connection/connection").OpenFeedConnection;
64+
});
65+
66+
afterEach(() => {
67+
client.dispose();
68+
jest.clearAllMocks();
69+
});
70+
71+
it("should handle login flow correctly", async () => {
72+
const sendSpy = jest.spyOn(communication, "send");
73+
74+
// Simulate socket open event
75+
mockSocket.onopen?.(emptyMessageEvent);
76+
77+
// Yield to allow the async import to resolve
78+
await yieldToEventLoop();
79+
80+
// Check that the login request was sent
81+
expect(sendSpy).toHaveBeenCalledTimes(1);
82+
const [[, loginRequest]] = sendSpy.mock.calls;
83+
const { correlationId, clientVersion, ...restRequest } = loginRequest.loginRequest;
84+
85+
expect(restRequest).toMatchObject({
86+
username: testUsername,
87+
password: testPassword,
88+
protocolVersion: 1,
89+
jwt: "",
90+
});
91+
92+
const expectedSubstrings = [testVersion, testRelease, testArch, `sdk-js:${version}`, `client-id:default`];
93+
94+
expect(expectedSubstrings.every((substring) => clientVersion.includes(substring))).toBe(true);
95+
96+
const token = "test-token";
97+
98+
// Simulate loginResponse message
99+
const loginResponse = {
100+
loginResponse: {
101+
token,
102+
correlationId,
103+
},
104+
};
105+
106+
communication.receive.mockReturnValue([loginResponse]);
107+
108+
mockSocket.onmessage?.(emptyMessageEvent);
109+
110+
// This is not how we'll actually use the connection, but it's a good test to ensure the connection is created
111+
const conn = await client.connection;
112+
113+
expect(conn).toBeDefined();
114+
expect(connectionMock).toHaveBeenCalledTimes(1);
115+
expect(connectionMock).toHaveBeenCalledWith(token, mockSocket, listeners, undefined);
116+
});
117+
118+
it("should throw an error if it receives invalid credentials", async () => {
119+
const sendSpy = jest.spyOn(communication, "send");
120+
121+
// Simulate socket open event
122+
mockSocket.onopen?.(emptyMessageEvent);
123+
124+
// Yield to allow the async import to resolve
125+
await yieldToEventLoop();
126+
127+
// Check that the login request was sent
128+
expect(sendSpy).toHaveBeenCalledTimes(1);
129+
130+
const [[, loginRequest]] = sendSpy.mock.calls;
131+
const { correlationId } = loginRequest.loginRequest;
132+
// Simulate invalid credentials response
133+
const loginResponse = {
134+
loginResponse: {
135+
status: {
136+
result: Result.INVALID_CREDENTIALS,
137+
},
138+
correlationId,
139+
},
140+
};
141+
142+
communication.receive.mockReturnValue([loginResponse]);
143+
mockSocket.onmessage?.(emptyMessageEvent);
144+
145+
await expect(client.connection).rejects.toThrow();
146+
});
147+
148+
// This is our most complex test, as it tests the full flow of the client:
149+
// It logs in, subscribes to a service, disconnects, reconnects, and ensures resubscribe happens automatically
150+
it("should handle disconnect and attempt to reconnect while there is a subscription", async () => {
151+
const sendSpy = jest.spyOn(communication, "send");
152+
const disconnectSpy = jest.spyOn(listeners, "onDisconnected");
153+
154+
// Simulate socket open event
155+
mockSocketInstances[0].onopen?.(emptyMessageEvent);
156+
157+
// Yield to the event loop to allow the async import to resolve
158+
await yieldToEventLoop();
159+
160+
const [[, loginRequest]] = sendSpy.mock.calls;
161+
const { correlationId } = loginRequest.loginRequest;
162+
163+
// Simulate loginResponse message
164+
const loginResponse = {
165+
loginResponse: {
166+
token: "test-token",
167+
correlationId,
168+
},
169+
};
170+
171+
communication.receive.mockReturnValue([loginResponse]);
172+
mockSocketInstances[0].onmessage?.(emptyMessageEvent);
173+
174+
const service = Service.DELAYED;
175+
const subscriptionType = SubscriptionType.DEPTH_PRICE;
176+
const snapshotIntervalSeconds = 9;
177+
const testSymbols = ["MSFT"];
178+
179+
client.subscribe(service, subscriptionType, snapshotIntervalSeconds, testSymbols);
180+
181+
// Yield to the event loop to allow for subscribe to go through
182+
await yieldToEventLoop();
183+
184+
mockSocketInstances[0].onclose?.(emptyCloseEvent);
185+
186+
// Use fake timers to fast-forward the reconnect delay
187+
jest.useFakeTimers();
188+
await jest.advanceTimersByTimeAsync(TIME.RECONNECT);
189+
jest.useRealTimers();
190+
191+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
192+
193+
// Simulate socket open event
194+
mockSocketInstances[1].onopen?.(emptyMessageEvent);
195+
196+
// Yield to the event loop to allow the async import to resolve
197+
await yieldToEventLoop();
198+
199+
expect(sendSpy).toHaveBeenCalledTimes(3);
200+
201+
const [, , [, loginRequest2]] = sendSpy.mock.calls;
202+
const { correlationId: correlationId2 } = loginRequest2.loginRequest;
203+
204+
// Simulate loginResponse message
205+
const loginResponse2 = {
206+
loginResponse: {
207+
token: "test-token",
208+
correlationId: correlationId2,
209+
},
210+
};
211+
212+
communication.receive.mockReturnValue([loginResponse2]);
213+
mockSocketInstances[1].onmessage?.(emptyMessageEvent);
214+
215+
await yieldToEventLoop();
216+
217+
expect(sendSpy).toHaveBeenCalledTimes(4);
218+
219+
const [, , , [, subscriptionRequest]] = sendSpy.mock.calls;
220+
expect(subscriptionRequest.subscriptionRequest.requests[0]).toMatchObject({
221+
symbol: testSymbols[0],
222+
subscriptionType: [subscriptionType],
223+
snapshotIntervalSeconds,
224+
});
225+
expect(subscriptionRequest.subscriptionRequest).toMatchObject({
226+
service,
227+
unsubscribe: false,
228+
});
229+
});
230+
});

0 commit comments

Comments
 (0)