@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.1",
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.1"
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
+ });