@truenas/api-client 6.0.1 → 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.
@@ -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.cjs';
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 };