Skip to content

Commit 4e0990b

Browse files
committed
test: test websocket keepalive timer
Ref: #75 Signed-off-by: Philip Gerke <me@philipgerke.com>
1 parent c1361fc commit 4e0990b

File tree

2 files changed

+101
-22
lines changed

2 files changed

+101
-22
lines changed

src/system-access-point.ts

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { EventEmitter } from "events";
22
import {
33
interval,
44
Observable,
5+
SchedulerLike,
56
Subject,
67
Subscription,
78
switchMap,
@@ -34,19 +35,15 @@ type HttpRequestMethod = "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
3435
export class SystemAccessPoint extends EventEmitter {
3536
/** The basic authentication key used for requests. */
3637
public readonly basicAuthKey: string;
37-
/** The host name of the system access point. */
38-
public readonly hostName: string;
39-
/** Determines whether requests to the system access point will use TLS. */
40-
public readonly tlsEnabled: boolean;
41-
private readonly logger: Logger;
42-
private readonly verboseErrors: boolean;
4338
private webSocket?: WebSocket;
4439
private readonly webSocketMessageSubject = new Subject<WebSocketMessage>();
4540
private readonly webSocketKeepaliveTimerReset$ = new Subject<void>();
4641
private readonly webSocketKeepaliveTimer$ =
4742
this.webSocketKeepaliveTimerReset$.pipe(
4843
switchMap(() =>
49-
interval(30000).pipe(takeUntil(this.webSocketKeepaliveTimerReset$))
44+
interval(30000, this.scheduler).pipe(
45+
takeUntil(this.webSocketKeepaliveTimerReset$)
46+
)
5047
)
5148
);
5249
private webSocketKeepaliveSubscription?: Subscription;
@@ -63,24 +60,20 @@ export class SystemAccessPoint extends EventEmitter {
6360
* @param logger {Logger} The logger instance to be used. If no explicit implementation is provided, the this.logger will be used for logging.
6461
*/
6562
constructor(
66-
hostName: string,
67-
userName: string,
68-
password: string,
69-
tlsEnabled = true,
70-
verboseErrors = false,
71-
logger?: Logger
63+
private readonly hostName: string,
64+
readonly userName: string,
65+
readonly password: string,
66+
private readonly tlsEnabled = true,
67+
private readonly verboseErrors = false,
68+
private readonly logger: Logger = console,
69+
readonly scheduler?: SchedulerLike
7270
) {
7371
super();
74-
// Configure logging
75-
this.logger = logger ?? console;
7672

7773
// Create Basic Authentication key
7874
this.basicAuthKey = Buffer.from(`${userName}:${password}`, "utf8").toString(
7975
"base64"
8076
);
81-
this.hostName = hostName;
82-
this.tlsEnabled = tlsEnabled;
83-
this.verboseErrors = verboseErrors;
8477
}
8578

8679
/**

test/system-access-point.spec.ts

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { SystemAccessPoint } from "../src";
2-
import { originalTimeout } from "../test";
1+
import { Subject, Subscription } from "rxjs";
2+
import { TestScheduler } from "rxjs/testing";
33
import { RawData, WebSocket } from "ws";
4+
import { SystemAccessPoint } from "../src";
45
import {
56
Configuration,
67
DeviceResponse,
@@ -11,7 +12,7 @@ import {
1112
VirtualDeviceType,
1213
WebSocketMessage,
1314
} from "../src/model";
14-
import { Subject } from "rxjs";
15+
import { originalTimeout } from "../test";
1516

1617
const logger: Logger = {
1718
debug: jasmine.createSpy(),
@@ -21,6 +22,14 @@ const logger: Logger = {
2122
};
2223

2324
describe("System Access Point", () => {
25+
let testScheduler: TestScheduler;
26+
27+
beforeEach(() => {
28+
testScheduler = new TestScheduler((actual, expected) => {
29+
expect(actual).toEqual(expected);
30+
});
31+
});
32+
2433
afterAll(() => {
2534
// Restore the default Jasmine timeout after the test suite.
2635
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
@@ -59,11 +68,88 @@ describe("System Access Point", () => {
5968
spyOn(WebSocket.prototype, "send");
6069
const spy = spyOn(console, "warn");
6170
const sysAp = new SystemAccessPoint("localhost", "username", "password");
62-
const instance = sysAp as unknown as { webSocket?: WebSocket };
71+
const instance = sysAp as unknown as {
72+
webSocket?: WebSocket;
73+
webSocketKeepaliveSubscription: Subscription | undefined;
74+
};
6375
expect(instance.webSocket).toBeUndefined();
76+
expect(instance.webSocketKeepaliveSubscription).toBeUndefined();
6477
sysAp.connectWebSocket(true);
6578
expect(spy).not.toHaveBeenCalled();
6679
expect(instance.webSocket).toBeInstanceOf(WebSocket);
80+
expect(instance.webSocketKeepaliveSubscription).toBeDefined();
81+
});
82+
83+
it("should send ping frame when keepalive timer expires and the web socket is open", () => {
84+
spyOn(WebSocket.prototype, "send");
85+
const mockWebSocket = {
86+
readyState: WebSocket.OPEN,
87+
ping: jasmine.createSpy(),
88+
};
89+
const sysAp = new SystemAccessPoint(
90+
"localhost",
91+
"username",
92+
"password",
93+
undefined,
94+
undefined,
95+
logger,
96+
testScheduler
97+
);
98+
const instance = sysAp as unknown as {
99+
webSocket?: WebSocket;
100+
webSocketKeepaliveTimerReset$: Subject<void>;
101+
createWebSocket: (certificateVerification: boolean) => WebSocket;
102+
};
103+
spyOn(instance, "createWebSocket").and.returnValue(
104+
mockWebSocket as unknown as WebSocket
105+
);
106+
expect(instance.webSocket).toBeUndefined();
107+
108+
sysAp.connectWebSocket(true);
109+
expect(instance.webSocket).toBeDefined();
110+
expect(instance.webSocket?.readyState).toEqual(WebSocket.OPEN);
111+
testScheduler.run(() => {
112+
instance.webSocketKeepaliveTimerReset$.next();
113+
testScheduler.maxFrames = 100_000;
114+
testScheduler.flush();
115+
expect(mockWebSocket.ping).toHaveBeenCalledTimes(3);
116+
});
117+
});
118+
119+
it("should not send ping frame when keepalive timer expires and the web socket is not open", () => {
120+
spyOn(WebSocket.prototype, "send");
121+
const mockWebSocket = {
122+
readyState: WebSocket.CLOSED,
123+
ping: jasmine.createSpy(),
124+
};
125+
const sysAp = new SystemAccessPoint(
126+
"localhost",
127+
"username",
128+
"password",
129+
undefined,
130+
undefined,
131+
logger,
132+
testScheduler
133+
);
134+
const instance = sysAp as unknown as {
135+
webSocket?: WebSocket;
136+
webSocketKeepaliveTimerReset$: Subject<void>;
137+
createWebSocket: (certificateVerification: boolean) => WebSocket;
138+
};
139+
spyOn(instance, "createWebSocket").and.returnValue(
140+
mockWebSocket as unknown as WebSocket
141+
);
142+
expect(instance.webSocket).toBeUndefined();
143+
144+
sysAp.connectWebSocket(true);
145+
expect(instance.webSocket).toBeDefined();
146+
expect(instance.webSocket?.readyState).toEqual(WebSocket.CLOSED);
147+
testScheduler.run(() => {
148+
instance.webSocketKeepaliveTimerReset$.next();
149+
testScheduler.maxFrames = 100_000;
150+
testScheduler.flush();
151+
expect(mockWebSocket.ping).not.toHaveBeenCalled();
152+
});
67153
});
68154

69155
it("should warn if certificate verification is disabled", () => {

0 commit comments

Comments
 (0)