socketduct 0.1.24 → 0.1.26
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/AGENTS.md +2 -2
- package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
- package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
- package/package.json +2 -2
- package/scripts/test.js +9 -14
- package/src/client/multiplexed-transport-set.js +20 -2
- package/src/logging/compressed-log-writer.js +1 -1
- package/src/logging/zstd-frame-codec.js +15 -1
- package/src/reverse/client.js +44 -1
- package/src/reverse/hub.js +10 -1
- package/src/reverse/runtime.js +11 -2
- package/test/e2e/gateway-process.test.js +93 -92
- package/test/e2e/logger-shutdown-process.test.js +40 -37
- package/test/e2e/reverse-client-recovery-process.test.js +107 -103
- package/test/e2e/reverse-gateway-process.test.js +328 -318
- package/test/integration/initial-target-recovery.test.js +333 -334
- package/test/integration/logging-cli.test.js +121 -119
- package/test/integration/logging-compressed-recovery.test.js +96 -95
- package/test/integration/multiplexed-compatibility.test.js +178 -177
- package/test/integration/multiplexed-corrections.test.js +360 -359
- package/test/integration/multiplexed-failures.test.js +253 -251
- package/test/integration/multiplexed-transport-set.test.js +334 -322
- package/test/integration/multiplexed-transport.test.js +135 -134
- package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
- package/test/integration/reverse-gateway.test.js +2309 -2218
- package/test/integration/reverse-resume-continuity.test.js +219 -218
- package/test/integration/reverse-route-class.test.js +363 -278
- package/test/integration/socketduct-http-agent.test.js +314 -312
- package/test/integration/socketduct-https-agent.test.js +516 -485
- package/test/integration/stream-compression.test.js +862 -824
- package/test/integration/tcp-forwarding.test.js +2550 -2447
- package/test/integration/tls-forwarding.test.js +593 -592
- package/test/unit/auth/auth.test.js +43 -44
- package/test/unit/auth/static-token-store.test.js +9 -8
- package/test/unit/client/connect.test.js +41 -40
- package/test/unit/client/deferred-virtual-socket.test.js +95 -94
- package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
- package/test/unit/client/multiplexed-transport-set.test.js +845 -732
- package/test/unit/client/socketduct-socket.test.js +768 -771
- package/test/unit/client/virtual-socket.test.js +198 -168
- package/test/unit/config/docker-gateway-files.test.js +224 -224
- package/test/unit/config/environment.test.js +472 -492
- package/test/unit/index.test.js +53 -52
- package/test/unit/logging/compressed-log-worker.test.js +23 -22
- package/test/unit/logging/compressed-log-writer.test.js +155 -154
- package/test/unit/logging/log-retention.test.js +52 -47
- package/test/unit/logging/log-segment-index.test.js +84 -81
- package/test/unit/logging/zstd-frame-codec.test.js +28 -19
- package/test/unit/protocol/compression.test.js +254 -298
- package/test/unit/protocol/frame.test.js +280 -276
- package/test/unit/protocol/v3-compression.test.js +21 -23
- package/test/unit/protocol/v3-frame.test.js +42 -41
- package/test/unit/release-configuration.test.js +15 -12
- package/test/unit/reverse/disconnect-attribution.test.js +65 -64
- package/test/unit/reverse-terminal-outcomes.test.js +53 -50
- package/test/unit/server/control-handler.test.js +232 -231
- package/test/unit/server/primary.test.js +51 -49
- package/test/unit/server/relay-runtime.test.js +280 -294
- package/test/unit/sessions/session.test.js +48 -62
- package/test/unit/spool/spool-file.test.js +401 -385
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import test from "node:test"
|
|
1
|
+
import {describe, expect, it} from "@velocious/testing"
|
|
3
2
|
|
|
4
3
|
import {InMemoryUserStore, login, TokenStore} from "../../../src/auth/user-store.js"
|
|
5
4
|
import {handleControlMessage as rawHandleControlMessage} from "../../../src/server/control-handler.js"
|
|
@@ -26,266 +25,268 @@ function handleControlMessage(options, message) {
|
|
|
26
25
|
return rawHandleControlMessage({...options, supportedFraming: [2]}, {...message, framing: [2]})
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
describe("control handler", () => {
|
|
29
|
+
it("handleControlMessage requires auth before creating sessions", () => {
|
|
30
|
+
const response = handleControlMessage({tokens: new TokenStore(), sessions: new SessionStore()}, {
|
|
31
|
+
type: "session.create",
|
|
32
|
+
name: "laptop shell"
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
expect(response).toEqual({
|
|
36
|
+
type: "error",
|
|
37
|
+
code: "AUTH_REQUIRED",
|
|
38
|
+
message: "Authentication is required.",
|
|
39
|
+
retryable: false
|
|
40
|
+
})
|
|
33
41
|
})
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
it("handleControlMessage creates a named session for authenticated users", async () => {
|
|
44
|
+
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
45
|
+
|
|
46
|
+
const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
|
|
47
|
+
type: "session.create",
|
|
48
|
+
token: token.value,
|
|
49
|
+
name: "laptop shell",
|
|
50
|
+
identifier: "session-123",
|
|
51
|
+
target: {host: "127.0.0.1", port: 5432}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
expect(response).toEqual({
|
|
55
|
+
type: "session.created",
|
|
56
|
+
identifier: "session-123",
|
|
57
|
+
name: "laptop shell",
|
|
58
|
+
state: "creating",
|
|
59
|
+
target: {host: "127.0.0.1", port: 5432},
|
|
60
|
+
framing: 2
|
|
61
|
+
})
|
|
40
62
|
})
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test("handleControlMessage creates a named session for authenticated users", async () => {
|
|
44
|
-
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
45
63
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
it("handleControlMessage returns a protocol error for duplicate session identifiers", async () => {
|
|
65
|
+
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
66
|
+
const message = {
|
|
67
|
+
type: "session.create",
|
|
68
|
+
token: token.value,
|
|
69
|
+
name: "laptop shell",
|
|
70
|
+
identifier: "session-123",
|
|
71
|
+
target: {host: "127.0.0.1", port: 5432}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
expect(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, message).type).toBe("session.created")
|
|
75
|
+
expect(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, message)).toEqual({
|
|
76
|
+
type: "error",
|
|
77
|
+
code: "SESSION_EXISTS",
|
|
78
|
+
message: "Session identifier already exists.",
|
|
79
|
+
retryable: false
|
|
80
|
+
})
|
|
52
81
|
})
|
|
53
82
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
it("handleControlMessage resumes only sessions owned by the authenticated user", async () => {
|
|
84
|
+
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
85
|
+
const created = handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
|
|
86
|
+
type: "session.create",
|
|
87
|
+
token: token.value,
|
|
88
|
+
name: "laptop shell",
|
|
89
|
+
identifier: "session-123",
|
|
90
|
+
target: {host: "127.0.0.1", port: 5432}
|
|
91
|
+
})
|
|
92
|
+
expect(created.type).toBe("session.created")
|
|
93
|
+
sessions.transition("session-123", "active")
|
|
94
|
+
sessions.updateSequences("session-123", {clientSequence: 42, relaySequence: 7})
|
|
95
|
+
sessions.transition("session-123", "interrupted")
|
|
96
|
+
|
|
97
|
+
const resumed = handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
|
|
98
|
+
type: "session.resume",
|
|
99
|
+
token: token.value,
|
|
100
|
+
identifier: "session-123"
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(resumed).toEqual({
|
|
104
|
+
type: "session.resumed",
|
|
105
|
+
identifier: "session-123",
|
|
106
|
+
name: "laptop shell",
|
|
107
|
+
state: "resuming",
|
|
108
|
+
serverLastSequence: 7,
|
|
109
|
+
serverLastAck: 42,
|
|
110
|
+
replayFromSequence: 43,
|
|
111
|
+
target: {host: "127.0.0.1", port: 5432},
|
|
112
|
+
framing: 2
|
|
113
|
+
})
|
|
61
114
|
})
|
|
62
|
-
})
|
|
63
115
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
116
|
+
it("handleControlMessage rejects missing or malformed session targets", async () => {
|
|
117
|
+
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
118
|
+
|
|
119
|
+
expect(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
|
|
120
|
+
type: "session.create",
|
|
121
|
+
token: token.value,
|
|
122
|
+
name: "missing target"
|
|
123
|
+
})).toEqual({
|
|
124
|
+
type: "error",
|
|
125
|
+
code: "PROTOCOL_VIOLATION",
|
|
126
|
+
message: "Target host and port are required.",
|
|
127
|
+
retryable: false
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
expect(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
|
|
131
|
+
type: "session.create",
|
|
132
|
+
token: token.value,
|
|
133
|
+
name: "bad target",
|
|
134
|
+
target: {host: "", port: 0}
|
|
135
|
+
})).toEqual({
|
|
136
|
+
type: "error",
|
|
137
|
+
code: "PROTOCOL_VIOLATION",
|
|
138
|
+
message: "Target host and port are required.",
|
|
139
|
+
retryable: false
|
|
140
|
+
})
|
|
80
141
|
})
|
|
81
|
-
})
|
|
82
142
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
143
|
+
it("handleControlMessage rejects unauthorized targets", async () => {
|
|
144
|
+
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
145
|
+
|
|
146
|
+
const response = handleControlMessage({
|
|
147
|
+
tokens,
|
|
148
|
+
sessions,
|
|
149
|
+
authorizeTarget: () => false
|
|
150
|
+
}, {
|
|
151
|
+
type: "session.create",
|
|
152
|
+
token: token.value,
|
|
153
|
+
name: "blocked target",
|
|
154
|
+
target: {host: "10.0.0.1", port: 22}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
expect(response).toEqual({
|
|
158
|
+
type: "error",
|
|
159
|
+
code: "TARGET_UNAUTHORIZED",
|
|
160
|
+
message: "Target is not authorized for this principal.",
|
|
161
|
+
retryable: false
|
|
162
|
+
})
|
|
101
163
|
})
|
|
102
164
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
identifier: "session-123",
|
|
106
|
-
name: "laptop shell",
|
|
107
|
-
state: "resuming",
|
|
108
|
-
serverLastSequence: 7,
|
|
109
|
-
serverLastAck: 42,
|
|
110
|
-
replayFromSequence: 43,
|
|
111
|
-
target: {host: "127.0.0.1", port: 5432},
|
|
112
|
-
framing: 2
|
|
113
|
-
})
|
|
114
|
-
})
|
|
165
|
+
it("handleControlMessage selects a mutually supported stream.data codec on create", async () => {
|
|
166
|
+
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
115
167
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}), {
|
|
124
|
-
type: "error",
|
|
125
|
-
code: "PROTOCOL_VIOLATION",
|
|
126
|
-
message: "Target host and port are required.",
|
|
127
|
-
retryable: false
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
assert.deepEqual(handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
|
|
131
|
-
type: "session.create",
|
|
132
|
-
token: token.value,
|
|
133
|
-
name: "bad target",
|
|
134
|
-
target: {host: "", port: 0}
|
|
135
|
-
}), {
|
|
136
|
-
type: "error",
|
|
137
|
-
code: "PROTOCOL_VIOLATION",
|
|
138
|
-
message: "Target host and port are required.",
|
|
139
|
-
retryable: false
|
|
140
|
-
})
|
|
141
|
-
})
|
|
168
|
+
const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
|
|
169
|
+
type: "session.create",
|
|
170
|
+
token: token.value,
|
|
171
|
+
name: "compressed",
|
|
172
|
+
target: {host: "127.0.0.1", port: 5432},
|
|
173
|
+
compression: ["zstd"]
|
|
174
|
+
})
|
|
142
175
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const response = handleControlMessage({
|
|
147
|
-
tokens,
|
|
148
|
-
sessions,
|
|
149
|
-
authorizeTarget: () => false
|
|
150
|
-
}, {
|
|
151
|
-
type: "session.create",
|
|
152
|
-
token: token.value,
|
|
153
|
-
name: "blocked target",
|
|
154
|
-
target: {host: "10.0.0.1", port: 22}
|
|
176
|
+
expect(response.type).toBe("session.created")
|
|
177
|
+
expect(response.compression).toBe("zstd")
|
|
155
178
|
})
|
|
156
179
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
code: "TARGET_UNAUTHORIZED",
|
|
160
|
-
message: "Target is not authorized for this principal.",
|
|
161
|
-
retryable: false
|
|
162
|
-
})
|
|
163
|
-
})
|
|
180
|
+
it("handleControlMessage omits compression for a legacy client that offers none", async () => {
|
|
181
|
+
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
164
182
|
|
|
165
|
-
|
|
166
|
-
|
|
183
|
+
const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
|
|
184
|
+
type: "session.create",
|
|
185
|
+
token: token.value,
|
|
186
|
+
name: "legacy",
|
|
187
|
+
target: {host: "127.0.0.1", port: 5432}
|
|
188
|
+
})
|
|
167
189
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
token: token.value,
|
|
171
|
-
name: "compressed",
|
|
172
|
-
target: {host: "127.0.0.1", port: 5432},
|
|
173
|
-
compression: ["zstd"]
|
|
190
|
+
expect(response.type).toBe("session.created")
|
|
191
|
+
expect("compression" in response).toBe(false)
|
|
174
192
|
})
|
|
175
193
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
})
|
|
194
|
+
it("handleControlMessage omits compression when the relay supports no codec", async () => {
|
|
195
|
+
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
179
196
|
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: []}, {
|
|
198
|
+
type: "session.create",
|
|
199
|
+
token: token.value,
|
|
200
|
+
name: "disabled",
|
|
201
|
+
target: {host: "127.0.0.1", port: 5432},
|
|
202
|
+
compression: ["zstd"]
|
|
203
|
+
})
|
|
182
204
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
token: token.value,
|
|
186
|
-
name: "legacy",
|
|
187
|
-
target: {host: "127.0.0.1", port: 5432}
|
|
205
|
+
expect(response.type).toBe("session.created")
|
|
206
|
+
expect("compression" in response).toBe(false)
|
|
188
207
|
})
|
|
189
208
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
})
|
|
209
|
+
it("handleControlMessage omits compression when no offered codec is supported", async () => {
|
|
210
|
+
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
193
211
|
|
|
194
|
-
|
|
195
|
-
|
|
212
|
+
const response = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
|
|
213
|
+
type: "session.create",
|
|
214
|
+
token: token.value,
|
|
215
|
+
name: "unknown codec",
|
|
216
|
+
target: {host: "127.0.0.1", port: 5432},
|
|
217
|
+
compression: ["brotli"]
|
|
218
|
+
})
|
|
196
219
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
token: token.value,
|
|
200
|
-
name: "disabled",
|
|
201
|
-
target: {host: "127.0.0.1", port: 5432},
|
|
202
|
-
compression: ["zstd"]
|
|
220
|
+
expect(response.type).toBe("session.created")
|
|
221
|
+
expect("compression" in response).toBe(false)
|
|
203
222
|
})
|
|
204
223
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
224
|
+
it("handleControlMessage renegotiates the stream.data codec on resume", async () => {
|
|
225
|
+
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
226
|
+
|
|
227
|
+
const created = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
|
|
228
|
+
type: "session.create",
|
|
229
|
+
token: token.value,
|
|
230
|
+
name: "resumable",
|
|
231
|
+
target: {host: "127.0.0.1", port: 5432},
|
|
232
|
+
compression: ["zstd"]
|
|
233
|
+
})
|
|
234
|
+
expect(created.type).toBe("session.created")
|
|
235
|
+
const identifier = /** @type {string} */ (created.identifier)
|
|
236
|
+
sessions.transition(identifier, "active")
|
|
237
|
+
|
|
238
|
+
const resumed = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
|
|
239
|
+
type: "session.resume",
|
|
240
|
+
token: token.value,
|
|
241
|
+
identifier,
|
|
242
|
+
compression: ["zstd"]
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
expect(resumed.type).toBe("session.resumed")
|
|
246
|
+
expect(resumed.compression).toBe("zstd")
|
|
218
247
|
})
|
|
219
248
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
type
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const resumed = handleControlMessage({tokens, sessions, authorizeTarget: () => true, supportedCodecs: ["zstd"]}, {
|
|
239
|
-
type: "session.resume",
|
|
240
|
-
token: token.value,
|
|
241
|
-
identifier,
|
|
242
|
-
compression: ["zstd"]
|
|
249
|
+
it("handleControlMessage negotiates compact framing v2 only when offered and supported", async () => {
|
|
250
|
+
const first = await createAuthenticatedStores()
|
|
251
|
+
const base = {type: "session.create", token: first.token.value, name: "compact", target: {host: "127.0.0.1", port: 80}}
|
|
252
|
+
const selected = rawHandleControlMessage({tokens: first.tokens, sessions: first.sessions, authorizeTarget: () => true, supportedFraming: [2]}, {...base, framing: [2]})
|
|
253
|
+
expect(selected.framing).toBe(2)
|
|
254
|
+
|
|
255
|
+
const second = await createAuthenticatedStores()
|
|
256
|
+
const missing = rawHandleControlMessage({tokens: second.tokens, sessions: second.sessions, authorizeTarget: () => true, supportedFraming: [2]}, {...base, token: second.token.value, identifier: "missing-framing"})
|
|
257
|
+
expect(missing.type).toBe("error")
|
|
258
|
+
expect(missing.code).toBe("PROTOCOL_VIOLATION")
|
|
259
|
+
expect(second.sessions.get("missing-framing")).toBeUndefined()
|
|
260
|
+
|
|
261
|
+
const third = await createAuthenticatedStores()
|
|
262
|
+
const legacy = rawHandleControlMessage({tokens: third.tokens, sessions: third.sessions, authorizeTarget: () => true, supportedFraming: [2]}, {...base, token: third.token.value, identifier: "legacy-framing", framing: [1]})
|
|
263
|
+
expect(legacy.type).toBe("error")
|
|
264
|
+
expect(legacy.code).toBe("PROTOCOL_VIOLATION")
|
|
265
|
+
expect(third.sessions.get("legacy-framing")).toBeUndefined()
|
|
243
266
|
})
|
|
244
267
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
test("handleControlMessage rejects legacy resume before authentication or session mutation", async () => {
|
|
269
|
-
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
270
|
-
handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
|
|
271
|
-
type: "session.create",
|
|
272
|
-
token: token.value,
|
|
273
|
-
name: "resumable",
|
|
274
|
-
identifier: "resume-framing",
|
|
275
|
-
target: {host: "127.0.0.1", port: 80}
|
|
268
|
+
it("handleControlMessage rejects legacy resume before authentication or session mutation", async () => {
|
|
269
|
+
const {token, tokens, sessions} = await createAuthenticatedStores()
|
|
270
|
+
handleControlMessage({tokens, sessions, authorizeTarget: () => true}, {
|
|
271
|
+
type: "session.create",
|
|
272
|
+
token: token.value,
|
|
273
|
+
name: "resumable",
|
|
274
|
+
identifier: "resume-framing",
|
|
275
|
+
target: {host: "127.0.0.1", port: 80}
|
|
276
|
+
})
|
|
277
|
+
sessions.transition("resume-framing", "active")
|
|
278
|
+
sessions.transition("resume-framing", "interrupted")
|
|
279
|
+
|
|
280
|
+
let authenticationCalls = 0
|
|
281
|
+
const response = rawHandleControlMessage({
|
|
282
|
+
tokens: {authenticate: () => { authenticationCalls += 1; return {userId: "ignored", username: "ignored"} }},
|
|
283
|
+
sessions,
|
|
284
|
+
supportedFraming: [2]
|
|
285
|
+
}, {type: "session.resume", token: "ignored", identifier: "resume-framing", framing: [1]})
|
|
286
|
+
|
|
287
|
+
expect(response.type).toBe("error")
|
|
288
|
+
expect(response.code).toBe("PROTOCOL_VIOLATION")
|
|
289
|
+
expect(authenticationCalls).toBe(0)
|
|
290
|
+
expect(sessions.get("resume-framing")?.state).toBe("interrupted")
|
|
276
291
|
})
|
|
277
|
-
sessions.transition("resume-framing", "active")
|
|
278
|
-
sessions.transition("resume-framing", "interrupted")
|
|
279
|
-
|
|
280
|
-
let authenticationCalls = 0
|
|
281
|
-
const response = rawHandleControlMessage({
|
|
282
|
-
tokens: {authenticate: () => { authenticationCalls += 1; return {userId: "ignored", username: "ignored"} }},
|
|
283
|
-
sessions,
|
|
284
|
-
supportedFraming: [2]
|
|
285
|
-
}, {type: "session.resume", token: "ignored", identifier: "resume-framing", framing: [1]})
|
|
286
|
-
|
|
287
|
-
assert.equal(response.type, "error")
|
|
288
|
-
assert.equal(response.code, "PROTOCOL_VIOLATION")
|
|
289
|
-
assert.equal(authenticationCalls, 0)
|
|
290
|
-
assert.equal(sessions.get("resume-framing")?.state, "interrupted")
|
|
291
292
|
})
|
|
@@ -1,59 +1,61 @@
|
|
|
1
|
-
import
|
|
2
|
-
import test from "node:test"
|
|
1
|
+
import {describe, expect, it} from "@velocious/testing"
|
|
3
2
|
|
|
4
3
|
import {resolveWorkerCount, startPrimary} from "../../../src/server/primary.js"
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
describe("primary", () => {
|
|
6
|
+
it("resolveWorkerCount uses configured workers or available CPUs", () => {
|
|
7
|
+
expect(resolveWorkerCount(3, () => 99)).toBe(3)
|
|
8
|
+
expect(resolveWorkerCount(undefined, () => 4)).toBe(4)
|
|
9
|
+
expect(resolveWorkerCount(undefined, () => 0)).toBe(1)
|
|
10
|
+
expect(() => resolveWorkerCount(0)).toThrow(/Invalid worker count/)
|
|
11
|
+
})
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
13
|
+
it("startPrimary forks configured workers and restarts exits", () => {
|
|
14
|
+
/** @type {Array<Record<string, string> | undefined>} */
|
|
15
|
+
const forks = []
|
|
16
|
+
/** @type {undefined | ((worker: {id: number}, code: number | null, signal: string | null) => void)} */
|
|
17
|
+
let exitListener
|
|
18
|
+
/** @type {string[]} */
|
|
19
|
+
const warnings = []
|
|
20
|
+
|
|
21
|
+
const result = startPrimary({
|
|
22
|
+
workers: 2,
|
|
23
|
+
clusterApi: {
|
|
24
|
+
isPrimary: true,
|
|
25
|
+
fork: (env) => {
|
|
26
|
+
forks.push(env)
|
|
27
|
+
return {id: forks.length}
|
|
28
|
+
},
|
|
29
|
+
on: (_event, listener) => {
|
|
30
|
+
exitListener = listener
|
|
31
|
+
}
|
|
28
32
|
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
33
|
+
logger: {warn: (message) => warnings.push(message)}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual({role: "primary", workers: 2})
|
|
37
|
+
expect(forks).toEqual([{SOCKETDUCT_WORKER_INDEX: "0"}, {SOCKETDUCT_WORKER_INDEX: "1"}])
|
|
38
|
+
|
|
39
|
+
expect(exitListener).toBeDefined()
|
|
40
|
+
if (exitListener === undefined) throw new Error("Expected an exit listener")
|
|
41
|
+
exitListener({id: 7}, 1, null)
|
|
42
|
+
expect(forks.length).toBe(3)
|
|
43
|
+
expect(warnings.length).toBe(1)
|
|
44
|
+
expect(String(warnings[0])).toMatch(/worker 7 exited/)
|
|
34
45
|
})
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
47
|
+
it("startPrimary reports worker role without forking", () => {
|
|
48
|
+
const result = startPrimary({
|
|
49
|
+
workers: 2,
|
|
50
|
+
clusterApi: {
|
|
51
|
+
isPrimary: false,
|
|
52
|
+
fork: () => {
|
|
53
|
+
throw new Error("should not fork from worker")
|
|
54
|
+
},
|
|
55
|
+
on: () => {}
|
|
56
|
+
}
|
|
57
|
+
})
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
const result = startPrimary({
|
|
48
|
-
workers: 2,
|
|
49
|
-
clusterApi: {
|
|
50
|
-
isPrimary: false,
|
|
51
|
-
fork: () => {
|
|
52
|
-
throw new Error("should not fork from worker")
|
|
53
|
-
},
|
|
54
|
-
on: () => {}
|
|
55
|
-
}
|
|
59
|
+
expect(result).toEqual({role: "worker", workers: 0})
|
|
56
60
|
})
|
|
57
|
-
|
|
58
|
-
assert.deepEqual(result, {role: "worker", workers: 0})
|
|
59
61
|
})
|