keryx 0.22.0 → 0.22.1
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 +1 -1
- package/testing/index.ts +8 -0
- package/testing/websocket.ts +171 -0
package/package.json
CHANGED
package/testing/index.ts
CHANGED
|
@@ -2,6 +2,14 @@ import { afterAll, beforeAll } from "bun:test";
|
|
|
2
2
|
import { api } from "../api";
|
|
3
3
|
import type { WebServer } from "../servers/web";
|
|
4
4
|
|
|
5
|
+
export {
|
|
6
|
+
buildWebSocket,
|
|
7
|
+
createSession,
|
|
8
|
+
createUser,
|
|
9
|
+
subscribeToChannel,
|
|
10
|
+
waitForBroadcastMessages,
|
|
11
|
+
} from "./websocket";
|
|
12
|
+
|
|
5
13
|
/**
|
|
6
14
|
* Generous lifecycle hook timeout (15s) for `beforeAll` / `afterAll`.
|
|
7
15
|
*
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { expect } from "bun:test";
|
|
2
|
+
import { api } from "../api";
|
|
3
|
+
import type { WebServer } from "../servers/web";
|
|
4
|
+
|
|
5
|
+
const wsUrl = () => {
|
|
6
|
+
const web = api.servers.servers.find(
|
|
7
|
+
(s: { name: string }) => s.name === "web",
|
|
8
|
+
) as WebServer | undefined;
|
|
9
|
+
return (web?.url || "")
|
|
10
|
+
.replace("https://", "wss://")
|
|
11
|
+
.replace("http://", "ws://");
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Open a WebSocket against the running test server and return the socket along
|
|
16
|
+
* with a mutable array that accumulates every `message` event as it arrives.
|
|
17
|
+
*
|
|
18
|
+
* The promise resolves once the socket's `open` event fires, so callers can
|
|
19
|
+
* immediately send actions without an additional readiness check.
|
|
20
|
+
*
|
|
21
|
+
* @param options.headers - Request headers to include in the WebSocket upgrade
|
|
22
|
+
* (for example a session cookie). Optional.
|
|
23
|
+
* @returns An object with the open `socket` and the live `messages` array that
|
|
24
|
+
* every subsequent handler populates.
|
|
25
|
+
*/
|
|
26
|
+
export const buildWebSocket = async (
|
|
27
|
+
options: { headers?: Record<string, string> } = {},
|
|
28
|
+
) => {
|
|
29
|
+
const socket = new WebSocket(wsUrl(), { headers: options.headers });
|
|
30
|
+
const messages: MessageEvent[] = [];
|
|
31
|
+
socket.addEventListener("message", (event) => {
|
|
32
|
+
messages.push(event);
|
|
33
|
+
});
|
|
34
|
+
socket.addEventListener("error", (event) => {
|
|
35
|
+
console.error(event);
|
|
36
|
+
});
|
|
37
|
+
await new Promise((resolve) => {
|
|
38
|
+
socket.addEventListener("open", resolve);
|
|
39
|
+
});
|
|
40
|
+
return { socket, messages };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Send a `user:create` action over the given WebSocket and return the created
|
|
45
|
+
* user from the server's response.
|
|
46
|
+
*
|
|
47
|
+
* Assumes this is the first action sent on the socket — it reads `messages[0]`.
|
|
48
|
+
*
|
|
49
|
+
* @throws {Error} If the server responds with an error payload.
|
|
50
|
+
*/
|
|
51
|
+
export const createUser = async (
|
|
52
|
+
socket: WebSocket,
|
|
53
|
+
messages: MessageEvent[],
|
|
54
|
+
name: string,
|
|
55
|
+
email: string,
|
|
56
|
+
password: string,
|
|
57
|
+
) => {
|
|
58
|
+
socket.send(
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
messageType: "action",
|
|
61
|
+
action: "user:create",
|
|
62
|
+
messageId: 1,
|
|
63
|
+
params: { name, email, password },
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
while (messages.length === 0) await Bun.sleep(10);
|
|
68
|
+
const response = JSON.parse(messages[0].data);
|
|
69
|
+
|
|
70
|
+
if (response.error) {
|
|
71
|
+
throw new Error(`User creation failed: ${response.error.message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return response.response.user;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Send a `session:create` action over the given WebSocket and return the
|
|
79
|
+
* response payload (user + session).
|
|
80
|
+
*
|
|
81
|
+
* Assumes `createUser` was invoked first — it reads `messages[1]`.
|
|
82
|
+
*
|
|
83
|
+
* @throws {Error} If the server responds with an error payload.
|
|
84
|
+
*/
|
|
85
|
+
export const createSession = async (
|
|
86
|
+
socket: WebSocket,
|
|
87
|
+
messages: MessageEvent[],
|
|
88
|
+
email: string,
|
|
89
|
+
password: string,
|
|
90
|
+
) => {
|
|
91
|
+
socket.send(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
messageType: "action",
|
|
94
|
+
action: "session:create",
|
|
95
|
+
messageId: 2,
|
|
96
|
+
params: { email, password },
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
while (messages.length < 2) await Bun.sleep(10);
|
|
101
|
+
const response = JSON.parse(messages[1].data);
|
|
102
|
+
|
|
103
|
+
if (response.error) {
|
|
104
|
+
throw new Error(`Session creation failed: ${response.error.message}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return response.response;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Subscribe the socket to a channel and wait for the server's subscribe
|
|
112
|
+
* confirmation.
|
|
113
|
+
*
|
|
114
|
+
* Matches the confirmation by content rather than index, because presence
|
|
115
|
+
* broadcast events (join/leave) delivered via Redis pub/sub can arrive before
|
|
116
|
+
* the subscribe confirmation and shift message indices.
|
|
117
|
+
*/
|
|
118
|
+
export const subscribeToChannel = async (
|
|
119
|
+
socket: WebSocket,
|
|
120
|
+
messages: MessageEvent[],
|
|
121
|
+
channel: string,
|
|
122
|
+
) => {
|
|
123
|
+
socket.send(JSON.stringify({ messageType: "subscribe", channel }));
|
|
124
|
+
|
|
125
|
+
let response: Record<string, any> | undefined;
|
|
126
|
+
while (!response) {
|
|
127
|
+
for (const m of messages) {
|
|
128
|
+
const parsed = JSON.parse(m.data);
|
|
129
|
+
if (parsed.subscribed?.channel === channel) {
|
|
130
|
+
response = parsed;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!response) await Bun.sleep(10);
|
|
135
|
+
}
|
|
136
|
+
return response;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Wait briefly and return all broadcast (non-action-reply) messages received on
|
|
141
|
+
* the socket so far, asserting the expected count.
|
|
142
|
+
*
|
|
143
|
+
* Broadcasts are distinguished from action replies by the absence of a
|
|
144
|
+
* `messageId` field. Uses `expect()` internally so callers see a readable
|
|
145
|
+
* failure with the raw broadcast payload dumped to stderr on mismatch.
|
|
146
|
+
*
|
|
147
|
+
* @throws {Error} When the observed broadcast count does not equal
|
|
148
|
+
* `expectedCount`.
|
|
149
|
+
*/
|
|
150
|
+
export const waitForBroadcastMessages = async (
|
|
151
|
+
messages: MessageEvent[],
|
|
152
|
+
expectedCount: number,
|
|
153
|
+
) => {
|
|
154
|
+
await Bun.sleep(100);
|
|
155
|
+
|
|
156
|
+
const broadcastMessages: Record<string, any>[] = [];
|
|
157
|
+
for (const message of messages) {
|
|
158
|
+
const parsedMessage = JSON.parse(message.data);
|
|
159
|
+
if (!parsedMessage.messageId) {
|
|
160
|
+
broadcastMessages.push(parsedMessage);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
expect(broadcastMessages.length).toBe(expectedCount);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.error(JSON.stringify(broadcastMessages, null, 2));
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
return broadcastMessages;
|
|
171
|
+
};
|