@truenas/api-client 6.0.0 → 6.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/README.md +144 -217
- package/dist/chunk-7QVRLQIZ.js +4211 -0
- package/dist/chunk-7QVRLQIZ.js.map +1 -0
- package/dist/chunk-PQIVBXN3.cjs +4251 -0
- package/dist/chunk-PQIVBXN3.cjs.map +1 -0
- package/dist/index.cjs +130 -4343
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -31388
- package/dist/index.d.ts +29 -31388
- package/dist/index.js +1 -4324
- package/dist/index.js.map +1 -1
- package/dist/testing/index.cjs +729 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +483 -0
- package/dist/testing/index.d.ts +483 -0
- package/dist/testing/index.js +716 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/truenas-auth-mechanism.enum-DikfjVxa.d.cts +30936 -0
- package/dist/truenas-auth-mechanism.enum-DikfjVxa.d.ts +30936 -0
- package/package.json +23 -2
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import * as rxjs from 'rxjs';
|
|
2
|
+
import { Subscription, Observable } from 'rxjs';
|
|
3
|
+
import { aE as TrueNasConnection, aF as TrueNasMessage, an as TrueNasErrorFrame, aG as TrueNasAuthenticator, d as ApiVersion, ak as TrueNasAuthMechanism, R as AuthResponse, S as AuthResponseType, a2 as Job, a5 as JobProgress, n as ApiDirectoryShape, V as CallMethod, ab as QueryMethod, X as CallResponse, W as CallParams, a9 as QueryEntity, a3 as JobMethod, a6 as JobResult, a0 as EventName, a1 as EventUnion, aj as SupportedApiVersion, T as TrueNasApiClient, aH as DerivedDirectory, am as TrueNasErrorData } from '../truenas-auth-mechanism.enum-DikfjVxa.js';
|
|
4
|
+
import 'rxjs/webSocket';
|
|
5
|
+
|
|
6
|
+
/** How a fake connection is set up. Every field has a usable default. */
|
|
7
|
+
interface FakeConnectionOptions {
|
|
8
|
+
/** Reported as the appliance's uuid; only ever used for log correlation. */
|
|
9
|
+
uuid?: string;
|
|
10
|
+
/** The path the real connection would have opened a socket on. */
|
|
11
|
+
websocketPath?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the connection starts in the opened state. Defaults to `true`, as
|
|
14
|
+
* it does through `createFakeClient`: a closed connection queues sends
|
|
15
|
+
* instead of writing them, which is rarely what a spec building one directly
|
|
16
|
+
* is after.
|
|
17
|
+
*/
|
|
18
|
+
opened?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Whether a frame nothing is scripted to answer fails instead of hanging.
|
|
21
|
+
* Defaults to `false`.
|
|
22
|
+
*
|
|
23
|
+
* Off by default because the two ways to answer cannot be told apart at
|
|
24
|
+
* `send` time. A spec that scripts everything with `mock` has registered its
|
|
25
|
+
* answers before the frame goes out; a spec that drives frames by hand calls
|
|
26
|
+
* `reply` *after* it — so refusing an unregistered method would break every
|
|
27
|
+
* hand-driven spec, 24 of them in this repo's own suite. Strictness is the
|
|
28
|
+
* spec's claim that it scripted everything, which only the spec can make.
|
|
29
|
+
*/
|
|
30
|
+
strict?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A `TrueNasConnection` that never opens a socket, and that a test drives by
|
|
34
|
+
* hand.
|
|
35
|
+
*
|
|
36
|
+
* A real subclass, so the real `TrueNasApi` and clients run on it without an
|
|
37
|
+
* `as unknown as` cast. `send` matches the real one: it queues while closed,
|
|
38
|
+
* writes on open, and drops frames whose caller unsubscribed. The rest is
|
|
39
|
+
* inherited and inert, except the base 20-second ping timer, which never sends
|
|
40
|
+
* but lives until `close()`.
|
|
41
|
+
*/
|
|
42
|
+
declare class FakeConnection extends TrueNasConnection {
|
|
43
|
+
private readonly incoming;
|
|
44
|
+
private readonly frames;
|
|
45
|
+
/** Frames handed to `send` while closed, waiting for the socket. */
|
|
46
|
+
private readonly queued;
|
|
47
|
+
private readonly autoReplies;
|
|
48
|
+
/** Set by `close()`. Latched, because the real connection cannot come back. */
|
|
49
|
+
private terminated;
|
|
50
|
+
/** See {@link FakeConnectionOptions.strict}. Assigned in the constructor. */
|
|
51
|
+
private readonly strict;
|
|
52
|
+
/**
|
|
53
|
+
* The canonical streams, re-pointed at what this class actually drives.
|
|
54
|
+
*
|
|
55
|
+
* The base derives all three from `connection$`, which on a disabled
|
|
56
|
+
* connection reports one state and then nothing: `{ state: 'closed' }`. So
|
|
57
|
+
* they describe the connection that never connects, not the one the test is
|
|
58
|
+
* driving — `messages$` stayed silent while `receive` delivered, `opened$`
|
|
59
|
+
* stayed `[false]` through every simulated open, and `closed$` reported a
|
|
60
|
+
* connection that had never been up. `messages()` is a shim over `messages$`;
|
|
61
|
+
* leaving them disagreeing puts the divergence in the members a consumer is
|
|
62
|
+
* most likely to reach for.
|
|
63
|
+
*/
|
|
64
|
+
messages$: rxjs.Observable<TrueNasMessage>;
|
|
65
|
+
opened$: rxjs.Observable<boolean>;
|
|
66
|
+
closed$: rxjs.Observable<undefined>;
|
|
67
|
+
constructor(options?: FakeConnectionOptions);
|
|
68
|
+
/** Every frame handed to `send`, in order. */
|
|
69
|
+
get sent(): readonly TrueNasMessage[];
|
|
70
|
+
/**
|
|
71
|
+
* Records the frame, or queues it until the connection opens.
|
|
72
|
+
*
|
|
73
|
+
* The real `send` queues on `ws$` until a socket exists and returns the
|
|
74
|
+
* subscription holding that queue open; a caller who gives up unsubscribes
|
|
75
|
+
* it and the frame never goes out. Both halves are reproduced, because
|
|
76
|
+
* "records everything regardless of `opened`" is precisely the divergence
|
|
77
|
+
* the hand-rolled double had — it forwarded straight through — and moving it
|
|
78
|
+
* in here would have been the same bug in a better-looking place.
|
|
79
|
+
*/
|
|
80
|
+
send(message: TrueNasMessage): Subscription;
|
|
81
|
+
/**
|
|
82
|
+
* Answer any frame for `method` as soon as it is written.
|
|
83
|
+
*
|
|
84
|
+
* The narrow ancestor of `mockCall`: enough to let a collaborator that talks
|
|
85
|
+
* to the appliance — the authenticator, for one — run its real code against
|
|
86
|
+
* a scripted answer rather than be replaced by a fake of itself.
|
|
87
|
+
*
|
|
88
|
+
* One answer per method, last registration wins. The one it replaced is
|
|
89
|
+
* returned so a caller that needs to share the method can chain to it rather
|
|
90
|
+
* than take it over silently.
|
|
91
|
+
*/
|
|
92
|
+
autoReply(method: string, answer: (frame: TrueNasMessage) => void): ((frame: TrueNasMessage) => void) | undefined;
|
|
93
|
+
private write;
|
|
94
|
+
/** Deliver a message as if middleware had sent it. */
|
|
95
|
+
receive(message: TrueNasMessage): void;
|
|
96
|
+
/**
|
|
97
|
+
* Answer the most recent frame carrying `method`, by its id.
|
|
98
|
+
*
|
|
99
|
+
* Ids are what correlate a reply to a request, and a test that had to read
|
|
100
|
+
* them itself would be asserting on the id generator. Most recent rather
|
|
101
|
+
* than first: a spec that calls the same method twice is asking about the
|
|
102
|
+
* second one.
|
|
103
|
+
*/
|
|
104
|
+
reply(method: string, result: unknown): void;
|
|
105
|
+
/**
|
|
106
|
+
* As {@link reply}, with a JSON-RPC error instead of a result.
|
|
107
|
+
*
|
|
108
|
+
* Typed as the frame `/api/<version>` sends and nothing else: `code` and
|
|
109
|
+
* `message` outside, the TrueNAS payload under `data`. The legacy
|
|
110
|
+
* `/websocket` shape — those payload fields at the top level — is not
|
|
111
|
+
* accepted, so a spec cannot script an error this client can never receive.
|
|
112
|
+
* It was accepted until now only because the parameter was the loose
|
|
113
|
+
* `ApiError` union, which admits both.
|
|
114
|
+
*
|
|
115
|
+
* The cast is gone with it: the frame's own type says this is what an error
|
|
116
|
+
* looks like.
|
|
117
|
+
*/
|
|
118
|
+
replyError(method: string, error: TrueNasErrorFrame): void;
|
|
119
|
+
/**
|
|
120
|
+
* Raise `opened` and flush whatever was queued, as the socket opening would.
|
|
121
|
+
*
|
|
122
|
+
* The queue drains after `opened` goes true, which is the order the real
|
|
123
|
+
* connection produces: `send` is waiting on `ws$`, and `ws$` emits because a
|
|
124
|
+
* socket arrived.
|
|
125
|
+
*/
|
|
126
|
+
simulateOpen(): void;
|
|
127
|
+
/**
|
|
128
|
+
* Lower `opened` and fire `closed`, as a socket closing would.
|
|
129
|
+
*
|
|
130
|
+
* Named for what it simulates rather than overriding `close()`, which on the
|
|
131
|
+
* real connection means "stop, and do not retry" — a different verb that
|
|
132
|
+
* callers already use for teardown.
|
|
133
|
+
*/
|
|
134
|
+
simulateClose(): void;
|
|
135
|
+
/**
|
|
136
|
+
* Terminal, as on the real connection: the streams end and nothing more is
|
|
137
|
+
* written.
|
|
138
|
+
*
|
|
139
|
+
* `simulateClose` is the socket dropping — recoverable, and the client will
|
|
140
|
+
* try again. This is the caller giving up on the connection for good, so the
|
|
141
|
+
* message stream completes and `send` has nowhere to put a frame.
|
|
142
|
+
*/
|
|
143
|
+
close(): void;
|
|
144
|
+
private idOf;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** One recorded login attempt. */
|
|
148
|
+
interface RecordedLogin {
|
|
149
|
+
mechanism: TrueNasAuthMechanism;
|
|
150
|
+
/** The username, token or key the caller passed, whichever the mechanism takes. */
|
|
151
|
+
credential: string;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A `TrueNasAuthenticator` that records its logins and answers them without a
|
|
155
|
+
* socket.
|
|
156
|
+
*
|
|
157
|
+
* Every override records the attempt and then calls the real method, so the
|
|
158
|
+
* real code sends the frame, stores credentials and interprets the response.
|
|
159
|
+
* `succeedNextLogin` / `failNextLogin` only script the connection's reply — a
|
|
160
|
+
* fake that decided success itself would disagree with the real client on
|
|
161
|
+
* what an answer means.
|
|
162
|
+
*/
|
|
163
|
+
declare class FakeAuthenticator extends TrueNasAuthenticator {
|
|
164
|
+
private readonly fakeConnection;
|
|
165
|
+
private readonly attempts;
|
|
166
|
+
/** Armed by the scripting methods; consumed by the next login that is made. */
|
|
167
|
+
private armed;
|
|
168
|
+
/**
|
|
169
|
+
* Answers waiting for their frames, oldest first.
|
|
170
|
+
*
|
|
171
|
+
* Bound when the login is *made*, not when the reply goes out. Reading an
|
|
172
|
+
* armed answer at reply time meant a second login armed in the meantime
|
|
173
|
+
* answered the first — two logins in flight is exactly what testing
|
|
174
|
+
* supersession looks like, so scripting both is the ordinary case, and it
|
|
175
|
+
* produced the same wrong verdict this class was rewritten to stop
|
|
176
|
+
* producing.
|
|
177
|
+
*/
|
|
178
|
+
private readonly pending;
|
|
179
|
+
constructor(fakeConnection: FakeConnection, version?: ApiVersion);
|
|
180
|
+
/**
|
|
181
|
+
* Every login attempt, in order.
|
|
182
|
+
*
|
|
183
|
+
* Including the ones the base class makes itself: the auto-relogin runs
|
|
184
|
+
* through these same methods, and nothing here distinguishes it from a
|
|
185
|
+
* caller's login. A spec counting logins across a reconnect should expect
|
|
186
|
+
* that one.
|
|
187
|
+
*/
|
|
188
|
+
get logins(): readonly RecordedLogin[];
|
|
189
|
+
/** The next login is answered with this response. Defaults to a success. */
|
|
190
|
+
succeedNextLogin(response?: Partial<AuthResponse>): void;
|
|
191
|
+
/**
|
|
192
|
+
* The next login is answered with a rejection.
|
|
193
|
+
*
|
|
194
|
+
* A `response_type` rather than an `Error`, so what the caller sees is
|
|
195
|
+
* whatever the real authenticator makes of it — the `AuthError` codes
|
|
196
|
+
* included, which a hand-thrown `Error` could not produce.
|
|
197
|
+
*/
|
|
198
|
+
failNextLogin(responseType?: AuthResponseType): void;
|
|
199
|
+
/**
|
|
200
|
+
* Records the attempt and binds its answer, in that order, before the real
|
|
201
|
+
* method puts a frame on the wire.
|
|
202
|
+
*/
|
|
203
|
+
private record;
|
|
204
|
+
private success;
|
|
205
|
+
loginWithUserPass(username: string, password: string): Observable<AuthResponse>;
|
|
206
|
+
loginWithToken(token: string): Observable<AuthResponse>;
|
|
207
|
+
loginWithApiKey(credentials: {
|
|
208
|
+
username: string;
|
|
209
|
+
key: string;
|
|
210
|
+
}): Observable<AuthResponse>;
|
|
211
|
+
loginWithOtp(code: string): Observable<AuthResponse>;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Overrides for {@link fakeJob}. `progress` is partial one level down, so a
|
|
216
|
+
* fixture naming a percent need not name the description beside it. Named so
|
|
217
|
+
* a consumer can write a helper that takes one, as the other builders are.
|
|
218
|
+
*/
|
|
219
|
+
interface FakeJobOverrides<R = unknown> extends Partial<Omit<Job<R>, 'progress'>> {
|
|
220
|
+
progress?: Partial<JobProgress>;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* A complete `Job`, so a scripted one is the shape a caller actually reads —
|
|
224
|
+
* a partial fixture cast into place leaves `progress` undefined and breaks
|
|
225
|
+
* `job.progress.percent`.
|
|
226
|
+
*
|
|
227
|
+
* Defaults are what middleware sends for a job not yet started, including
|
|
228
|
+
* `result: null`. `satisfies` (not a cast) makes a regeneration that adds a
|
|
229
|
+
* required field fail here.
|
|
230
|
+
*/
|
|
231
|
+
declare function fakeJob<R = unknown>(overrides?: FakeJobOverrides<R>): Job<R>;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Scripted answers, typed by the directory the client is typed against.
|
|
235
|
+
*
|
|
236
|
+
* Every one of these registers an *answer to a frame*, not a replacement for a
|
|
237
|
+
* verb. The client's own `dispatch`, job correlation and subscription
|
|
238
|
+
* bookkeeping run exactly as they do against an appliance; what changes is only
|
|
239
|
+
* what comes back. That is what makes a spec written against this a test of the
|
|
240
|
+
* client rather than a test of the double.
|
|
241
|
+
*/
|
|
242
|
+
interface MockAnswers<D extends ApiDirectoryShape> {
|
|
243
|
+
/**
|
|
244
|
+
* Answer `method` with `response`, or with whatever the function makes of
|
|
245
|
+
* the params it was called with.
|
|
246
|
+
*
|
|
247
|
+
* Query methods are excluded, and the exclusion is the point rather than a
|
|
248
|
+
* restriction: they live in the call directory, so `mock.call('user.query',
|
|
249
|
+
* 3)` is well-typed — a query method's response is the five-way union the
|
|
250
|
+
* server may return, and a number is one arm — and then answers all three
|
|
251
|
+
* query verbs from that one value. `mock.query` is where a collection is
|
|
252
|
+
* scripted, from rows that feed the three of them consistently.
|
|
253
|
+
*/
|
|
254
|
+
call<M extends Exclude<CallMethod<D>, QueryMethod<D['call']> & string>>(method: M, response: CallResponse<D, M> | ((params: CallParams<D, M>) => CallResponse<D, M>)): void;
|
|
255
|
+
/**
|
|
256
|
+
* Answer a query method from one set of rows.
|
|
257
|
+
*
|
|
258
|
+
* `filters`, `select`, `order_by`, `limit` and `offset` are not applied: the
|
|
259
|
+
* rows given are the rows answered. The frame still carries whatever the
|
|
260
|
+
* caller sent, so a spec asserting on `sent` sees the real request — what is
|
|
261
|
+
* not modelled is middleware's evaluation of it.
|
|
262
|
+
*
|
|
263
|
+
* Feeds all three verbs, because on the wire they are one method
|
|
264
|
+
* distinguished by its options: `queryOne` sends `get: true` and gets the
|
|
265
|
+
* first row, `queryCount` sends `count: true` and gets the row count, and
|
|
266
|
+
* `query` gets the rows. Scripting them separately would let a spec disagree
|
|
267
|
+
* with itself about what the collection holds.
|
|
268
|
+
*/
|
|
269
|
+
query<M extends QueryMethod<D['call']> & string>(method: M, rows: QueryEntity<D['call'], M>[]): void;
|
|
270
|
+
/**
|
|
271
|
+
* Answer a job method with the updates it should report, in order. Nothing
|
|
272
|
+
* is synthesised: a sequence with no terminal state never completes.
|
|
273
|
+
*
|
|
274
|
+
* Each update is completed by {@link fakeJob} from its defaults, not from the
|
|
275
|
+
* previous update — progress does not carry and success is not forced to
|
|
276
|
+
* 100. `arguments` is not filled in from the starting call either, since a
|
|
277
|
+
* job reached through `trackJob` has no such call.
|
|
278
|
+
*
|
|
279
|
+
* Returns the job's id (the first update may set it), for `trackJob`.
|
|
280
|
+
*/
|
|
281
|
+
job<M extends JobMethod<D>>(method: M, updates: JobUpdate<JobResult<D, M>> | JobUpdate<JobResult<D, M>>[]): number;
|
|
282
|
+
/** Push a change to every subscriber of `events(event)`. */
|
|
283
|
+
emit<E extends EventName<D>>(event: E, change: EventUnion<D, E>): void;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* One scripted job update. An alias rather than a second statement of the
|
|
287
|
+
* shape: `job` completes each update through {@link fakeJob}, so the two
|
|
288
|
+
* cannot usefully differ.
|
|
289
|
+
*/
|
|
290
|
+
type JobUpdate<R> = FakeJobOverrides<R>;
|
|
291
|
+
|
|
292
|
+
/** A real client whose collaborators are the fakes, so a test can drive them. */
|
|
293
|
+
type FakeTrueNasClient<D extends ApiDirectoryShape> = TrueNasApiClient<D> & {
|
|
294
|
+
readonly connection: FakeConnection;
|
|
295
|
+
readonly authenticator: FakeAuthenticator;
|
|
296
|
+
/** Scripted answers, typed by this client's directory. */
|
|
297
|
+
readonly mock: MockAnswers<D>;
|
|
298
|
+
};
|
|
299
|
+
/** How a fake client is set up. Every field has a usable default. */
|
|
300
|
+
interface FakeClientOptions<V extends SupportedApiVersion> {
|
|
301
|
+
/** Selects both the client class and the directory the result is typed against. */
|
|
302
|
+
version?: V;
|
|
303
|
+
/**
|
|
304
|
+
* Whether the client starts authenticated. Defaults to `true`.
|
|
305
|
+
*
|
|
306
|
+
* Not because delivery depends on it — `events()` and the job stream filter
|
|
307
|
+
* the message, not the auth state, so a `receive` arrives either way — but
|
|
308
|
+
* because the client only *sends* its `core.subscribe` frames once
|
|
309
|
+
* `authenticated$` is true. A fake that starts unauthenticated has an empty
|
|
310
|
+
* `sent` where a real one would have registered its subscriptions, and a
|
|
311
|
+
* spec asserting on the wire sees a client that never subscribed.
|
|
312
|
+
*/
|
|
313
|
+
authenticated?: boolean;
|
|
314
|
+
/** Whether the connection starts opened. Defaults to `true`. */
|
|
315
|
+
opened?: boolean;
|
|
316
|
+
/**
|
|
317
|
+
* Whether a call nothing is scripted to answer fails with
|
|
318
|
+
* {@link UnmockedCallError} instead of hanging. Defaults to `false`.
|
|
319
|
+
*
|
|
320
|
+
* A spec that scripts every call with `mock` can turn this on and find out
|
|
321
|
+
* which one it missed, by name, instead of watching a promise never settle.
|
|
322
|
+
* A spec that answers frames by hand with `connection.reply` cannot: the
|
|
323
|
+
* answer is registered after the frame goes out, and there is no way to tell
|
|
324
|
+
* that apart from a frame nobody will ever answer. See
|
|
325
|
+
* {@link FakeConnectionOptions.strict}.
|
|
326
|
+
*/
|
|
327
|
+
strict?: boolean;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* A real `TrueNasApiClient` with no socket or version discovery: only the
|
|
331
|
+
* connection and authenticator are fakes, so the real dispatch runs.
|
|
332
|
+
*
|
|
333
|
+
* ```typescript
|
|
334
|
+
* const client = createFakeClient({ version: 'v27.0.0' });
|
|
335
|
+
* client.mock.call('system.info', { hostname: 'truenas.local' });
|
|
336
|
+
* client.api.call('system.info').subscribe(info => …);
|
|
337
|
+
*
|
|
338
|
+
* // Instead of `mock.call`, a spec can answer the sent frame by hand:
|
|
339
|
+
* client.connection.reply('system.info', { hostname: 'truenas.local' });
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
declare function createFakeClient<V extends SupportedApiVersion = SupportedApiVersion>(options?: FakeClientOptions<V>): FakeTrueNasClient<DerivedDirectory<V>>;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Thrown when a strict fake connection is handed a frame nothing will answer.
|
|
346
|
+
*
|
|
347
|
+
* Named rather than a bare `Error` so a consumer's global guard can tell "the
|
|
348
|
+
* spec forgot a mock" from a failure the code under test produced.
|
|
349
|
+
*
|
|
350
|
+
* It cannot travel as an error *frame* — `dispatch` reduces any frame's error
|
|
351
|
+
* to `new Error(getApiErrorMessage(…))` — so it is thrown from `send`. A
|
|
352
|
+
* `TrueNasApi` verb dispatches inside a `defer` and so fails its observable;
|
|
353
|
+
* the authenticator sends from its own body and throws at the call site, which
|
|
354
|
+
* in practice is `logout` and `newApiKey` — the two `FakeAuthenticator` does
|
|
355
|
+
* not auto-reply.
|
|
356
|
+
*/
|
|
357
|
+
declare class UnmockedCallError extends Error {
|
|
358
|
+
readonly method: string;
|
|
359
|
+
readonly params: unknown;
|
|
360
|
+
constructor(method: string, params: unknown);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* What this needs of a client, which is less than a client.
|
|
365
|
+
*
|
|
366
|
+
* Written structurally rather than as `FakeTrueNasClient<D>` so the directory
|
|
367
|
+
* type does not have to be named at the call site, and so a spec holding a
|
|
368
|
+
* client narrowed to its own version still passes one.
|
|
369
|
+
*/
|
|
370
|
+
interface SpyableClient {
|
|
371
|
+
readonly api: object;
|
|
372
|
+
readonly connection: object;
|
|
373
|
+
readonly authenticator: object;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* A test runner's spy factory: `vi.fn` or `jest.fn`.
|
|
377
|
+
*
|
|
378
|
+
* Taken as an argument rather than imported, because this package does not
|
|
379
|
+
* depend on a runner and must not start.
|
|
380
|
+
*
|
|
381
|
+
* **One requirement the type cannot express: the returned function must invoke
|
|
382
|
+
* the implementation with its own `this`.** `vi.fn` and `jest.fn` do; an arrow
|
|
383
|
+
* does not, so `(impl) => vi.fn((...args) => impl(...args))` type-checks and
|
|
384
|
+
* hands every verb `undefined`. `withSpies` detects that and says so.
|
|
385
|
+
*/
|
|
386
|
+
type SpyFactory = <A extends unknown[], R>(implementation: (...args: A) => R) => (...args: A) => R;
|
|
387
|
+
/**
|
|
388
|
+
* Wrap a fake client's verbs in the caller's spies, in place.
|
|
389
|
+
*
|
|
390
|
+
* ```typescript
|
|
391
|
+
* const client = withSpies(createFakeClient({ version: 'v27.0.0' }), vi.fn);
|
|
392
|
+
* expect(client.api.call).toHaveBeenCalledWith('system.info');
|
|
393
|
+
* ```
|
|
394
|
+
*
|
|
395
|
+
* Behaviour is preserved: each spy wraps the real method, which the runner
|
|
396
|
+
* invokes with the `this` of the call, so a spied call still dispatches and
|
|
397
|
+
* still answers from whatever `mock` scripted. Nothing is bound — see
|
|
398
|
+
* `spyOnMethods`. The client is mutated and returned rather than proxied,
|
|
399
|
+
* because a proxy would not be the object its own collaborators hold.
|
|
400
|
+
*/
|
|
401
|
+
declare function withSpies<C extends SpyableClient>(client: C, spy: SpyFactory): C;
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Flat overrides for a nested frame: say what varies, get the real shape.
|
|
405
|
+
*
|
|
406
|
+
* Picked field by field rather than `Partial<TrueNasErrorData>`, which would
|
|
407
|
+
* inherit that type's `[key: string]: unknown` — legitimate on the payload,
|
|
408
|
+
* since middleware adds `py_exception` there, and fatal on an overrides bag,
|
|
409
|
+
* because it turns off excess-property checking. `fakeApiError({ resaon: … })`
|
|
410
|
+
* typechecked, put `resaon` in the payload and left `reason` at its default.
|
|
411
|
+
*/
|
|
412
|
+
interface FakeApiErrorOverrides extends Partial<Pick<TrueNasErrorData, 'error' | 'errname' | 'reason' | 'extra' | 'trace'>> {
|
|
413
|
+
/** JSON-RPC code. Defaults to `-32001`, middleware's "method call error". */
|
|
414
|
+
code?: number;
|
|
415
|
+
/** JSON-RPC message. Defaults to `'Method call error'`, which is what `rpc.py` sends. */
|
|
416
|
+
message?: string;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* The error a method call fails with on `/api/<version>`.
|
|
420
|
+
*
|
|
421
|
+
* The versioned endpoint wraps the TrueNAS payload in a JSON-RPC error —
|
|
422
|
+
* `{code: -32001, message: 'Method call error', data: {…}}` — and that nesting
|
|
423
|
+
* is the part worth a builder. The flat `{error, errname, extra, reason}` is
|
|
424
|
+
* `/websocket`'s, a route this client never opens; `getApiErrorMessage` reads
|
|
425
|
+
* either, so only a consumer branching on `error.code` or `error.data.errname`
|
|
426
|
+
* finds out. Verified at middleware `4303dc8`, `ws_handler/rpc.py:81-124`.
|
|
427
|
+
*
|
|
428
|
+
* `error` and `errname` are the caller's to keep consistent; middleware
|
|
429
|
+
* derives one from the other. `trace` defaults to one whose `class` and `repr`
|
|
430
|
+
* agree with `reason`; pass `null` only for the job-event payload.
|
|
431
|
+
*/
|
|
432
|
+
declare function fakeApiError(overrides?: FakeApiErrorOverrides): TrueNasErrorFrame;
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Overrides for {@link fakeApiVersion}: the fields the parser would have
|
|
436
|
+
* derived, for the versions it will not produce.
|
|
437
|
+
*/
|
|
438
|
+
type FakeApiVersionOverrides = Partial<ApiVersion>;
|
|
439
|
+
/**
|
|
440
|
+
* A parsed `ApiVersion`, built by the parser the client itself uses.
|
|
441
|
+
*
|
|
442
|
+
* Going through `parseApiVersion` is the point: the year/minor/patch split and
|
|
443
|
+
* the websocket path are its rules, and writing them out here would be a
|
|
444
|
+
* second statement free to drift. It returns `ApiVersion | null`, so this
|
|
445
|
+
* throws on a string that does not parse rather than handing back a `null` a
|
|
446
|
+
* spec then carries a non-null assertion for.
|
|
447
|
+
*
|
|
448
|
+
* `overrides` is for versions the parser will not produce — a compatibility
|
|
449
|
+
* spec may want a year this package does not support — and is visible at the
|
|
450
|
+
* call site rather than smuggled in through a string.
|
|
451
|
+
*/
|
|
452
|
+
declare function fakeApiVersion(version?: SupportedApiVersion | (string & {}), overrides?: FakeApiVersionOverrides): ApiVersion;
|
|
453
|
+
|
|
454
|
+
type UserInfo = NonNullable<AuthResponse['user_info']>;
|
|
455
|
+
/**
|
|
456
|
+
* Overrides for {@link fakeAuthResponse}.
|
|
457
|
+
*
|
|
458
|
+
* `user_info` is partial one level down, like `fakeJob`'s `progress`: a spec
|
|
459
|
+
* that wants a different username should not have to restate twenty-odd fields
|
|
460
|
+
* around it. The structures below it — `privilege`, `group`, `attributes` —
|
|
461
|
+
* are replaced whole when given, because a partial `privilege` is a shape that
|
|
462
|
+
* cannot reach a consumer and merging it would invent one.
|
|
463
|
+
*/
|
|
464
|
+
interface FakeAuthResponseOverrides extends Partial<Omit<AuthResponse, 'user_info'>> {
|
|
465
|
+
user_info?: Partial<UserInfo>;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* A complete `AuthResponse`, so a scripted login is the shape a caller reads.
|
|
469
|
+
*
|
|
470
|
+
* The envelope follows `response_type`: `auth.login_ex` is a discriminated
|
|
471
|
+
* union and each arm carries only its own fields, so `AUTH_ERR` comes back as
|
|
472
|
+
* `{ response_type }` rather than a success wearing a failure's label.
|
|
473
|
+
*
|
|
474
|
+
* | `response_type` | fields |
|
|
475
|
+
* |---|---|
|
|
476
|
+
* | `SUCCESS` | `user_info`, `authenticator`, `reconnect_token` |
|
|
477
|
+
* | `OTP_REQUIRED` | `username` |
|
|
478
|
+
* | `REDIRECT` | `urls` |
|
|
479
|
+
* | `AUTH_ERR`, `EXPIRED` | none |
|
|
480
|
+
*/
|
|
481
|
+
declare function fakeAuthResponse(overrides?: FakeAuthResponseOverrides): AuthResponse;
|
|
482
|
+
|
|
483
|
+
export { AuthResponseType, type FakeApiErrorOverrides, type FakeApiVersionOverrides, type FakeAuthResponseOverrides, FakeAuthenticator, type FakeClientOptions, FakeConnection, type FakeConnectionOptions, type FakeJobOverrides, type FakeTrueNasClient, type JobUpdate, type MockAnswers, type RecordedLogin, type SpyFactory, type SpyableClient, TrueNasErrorData, TrueNasErrorFrame, UnmockedCallError, createFakeClient, fakeApiError, fakeApiVersion, fakeAuthResponse, fakeJob, withSpies };
|