diskeyval 1.0.0
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/.github/workflows/test.yml +31 -0
- package/LICENSE +7 -0
- package/PLAN.md +169 -0
- package/README.md +119 -0
- package/TESTING.md +94 -0
- package/example.ts +672 -0
- package/lib/index.ts +448 -0
- package/lib/raft/in-memory-network.ts +96 -0
- package/lib/raft/index.ts +24 -0
- package/lib/raft/node.ts +883 -0
- package/lib/raft/tls-transport.ts +451 -0
- package/lib/raft/types.ts +150 -0
- package/lib/storage/file-raft-storage.ts +84 -0
- package/package.json +23 -0
- package/test/e2e/cluster.e2e.test.ts +280 -0
- package/test/sim/cluster.sim.test.ts +105 -0
- package/test/soak/cluster.soak.test.ts +97 -0
- package/test/support/certs.ts +126 -0
- package/test/support/helpers.ts +51 -0
- package/test/support/ports.ts +33 -0
- package/test/support/tls-cluster.ts +84 -0
- package/test/unit/diskeyval.coverage.test.ts +193 -0
- package/test/unit/in-memory-network.coverage.test.ts +111 -0
- package/test/unit/raft.coverage.test.ts +534 -0
- package/test/unit/raft.election.test.ts +101 -0
- package/test/unit/raft.phase5.test.ts +90 -0
- package/test/unit/raft.reads.test.ts +96 -0
- package/test/unit/raft.replication.test.ts +106 -0
- package/test/unit/tls-transport.coverage.test.ts +332 -0
- package/test/unit/tls-transport.internals.coverage.test.ts +209 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { __tlsTransportInternals } from '../../lib/raft/tls-transport.ts';
|
|
5
|
+
|
|
6
|
+
type MockSocket = EventEmitter & {
|
|
7
|
+
authorized: boolean;
|
|
8
|
+
authorizationError?: Error;
|
|
9
|
+
peerCertificate: Record<string, unknown>;
|
|
10
|
+
writes: string[];
|
|
11
|
+
ended: boolean;
|
|
12
|
+
destroyed: boolean;
|
|
13
|
+
getPeerCertificate: (_detailed?: boolean) => Record<string, unknown>;
|
|
14
|
+
write: (chunk: string) => void;
|
|
15
|
+
end: () => void;
|
|
16
|
+
destroy: () => void;
|
|
17
|
+
off: (eventName: string, listener: (...args: unknown[]) => void) => MockSocket;
|
|
18
|
+
on: (eventName: string, listener: (...args: unknown[]) => void) => MockSocket;
|
|
19
|
+
once: (eventName: string, listener: (...args: unknown[]) => void) => MockSocket;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function createMockSocket(options?: {
|
|
23
|
+
authorized?: boolean;
|
|
24
|
+
authorizationError?: Error;
|
|
25
|
+
peerCertificate?: Record<string, unknown>;
|
|
26
|
+
writeThrows?: boolean;
|
|
27
|
+
}): MockSocket {
|
|
28
|
+
const emitter = new EventEmitter() as MockSocket;
|
|
29
|
+
emitter.authorized = options?.authorized ?? true;
|
|
30
|
+
emitter.authorizationError = options?.authorizationError;
|
|
31
|
+
emitter.peerCertificate = options?.peerCertificate ?? {
|
|
32
|
+
subject: { CN: 'node-a' },
|
|
33
|
+
subjectaltname: 'URI:spiffe://diskeyval/node-a,DNS:node-a'
|
|
34
|
+
};
|
|
35
|
+
emitter.writes = [];
|
|
36
|
+
emitter.ended = false;
|
|
37
|
+
emitter.destroyed = false;
|
|
38
|
+
emitter.getPeerCertificate = () => emitter.peerCertificate;
|
|
39
|
+
emitter.write = (chunk: string) => {
|
|
40
|
+
if (options?.writeThrows) {
|
|
41
|
+
throw new Error('write failed');
|
|
42
|
+
}
|
|
43
|
+
emitter.writes.push(chunk);
|
|
44
|
+
};
|
|
45
|
+
emitter.end = () => {
|
|
46
|
+
emitter.ended = true;
|
|
47
|
+
};
|
|
48
|
+
emitter.destroy = () => {
|
|
49
|
+
emitter.destroyed = true;
|
|
50
|
+
};
|
|
51
|
+
return emitter;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test('[unit/tls-internals] parseNodeIdFromCertificate covers CN, URI, DNS and null', () => {
|
|
55
|
+
const fromCn = __tlsTransportInternals.parseNodeIdFromCertificate({
|
|
56
|
+
subject: { CN: 'node-cn' }
|
|
57
|
+
} as never);
|
|
58
|
+
assert.equal(fromCn, 'node-cn');
|
|
59
|
+
|
|
60
|
+
const fromUri = __tlsTransportInternals.parseNodeIdFromCertificate({
|
|
61
|
+
subject: {},
|
|
62
|
+
subjectaltname: 'URI:spiffe://diskeyval/node-uri'
|
|
63
|
+
} as never);
|
|
64
|
+
assert.equal(fromUri, 'node-uri');
|
|
65
|
+
|
|
66
|
+
const fromDns = __tlsTransportInternals.parseNodeIdFromCertificate({
|
|
67
|
+
subject: {},
|
|
68
|
+
subjectaltname: 'DNS:node-dns'
|
|
69
|
+
} as never);
|
|
70
|
+
assert.equal(fromDns, 'node-dns');
|
|
71
|
+
|
|
72
|
+
const none = __tlsTransportInternals.parseNodeIdFromCertificate({
|
|
73
|
+
subject: {},
|
|
74
|
+
subjectaltname: 'DNS:'
|
|
75
|
+
} as never);
|
|
76
|
+
assert.equal(none, null);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('[unit/tls-internals] assertCertificateNodeId mismatch throws', () => {
|
|
80
|
+
assert.throws(
|
|
81
|
+
() =>
|
|
82
|
+
__tlsTransportInternals.assertCertificateNodeId(
|
|
83
|
+
{
|
|
84
|
+
subject: { CN: 'node-a' }
|
|
85
|
+
} as never,
|
|
86
|
+
'node-b'
|
|
87
|
+
),
|
|
88
|
+
/Peer certificate identity mismatch/i
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('[unit/tls-internals] readSingleLine handles newline, error, and end branches', async () => {
|
|
93
|
+
const socketOk = createMockSocket();
|
|
94
|
+
const okPromise = __tlsTransportInternals.readSingleLine(socketOk as never, 200);
|
|
95
|
+
socketOk.emit('data', Buffer.from('partial'));
|
|
96
|
+
socketOk.emit('data', Buffer.from('-line\ntrailing'));
|
|
97
|
+
const line = await okPromise;
|
|
98
|
+
assert.equal(line, 'partial-line');
|
|
99
|
+
|
|
100
|
+
const socketErr = createMockSocket();
|
|
101
|
+
const errPromise = __tlsTransportInternals.readSingleLine(socketErr as never, 200);
|
|
102
|
+
socketErr.emit('error', new Error('boom'));
|
|
103
|
+
await assert.rejects(() => errPromise, /boom/i);
|
|
104
|
+
|
|
105
|
+
const socketEnd = createMockSocket();
|
|
106
|
+
const endPromise = __tlsTransportInternals.readSingleLine(socketEnd as never, 200);
|
|
107
|
+
socketEnd.emit('end');
|
|
108
|
+
await assert.rejects(
|
|
109
|
+
() => endPromise,
|
|
110
|
+
/Socket ended before newline-terminated message was received/i
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('[unit/tls-internals] withTimeout rejection path is covered', async () => {
|
|
115
|
+
await assert.rejects(
|
|
116
|
+
() =>
|
|
117
|
+
__tlsTransportInternals.withTimeout(
|
|
118
|
+
new Promise<void>(() => undefined),
|
|
119
|
+
10,
|
|
120
|
+
'timed out internal'
|
|
121
|
+
),
|
|
122
|
+
/timed out internal/i
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('[unit/tls-internals] handleIncomingSocket covers endpoint/cert/auth/error branches', async () => {
|
|
127
|
+
const endpoint = {
|
|
128
|
+
onRequestVote: async () => ({ term: 1, voteGranted: true }),
|
|
129
|
+
onAppendEntries: async () => ({ term: 1, success: true, matchIndex: 0 })
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const baseContext = {
|
|
133
|
+
nodeId: 'node-a',
|
|
134
|
+
host: '127.0.0.1',
|
|
135
|
+
port: 6000,
|
|
136
|
+
peers: new Map(),
|
|
137
|
+
cert: '',
|
|
138
|
+
key: '',
|
|
139
|
+
ca: '',
|
|
140
|
+
minVersion: 'TLSv1.3',
|
|
141
|
+
rpcTimeoutMs: 200,
|
|
142
|
+
endpoint,
|
|
143
|
+
server: null
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const endpointMissingSocket = createMockSocket();
|
|
147
|
+
await __tlsTransportInternals.handleIncomingSocket(
|
|
148
|
+
{ ...baseContext, endpoint: null } as never,
|
|
149
|
+
endpointMissingSocket as never
|
|
150
|
+
);
|
|
151
|
+
assert.equal(endpointMissingSocket.destroyed, true);
|
|
152
|
+
|
|
153
|
+
const unauthorizedSocket = createMockSocket({
|
|
154
|
+
authorized: false,
|
|
155
|
+
authorizationError: new Error('unauthorized')
|
|
156
|
+
});
|
|
157
|
+
await __tlsTransportInternals.handleIncomingSocket(baseContext as never, unauthorizedSocket as never);
|
|
158
|
+
assert.equal(unauthorizedSocket.destroyed, true);
|
|
159
|
+
|
|
160
|
+
const noCertSocket = createMockSocket({
|
|
161
|
+
peerCertificate: {}
|
|
162
|
+
});
|
|
163
|
+
await __tlsTransportInternals.handleIncomingSocket(baseContext as never, noCertSocket as never);
|
|
164
|
+
assert.equal(noCertSocket.destroyed, true);
|
|
165
|
+
|
|
166
|
+
const noIdentitySocket = createMockSocket({
|
|
167
|
+
peerCertificate: {
|
|
168
|
+
subject: {},
|
|
169
|
+
subjectaltname: 'IP:127.0.0.1'
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
await __tlsTransportInternals.handleIncomingSocket(baseContext as never, noIdentitySocket as never);
|
|
173
|
+
assert.equal(noIdentitySocket.destroyed, true);
|
|
174
|
+
|
|
175
|
+
const payloadMismatchSocket = createMockSocket({
|
|
176
|
+
peerCertificate: {
|
|
177
|
+
subject: { CN: 'node-a' }
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
const mismatchPromise = __tlsTransportInternals.handleIncomingSocket(
|
|
181
|
+
baseContext as never,
|
|
182
|
+
payloadMismatchSocket as never
|
|
183
|
+
);
|
|
184
|
+
payloadMismatchSocket.emit(
|
|
185
|
+
'data',
|
|
186
|
+
Buffer.from(
|
|
187
|
+
`${JSON.stringify({
|
|
188
|
+
id: '1',
|
|
189
|
+
method: 'requestVote',
|
|
190
|
+
fromNodeId: 'node-z',
|
|
191
|
+
payload: {
|
|
192
|
+
term: 1,
|
|
193
|
+
candidateId: 'node-z',
|
|
194
|
+
lastLogIndex: 0,
|
|
195
|
+
lastLogTerm: 0
|
|
196
|
+
}
|
|
197
|
+
})}\n`
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
await mismatchPromise;
|
|
201
|
+
assert.equal(payloadMismatchSocket.destroyed, true);
|
|
202
|
+
|
|
203
|
+
const writeFailSocket = createMockSocket({
|
|
204
|
+
writeThrows: true
|
|
205
|
+
});
|
|
206
|
+
await __tlsTransportInternals.handleIncomingSocket(baseContext as never, writeFailSocket as never);
|
|
207
|
+
assert.equal(writeFailSocket.destroyed, true);
|
|
208
|
+
});
|
|
209
|
+
|