@connexis/testing 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@connexis/testing",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Testing helpers and mocks for @connexis",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -13,9 +13,14 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@connexis/core": "1.0.
|
|
16
|
+
"@connexis/core": "1.0.2",
|
|
17
|
+
"@connexis/transport-polling": "1.0.2",
|
|
18
|
+
"@connexis/transport-sse": "1.0.2",
|
|
19
|
+
"@connexis/transport-websocket": "1.0.2"
|
|
17
20
|
},
|
|
18
21
|
"devDependencies": {
|
|
22
|
+
"@types/eventsource": "^3.0.0",
|
|
23
|
+
"eventsource": "^4.1.0",
|
|
19
24
|
"typescript": "^5.5.2"
|
|
20
25
|
},
|
|
21
26
|
"repository": {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, beforeAll } from 'vitest';
|
|
2
|
+
import { createRealtimeClient } from '@connexis/core';
|
|
3
|
+
import { WebSocketTransport } from '@connexis/transport-websocket';
|
|
4
|
+
import { SSETransport } from '@connexis/transport-sse';
|
|
5
|
+
import { PollingTransport } from '@connexis/transport-polling';
|
|
6
|
+
import * as ES from 'eventsource';
|
|
7
|
+
|
|
8
|
+
// Resolve ESM default export wrapper for EventSource
|
|
9
|
+
const EventSource = (ES as any).default || ES;
|
|
10
|
+
|
|
11
|
+
if (typeof globalThis.EventSource === 'undefined') {
|
|
12
|
+
(globalThis as any).EventSource = EventSource;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const checkBackendRunning = async (): Promise<boolean> => {
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch('http://localhost:3000/api/auth/token', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({ username: 'HealthCheck' })
|
|
21
|
+
});
|
|
22
|
+
return res.status === 200 || res.status === 201;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const backendRunning = await checkBackendRunning();
|
|
29
|
+
|
|
30
|
+
if (!backendRunning) {
|
|
31
|
+
console.log('⚠️ NestJS backend is not running at http://localhost:3000. Skipping live integration tests.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe.runIf(backendRunning)('Aggressive Live Backend Integration Tests', () => {
|
|
35
|
+
let activeToken = '';
|
|
36
|
+
|
|
37
|
+
const getValidToken = async () => {
|
|
38
|
+
const res = await fetch('http://localhost:3000/api/auth/token', {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify({ username: 'IntegrationTest' })
|
|
42
|
+
});
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
return data.token;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
beforeEach(async () => {
|
|
48
|
+
activeToken = await getValidToken();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should successfully connect, subscribe, and publish over real WebSockets', async () => {
|
|
52
|
+
const transport = new WebSocketTransport('ws://localhost:3000/api/realtime/socket', {
|
|
53
|
+
authToken: () => Promise.resolve(activeToken)
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const client = createRealtimeClient({
|
|
57
|
+
transport,
|
|
58
|
+
connectionPolicy: 'direct'
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const received: any[] = [];
|
|
62
|
+
const unsubscribe = await client.subscribe('ticks', (tick) => {
|
|
63
|
+
received.push(tick);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Wait for price ticker events to stream over WebSocket
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
68
|
+
|
|
69
|
+
expect(client.state).toBe('connected');
|
|
70
|
+
expect(received.length).toBeGreaterThan(0);
|
|
71
|
+
expect(received[0].symbol).toBe('BTC/USD');
|
|
72
|
+
|
|
73
|
+
await unsubscribe();
|
|
74
|
+
await client.destroy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should successfully connect and stream ticks over real SSE transport', async () => {
|
|
78
|
+
const transport = new SSETransport('http://localhost:3000/api/realtime/stream', {
|
|
79
|
+
publishUrl: 'http://localhost:3000/api/realtime/publish',
|
|
80
|
+
authToken: () => Promise.resolve(activeToken)
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const client = createRealtimeClient({
|
|
84
|
+
transport,
|
|
85
|
+
connectionPolicy: 'direct'
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
(client as any).manager.on('stateChange', ({ state, error }: any) => {
|
|
89
|
+
console.log(`[Test SSE Connection State] -> ${state}, error:`, error?.message || error);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const received: any[] = [];
|
|
93
|
+
const unsubscribe = await client.subscribe('ticks', (tick) => {
|
|
94
|
+
received.push(tick);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Wait for price ticker to stream over SSE
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 4000));
|
|
99
|
+
|
|
100
|
+
expect(client.state).toBe('connected');
|
|
101
|
+
expect(received.length).toBeGreaterThan(0);
|
|
102
|
+
expect(received[0].symbol).toBe('BTC/USD');
|
|
103
|
+
|
|
104
|
+
await unsubscribe();
|
|
105
|
+
await client.destroy();
|
|
106
|
+
}, 10000);
|
|
107
|
+
|
|
108
|
+
it('should successfully connect and poll ticks over real HTTP Polling transport', async () => {
|
|
109
|
+
const transport = new PollingTransport('http://localhost:3000/api/realtime/poll', {
|
|
110
|
+
publishUrl: 'http://localhost:3000/api/realtime/publish',
|
|
111
|
+
authToken: () => Promise.resolve(activeToken),
|
|
112
|
+
pollInterval: 1000
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const client = createRealtimeClient({
|
|
116
|
+
transport,
|
|
117
|
+
connectionPolicy: 'direct'
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const received: any[] = [];
|
|
121
|
+
const unsubscribe = await client.subscribe('ticks', (tick) => {
|
|
122
|
+
received.push(tick);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Wait for at least one HTTP poll interval
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 4000));
|
|
127
|
+
|
|
128
|
+
expect(client.state).toBe('connected');
|
|
129
|
+
expect(received.length).toBeGreaterThan(0);
|
|
130
|
+
expect(received[0].symbol).toBe('BTC/USD');
|
|
131
|
+
|
|
132
|
+
await unsubscribe();
|
|
133
|
+
await client.destroy();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should fail connection and transition to error on invalid tokens', async () => {
|
|
137
|
+
const transport = new WebSocketTransport('ws://localhost:3000/api/realtime/socket', {
|
|
138
|
+
authToken: 'invalid-auth-token'
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const client = createRealtimeClient({
|
|
142
|
+
transport,
|
|
143
|
+
connectionPolicy: 'direct',
|
|
144
|
+
reconnectOptions: { maxAttempts: 1, delay: 500 }
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Make a subscription to trigger connection
|
|
148
|
+
const unsubscribe = await client.subscribe('ticks', () => {});
|
|
149
|
+
|
|
150
|
+
// Wait ample time (4s) for the connection attempts to fail and exhaust retries
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 4000));
|
|
152
|
+
|
|
153
|
+
expect(client.state).toBe('error');
|
|
154
|
+
|
|
155
|
+
await unsubscribe();
|
|
156
|
+
await client.destroy();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should dynamically recover from expired tokens using authToken callbacks on retry', async () => {
|
|
160
|
+
let tokenToUse = 'expired-or-invalid-token';
|
|
161
|
+
|
|
162
|
+
const transport = new WebSocketTransport('ws://localhost:3000/api/realtime/socket', {
|
|
163
|
+
authToken: () => Promise.resolve(tokenToUse)
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const client = createRealtimeClient({
|
|
167
|
+
transport,
|
|
168
|
+
connectionPolicy: 'direct',
|
|
169
|
+
reconnectOptions: { maxAttempts: 5, delay: 1000 }
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Subscribe to trigger connection
|
|
173
|
+
const unsubscribe = await client.subscribe('ticks', () => {});
|
|
174
|
+
|
|
175
|
+
// Wait for connection to fail initially
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
177
|
+
expect(client.state).toBe('reconnecting');
|
|
178
|
+
|
|
179
|
+
// Dynamically update the token variable to a valid token in the background
|
|
180
|
+
const freshToken = await getValidToken();
|
|
181
|
+
tokenToUse = freshToken;
|
|
182
|
+
|
|
183
|
+
// Await core reconnect loop to retry and succeed
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, 4000));
|
|
185
|
+
expect(client.state).toBe('connected');
|
|
186
|
+
|
|
187
|
+
await unsubscribe();
|
|
188
|
+
await client.destroy();
|
|
189
|
+
}, 10000);
|
|
190
|
+
});
|