lambder 11.1.1 → 13.0.1
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/CHANGELOG.md +217 -0
- package/README.md +25 -15
- package/dist/api/LambderApiAnswer.d.ts +7 -0
- package/dist/api/LambderApiDeclarations.d.ts +194 -0
- package/dist/api/LambderApiDeclarations.js +52 -0
- package/dist/api/LambderApiDefinition.d.ts +1 -1
- package/dist/api/LambderApiEnvelope.js +4 -1
- package/dist/api/LambderApiGuards.d.ts +21 -22
- package/dist/api/LambderApiGuards.js +1 -1
- package/dist/api/LambderApiRateLimits.d.ts +19 -19
- package/dist/api/LambderApiRateLimits.js +1 -1
- package/dist/api/LambderApiRefusals.d.ts +22 -0
- package/dist/api/LambderApiRefusals.js +13 -0
- package/dist/api/LambderApiRequest.d.ts +15 -6
- package/dist/api/LambderApiRequest.js +8 -9
- package/dist/build/generateApiFiles.d.ts +60 -0
- package/dist/build/generateApiFiles.js +85 -0
- package/dist/build/generatedTables.d.ts +2 -1
- package/dist/build/generatedTables.js +1 -1
- package/dist/build.d.ts +4 -1
- package/dist/build.js +3 -1
- package/dist/client/LambderCaller.d.ts +36 -23
- package/dist/client/LambderCaller.js +88 -67
- package/dist/client/lambderFetchTransport.js +2 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.js +2 -0
- package/dist/core/Lambder.d.ts +232 -92
- package/dist/core/Lambder.js +400 -103
- package/dist/core/LambderCallSummary.d.ts +43 -0
- package/dist/core/LambderCallSummary.js +10 -0
- package/dist/core/LambderContext.d.ts +15 -2
- package/dist/core/LambderContext.js +32 -9
- package/dist/core/LambderCreateOptions.d.ts +78 -64
- package/dist/core/LambderCreateOptions.js +39 -21
- package/dist/core/LambderInvocationScope.d.ts +25 -0
- package/dist/core/LambderInvocationScope.js +21 -0
- package/dist/core/LambderResponse.d.ts +11 -0
- package/dist/core/LambderResponse.js +12 -0
- package/dist/core/LambderResponseBuilder.js +5 -1
- package/dist/index.d.ts +13 -7
- package/dist/index.js +3 -1
- package/dist/invoke/LambderInvokeCaller.d.ts +48 -31
- package/dist/invoke/LambderInvokeCaller.js +110 -94
- package/dist/invoke/LambderLambdaEvent.d.ts +2 -1
- package/dist/invoke/LambderLambdaEvent.js +8 -3
- package/dist/invoke/lambderHandlerTransport.js +2 -1
- package/dist/mock/LambderMockApp.d.ts +39 -33
- package/dist/mock/LambderMockApp.js +66 -55
- package/dist/mock/LambderMockCreateOptions.d.ts +8 -10
- package/dist/mock/LambderMockTypes.d.ts +44 -7
- package/dist/mock/lambderMockInvokeTransport.d.ts +9 -1
- package/dist/mock/lambderMockInvokeTransport.js +15 -7
- package/dist/mock/lambderMockMswHandler.js +8 -4
- package/dist/mock.d.ts +2 -2
- package/dist/session/LambderSessionManager.d.ts +46 -3
- package/dist/session/LambderSessionManager.js +62 -11
- package/dist/shared/LambderI18n.js +49 -4
- package/dist/shared/contracts/LambderRateLimiter.d.ts +2 -2
- package/dist/shared/contracts/LambderRateLimiter.js +2 -2
- package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
- package/dist/shared/transport/LambderApiTransport.js +2 -3
- package/dist/shared/transport/lambderCookieJarTransport.js +2 -1
- package/dist/shared/util/LambderInvocationScope.d.ts +10 -0
- package/dist/shared/util/LambderInvocationScope.js +5 -0
- package/dist/shared/util/LambderNamedMaps.d.ts +22 -0
- package/dist/shared/util/LambderNamedMaps.js +19 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +1 -1
- package/dist/shared/wire/LambderApiContract.d.ts +4 -18
- package/dist/shared/wire/LambderApiContract.js +1 -1
- package/dist/shared/wire/LambderApiGroupCalls.d.ts +31 -0
- package/dist/shared/wire/LambderApiGroupCalls.js +68 -0
- package/dist/shared/wire/LambderApiNames.d.ts +55 -0
- package/dist/shared/wire/LambderApiNames.js +81 -0
- package/dist/shared/wire/LambderCallOutcome.d.ts +27 -0
- package/dist/shared/wire/LambderCallOutcome.js +47 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +10 -0
- package/dist/shared/wire/LambderInvokeApiId.js +10 -0
- package/dist/shared/wire/LambderUploadSchemas.js +2 -1
- package/dist/stores/LambderDdbRateLimiter.d.ts +24 -16
- package/dist/stores/LambderDdbRateLimiter.js +64 -45
- package/dist/stores/LambderMemoryRateLimiter.d.ts +9 -4
- package/dist/stores/LambderMemoryRateLimiter.js +20 -15
- package/dist/testing/LambderTestApp.d.ts +18 -5
- package/dist/testing/LambderTestApp.js +21 -5
- package/dist/testing/LambderTestVisitor.d.ts +16 -8
- package/dist/testing/LambderTestVisitor.js +33 -25
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.
|
|
|
3
3
|
import { type LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
4
4
|
import { type LambderApiAnswer } from "../api/LambderApiAnswer.js";
|
|
5
5
|
import { type LambderDeclaredRefuse, type LambderRefusalMessage } from "../shared/wire/LambderApiRefusal.js";
|
|
6
|
-
import { type LambderDeclaredVocabulary, type LambderHandlerRefusalsOf, type LambderRefusalVocabulary, type
|
|
6
|
+
import { type LambderDeclaredVocabulary, type LambderHandlerRefusalsOf, type LambderRefusalVocabulary, type LambderRefusalVocabularyOption, type LambderRefusalVocabularyOptionChecks, type LambderMergedRefusalVocabulary } from "../api/LambderApiRefusals.js";
|
|
7
7
|
import { type LambderApiTransport, type LambderApiTransportRequest } from "../shared/transport/LambderApiTransport.js";
|
|
8
8
|
import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
|
|
9
9
|
import { type LambderApiGuard, type LambderGuardBuilder } from "../api/LambderApiGuards.js";
|
|
@@ -13,7 +13,7 @@ import { LambderMemorySessionStore } from "../stores/LambderMemorySessionStore.j
|
|
|
13
13
|
import LambderSessionManager, { type LambderCreatedSession } from "../session/LambderSessionManager.js";
|
|
14
14
|
import type { LambderMockAppOptions, LambderMockIdempotencyOptions, LambderMockTransport, LambderMockTransportOptions } from "./LambderMockCreateOptions.js";
|
|
15
15
|
import type { LambderApiOptionEntry, LambderGuardDeclarationEntry } from "../shared/wire/LambderApiOptionEntries.js";
|
|
16
|
-
import type { LambderMockCallContext, LambderMockCallRecord, LambderMockEntry, LambderMockEntryInput, LambderMockFailure, LambderMockFailureReason, LambderMockHandler, LambderMockLatency, LambderMockListener,
|
|
16
|
+
import type { LambderMockCallContext, LambderMockCallRecord, LambderMockEntry, LambderMockEntryInput, LambderMockFailure, LambderMockFailureReason, LambderMockHandler, LambderMockLatency, LambderMockListener, LambderMockNotMockedInput, LambderMockRateLimitPolicies, LambderMockRegistryCheck, LambderMockRestEntry, LambderMockSessionCallContext, LambderMockSlice, LambderMockOverride, LambderMockRefusalsOf } from "./LambderMockTypes.js";
|
|
17
17
|
/**
|
|
18
18
|
* The mock runtime: the API core (LambderApiPipeline, the same class the
|
|
19
19
|
* Lambda server runs) over memory stores, with typed mock handlers and mock
|
|
@@ -69,6 +69,8 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
69
69
|
private readonly registry;
|
|
70
70
|
/** The server's declared options per API, when create() was given the generated table; the entries' declarations come from here. */
|
|
71
71
|
private readonly apiOptions;
|
|
72
|
+
/** The mock's own guards as create() was given them: which of them need a session, for an entry's mode without the apiOptions table. */
|
|
73
|
+
private readonly mockGuards;
|
|
72
74
|
/** The server's guard declarations, when create() was given the generated table: the refusal codes a declared guard adds to an entry. */
|
|
73
75
|
private readonly guardDeclarations;
|
|
74
76
|
/** The server's refusal vocabulary, when create() was given the generated table: whether each code an entry may refuse with carries data. */
|
|
@@ -93,12 +95,19 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
93
95
|
private assertEntryRegistration;
|
|
94
96
|
/**
|
|
95
97
|
* The table's entry for an endpoint, when create() was given one: null
|
|
96
|
-
* without a table, and a throw for a name the table does not hold
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* the compiler did not see.
|
|
98
|
+
* without a table, and a throw for a name the table does not hold. The
|
|
99
|
+
* compiler already refuses it against the contract; this is where a
|
|
100
|
+
* stale table meets a caller the compiler did not see.
|
|
100
101
|
*/
|
|
101
102
|
private declaredOptionsOf;
|
|
103
|
+
/**
|
|
104
|
+
* An entry's mode, by the server's rule: a session endpoint when one of
|
|
105
|
+
* its guards needs a session. The guards are the apiOptions table's when
|
|
106
|
+
* create() was given one (which records the mode it came to as well), and
|
|
107
|
+
* otherwise the ones the entry restates, each read off the guardDeclarations
|
|
108
|
+
* table or the mock's own guards.
|
|
109
|
+
*/
|
|
110
|
+
private modeOf;
|
|
102
111
|
/**
|
|
103
112
|
* The codes an entry may refuse with, read off the generated tables (its
|
|
104
113
|
* own from apiOptions, its guards' from guardDeclarations), each resolved
|
|
@@ -112,25 +121,23 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
112
121
|
/** The mode the apiOptions table gives a name, or null without a table or for a name it does not hold. */
|
|
113
122
|
private declaredModeOf;
|
|
114
123
|
private buildEntry;
|
|
115
|
-
/** A mock for a public endpoint: a handler, or the handler with the endpoint's declarations restated. */
|
|
116
|
-
publicApi<K extends LambderMockPublicNames<C>, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema, TDerived, TVocabulary, TCodesRequired>): LambderMockEntry<C, K>;
|
|
117
|
-
/** A mock for a session endpoint: the pipeline fetches the session before the handler runs, and refuses without one. */
|
|
118
|
-
sessionApi<K extends LambderMockSessionNames<C>, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema, TDerived, TVocabulary, TCodesRequired>): LambderMockEntry<C, K>;
|
|
119
124
|
/**
|
|
120
|
-
* A
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* so the
|
|
125
|
+
* A mock for an endpoint: a handler, or the handler with the endpoint's
|
|
126
|
+
* declarations restated. Its mode is the server's (see modeOf): a
|
|
127
|
+
* session endpoint's session is fetched before the handler runs, and a
|
|
128
|
+
* call without one is refused as on the server.
|
|
129
|
+
*/
|
|
130
|
+
api<K extends keyof C & string, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema, TDerived, TVocabulary, TCodesRequired>): LambderMockEntry<C, K>;
|
|
131
|
+
/**
|
|
132
|
+
* An endpoint deliberately left without a mock; a call answers the
|
|
133
|
+
* notMocked refusal carrying the reason. The refusal runs through the
|
|
134
|
+
* pipeline, so the steps before dispatch still happen, the session read
|
|
135
|
+
* among them, under the server's mode (see modeOf): a signed-out call to
|
|
136
|
+
* a session endpoint answers sessionExpired as on the server. Without the
|
|
137
|
+
* apiOptions table a session endpoint restates its guards to say so,
|
|
138
|
+
* `notMocked(name, { reason, guards })`; no guard runs.
|
|
130
139
|
*/
|
|
131
|
-
notMocked<K extends
|
|
132
|
-
/** A session endpoint deliberately left without a mock: the session is still read, and refused before the notMocked refusal. */
|
|
133
|
-
sessionNotMocked<K extends LambderMockSessionNames<C>>(name: K, reason: string): LambderMockEntry<C, K>;
|
|
140
|
+
notMocked<K extends keyof C & string>(name: K, input: LambderMockNotMockedInput<C, K, TDerived>): LambderMockEntry<C, K>;
|
|
134
141
|
/**
|
|
135
142
|
* "Everything I did not register is not mocked, for this reason", as an
|
|
136
143
|
* argument to the same register() call:
|
|
@@ -154,11 +161,10 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
154
161
|
* a type), and a call it answers is processed as public: the protocol's
|
|
155
162
|
* pre-pass still runs, so a stale client still hears versionExpired, but
|
|
156
163
|
* a signed-out call to an unmocked session endpoint answers "not mocked"
|
|
157
|
-
* where the server answers sessionExpired.
|
|
158
|
-
*
|
|
164
|
+
* where the server answers sessionExpired. Give create() the table where
|
|
165
|
+
* a test cares about that path.
|
|
159
166
|
*/
|
|
160
167
|
restNotMocked(reason: string): LambderMockRestEntry;
|
|
161
|
-
private buildNotMockedEntry;
|
|
162
168
|
/** The entries of one module as a slice, keyed by name. Two entries for one endpoint is an error here. */
|
|
163
169
|
apiSlice<const E extends readonly LambderMockEntry<C, keyof C & string>[]>(...entries: E): LambderMockSlice<C, E[number]["name"]>;
|
|
164
170
|
private addSlices;
|
|
@@ -343,16 +349,16 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
|
|
|
343
349
|
*/
|
|
344
350
|
export declare const initLambderMock: <C extends LambderApiContractShape, S = any>() => {
|
|
345
351
|
/**
|
|
346
|
-
* Declares the server's refusal vocabulary on the mock: the same
|
|
347
|
-
* and the same `requireCodes`, the server's init declares,
|
|
348
|
-
* shared code (codes, zod schemas, statuses and flags hold
|
|
349
|
-
* secret). Given, every refusal an entry answers with is checked and sent
|
|
352
|
+
* Declares the server's refusal vocabulary on the mock: the same map, or
|
|
353
|
+
* list of maps, and the same `requireCodes`, the server's init declares,
|
|
354
|
+
* imported from shared code (codes, zod schemas, statuses and flags hold
|
|
355
|
+
* nothing secret). Given, every refusal an entry answers with is checked and sent
|
|
350
356
|
* as the server would send it: the code among the codes the tables give
|
|
351
357
|
* the entry, its data parsed through the code's schema from the input
|
|
352
358
|
* form, and the declaration's status and flag. An entry whose tables name
|
|
353
359
|
* a code needs it.
|
|
354
360
|
*/
|
|
355
|
-
declareRefusals<const TVocabulary extends
|
|
361
|
+
declareRefusals<const TVocabulary extends LambderRefusalVocabularyOption, const TRequireCodes extends boolean = false>(refusals: TVocabulary & LambderRefusalVocabularyOptionChecks<TVocabulary>, options?: {
|
|
356
362
|
requireCodes?: TRequireCodes;
|
|
357
363
|
}): {
|
|
358
364
|
/** Builds a mock guard: the server guard's shape, the handler seeing the mock's contexts. */
|
|
@@ -370,7 +376,7 @@ export declare const initLambderMock: <C extends LambderApiContractShape, S = an
|
|
|
370
376
|
* codes and `data` the schema's input form, parsed as the server parses
|
|
371
377
|
* it. The free refuse() where the mock declared none.
|
|
372
378
|
*/
|
|
373
|
-
refuse: [TVocabulary] extends [never] ? (content: string, options?: import("../shared/wire/LambderApiRefusal.js").LambderRefuseOptions) => never : LambderDeclaredRefuse<LambderHandlerRefusalsOf<TVocabulary
|
|
379
|
+
refuse: [LambderMergedRefusalVocabulary<TVocabulary>] extends [never] ? (content: string, options?: import("../shared/wire/LambderApiRefusal.js").LambderRefuseOptions) => never : LambderDeclaredRefuse<LambderHandlerRefusalsOf<LambderMergedRefusalVocabulary<TVocabulary>, keyof LambderMergedRefusalVocabulary<TVocabulary> & string>, TRequireCodes>;
|
|
374
380
|
/**
|
|
375
381
|
* The mock app, with the guard map and the rate-limit policies inferred
|
|
376
382
|
* from the options.
|
|
@@ -382,7 +388,7 @@ export declare const initLambderMock: <C extends LambderApiContractShape, S = an
|
|
|
382
388
|
* dropped. `I` exists for the same reason `P` does, and
|
|
383
389
|
* LambderMockSurplusKeys puts the error back on the key.
|
|
384
390
|
*/
|
|
385
|
-
create<const G extends Record<string, LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>> = {}, const P extends LambderMockRateLimitPolicies<S> = {}, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>, const D extends Record<string, LambderGuardDeclarationEntry> = {}, const A extends Record<string, LambderApiOptionEntry> | undefined = undefined>(options: LambderMockAppOptions<C, S, G, P, I, D, A>): LambderMockApp<C, S, G, [A] extends [undefined] ? false : true, TVocabulary
|
|
391
|
+
create<const G extends Record<string, LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>> = {}, const P extends LambderMockRateLimitPolicies<S> = {}, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>, const D extends Record<string, LambderGuardDeclarationEntry> = {}, const A extends Record<string, LambderApiOptionEntry> | undefined = undefined>(options: LambderMockAppOptions<C, S, G, P, I, D, A>): LambderMockApp<C, S, G, [A] extends [undefined] ? false : true, LambderMergedRefusalVocabulary<TVocabulary>, TRequireCodes>;
|
|
386
392
|
};
|
|
387
393
|
/** Builds a mock guard: the server guard's shape, the handler seeing the mock's contexts. */
|
|
388
394
|
guard: LambderGuardBuilder<LambderMockCallContext<S>, LambderMockSessionCallContext<S>>;
|
|
@@ -7,7 +7,7 @@ import { envelopeAnswer, crashAnswer, plainRefusalEnvelope, refusalEnvelope, suc
|
|
|
7
7
|
import { LambderApiOutputValidationError } from "../api/LambderApiOutputValidationError.js";
|
|
8
8
|
import { describePayloadKind, isObjectPayload } from "../shared/wire/LambderObjectPayload.js";
|
|
9
9
|
import { LambderApiRefusal, LAMBDER_REFUSAL_CODES, refuse as refuseApiCall } from "../shared/wire/LambderApiRefusal.js";
|
|
10
|
-
import { allowedRefusalOf,
|
|
10
|
+
import { allowedRefusalOf, declaredRefusalVocabulary, toRefusalCodes, } from "../api/LambderApiRefusals.js";
|
|
11
11
|
import { coerceToError } from "../shared/wire/LambderCrashDetail.js";
|
|
12
12
|
import { buildTransportEnvelope } from "../shared/transport/LambderApiTransport.js";
|
|
13
13
|
import { lambderCookieJarTransport } from "../shared/transport/lambderCookieJarTransport.js";
|
|
@@ -93,6 +93,8 @@ export class LambderMockApp {
|
|
|
93
93
|
registry = new LambderMockEntryRegistry();
|
|
94
94
|
/** The server's declared options per API, when create() was given the generated table; the entries' declarations come from here. */
|
|
95
95
|
apiOptions;
|
|
96
|
+
/** The mock's own guards as create() was given them: which of them need a session, for an entry's mode without the apiOptions table. */
|
|
97
|
+
mockGuards;
|
|
96
98
|
/** The server's guard declarations, when create() was given the generated table: the refusal codes a declared guard adds to an entry. */
|
|
97
99
|
guardDeclarations;
|
|
98
100
|
/** The server's refusal vocabulary, when create() was given the generated table: whether each code an entry may refuse with carries data. */
|
|
@@ -106,6 +108,7 @@ export class LambderMockApp {
|
|
|
106
108
|
declared = null) {
|
|
107
109
|
this.apiVersion = options.apiVersion ?? null;
|
|
108
110
|
this.apiOptions = options.apiOptions ?? null;
|
|
111
|
+
this.mockGuards = options.guards ?? null;
|
|
109
112
|
this.guardDeclarations = options.guardDeclarations ?? null;
|
|
110
113
|
this.refusalVocabulary = declared?.vocabulary ?? null;
|
|
111
114
|
this.requireRefusalCodes = declared?.requireCodes ?? false;
|
|
@@ -160,7 +163,9 @@ export class LambderMockApp {
|
|
|
160
163
|
sessionSalt: sessionOptions.sessionSalt ?? "lambder-mock",
|
|
161
164
|
enableSlidingExpiration: sessionOptions.enableSlidingExpiration,
|
|
162
165
|
slidingWriteIntervalSeconds: sessionOptions.slidingWriteIntervalSeconds,
|
|
163
|
-
|
|
166
|
+
// As given: the manager refuses dataRefresh without
|
|
167
|
+
// the schema, which an untyped caller can still pass.
|
|
168
|
+
...{ dataRefresh: sessionOptions.dataRefresh, dataSchema: sessionOptions.dataSchema },
|
|
164
169
|
// A plain-http page (device testing on a LAN) has no
|
|
165
170
|
// crypto.subtle, and a memory-only store is nothing
|
|
166
171
|
// anyone can leak, so hashing there protects nothing.
|
|
@@ -220,23 +225,33 @@ export class LambderMockApp {
|
|
|
220
225
|
}
|
|
221
226
|
/**
|
|
222
227
|
* The table's entry for an endpoint, when create() was given one: null
|
|
223
|
-
* without a table, and a throw for a name the table does not hold
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* the compiler did not see.
|
|
228
|
+
* without a table, and a throw for a name the table does not hold. The
|
|
229
|
+
* compiler already refuses it against the contract; this is where a
|
|
230
|
+
* stale table meets a caller the compiler did not see.
|
|
227
231
|
*/
|
|
228
|
-
declaredOptionsOf(name
|
|
232
|
+
declaredOptionsOf(name) {
|
|
229
233
|
if (!this.apiOptions)
|
|
230
234
|
return null;
|
|
231
235
|
const declared = Object.prototype.hasOwnProperty.call(this.apiOptions, name) ? this.apiOptions[name] : undefined;
|
|
232
236
|
if (!declared) {
|
|
233
237
|
throw new Error(`LambderMockApp: "${name}" has no entry in the apiOptions table given to create(). The table predates this endpoint: regenerate it with writeApiOptions.`);
|
|
234
238
|
}
|
|
235
|
-
if (declared.mode !== mode) {
|
|
236
|
-
throw new Error(`LambderMockApp: "${name}" is a ${declared.mode} endpoint in the apiOptions table, registered here as a ${mode} one.`);
|
|
237
|
-
}
|
|
238
239
|
return declared;
|
|
239
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* An entry's mode, by the server's rule: a session endpoint when one of
|
|
243
|
+
* its guards needs a session. The guards are the apiOptions table's when
|
|
244
|
+
* create() was given one (which records the mode it came to as well), and
|
|
245
|
+
* otherwise the ones the entry restates, each read off the guardDeclarations
|
|
246
|
+
* table or the mock's own guards.
|
|
247
|
+
*/
|
|
248
|
+
modeOf(declared, restatedGuards) {
|
|
249
|
+
if (declared)
|
|
250
|
+
return declared.mode;
|
|
251
|
+
const needsSession = (guard) => (this.guardDeclarations && Object.prototype.hasOwnProperty.call(this.guardDeclarations, guard) && this.guardDeclarations[guard].session === true)
|
|
252
|
+
|| (this.mockGuards && Object.prototype.hasOwnProperty.call(this.mockGuards, guard) && this.mockGuards[guard].session === true);
|
|
253
|
+
return toGuardEntries(restatedGuards).some(({ name: guard }) => needsSession(guard)) ? "session" : "public";
|
|
254
|
+
}
|
|
240
255
|
/**
|
|
241
256
|
* The codes an entry may refuse with, read off the generated tables (its
|
|
242
257
|
* own from apiOptions, its guards' from guardDeclarations), each resolved
|
|
@@ -276,9 +291,10 @@ export class LambderMockApp {
|
|
|
276
291
|
return null;
|
|
277
292
|
return this.apiOptions[name].mode;
|
|
278
293
|
}
|
|
279
|
-
buildEntry(name,
|
|
294
|
+
buildEntry(name, input) {
|
|
280
295
|
const options = (typeof input === "function" ? { handler: input } : input);
|
|
281
|
-
const declared = this.declaredOptionsOf(name
|
|
296
|
+
const declared = this.declaredOptionsOf(name);
|
|
297
|
+
const mode = this.modeOf(declared, options.guards);
|
|
282
298
|
if (declared) {
|
|
283
299
|
// A caller the compiler did not see (a JavaScript slice, a cast)
|
|
284
300
|
// would otherwise have its restatement silently lose to the table.
|
|
@@ -306,32 +322,34 @@ export class LambderMockApp {
|
|
|
306
322
|
const handler = options.handler;
|
|
307
323
|
return { name, mode, definition, handler: async (ctx) => await handler(ctx), notMockedReason: null };
|
|
308
324
|
}
|
|
309
|
-
/** A mock for a public endpoint: a handler, or the handler with the endpoint's declarations restated. */
|
|
310
|
-
publicApi(name, entry) {
|
|
311
|
-
return this.buildEntry(name, "public", entry);
|
|
312
|
-
}
|
|
313
|
-
/** A mock for a session endpoint: the pipeline fetches the session before the handler runs, and refuses without one. */
|
|
314
|
-
sessionApi(name, entry) {
|
|
315
|
-
return this.buildEntry(name, "session", entry);
|
|
316
|
-
}
|
|
317
325
|
/**
|
|
318
|
-
* A
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
* do, because the refusal runs through the pipeline so the steps BEFORE
|
|
323
|
-
* dispatch still happen, and the session read is one of them. Were every
|
|
324
|
-
* not-mocked endpoint public, a session endpoint with no session would
|
|
325
|
-
* answer "not mocked" where the server answers sessionExpired, and its
|
|
326
|
-
* events and call log would carry the wrong mode. The contract is a type,
|
|
327
|
-
* so the mode cannot be recovered at runtime: the builder has to say it.
|
|
326
|
+
* A mock for an endpoint: a handler, or the handler with the endpoint's
|
|
327
|
+
* declarations restated. Its mode is the server's (see modeOf): a
|
|
328
|
+
* session endpoint's session is fetched before the handler runs, and a
|
|
329
|
+
* call without one is refused as on the server.
|
|
328
330
|
*/
|
|
329
|
-
|
|
330
|
-
return this.
|
|
331
|
+
api(name, entry) {
|
|
332
|
+
return this.buildEntry(name, entry);
|
|
331
333
|
}
|
|
332
|
-
/**
|
|
333
|
-
|
|
334
|
-
|
|
334
|
+
/**
|
|
335
|
+
* An endpoint deliberately left without a mock; a call answers the
|
|
336
|
+
* notMocked refusal carrying the reason. The refusal runs through the
|
|
337
|
+
* pipeline, so the steps before dispatch still happen, the session read
|
|
338
|
+
* among them, under the server's mode (see modeOf): a signed-out call to
|
|
339
|
+
* a session endpoint answers sessionExpired as on the server. Without the
|
|
340
|
+
* apiOptions table a session endpoint restates its guards to say so,
|
|
341
|
+
* `notMocked(name, { reason, guards })`; no guard runs.
|
|
342
|
+
*/
|
|
343
|
+
notMocked(name, input) {
|
|
344
|
+
const { reason, guards } = (typeof input === "string" ? { reason: input } : input);
|
|
345
|
+
const declared = this.declaredOptionsOf(name);
|
|
346
|
+
if (declared && guards !== undefined) {
|
|
347
|
+
throw new Error(`LambderMockApp: "${name}" restates its guards option, which the apiOptions table given to create() already declares. Leave it out of the entry.`);
|
|
348
|
+
}
|
|
349
|
+
const mode = this.modeOf(declared, guards);
|
|
350
|
+
const definition = { name, mode };
|
|
351
|
+
this.assertEntryRegistration(definition);
|
|
352
|
+
return { name, mode, definition, handler: null, notMockedReason: reason };
|
|
335
353
|
}
|
|
336
354
|
/**
|
|
337
355
|
* "Everything I did not register is not mocked, for this reason", as an
|
|
@@ -356,18 +374,12 @@ export class LambderMockApp {
|
|
|
356
374
|
* a type), and a call it answers is processed as public: the protocol's
|
|
357
375
|
* pre-pass still runs, so a stale client still hears versionExpired, but
|
|
358
376
|
* a signed-out call to an unmocked session endpoint answers "not mocked"
|
|
359
|
-
* where the server answers sessionExpired.
|
|
360
|
-
*
|
|
377
|
+
* where the server answers sessionExpired. Give create() the table where
|
|
378
|
+
* a test cares about that path.
|
|
361
379
|
*/
|
|
362
380
|
restNotMocked(reason) {
|
|
363
381
|
return { restNotMockedReason: reason };
|
|
364
382
|
}
|
|
365
|
-
buildNotMockedEntry(name, mode, reason) {
|
|
366
|
-
this.declaredOptionsOf(name, mode);
|
|
367
|
-
const definition = { name, mode };
|
|
368
|
-
this.assertEntryRegistration(definition);
|
|
369
|
-
return { name, mode, definition, handler: null, notMockedReason: reason };
|
|
370
|
-
}
|
|
371
383
|
/** The entries of one module as a slice, keyed by name. Two entries for one endpoint is an error here. */
|
|
372
384
|
apiSlice(...entries) {
|
|
373
385
|
const slice = {};
|
|
@@ -809,10 +821,7 @@ export class LambderMockApp {
|
|
|
809
821
|
// the "saved" record with no call), a Date would stay a Date and a
|
|
810
822
|
// key set to undefined keep its place, none of which a server sees.
|
|
811
823
|
const envelope = JSON.parse(JSON.stringify(buildTransportEnvelope(transportRequest)));
|
|
812
|
-
|
|
813
|
-
if (!request)
|
|
814
|
-
throw new Error("LambderMockApp: the transport request names no api.");
|
|
815
|
-
return request;
|
|
824
|
+
return readApiEnvelope(envelope, info, transportRequest.apiName);
|
|
816
825
|
}
|
|
817
826
|
/** One call from a transport request to its answer: what the mock transport and the adapters call. */
|
|
818
827
|
async handle(transportRequest) {
|
|
@@ -950,19 +959,21 @@ const lambderMockInitOf = (declared) => ({
|
|
|
950
959
|
export const initLambderMock = () => ({
|
|
951
960
|
...lambderMockInitOf(null),
|
|
952
961
|
/**
|
|
953
|
-
* Declares the server's refusal vocabulary on the mock: the same
|
|
954
|
-
* and the same `requireCodes`, the server's init declares,
|
|
955
|
-
* shared code (codes, zod schemas, statuses and flags hold
|
|
956
|
-
* secret). Given, every refusal an entry answers with is checked and sent
|
|
962
|
+
* Declares the server's refusal vocabulary on the mock: the same map, or
|
|
963
|
+
* list of maps, and the same `requireCodes`, the server's init declares,
|
|
964
|
+
* imported from shared code (codes, zod schemas, statuses and flags hold
|
|
965
|
+
* nothing secret). Given, every refusal an entry answers with is checked and sent
|
|
957
966
|
* as the server would send it: the code among the codes the tables give
|
|
958
967
|
* the entry, its data parsed through the code's schema from the input
|
|
959
968
|
* form, and the declaration's status and flag. An entry whose tables name
|
|
960
969
|
* a code needs it.
|
|
961
970
|
*/
|
|
962
971
|
declareRefusals(refusals, options = {}) {
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
972
|
+
const declared = declaredRefusalVocabulary(refusals, "LambderMockApp");
|
|
973
|
+
return lambderMockInitOf({
|
|
974
|
+
refusals: declared.refusals,
|
|
975
|
+
vocabulary: declared.vocabulary,
|
|
976
|
+
requireCodes: (options.requireCodes ?? false),
|
|
977
|
+
});
|
|
967
978
|
},
|
|
968
979
|
});
|
|
@@ -13,10 +13,10 @@ import type { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
|
|
|
13
13
|
import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
|
|
14
14
|
import type { LambderRateLimiter, LambderRateLimitWindow } from "../shared/contracts/LambderRateLimiter.js";
|
|
15
15
|
import type { LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
|
|
16
|
-
import type {
|
|
16
|
+
import type { LambderSessionDataOptions } from "../session/LambderSessionManager.js";
|
|
17
17
|
import type { LambderSessionCookieOptions } from "../session/LambderSessionController.js";
|
|
18
18
|
import type { LambderSessionCrypto } from "../session/LambderSessionCrypto.js";
|
|
19
|
-
import type { LambderMockCallContext, LambderMockGuards, LambderMockLatency, LambderMockRateLimitPolicies, LambderMockSessionCallContext, LambderMockSurplusKeys } from "./LambderMockTypes.js";
|
|
19
|
+
import type { LambderMockCallContext, LambderMockGuards, LambderMockLatency, LambderMockRateLimitPolicies, LambderMockSessionCallContext, LambderMockSessionGuardsCheck, LambderMockSurplusKeys } from "./LambderMockTypes.js";
|
|
20
20
|
export type LambderMockSessionsOptions<S> = {
|
|
21
21
|
/** Where the mock's sessions rest. Default: a fresh LambderMemorySessionStore. */
|
|
22
22
|
store?: LambderSessionStore<S>;
|
|
@@ -26,13 +26,12 @@ export type LambderMockSessionsOptions<S> = {
|
|
|
26
26
|
ttlSeconds?: number;
|
|
27
27
|
/** Hashing and randomness. Default: WebCrypto where the runtime offers it, the plain stand-in over a memory-only store otherwise. */
|
|
28
28
|
crypto?: LambderSessionCrypto;
|
|
29
|
-
dataRefresh?: LambderSessionDataRefreshConfig<S>;
|
|
30
29
|
enableSlidingExpiration?: boolean;
|
|
31
30
|
slidingWriteIntervalSeconds?: number;
|
|
32
31
|
tokenCookieKey?: string;
|
|
33
32
|
csrfCookieKey?: string;
|
|
34
33
|
cookieOptions?: LambderSessionCookieOptions;
|
|
35
|
-
}
|
|
34
|
+
} & LambderSessionDataOptions<S>;
|
|
36
35
|
/**
|
|
37
36
|
* Idempotency in the mock: the same engine the server runs, over a memory
|
|
38
37
|
* store unless one is given, and carrying every knob the server's own option
|
|
@@ -158,9 +157,9 @@ type LambderMockGuardsAgree<G, D> = {
|
|
|
158
157
|
type LambderMockGuardsOption<C, S, G, D> = [
|
|
159
158
|
LambderContractGuardNames<C>
|
|
160
159
|
] extends [never] ? {
|
|
161
|
-
guards?: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D
|
|
160
|
+
guards?: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>> & LambderMockSessionGuardsCheck<C, G>;
|
|
162
161
|
} : {
|
|
163
|
-
guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D
|
|
162
|
+
guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>> & LambderMockSessionGuardsCheck<C, G>;
|
|
164
163
|
};
|
|
165
164
|
/**
|
|
166
165
|
* The sessions, idempotency and rateLimits options: each required whenever
|
|
@@ -225,10 +224,9 @@ export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicie
|
|
|
225
224
|
* exports them (`apiOptions`). Given, every entry's guards, rateLimit and
|
|
226
225
|
* idempotency are read off the table rather than restated: an entry is
|
|
227
226
|
* its handler (and an input schema, if it has one), a restated option is
|
|
228
|
-
* a compile error, and
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* mocks still reads the session first. The table has to cover the
|
|
227
|
+
* a compile error, and the entry takes the table's mode. A notMocked
|
|
228
|
+
* entry and a restNotMocked answer read the endpoint's mode off the table
|
|
229
|
+
* too, so a session endpoint nothing mocks still reads the session first. The table has to cover the
|
|
232
230
|
* contract (see LambderMockApiOptionsCover).
|
|
233
231
|
*/
|
|
234
232
|
apiOptions?: A & LambderMockApiOptionsCover<C>;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the contract stays a type-only import in the consuming app.
|
|
7
7
|
*/
|
|
8
8
|
import type { z } from "zod";
|
|
9
|
-
import type { LambderApiMode, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractGuardInputsOf, LambderContractGuardNames, LambderContractGuardsOf, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractMode, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
|
|
9
|
+
import type { LambderApiMode, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractGuardInputsOf, LambderContractGuardNames, LambderContractGuardsOf, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractMode, LambderContractRateLimitOf, LambderGuardNamesIn } from "../shared/wire/LambderApiContract.js";
|
|
10
10
|
import type { LambderApiGuard, LambderGuardDataOf, LambderGuardMetaMap } from "../api/LambderApiGuards.js";
|
|
11
11
|
import type { LambderApiRateLimitPolicyConfig, LambderContextRateLimit, LambderContextRateLimitCheck } from "../api/LambderApiRateLimits.js";
|
|
12
12
|
import type { LambderApiCallContext, LambderResponseTools } from "../api/LambderApiCallContext.js";
|
|
@@ -105,8 +105,9 @@ export type LambderMockContext<C, K extends keyof C, S, G, TVocabulary = never,
|
|
|
105
105
|
* the contract declares, plus, for each guardInput-mode guard, a `guardInput`
|
|
106
106
|
* schema whose input is what the contract says a client sends. A guard a
|
|
107
107
|
* public endpoint names may not require a session. A missing name, a schema
|
|
108
|
-
* that parses to something else, or
|
|
109
|
-
* `guards` option
|
|
108
|
+
* that parses to something else, or a public endpoint's guard that requires
|
|
109
|
+
* a session fails at the `guards` option; LambderMockSessionGuardsCheck
|
|
110
|
+
* holds the session endpoints to the same rule from the other side.
|
|
110
111
|
*/
|
|
111
112
|
export type LambderMockGuards<C, S = any> = {
|
|
112
113
|
[N in LambderContractGuardNames<C>]: LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>;
|
|
@@ -119,6 +120,33 @@ export type LambderMockGuards<C, S = any> = {
|
|
|
119
120
|
session?: false;
|
|
120
121
|
};
|
|
121
122
|
};
|
|
123
|
+
/** The guards of a mock guard map that need a session. */
|
|
124
|
+
type LambderMockSessionGuardNames<G> = {
|
|
125
|
+
[N in keyof G]: G[N] extends {
|
|
126
|
+
session: true;
|
|
127
|
+
} ? N : never;
|
|
128
|
+
}[keyof G];
|
|
129
|
+
/** The contract's session endpoints none of whose guards the mock guard map declares `session: true`. */
|
|
130
|
+
type LambderMockSessionEndpointsWithoutSessionGuard<C, G> = {
|
|
131
|
+
[K in LambderContractKeysWithMode<C, "session">]: [
|
|
132
|
+
Extract<LambderGuardNamesIn<LambderContractGuardsOf<C, K>>, LambderMockSessionGuardNames<G>>
|
|
133
|
+
] extends [never] ? K : never;
|
|
134
|
+
}[LambderContractKeysWithMode<C, "session">];
|
|
135
|
+
/**
|
|
136
|
+
* The mock guard map held to the server's rule for session endpoints: each
|
|
137
|
+
* names at least one guard that needs a session. The guards are what make
|
|
138
|
+
* an endpoint a session one, on the server and in the mock alike, and
|
|
139
|
+
* without the apiOptions table the mock reads an entry's mode off the guards
|
|
140
|
+
* it restates, so a session endpoint none of whose mock guards says
|
|
141
|
+
* `session: true` would run as public: a signed-out call would reach a
|
|
142
|
+
* handler typed with its session present. The property name is the message
|
|
143
|
+
* and its value the endpoints at fault.
|
|
144
|
+
*/
|
|
145
|
+
export type LambderMockSessionGuardsCheck<C, G> = [
|
|
146
|
+
LambderMockSessionEndpointsWithoutSessionGuard<C, G>
|
|
147
|
+
] extends [never] ? unknown : {
|
|
148
|
+
readonly "lambder: these session endpoints name no mock guard declared session: true, so the mock would run them as public": LambderMockSessionEndpointsWithoutSessionGuard<C, G>;
|
|
149
|
+
};
|
|
122
150
|
export type LambderMockHandler<C, K extends keyof C, S, G, TVocabulary = never, TCodesRequired extends boolean = false> = (ctx: LambderMockContext<C, K, S, G, TVocabulary, TCodesRequired>) => LambderMockOutputOf<C, K> | Promise<LambderMockOutputOf<C, K>>;
|
|
123
151
|
/**
|
|
124
152
|
* The guards field of an entry: required whenever the contract declares any
|
|
@@ -230,7 +258,7 @@ export type LambderMockEntryOptions<C, K extends keyof C, S, G, TInputSchema ext
|
|
|
230
258
|
handler: LambderMockHandler<C, K, S, G, TVocabulary, TCodesRequired>;
|
|
231
259
|
};
|
|
232
260
|
/**
|
|
233
|
-
* What
|
|
261
|
+
* What api() accepts. On a mock created with the generated
|
|
234
262
|
* `apiOptions` table (`TDerived`), a bare handler or the options without the
|
|
235
263
|
* declarations, for every endpoint. Otherwise a bare handler only for an
|
|
236
264
|
* endpoint the contract declares nothing for, the full options elsewhere, so
|
|
@@ -240,6 +268,18 @@ export type LambderMockEntryOptions<C, K extends keyof C, S, G, TInputSchema ext
|
|
|
240
268
|
* bare form.
|
|
241
269
|
*/
|
|
242
270
|
export type LambderMockEntryInput<C, K extends keyof C, S, G, TInputSchema extends z.ZodType = z.ZodType, TDerived extends boolean = false, TVocabulary = never, TCodesRequired extends boolean = false> = TDerived extends true ? LambderMockHandler<C, K, S, G, TVocabulary, TCodesRequired> | LambderMockEntryOptions<C, K, S, G, TInputSchema, true, TVocabulary, TCodesRequired> : [LambderContractGuardsOf<C, K> | LambderContractRateLimitOf<C, K> | LambderContractIdempotencyOf<C, K>] extends [never] ? LambderMockHandler<C, K, S, G, TVocabulary, TCodesRequired> | LambderMockEntryOptions<C, K, S, G, TInputSchema, false, TVocabulary, TCodesRequired> : LambderMockEntryOptions<C, K, S, G, TInputSchema, false, TVocabulary, TCodesRequired>;
|
|
271
|
+
/**
|
|
272
|
+
* What notMocked() accepts: the reason, or, for a session endpoint on a mock
|
|
273
|
+
* created without the generated `apiOptions` table, the reason with the
|
|
274
|
+
* endpoint's guards restated. The entry answers before any guard runs, but
|
|
275
|
+
* its mode is still the server's (the session read before the notMocked
|
|
276
|
+
* refusal), and without the table only the guards can say it, as they do
|
|
277
|
+
* for api().
|
|
278
|
+
*/
|
|
279
|
+
export type LambderMockNotMockedInput<C, K extends keyof C, TDerived extends boolean = false> = TDerived extends true ? string : LambderContractMode<C, K> extends "session" ? {
|
|
280
|
+
reason: string;
|
|
281
|
+
guards: LambderContractGuardsOf<C, K>;
|
|
282
|
+
} : string;
|
|
243
283
|
/** One registry entry: the endpoint's definition as the pipeline runs it, and its handler (null when registered as not mocked). */
|
|
244
284
|
export type LambderMockEntry<C, K extends keyof C & string> = {
|
|
245
285
|
readonly name: K;
|
|
@@ -248,9 +288,6 @@ export type LambderMockEntry<C, K extends keyof C & string> = {
|
|
|
248
288
|
readonly handler: ((ctx: any) => Promise<unknown>) | null;
|
|
249
289
|
readonly notMockedReason: string | null;
|
|
250
290
|
};
|
|
251
|
-
/** The endpoint names a mock app may register under each mode. */
|
|
252
|
-
export type LambderMockPublicNames<C> = LambderContractKeysWithMode<C, "public">;
|
|
253
|
-
export type LambderMockSessionNames<C> = LambderContractKeysWithMode<C, "session">;
|
|
254
291
|
/** A slice: the entries of one module, keyed by endpoint name. */
|
|
255
292
|
export type LambderMockSlice<C, K extends keyof C & string> = {
|
|
256
293
|
readonly [P in K]: LambderMockEntry<C, P>;
|
|
@@ -12,17 +12,25 @@ import { type LambderApiRequest } from "../api/LambderApiRequest.js";
|
|
|
12
12
|
* returned function still fits LambderInvokeTransport.
|
|
13
13
|
*/
|
|
14
14
|
export type LambderMockInvokeEvent = {
|
|
15
|
+
/** The path the call was made to, `{apiPath}/{group}/{action}`. */
|
|
16
|
+
rawPath?: string | undefined;
|
|
15
17
|
body?: string | undefined;
|
|
16
18
|
isBase64Encoded?: boolean | undefined;
|
|
17
19
|
headers?: Record<string, string | undefined> | undefined;
|
|
18
20
|
cookies?: string[] | undefined;
|
|
19
21
|
requestContext?: {
|
|
20
22
|
http?: {
|
|
23
|
+
method?: string;
|
|
21
24
|
sourceIp?: string;
|
|
22
25
|
} | undefined;
|
|
23
26
|
domainName?: string;
|
|
24
27
|
} | undefined;
|
|
25
28
|
};
|
|
29
|
+
/** The options of lambderMockInvokeTransport. */
|
|
30
|
+
export type LambderMockInvokeTransportOptions = {
|
|
31
|
+
/** The apiPath the mocked function serves its calls under, as its LambderInvokeCaller names it. Default: "/api", as there. */
|
|
32
|
+
apiPath?: string;
|
|
33
|
+
};
|
|
26
34
|
/** What the callee answers with: the Lambda response object LambderInvokeCaller decodes. */
|
|
27
35
|
export type LambderMockInvokeResult = {
|
|
28
36
|
functionError: string | null;
|
|
@@ -43,6 +51,6 @@ export type LambderMockInvokeResult = {
|
|
|
43
51
|
*/
|
|
44
52
|
export declare const lambderMockInvokeTransport: (mockApp: {
|
|
45
53
|
handleRequest(request: LambderApiRequest): Promise<LambderApiAnswer>;
|
|
46
|
-
}) => ((event: LambderMockInvokeEvent, options: {
|
|
54
|
+
}, options?: LambderMockInvokeTransportOptions) => ((event: LambderMockInvokeEvent, options: {
|
|
47
55
|
signal?: AbortSignal;
|
|
48
56
|
}) => Promise<LambderMockInvokeResult>);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { apiNameOfCallPath } from "../shared/wire/LambderApiNames.js";
|
|
2
|
+
import { DEFAULT_API_PATH } from "../shared/wire/LambderDefaultApiPath.js";
|
|
1
3
|
import { readApiEnvelope, cookieValuesByName, isApiCallContentType, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
|
|
2
4
|
import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
|
|
3
5
|
import { base64ToText } from "../shared/util/LambderBase64.js";
|
|
@@ -9,7 +11,8 @@ import { normalizeClientIp } from "../shared/util/LambderClientIp.js";
|
|
|
9
11
|
* can then point its typed invoke caller at a mock of the function it
|
|
10
12
|
* depends on, with the same registry a browser test uses.
|
|
11
13
|
*/
|
|
12
|
-
export const lambderMockInvokeTransport = (mockApp) => async (event, { signal }) => {
|
|
14
|
+
export const lambderMockInvokeTransport = (mockApp, options = {}) => async (event, { signal }) => {
|
|
15
|
+
const apiPath = options.apiPath ?? DEFAULT_API_PATH;
|
|
13
16
|
const rawBody = event.body ?? "";
|
|
14
17
|
const body = event.isBase64Encoded ? base64ToText(rawBody) : rawBody;
|
|
15
18
|
let post = {};
|
|
@@ -21,16 +24,21 @@ export const lambderMockInvokeTransport = (mockApp) => async (event, { signal })
|
|
|
21
24
|
}
|
|
22
25
|
const headers = lowercaseHeaderNames(event.headers);
|
|
23
26
|
const cookies = cookieValuesByName(event.cookies ?? []);
|
|
24
|
-
// A POST
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
27
|
+
// A JSON POST to `{apiPath}/{group}/{action}` is a call, as the server's
|
|
28
|
+
// createContext reads it; anything else (another method or type, a path
|
|
29
|
+
// outside apiPath) is no API call on the server either, and a mock
|
|
30
|
+
// serves no routes. The address is the one the synthesized event carries
|
|
31
|
+
// in sourceIp, as createContext reads it; no forwarding header is trusted
|
|
32
|
+
// here either, so a per-IP limit keys the same address under both adapters.
|
|
33
|
+
const apiName = event.requestContext?.http?.method === "POST" && isApiCallContentType(headers)
|
|
34
|
+
? apiNameOfCallPath(apiPath, event.rawPath ?? "")
|
|
35
|
+
: null;
|
|
36
|
+
const request = apiName !== null && readApiEnvelope(post, {
|
|
29
37
|
headers, cookies,
|
|
30
38
|
ip: normalizeClientIp(event.requestContext?.http?.sourceIp ?? ""),
|
|
31
39
|
host: headers.host || event.requestContext?.domainName || "lambder-invoke",
|
|
32
40
|
...(signal ? { signal } : {}),
|
|
33
|
-
});
|
|
41
|
+
}, apiName);
|
|
34
42
|
if (!request) {
|
|
35
43
|
return { functionError: null, result: { statusCode: 404, headers: { "content-type": "text/plain; charset=utf-8" }, body: "Not found.", isBase64Encoded: false } };
|
|
36
44
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LambderMockTransportError } from "./LambderMockFailureInjector.js";
|
|
2
|
+
import { apiNameOfCallPath } from "../shared/wire/LambderApiNames.js";
|
|
2
3
|
import { readApiEnvelope, cookieValuesByName, isApiCallContentType, lowercaseHeaderNames } from "../api/LambderApiRequest.js";
|
|
3
4
|
import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
|
|
4
5
|
import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
|
|
@@ -50,7 +51,9 @@ export const lambderMockMswHandler = (mockApp, options) => {
|
|
|
50
51
|
// adapter's is, so one address is one counter under a `per: "ip"` limit
|
|
51
52
|
// however it was spelled.
|
|
52
53
|
const clientIp = normalizeClientIp(options.clientIp ?? mockApp.defaultClientIp);
|
|
53
|
-
|
|
54
|
+
// Every call goes to `{apiPath}/{group}/{action}`; the endpoint is read
|
|
55
|
+
// off the path as the server reads it (apiNameOfCallPath).
|
|
56
|
+
const handler = msw.http.post(`${apiPath.replace(/\/+$/, "")}/:group/:action`, async ({ request }) => {
|
|
54
57
|
let post;
|
|
55
58
|
try {
|
|
56
59
|
post = await request.clone().json();
|
|
@@ -91,11 +94,12 @@ export const lambderMockMswHandler = (mockApp, options) => {
|
|
|
91
94
|
(cookies[name] ??= []).push(value);
|
|
92
95
|
}
|
|
93
96
|
}
|
|
97
|
+
const apiName = apiNameOfCallPath(new URL(apiPath, url).pathname, url.pathname);
|
|
98
|
+
if (apiName === null)
|
|
99
|
+
return undefined;
|
|
94
100
|
const parsed = readApiEnvelope(post, {
|
|
95
101
|
headers, cookies, ip: clientIp, host: url.host, signal: request.signal,
|
|
96
|
-
});
|
|
97
|
-
if (!parsed)
|
|
98
|
-
return undefined;
|
|
102
|
+
}, apiName);
|
|
99
103
|
// Returning undefined hands the request back to MSW, which tries its
|
|
100
104
|
// other handlers and then the network. Noted on the runtime first:
|
|
101
105
|
// a passthrough that leaves no event and no call-log row is a
|