apache-iggy 0.10.0 → 0.10.1-edge.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/README.md +248 -28
- package/dist/client/client.config.d.ts +2 -0
- package/dist/client/client.config.js +13 -2
- package/dist/client/client.config.test.js +15 -2
- package/dist/client/client.connection-string.js +13 -1
- package/dist/client/client.connection-string.test.js +43 -1
- package/dist/client/client.connection.d.ts +33 -135
- package/dist/client/client.connection.js +190 -453
- package/dist/client/client.connection.test.js +244 -669
- package/dist/client/client.d.ts +4 -35
- package/dist/client/client.js +8 -101
- package/dist/client/client.operation.d.ts +120 -0
- package/dist/client/client.operation.js +428 -0
- package/dist/client/client.operation.test.d.ts +2 -0
- package/dist/client/client.operation.test.js +285 -0
- package/dist/client/client.socket.d.ts +92 -209
- package/dist/client/client.socket.js +1205 -981
- package/dist/client/client.socket.test.js +4865 -679
- package/dist/client/client.type.d.ts +65 -22
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client.e2e.js +3 -5
- package/dist/e2e/resource-options.e2e.d.ts +2 -0
- package/dist/{wire/stream/purge-stream.command.js → e2e/resource-options.e2e.js} +12 -20
- package/dist/e2e/resource-options.suite.d.ts +15 -0
- package/dist/e2e/resource-options.suite.js +272 -0
- package/dist/e2e/tcp.cluster.e2e.js +1 -0
- package/dist/e2e/tcp.consumer-stream.e2e.js +4 -0
- package/dist/e2e/tcp.parallel.e2e.js +5 -6
- package/dist/e2e/tcp.stream.e2e.js +0 -3
- package/dist/e2e/tcp.system.e2e.js +10 -1
- package/dist/e2e/tcp.token.e2e.js +8 -2
- package/dist/e2e/tcp.topic.e2e.js +0 -3
- package/dist/e2e/tcp.user.e2e.js +46 -3
- package/dist/e2e/tls.system.e2e.js +21 -12
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/stream/consumer-stream.js +97 -12
- package/dist/wire/cluster/cluster.type.d.ts +2 -2
- package/dist/wire/cluster/cluster.utils.d.ts +1 -0
- package/dist/wire/cluster/cluster.utils.js +4 -1
- package/dist/wire/command-set.d.ts +8 -8
- package/dist/wire/command-set.js +18 -9
- package/dist/wire/command.code.d.ts +3 -10
- package/dist/wire/command.code.js +9 -11
- package/dist/wire/command.utils.d.ts +3 -2
- package/dist/wire/command.utils.js +3 -2
- package/dist/wire/error.code.js +17 -1
- package/dist/wire/error.code.test.js +28 -1
- package/dist/wire/error.utils.d.ts +4 -2
- package/dist/wire/error.utils.js +6 -3
- package/dist/wire/identifier.utils.d.ts +2 -0
- package/dist/wire/identifier.utils.js +2 -0
- package/dist/wire/index.d.ts +1 -0
- package/dist/wire/index.js +1 -0
- package/dist/wire/message/header.type.d.ts +1 -1
- package/dist/wire/message/message-batch.test.js +11 -5
- package/dist/wire/message/message.utils.d.ts +0 -10
- package/dist/wire/message/message.utils.js +1 -22
- package/dist/wire/message/poll-messages.command.d.ts +12 -1
- package/dist/wire/message/poll-messages.command.js +64 -16
- package/dist/wire/message/poll-messages.command.test.js +143 -5
- package/dist/wire/message/poll.utils.d.ts +4 -0
- package/dist/wire/message/poll.utils.js +6 -1
- package/dist/wire/message/send-messages.command.d.ts +3 -3
- package/dist/wire/message/send-messages.command.js +103 -3
- package/dist/wire/message/send-messages.command.test.js +113 -2
- package/dist/wire/offset/delete-offset.command.d.ts +11 -4
- package/dist/wire/offset/delete-offset.command.js +1 -1
- package/dist/wire/offset/get-offset.command.d.ts +1 -1
- package/dist/wire/offset/offset-write.command.test.d.ts +2 -0
- package/dist/wire/offset/offset-write.command.test.js +43 -0
- package/dist/wire/offset/offset.utils.js +10 -6
- package/dist/wire/offset/store-offset.command.d.ts +10 -0
- package/dist/wire/offset/store-offset.command.js +1 -1
- package/dist/wire/options.utils.d.ts +46 -0
- package/dist/wire/options.utils.js +57 -0
- package/dist/wire/partition/partition-route.d.ts +12 -0
- package/dist/wire/partition/partition-route.js +40 -0
- package/dist/wire/partition/partition-route.test.d.ts +2 -0
- package/dist/wire/partition/partition-route.test.js +42 -0
- package/dist/wire/session/login-with-token.command.d.ts +2 -2
- package/dist/wire/session/login-with-token.command.js +7 -2
- package/dist/wire/session/login.command.d.ts +2 -2
- package/dist/wire/session/login.command.js +7 -2
- package/dist/wire/stream/create-stream.command.d.ts +5 -2
- package/dist/wire/stream/create-stream.command.js +20 -2
- package/dist/wire/stream/index.d.ts +1 -1
- package/dist/wire/stream/index.js +1 -1
- package/dist/wire/stream/stream-options.d.ts +77 -0
- package/dist/wire/stream/stream-options.js +101 -0
- package/dist/wire/stream/stream-options.test.d.ts +2 -0
- package/dist/wire/stream/stream-options.test.js +103 -0
- package/dist/wire/stream/stream.utils.d.ts +9 -0
- package/dist/wire/stream/stream.utils.js +17 -3
- package/dist/wire/stream/update-stream.command.d.ts +23 -5
- package/dist/wire/stream/update-stream.command.js +10 -17
- package/dist/wire/system/describe-options.command.d.ts +23 -6
- package/dist/wire/system/describe-options.command.js +21 -2
- package/dist/wire/system/get-stats.command.d.ts +17 -0
- package/dist/wire/system/get-stats.command.js +69 -16
- package/dist/wire/system/get-stats.command.test.d.ts +2 -0
- package/dist/wire/system/get-stats.command.test.js +97 -0
- package/dist/wire/system/index.d.ts +2 -0
- package/dist/wire/system/index.js +2 -0
- package/dist/wire/system/snapshot.command.d.ts +64 -0
- package/dist/wire/system/snapshot.command.js +76 -0
- package/dist/wire/system/snapshot.command.test.d.ts +2 -0
- package/dist/wire/system/snapshot.command.test.js +86 -0
- package/dist/wire/system/update-resource.command.d.ts +53 -0
- package/dist/wire/system/update-resource.command.js +130 -0
- package/dist/wire/system/update-resource.command.test.d.ts +2 -0
- package/dist/wire/system/update-resource.command.test.js +497 -0
- package/dist/wire/token/create-token.command.d.ts +1 -1
- package/dist/wire/token/create-token.command.js +1 -1
- package/dist/wire/token/create-token.command.test.js +6 -0
- package/dist/wire/topic/create-topic.command.d.ts +20 -12
- package/dist/wire/topic/create-topic.command.js +11 -4
- package/dist/wire/topic/index.d.ts +1 -2
- package/dist/wire/topic/index.js +1 -2
- package/dist/wire/topic/topic.utils.d.ts +14 -5
- package/dist/wire/topic/topic.utils.js +26 -11
- package/dist/wire/topic/topic.utils.test.js +13 -13
- package/dist/wire/topic/update-topic.command.d.ts +22 -15
- package/dist/wire/topic/update-topic.command.js +12 -34
- package/dist/wire/user/create-user.command.d.ts +12 -2
- package/dist/wire/user/create-user.command.js +7 -3
- package/dist/wire/user/index.d.ts +1 -0
- package/dist/wire/user/index.js +1 -0
- package/dist/wire/user/update-user.command.d.ts +25 -7
- package/dist/wire/user/update-user.command.js +18 -31
- package/dist/wire/user/user.utils.d.ts +2 -0
- package/dist/wire/user/user.utils.js +5 -3
- package/dist/wire/vsr/header.d.ts +23 -8
- package/dist/wire/vsr/header.js +32 -10
- package/dist/wire/vsr/header.test.js +5 -1
- package/dist/wire/vsr/index.d.ts +8 -2
- package/dist/wire/vsr/index.js +44 -11
- package/dist/wire/vsr/operation.d.ts +18 -6
- package/dist/wire/vsr/operation.js +46 -16
- package/dist/wire/vsr/operation.test.js +32 -8
- package/dist/wire/vsr/register.d.ts +10 -6
- package/dist/wire/vsr/register.js +24 -7
- package/dist/wire/vsr/register.test.js +28 -8
- package/dist/wire/vsr/reply.d.ts +4 -1
- package/dist/wire/vsr/reply.js +42 -7
- package/dist/wire/vsr/reply.test.js +11 -1
- package/dist/wire/vsr/session.d.ts +9 -10
- package/dist/wire/vsr/session.js +32 -23
- package/dist/wire/vsr/session.test.js +33 -7
- package/dist/wire/vsr/vsr.test.js +57 -6
- package/package.json +6 -7
- package/dist/wire/message/flush-unsaved-buffers.command.d.ts +0 -26
- package/dist/wire/message/flush-unsaved-buffers.command.js +0 -36
- package/dist/wire/stream/purge-stream.command.d.ts +0 -22
- package/dist/wire/topic/purge-topic.command.d.ts +0 -24
- package/dist/wire/topic/purge-topic.command.js +0 -39
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Readable } from 'stream';
|
|
2
2
|
import { type TcpNetConnectOpts } from 'node:net';
|
|
3
3
|
import { type ConnectionOptions } from 'node:tls';
|
|
4
|
+
import type { PartitionContext } from '../wire/vsr/header.js';
|
|
4
5
|
/**
|
|
5
6
|
* TCP socket connection options.
|
|
6
7
|
* Alias for Node.js TcpNetConnectOpts, what net.createConnection accepts.
|
|
@@ -29,23 +30,33 @@ export type CommandResponse = {
|
|
|
29
30
|
data: Buffer;
|
|
30
31
|
};
|
|
31
32
|
export type SendCommandOptions = {
|
|
33
|
+
/** Cancels this call without closing its shared replica link. */
|
|
34
|
+
signal?: AbortSignal;
|
|
32
35
|
/** Whether the response uses the standard command response decoder */
|
|
33
36
|
handleResponse?: boolean;
|
|
34
|
-
/**
|
|
37
|
+
/** @deprecated Multiplexed calls ignore this queue-order hint. */
|
|
35
38
|
last?: boolean;
|
|
36
|
-
/**
|
|
37
|
-
* Whether a not-admitted refusal re-checks the leader and re-issues the
|
|
38
|
-
* command. False for the roster read a re-check itself runs: answering a
|
|
39
|
-
* leader check with another leader check would recurse.
|
|
40
|
-
*/
|
|
39
|
+
/** @deprecated The shared session owner manages replica routing. */
|
|
41
40
|
followsLeaderMoves?: boolean;
|
|
42
41
|
/**
|
|
43
42
|
* When the whole request gives up, as an epoch timestamp in milliseconds.
|
|
44
|
-
*
|
|
45
|
-
* leader move keeps the budget it was first submitted with, rather than
|
|
46
|
-
* opening a second one on top of it. Defaults to a fresh response timeout.
|
|
43
|
+
* Includes admission, discovery, link waits and retries. Defaults to 30 seconds.
|
|
47
44
|
*/
|
|
48
45
|
deadline?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Partition context of a poll, an offset store or delete, or a send to an
|
|
48
|
+
* explicit partition, taken from an earlier reply. It replaces the context
|
|
49
|
+
* the client would capture, stays on every retry, and is never refreshed
|
|
50
|
+
* after a history refusal (87): work from one partition incarnation must
|
|
51
|
+
* not land in another.
|
|
52
|
+
*/
|
|
53
|
+
context?: PartitionContext;
|
|
54
|
+
/**
|
|
55
|
+
* The `RawClient.lineage` that a call of several commands started with. A
|
|
56
|
+
* command of an older lineage fails with status 40 and sends nothing, so no
|
|
57
|
+
* part of the call runs under a session the caller opened after it started.
|
|
58
|
+
*/
|
|
59
|
+
lineage?: number;
|
|
49
60
|
};
|
|
50
61
|
/**
|
|
51
62
|
* Low-level client interface for communicating with the Iggy server.
|
|
@@ -56,11 +67,35 @@ export type RawClient = {
|
|
|
56
67
|
sendCommand: (code: number, payload: Buffer, options?: SendCommandOptions) => Promise<CommandResponse>;
|
|
57
68
|
/** Whether the client has been authenticated */
|
|
58
69
|
isAuthenticated: boolean;
|
|
59
|
-
/**
|
|
70
|
+
/** Raised by each sign-in and logout of the caller. */
|
|
71
|
+
readonly lineage?: number;
|
|
72
|
+
/** Uses caller credentials; any failed attempt withdraws automatic sign-in. */
|
|
60
73
|
authenticate: (c: ClientCredentials) => Promise<boolean>;
|
|
74
|
+
/** Typed login delegates validation to the session owner when supported. */
|
|
75
|
+
_authWithPassword?: (c: PasswordCredentials) => Promise<{
|
|
76
|
+
userId: number;
|
|
77
|
+
}>;
|
|
78
|
+
_authWithToken?: (c: TokenCredentials) => Promise<{
|
|
79
|
+
userId: number;
|
|
80
|
+
}>;
|
|
81
|
+
/**
|
|
82
|
+
* Resolves the original frozen operation; never sends it under a fresh identity.
|
|
83
|
+
* The client keeps the proved outcomes of its last `maxPendingOperations`
|
|
84
|
+
* replays, also across a session reset, so a later resolve of the same
|
|
85
|
+
* operation returns the same outcome without a request.
|
|
86
|
+
* `OperationNotRetainedError` means that a session reset or an abandon came
|
|
87
|
+
* before a replay proved the outcome, or that the proved outcome aged out.
|
|
88
|
+
*/
|
|
89
|
+
resolveOperation?: (operationId: bigint, options?: SendCommandOptions) => Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Releases a retained lane without asserting that the operation did not commit.
|
|
92
|
+
* A replay that is already running keeps the lane until it ends, and the
|
|
93
|
+
* client keeps the outcome that it proves.
|
|
94
|
+
*/
|
|
95
|
+
abandonOperation?: (operationId: bigint) => void;
|
|
61
96
|
/** Destroys the client connection */
|
|
62
97
|
destroy: () => void;
|
|
63
|
-
/**
|
|
98
|
+
/** Tracks one facade operation across multiple command submissions. */
|
|
64
99
|
hold?: () => () => void;
|
|
65
100
|
/** Registers an event listener */
|
|
66
101
|
on: (ev: string, cb: (e?: unknown) => void) => void;
|
|
@@ -71,7 +106,7 @@ export type RawClient = {
|
|
|
71
106
|
};
|
|
72
107
|
/**
|
|
73
108
|
* Function type that provides a RawClient instance.
|
|
74
|
-
* Used for dependency injection
|
|
109
|
+
* Used for dependency injection.
|
|
75
110
|
*/
|
|
76
111
|
export type ClientProvider = () => Promise<RawClient>;
|
|
77
112
|
/**
|
|
@@ -90,14 +125,15 @@ export type ReconnectOption = {
|
|
|
90
125
|
/** Whether automatic reconnection is enabled */
|
|
91
126
|
enabled: boolean;
|
|
92
127
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
128
|
+
* Initial reconnect delay in milliseconds. Each replica uses exponential
|
|
129
|
+
* backoff capped at 10 seconds with jitter.
|
|
95
130
|
*/
|
|
96
131
|
interval: number;
|
|
97
132
|
/**
|
|
98
|
-
* Maximum
|
|
99
|
-
*
|
|
100
|
-
*
|
|
133
|
+
* Maximum retries per replica after the first attempt. Bootstrap retries
|
|
134
|
+
* sweep the configured seed and remembered endpoints. The budget limits one
|
|
135
|
+
* reconnect burst: while the client stays signed in, each heartbeat restarts
|
|
136
|
+
* a replica link that used its budget, with a fresh budget.
|
|
101
137
|
*/
|
|
102
138
|
maxRetries: number;
|
|
103
139
|
};
|
|
@@ -127,12 +163,12 @@ export type PasswordCredentials = {
|
|
|
127
163
|
*/
|
|
128
164
|
export type ClientCredentials = TokenCredentials | PasswordCredentials;
|
|
129
165
|
/**
|
|
130
|
-
*
|
|
166
|
+
* Legacy logical-client count. Replica links are managed independently.
|
|
131
167
|
*/
|
|
132
168
|
export type PoolSizeOption = {
|
|
133
|
-
/** Minimum
|
|
169
|
+
/** Minimum logical clients; normalized to one */
|
|
134
170
|
min?: number;
|
|
135
|
-
/** Maximum
|
|
171
|
+
/** Maximum logical clients; only one is supported */
|
|
136
172
|
max?: number;
|
|
137
173
|
};
|
|
138
174
|
/**
|
|
@@ -150,13 +186,20 @@ export type ClientConfig = {
|
|
|
150
186
|
options: TransportOption;
|
|
151
187
|
/** Authentication credentials */
|
|
152
188
|
credentials: ClientCredentials;
|
|
153
|
-
/**
|
|
189
|
+
/** Legacy logical-client count; does not limit replica links. */
|
|
154
190
|
poolSize?: PoolSizeOption;
|
|
155
191
|
/** Automatic reconnection configuration */
|
|
156
192
|
reconnect?: ReconnectOption;
|
|
157
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Interval for sending heartbeat pings, in milliseconds. Each heartbeat also
|
|
195
|
+
* restarts replica links that used their reconnect budget. 0 disables both.
|
|
196
|
+
*/
|
|
158
197
|
heartbeatInterval?: number;
|
|
159
198
|
/** Maximum accepted response frame size in bytes */
|
|
160
199
|
maxResponseFrameSize?: number;
|
|
200
|
+
/** Maximum active plus retained mutations across this client. Default: 256. */
|
|
201
|
+
maxPendingOperations?: number;
|
|
202
|
+
/** Frozen mutation headers and bodies, including retained and writer-owned bytes. Default: 64 MiB. */
|
|
203
|
+
maxPendingOperationBytes?: number;
|
|
161
204
|
};
|
|
162
205
|
//# sourceMappingURL=client.type.d.ts.map
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Client, SimpleClient, SingleClient } from './client.js';
|
|
2
2
|
export * from './client.config.js';
|
|
3
|
+
export { ConnectionClosedError } from './client.connection.js';
|
|
3
4
|
export { parseConnectionString } from './client.connection-string.js';
|
|
4
5
|
export * from './client.utils.js';
|
|
5
6
|
export * from './client.socket.js';
|
package/dist/client/index.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// under the License.
|
|
17
17
|
export { Client, SimpleClient, SingleClient } from './client.js';
|
|
18
18
|
export * from './client.config.js';
|
|
19
|
+
export { ConnectionClosedError } from './client.connection.js';
|
|
19
20
|
export { parseConnectionString } from './client.connection-string.js';
|
|
20
21
|
export * from './client.utils.js';
|
|
21
22
|
export * from './client.socket.js';
|
package/dist/client.e2e.js
CHANGED
|
@@ -18,7 +18,6 @@ import assert from 'node:assert/strict';
|
|
|
18
18
|
import { Client } from './client/client.js';
|
|
19
19
|
try {
|
|
20
20
|
const credentials = { username: 'iggy', password: 'iggy' };
|
|
21
|
-
// pooled client
|
|
22
21
|
const c = new Client({
|
|
23
22
|
transport: 'TCP',
|
|
24
23
|
options: { port: 8090, host: '127.0.0.1' },
|
|
@@ -28,8 +27,7 @@ try {
|
|
|
28
27
|
console.log('R2', await c.user.list());
|
|
29
28
|
console.log('R3', await c.client.list());
|
|
30
29
|
console.log('R4', await c.system.getStats());
|
|
31
|
-
|
|
32
|
-
assert.equal(c._pool.size, 1);
|
|
30
|
+
const raw = await c.clientProvider();
|
|
33
31
|
const resp = await Promise.all([
|
|
34
32
|
c.user.list(),
|
|
35
33
|
c.stream.list(),
|
|
@@ -42,8 +40,8 @@ try {
|
|
|
42
40
|
c.system.getStats(),
|
|
43
41
|
]);
|
|
44
42
|
console.log('parallel call resp', resp);
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
assert.equal(await c.clientProvider(), raw);
|
|
44
|
+
await c.destroy();
|
|
47
45
|
}
|
|
48
46
|
catch (err) {
|
|
49
47
|
console.error('FAILED', err);
|
|
@@ -14,23 +14,15 @@
|
|
|
14
14
|
// KIND, either express or implied. See the License for the
|
|
15
15
|
// specific language governing permissions and limitations
|
|
16
16
|
// under the License.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
},
|
|
30
|
-
deserialize: deserializeVoidResponse
|
|
31
|
-
};
|
|
32
|
-
/**
|
|
33
|
-
* Executable purge stream command function.
|
|
34
|
-
*/
|
|
35
|
-
export const purgeStream = wrapCommand(PURGE_STREAM);
|
|
36
|
-
//# sourceMappingURL=purge-stream.command.js.map
|
|
17
|
+
// These tests need a server started with encryption enabled and a config key
|
|
18
|
+
// (IGGY_ENCRYPTION_ENABLED=true, IGGY_ENCRYPTION_KEY), so a stream created
|
|
19
|
+
// without a key reports a server-generated one.
|
|
20
|
+
import { Client } from '../client/client.js';
|
|
21
|
+
import { getIggyAddress } from '../tcp.sm.utils.js';
|
|
22
|
+
import { EncryptionKeySource } from '../wire/index.js';
|
|
23
|
+
import { describeResourceOptions } from './resource-options.suite.js';
|
|
24
|
+
const [host, port] = getIggyAddress();
|
|
25
|
+
describeResourceOptions('e2e -> resource options', 'e2e-ro', (credentials) => new Client({
|
|
26
|
+
transport: 'TCP', options: { host, port }, credentials
|
|
27
|
+
}), EncryptionKeySource.Server);
|
|
28
|
+
//# sourceMappingURL=resource-options.e2e.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Client } from '../client/client.js';
|
|
2
|
+
import { EncryptionKeySource } from '../wire/index.js';
|
|
3
|
+
type Credentials = {
|
|
4
|
+
username: string;
|
|
5
|
+
password: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Registers the resource options cases for one transport. `connect` builds a
|
|
9
|
+
* client of that transport signed in with the given credentials.
|
|
10
|
+
* `keylessSource` is what a stream created without a key reports: `server`
|
|
11
|
+
* when the server runs with encryption enabled, `none` otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export declare const describeResourceOptions: (label: string, prefix: string, connect: (credentials: Credentials) => Client, keylessSource: EncryptionKeySource, skip?: string | false) => Promise<void>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=resource-options.suite.d.ts.map
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
// or more contributor license agreements. See the NOTICE file
|
|
3
|
+
// distributed with this work for additional information
|
|
4
|
+
// regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
// to you under the Apache License, Version 2.0 (the
|
|
6
|
+
// "License"); you may not use this file except in compliance
|
|
7
|
+
// with the License. You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing,
|
|
12
|
+
// software distributed under the License is distributed on an
|
|
13
|
+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
// KIND, either express or implied. See the License for the
|
|
15
|
+
// specific language governing permissions and limitations
|
|
16
|
+
// under the License.
|
|
17
|
+
import { randomBytes } from 'node:crypto';
|
|
18
|
+
import { after, describe, it } from 'node:test';
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
import { HeaderValue } from '../wire/message/header.utils.js';
|
|
21
|
+
import { ResponseError } from '../wire/error.utils.js';
|
|
22
|
+
import { Consumer, EncryptionKeySource, OptionsScope, PartitionResizePolicy, Partitioning, PollingStrategy, StreamEncryptionKey, UserStatus, encryptionKeyFingerprint } from '../wire/index.js';
|
|
23
|
+
const NEVER_RETURNED = ['encryption_key', 'encryption_key_wrapped', 'critical_keys'];
|
|
24
|
+
const FINGERPRINT = /^[0-9a-f]{16}$/;
|
|
25
|
+
const UNLIMITED = 18446744073709551615n;
|
|
26
|
+
const UNSUPPORTED_OPTION_KEY = 4041;
|
|
27
|
+
const OPTION_NOT_UPDATABLE = 4103;
|
|
28
|
+
const assertProjection = (stream, source, fingerprint) => {
|
|
29
|
+
assert.equal(stream.encryptionKeySource, source);
|
|
30
|
+
if (source === EncryptionKeySource.None) {
|
|
31
|
+
assert.equal(stream.encryptionKeyFingerprint, null);
|
|
32
|
+
assert.deepEqual(stream.encryptionKekFingerprints, []);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
assert.match(stream.encryptionKeyFingerprint ?? '', FINGERPRINT);
|
|
36
|
+
if (fingerprint !== undefined)
|
|
37
|
+
assert.equal(stream.encryptionKeyFingerprint, fingerprint);
|
|
38
|
+
assert.ok(stream.encryptionKekFingerprints.length >= 1);
|
|
39
|
+
for (const kek of stream.encryptionKekFingerprints)
|
|
40
|
+
assert.match(kek, FINGERPRINT);
|
|
41
|
+
};
|
|
42
|
+
const assertNoKeyMaterial = (stream, key) => {
|
|
43
|
+
for (const block of [stream.options, stream.derivedOptions]) {
|
|
44
|
+
for (const name of NEVER_RETURNED)
|
|
45
|
+
assert.equal(name in block, false, `${name} was returned`);
|
|
46
|
+
if (key)
|
|
47
|
+
for (const value of Object.values(block))
|
|
48
|
+
assert.ok(!(Buffer.isBuffer(value) && value.equals(key)) && value !== key.toString('base64'), 'a returned option holds the key');
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Registers the resource options cases for one transport. `connect` builds a
|
|
53
|
+
* client of that transport signed in with the given credentials.
|
|
54
|
+
* `keylessSource` is what a stream created without a key reports: `server`
|
|
55
|
+
* when the server runs with encryption enabled, `none` otherwise.
|
|
56
|
+
*/
|
|
57
|
+
export const describeResourceOptions = (label, prefix, connect, keylessSource, skip = false) => describe(label, { skip }, async () => {
|
|
58
|
+
const c = connect({ username: 'iggy', password: 'iggy' });
|
|
59
|
+
it('given stream key when creating should read matching projection', async () => {
|
|
60
|
+
const key = randomBytes(32);
|
|
61
|
+
const name = `${prefix}-keyed`;
|
|
62
|
+
const created = await c.stream.create({ name, encryptionKey: StreamEncryptionKey.from(key) });
|
|
63
|
+
try {
|
|
64
|
+
assertProjection(created, EncryptionKeySource.Stream, encryptionKeyFingerprint(key));
|
|
65
|
+
assertNoKeyMaterial(created, key);
|
|
66
|
+
const fetched = await c.stream.get({ streamId: name });
|
|
67
|
+
assert.ok(fetched);
|
|
68
|
+
assertProjection(fetched, EncryptionKeySource.Stream, encryptionKeyFingerprint(key));
|
|
69
|
+
assert.deepEqual(fetched.encryptionKekFingerprints, created.encryptionKekFingerprints);
|
|
70
|
+
assertNoKeyMaterial(fetched, key);
|
|
71
|
+
const listed = (await c.stream.list()).find((stream) => stream.id === created.id);
|
|
72
|
+
assert.ok(listed);
|
|
73
|
+
assertProjection(listed, EncryptionKeySource.Stream, encryptionKeyFingerprint(key));
|
|
74
|
+
assertNoKeyMaterial(listed, key);
|
|
75
|
+
const base64 = `${prefix}-keyed-base64`;
|
|
76
|
+
const fromText = await c.stream.create({
|
|
77
|
+
name: base64, encryptionKey: StreamEncryptionKey.from(key.toString('base64'))
|
|
78
|
+
});
|
|
79
|
+
assert.equal(fromText.encryptionKeyFingerprint, encryptionKeyFingerprint(key));
|
|
80
|
+
await c.stream.delete({ streamId: fromText.id });
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await c.stream.delete({ streamId: created.id });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
it(`given keyless stream when reading should report key source ${keylessSource}`, async () => {
|
|
87
|
+
const created = await c.stream.create({ name: `${prefix}-keyless` });
|
|
88
|
+
try {
|
|
89
|
+
assertProjection(created, keylessSource);
|
|
90
|
+
const fetched = await c.stream.get({ streamId: created.id });
|
|
91
|
+
assert.ok(fetched);
|
|
92
|
+
assertProjection(fetched, keylessSource, created.encryptionKeyFingerprint ?? undefined);
|
|
93
|
+
assertNoKeyMaterial(fetched);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
await c.stream.delete({ streamId: created.id });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
it('given stream rename when updating should preserve encryption projection', async () => {
|
|
100
|
+
const key = randomBytes(32);
|
|
101
|
+
const created = await c.stream.create({
|
|
102
|
+
name: `${prefix}-rename`, encryptionKey: StreamEncryptionKey.from(key)
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
await c.topic.create({ streamId: created.id, name: 'events', partitionCount: 1 });
|
|
106
|
+
await c.message.send({
|
|
107
|
+
streamId: created.id, topicId: 'events', partition: Partitioning.PartitionId(0),
|
|
108
|
+
messages: [{ payload: 'before-rename', headers: [] }]
|
|
109
|
+
});
|
|
110
|
+
assert.ok(await c.stream.update({ streamId: created.id, name: `${prefix}-renamed` }));
|
|
111
|
+
await assert.rejects(c.stream.update({ streamId: created.id, reset: ['encryption_key'] }), (error) => error instanceof ResponseError &&
|
|
112
|
+
error.errorCode === OPTION_NOT_UPDATABLE && !error.fromServer);
|
|
113
|
+
const renamed = await c.stream.get({ streamId: created.id });
|
|
114
|
+
assert.ok(renamed);
|
|
115
|
+
assert.equal(renamed.id, created.id);
|
|
116
|
+
assert.equal(renamed.name, `${prefix}-renamed`);
|
|
117
|
+
assertProjection(renamed, EncryptionKeySource.Stream, encryptionKeyFingerprint(key));
|
|
118
|
+
assert.deepEqual(renamed.encryptionKekFingerprints, created.encryptionKekFingerprints);
|
|
119
|
+
const polled = await c.message.poll({
|
|
120
|
+
streamId: created.id, topicId: 'events', consumer: Consumer.Single, partitionId: 0,
|
|
121
|
+
pollingStrategy: PollingStrategy.First, count: 10, autocommit: false
|
|
122
|
+
});
|
|
123
|
+
assert.deepEqual(polled.messages.map((message) => message.payload.toString()), ['before-rename']);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await c.stream.delete({ streamId: created.id });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
it('given topic reset when updating should preserve unmentioned values', async () => {
|
|
130
|
+
const stream = await c.stream.create({ name: `${prefix}-topic-reset` });
|
|
131
|
+
try {
|
|
132
|
+
const topic = await c.topic.create({
|
|
133
|
+
streamId: stream.id, name: 'settings', partitionCount: 1,
|
|
134
|
+
compressionAlgorithm: 2, messageExpiry: 3600000000n, maxTopicSize: 1073741824n
|
|
135
|
+
});
|
|
136
|
+
assert.ok(await c.topic.update({
|
|
137
|
+
streamId: stream.id, topicId: topic.id,
|
|
138
|
+
messageExpiry: 7200000000n, reset: ['max_topic_size']
|
|
139
|
+
}));
|
|
140
|
+
const updated = await c.topic.get({ streamId: stream.id, topicId: topic.id });
|
|
141
|
+
assert.ok(updated);
|
|
142
|
+
assert.equal(updated.name, 'settings');
|
|
143
|
+
assert.equal(updated.options.compression_algorithm, 'gzip');
|
|
144
|
+
assert.equal(updated.compressionAlgorithm, 2);
|
|
145
|
+
assert.equal(updated.options.message_expiry, 7200000000n);
|
|
146
|
+
assert.equal(updated.messageExpiry, 7200000000n);
|
|
147
|
+
assert.equal('max_topic_size' in updated.options, false);
|
|
148
|
+
assert.equal(updated.derivedOptions.max_topic_size, UNLIMITED);
|
|
149
|
+
assert.equal(updated.maxTopicSize, UNLIMITED);
|
|
150
|
+
assert.deepEqual(updated.derivedOptions, {
|
|
151
|
+
...topic.derivedOptions, max_topic_size: UNLIMITED
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
await c.stream.delete({ streamId: stream.id });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
it('given user options when creating and updating should preserve user behavior', async () => {
|
|
159
|
+
const password = 'e2e-ro-password';
|
|
160
|
+
const first = await c.user.create({
|
|
161
|
+
username: `${prefix}-user`, password, status: UserStatus.Active, options: []
|
|
162
|
+
});
|
|
163
|
+
const second = await c.user.create({
|
|
164
|
+
username: `${prefix}-user-taken`, password, status: UserStatus.Active
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
assert.deepEqual(first.options, {});
|
|
168
|
+
assert.deepEqual(first.derivedOptions, {});
|
|
169
|
+
assert.ok(await c.user.update({
|
|
170
|
+
userId: first.id, username: `${prefix}-user-renamed`, status: UserStatus.Inactive
|
|
171
|
+
}));
|
|
172
|
+
const updated = await c.user.get({ userId: first.id });
|
|
173
|
+
assert.ok(updated);
|
|
174
|
+
assert.equal(updated.userName, `${prefix}-user-renamed`);
|
|
175
|
+
assert.equal(updated.status, 'Inactive');
|
|
176
|
+
assert.deepEqual(updated.options, {});
|
|
177
|
+
await assert.rejects(c.user.update({ userId: first.id, username: `${prefix}-user-taken`, status: UserStatus.Active }), (error) => error instanceof ResponseError && error.fromServer);
|
|
178
|
+
const unchanged = await c.user.get({ userId: first.id });
|
|
179
|
+
assert.ok(unchanged);
|
|
180
|
+
assert.equal(unchanged.userName, `${prefix}-user-renamed`);
|
|
181
|
+
assert.equal(unchanged.status, 'Inactive');
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
await c.user.delete({ userId: first.id });
|
|
185
|
+
await c.user.delete({ userId: second.id });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
it('given logged in user rename when reconnecting should keep new username', async () => {
|
|
189
|
+
const password = 'e2e-ro-self-password';
|
|
190
|
+
const user = await c.user.create({
|
|
191
|
+
username: `${prefix}-self`, password, status: UserStatus.Active,
|
|
192
|
+
permissions: {
|
|
193
|
+
global: {
|
|
194
|
+
ManageServers: false, ReadServers: false, ManageUsers: true, ReadUsers: true,
|
|
195
|
+
ManageStreams: true, ReadStreams: true, ManageTopics: true, ReadTopics: true,
|
|
196
|
+
PollMessages: false, SendMessages: false
|
|
197
|
+
},
|
|
198
|
+
streams: []
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
const taken = await c.user.create({
|
|
202
|
+
username: `${prefix}-self-taken`, password, status: UserStatus.Active
|
|
203
|
+
});
|
|
204
|
+
const self = connect({ username: `${prefix}-self`, password });
|
|
205
|
+
const signedInAs = async (username) => {
|
|
206
|
+
await self.session.logout();
|
|
207
|
+
await self.session.login({ username, password });
|
|
208
|
+
const me = await self.client.getMe();
|
|
209
|
+
return (await c.user.get({ userId: me.userId }))?.userName;
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
assert.equal((await self.client.getMe()).userId, user.id);
|
|
213
|
+
await assert.rejects(self.user.update({ userId: user.id, username: `${prefix}-self-taken` }), (error) => error instanceof ResponseError && error.fromServer);
|
|
214
|
+
assert.equal(await signedInAs(`${prefix}-self`), `${prefix}-self`);
|
|
215
|
+
const stream = await self.stream.create({ name: `${prefix}-self-stream` });
|
|
216
|
+
await self.stream.update({ streamId: stream.id, name: `${prefix}-self` });
|
|
217
|
+
const topic = await self.topic.create({ streamId: stream.id, name: 'events', partitionCount: 1 });
|
|
218
|
+
await self.topic.update({ streamId: stream.id, topicId: topic.id, name: `${prefix}-self-topic` });
|
|
219
|
+
await self.stream.delete({ streamId: stream.id });
|
|
220
|
+
assert.equal(await signedInAs(`${prefix}-self`), `${prefix}-self`);
|
|
221
|
+
assert.ok(await self.user.update({ userId: user.id, username: `${prefix}-self-renamed` }));
|
|
222
|
+
assert.equal(await signedInAs(`${prefix}-self-renamed`), `${prefix}-self-renamed`);
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
await self.destroy();
|
|
226
|
+
await c.user.delete({ userId: user.id });
|
|
227
|
+
await c.user.delete({ userId: taken.id });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
it('given raw create-only key or unknown raw reset when updating should let the server refuse', async () => {
|
|
231
|
+
const stream = await c.stream.create({ name: `${prefix}-raw` });
|
|
232
|
+
try {
|
|
233
|
+
const topic = await c.topic.create({
|
|
234
|
+
streamId: stream.id, name: 'fixed', partitionCount: 1,
|
|
235
|
+
partitionResizePolicy: PartitionResizePolicy.Fixed
|
|
236
|
+
});
|
|
237
|
+
assert.equal(topic.options.partition_resize_policy, PartitionResizePolicy.Fixed);
|
|
238
|
+
const fromServer = (code) => (error) => error instanceof ResponseError && error.errorCode === code && error.fromServer;
|
|
239
|
+
await assert.rejects(c.topic.update({
|
|
240
|
+
streamId: stream.id, topicId: topic.id,
|
|
241
|
+
options: [{ key: 'compression_algorithm', value: HeaderValue.String('gzip') }]
|
|
242
|
+
}), fromServer(OPTION_NOT_UPDATABLE));
|
|
243
|
+
await assert.rejects(c.topic.update({ streamId: stream.id, topicId: topic.id, rawReset: ['future_option'] }), fromServer(UNSUPPORTED_OPTION_KEY));
|
|
244
|
+
const unchanged = await c.topic.get({ streamId: stream.id, topicId: topic.id });
|
|
245
|
+
assert.equal(unchanged?.compressionAlgorithm, topic.compressionAlgorithm);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
await c.stream.delete({ streamId: stream.id });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
it('given stream scope when describing options should report sensitivity and mutability', async () => {
|
|
252
|
+
const specs = await c.system.describeOptions({ scope: OptionsScope.Stream });
|
|
253
|
+
const byKey = new Map(specs.map((spec) => [spec.key, spec]));
|
|
254
|
+
assert.deepEqual([...byKey.keys()], [
|
|
255
|
+
'name', 'encryption_key', 'encryption_key_source',
|
|
256
|
+
'encryption_key_fingerprint', 'encryption_kek_fingerprint'
|
|
257
|
+
]);
|
|
258
|
+
assert.equal(byKey.get('name')?.updatable, true);
|
|
259
|
+
assert.equal(byKey.get('name')?.secret, false);
|
|
260
|
+
assert.equal(byKey.get('encryption_key')?.updatable, false);
|
|
261
|
+
assert.equal(byKey.get('encryption_key')?.secret, true);
|
|
262
|
+
assert.equal(byKey.get('encryption_key')?.critical, true);
|
|
263
|
+
assert.equal(byKey.get('encryption_key_fingerprint')?.readOnly, true);
|
|
264
|
+
});
|
|
265
|
+
it(`${label}::cleanup`, async () => {
|
|
266
|
+
assert.ok(await c.session.logout());
|
|
267
|
+
});
|
|
268
|
+
after(() => {
|
|
269
|
+
c.destroy();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
//# sourceMappingURL=resource-options.suite.js.map
|
|
@@ -80,6 +80,10 @@ describe('e2e -> consumer-stream', async () => {
|
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
82
|
after(async () => {
|
|
83
|
+
// The member that did not get the last batch can still be polling,
|
|
84
|
+
// and a poll that fails after the delete has no error listener.
|
|
85
|
+
s.destroy();
|
|
86
|
+
s2.destroy();
|
|
83
87
|
assert.ok(await c.stream.delete({ streamId: streamName }));
|
|
84
88
|
assert.ok(await c.session.logout());
|
|
85
89
|
await c.destroy();
|
|
@@ -20,30 +20,29 @@ import { getTestClient } from './test-client.utils.js';
|
|
|
20
20
|
describe('e2e -> parallel', async () => {
|
|
21
21
|
const c = getTestClient();
|
|
22
22
|
const baseGetMe = await c.client.getMe();
|
|
23
|
-
const credentials = { username: 'iggy', password: 'iggy' };
|
|
24
23
|
it('e2e -> parallel::mix calls', async () => {
|
|
25
24
|
const resp = await Promise.all([
|
|
26
25
|
c.client.getMe(),
|
|
27
26
|
c.system.getStats(),
|
|
28
27
|
c.system.ping(),
|
|
29
|
-
c.session.login(credentials),
|
|
30
28
|
c.client.getMe(),
|
|
31
29
|
c.system.getStats(),
|
|
32
30
|
c.system.ping(),
|
|
33
|
-
c.session.login(credentials),
|
|
34
31
|
c.client.getMe(),
|
|
35
32
|
c.system.getStats(),
|
|
36
33
|
c.system.ping(),
|
|
37
|
-
c.session.login(credentials),
|
|
38
34
|
]);
|
|
39
|
-
assert.
|
|
35
|
+
assert.equal(resp.length, 9);
|
|
40
36
|
assert.deepEqual(resp[0], baseGetMe);
|
|
41
37
|
assert.deepEqual(Object.keys(resp[1]), [
|
|
42
38
|
'processId', 'cpuUsage', 'totalCpuUsage', 'memoryUsage', 'totalMemory',
|
|
43
39
|
'availableMemory', 'runTime', 'startTime', 'readBytes', 'writtenBytes',
|
|
44
40
|
'messagesSizeBytes', 'streamsCount', 'topicsCount', 'partitionsCount',
|
|
45
41
|
'segmentsCount', 'messagesCount', 'clientsCount', 'consumersGroupsCount',
|
|
46
|
-
'hostname', 'osName', 'osVersion', 'kernelVersion'
|
|
42
|
+
'hostname', 'osName', 'osVersion', 'kernelVersion',
|
|
43
|
+
'iggyServerVersion', 'iggyServerSemver', 'cacheMetrics',
|
|
44
|
+
'threadsCount', 'freeDiskSpace', 'totalDiskSpace',
|
|
45
|
+
'openFilesCount', 'openFilesLimit'
|
|
47
46
|
]);
|
|
48
47
|
resp.forEach(r => assert.ok(r));
|
|
49
48
|
});
|
|
@@ -40,9 +40,6 @@ describe('e2e -> stream', async () => {
|
|
|
40
40
|
});
|
|
41
41
|
assert.ok(stream);
|
|
42
42
|
});
|
|
43
|
-
it('e2e -> stream::purge', async () => {
|
|
44
|
-
assert.ok(await c.stream.purge({ streamId: name2 }));
|
|
45
|
-
});
|
|
46
43
|
it('e2e -> stream::delete', async () => {
|
|
47
44
|
assert.ok(await c.stream.delete({ streamId: name2 }));
|
|
48
45
|
});
|
|
@@ -31,9 +31,18 @@ describe('e2e -> system', async () => {
|
|
|
31
31
|
'availableMemory', 'runTime', 'startTime', 'readBytes', 'writtenBytes',
|
|
32
32
|
'messagesSizeBytes', 'streamsCount', 'topicsCount', 'partitionsCount',
|
|
33
33
|
'segmentsCount', 'messagesCount', 'clientsCount', 'consumersGroupsCount',
|
|
34
|
-
'hostname', 'osName', 'osVersion', 'kernelVersion'
|
|
34
|
+
'hostname', 'osName', 'osVersion', 'kernelVersion',
|
|
35
|
+
'iggyServerVersion', 'iggyServerSemver', 'cacheMetrics',
|
|
36
|
+
'threadsCount', 'freeDiskSpace', 'totalDiskSpace',
|
|
37
|
+
'openFilesCount', 'openFilesLimit'
|
|
35
38
|
]);
|
|
36
39
|
});
|
|
40
|
+
it('e2e -> system::snapshot', async () => {
|
|
41
|
+
const data = await c.system.snapshot();
|
|
42
|
+
assert.ok(data.length > 0);
|
|
43
|
+
assert.equal(data[0], 0x50);
|
|
44
|
+
assert.equal(data[1], 0x4B);
|
|
45
|
+
});
|
|
37
46
|
it('e2e -> system::logout', async () => {
|
|
38
47
|
assert.ok(await c.session.logout());
|
|
39
48
|
});
|
|
@@ -20,13 +20,19 @@ import { getTestClient } from './test-client.utils.js';
|
|
|
20
20
|
describe('e2e -> token', async () => {
|
|
21
21
|
const c = getTestClient();
|
|
22
22
|
const tokenName = 'yolo-token-test';
|
|
23
|
+
const lifetimeMs = 30 * 60 * 1000;
|
|
24
|
+
let createdAt = 0;
|
|
23
25
|
it('e2e -> token::create', async () => {
|
|
24
|
-
|
|
26
|
+
createdAt = Date.now();
|
|
27
|
+
const tk = await c.token.create({ name: tokenName, expiry: BigInt(lifetimeMs) * 1000n });
|
|
25
28
|
assert.ok(tk.token.length > 1);
|
|
26
29
|
});
|
|
27
30
|
it('e2e -> token::list', async () => {
|
|
28
31
|
const tks = await c.token.list();
|
|
29
|
-
|
|
32
|
+
const listed = tks.find((tk) => tk.name === tokenName);
|
|
33
|
+
assert.ok(listed?.expiry, 'the token lists its expiry');
|
|
34
|
+
const remainingMs = listed.expiry.getTime() - createdAt;
|
|
35
|
+
assert.ok(Math.abs(remainingMs - lifetimeMs) < 5000, `expiry ${remainingMs} ms after create, want ${lifetimeMs}`);
|
|
30
36
|
});
|
|
31
37
|
it('e2e -> token::delete', async () => {
|
|
32
38
|
assert.ok(await c.token.delete({ name: tokenName }));
|