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,10 +3,15 @@ import { LambderExpiringMap } from "../shared/util/LambderExpiringMap.js";
|
|
|
3
3
|
import { joinKeyFields } from "../shared/util/joinKeyFields.js";
|
|
4
4
|
/**
|
|
5
5
|
* Fixed-window rate limiter held in memory: the same windows, the same
|
|
6
|
-
* attempts-count semantics and the same
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* attempts-count semantics and the same refusals as LambderDdbRateLimiter,
|
|
7
|
+
* with a Map in place of the table. For tests and for the mock runtime; a
|
|
8
|
+
* single-process server could use it too, with the caveat that its counters
|
|
9
|
+
* are per process and vanish with it.
|
|
10
|
+
*
|
|
11
|
+
* Every counter is in hand, so an attempt a window refuses is counted in no
|
|
12
|
+
* window: what the DynamoDB limiter reaches once it remembers the refusing
|
|
13
|
+
* window, which the first refused attempt in each of its processes still
|
|
14
|
+
* counts in the others.
|
|
10
15
|
*
|
|
11
16
|
* `now` is injectable so a test can move time forward and watch a window
|
|
12
17
|
* reset without waiting for it.
|
|
@@ -31,24 +36,24 @@ export class LambderMemoryRateLimiter {
|
|
|
31
36
|
}
|
|
32
37
|
async isRateLimited(trackerKey, policy) {
|
|
33
38
|
const nowSeconds = Math.floor(this.now() / 1000);
|
|
34
|
-
|
|
35
|
-
const limit = policy[key];
|
|
36
|
-
if (!limit)
|
|
37
|
-
continue;
|
|
39
|
+
const windows = RATE_LIMIT_WINDOWS.filter(({ key }) => policy[key]).map(({ key, seconds }) => {
|
|
38
40
|
const windowStart = Math.floor(nowSeconds / seconds) * seconds;
|
|
39
41
|
// The table keeps the tracker key and the window in separate
|
|
40
42
|
// attributes; one string has to keep them as distinct, hence the
|
|
41
43
|
// escaping join.
|
|
42
44
|
const counterKey = joinKeyFields(trackerKey, key, String(windowStart));
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
return { key, limit: policy[key], resetAt: windowStart + seconds, counterKey, counter: this.counters.get(counterKey) ?? { count: 0 } };
|
|
46
|
+
});
|
|
47
|
+
// The DynamoDB limiter's conditional ADD: allowed while the count
|
|
48
|
+
// before this attempt is under the limit, and the smallest window at
|
|
49
|
+
// its limit names the refusal.
|
|
50
|
+
const refusing = windows.find(({ counter, limit }) => counter.count >= limit);
|
|
51
|
+
if (refusing)
|
|
52
|
+
return { window: refusing.key, limit: refusing.limit, resetAt: refusing.resetAt };
|
|
53
|
+
for (const { counterKey, counter, resetAt } of windows) {
|
|
49
54
|
counter.count += 1;
|
|
50
55
|
// The counter dies with its window, which is also the table's TTL.
|
|
51
|
-
this.counters.set(counterKey, counter,
|
|
56
|
+
this.counters.set(counterKey, counter, resetAt);
|
|
52
57
|
}
|
|
53
58
|
return false;
|
|
54
59
|
}
|
|
@@ -7,7 +7,9 @@ import type { LambderFileSource } from "../shared/contracts/LambderFileSource.js
|
|
|
7
7
|
import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
|
|
8
8
|
import type { LambderRateLimiter } from "../shared/contracts/LambderRateLimiter.js";
|
|
9
9
|
import type { LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
|
|
10
|
+
import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
|
|
10
11
|
import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.js";
|
|
12
|
+
import type { LambderCallSummary } from "../core/LambderCallSummary.js";
|
|
11
13
|
import { LambderTestVisitor, type LambderTestVisitorArgs, type LambderTestVisitorOptions } from "./LambderTestVisitor.js";
|
|
12
14
|
/**
|
|
13
15
|
* What a test app may be told. Nothing is required: `lambderTestApp(lambder)`
|
|
@@ -51,7 +53,10 @@ export type LambderTestAppOptions = {
|
|
|
51
53
|
* (`lambderTestApp<SessionData, ApiContractType>(lambder)`), so the tests type
|
|
52
54
|
* their calls against plain members rather than the chained intersection.
|
|
53
55
|
*/
|
|
54
|
-
export type LambderTestedInstance<TSessionData, TContract> = Lambder<
|
|
56
|
+
export type LambderTestedInstance<TSessionData, TContract> = Pick<Lambder<any, any>, "apiPath" | "getHandler" | "getSessionController" | "getSessionManager" | typeof LAMBDER_BACKEND_SWAP | typeof LAMBDER_CRASH_WATCH> & {
|
|
57
|
+
readonly AppTypes: {
|
|
58
|
+
session: TSessionData;
|
|
59
|
+
};
|
|
55
60
|
readonly ApiContract: TContract;
|
|
56
61
|
};
|
|
57
62
|
/**
|
|
@@ -96,6 +101,8 @@ export declare class LambderTestApp<TContract extends LambderApiContractShape =
|
|
|
96
101
|
private visitorCount;
|
|
97
102
|
private resetCount;
|
|
98
103
|
private readonly crashList;
|
|
104
|
+
/** Every call summary the app wrote since the last reset; see callSummaries. */
|
|
105
|
+
private readonly callSummaryList;
|
|
99
106
|
/**
|
|
100
107
|
* The call a crash happened under. The app's 500 says nothing about the
|
|
101
108
|
* crash, so the error travels beside the answer, and with concurrent
|
|
@@ -113,6 +120,12 @@ export declare class LambderTestApp<TContract extends LambderApiContractShape =
|
|
|
113
120
|
* error an `event()` rejects with, which the test already holds.
|
|
114
121
|
*/
|
|
115
122
|
get crashes(): readonly Error[];
|
|
123
|
+
/**
|
|
124
|
+
* The summary of every API call the app answered since the last reset,
|
|
125
|
+
* in order (see LambderCallSummary): what its callSummary option would
|
|
126
|
+
* have been handed, collected here instead of written to stdout.
|
|
127
|
+
*/
|
|
128
|
+
get callSummaries(): readonly LambderCallSummary[];
|
|
116
129
|
/** The session manager, for tests that inspect or manipulate sessions directly. Throws when the app has no sessions. */
|
|
117
130
|
get sessionManager(): LambderSessionManager<TSessionData>;
|
|
118
131
|
/**
|
|
@@ -155,10 +168,10 @@ export declare class LambderTestApp<TContract extends LambderApiContractShape =
|
|
|
155
168
|
event(event: unknown, context?: Partial<Context>): Promise<unknown>;
|
|
156
169
|
/**
|
|
157
170
|
* Rewinds what accumulated: sessions, rate-limit counters and replay
|
|
158
|
-
* records in the stores this test app made, the crashes
|
|
159
|
-
* the cookies of every visitor it created
|
|
160
|
-
* time it is used). For a beforeEach. The
|
|
161
|
-
* is the app's to rewind.
|
|
171
|
+
* records in the stores this test app made, the crashes and call
|
|
172
|
+
* summaries it recorded, and the cookies of every visitor it created
|
|
173
|
+
* (each empties its jar the next time it is used). For a beforeEach. The
|
|
174
|
+
* app's own data (its database) is the app's to rewind.
|
|
162
175
|
*/
|
|
163
176
|
reset(): void;
|
|
164
177
|
}
|
|
@@ -49,6 +49,8 @@ export class LambderTestApp {
|
|
|
49
49
|
visitorCount = 0;
|
|
50
50
|
resetCount = 0;
|
|
51
51
|
crashList = [];
|
|
52
|
+
/** Every call summary the app wrote since the last reset; see callSummaries. */
|
|
53
|
+
callSummaryList = [];
|
|
52
54
|
/**
|
|
53
55
|
* The call a crash happened under. The app's 500 says nothing about the
|
|
54
56
|
* crash, so the error travels beside the answer, and with concurrent
|
|
@@ -66,7 +68,12 @@ export class LambderTestApp {
|
|
|
66
68
|
const sessionStore = options.session?.store ?? own(new LambderMemorySessionStore());
|
|
67
69
|
const rateLimiter = options.rateLimits?.limiter ?? own(new LambderMemoryRateLimiter());
|
|
68
70
|
const idempotencyStore = options.idempotency?.store ?? own(new LambderMemoryIdempotencyStore());
|
|
69
|
-
|
|
71
|
+
// The summaries are collected rather than written, so a suite's
|
|
72
|
+
// output stays its own and a test can read what a call recorded.
|
|
73
|
+
const swap = lambder[LAMBDER_BACKEND_SWAP]({
|
|
74
|
+
sessionStore, rateLimiter, idempotencyStore, fileSource: options.files,
|
|
75
|
+
callSummary: (summary) => { this.callSummaryList.push(summary); },
|
|
76
|
+
});
|
|
70
77
|
if (options.files && !swap.files) {
|
|
71
78
|
throw new Error("lambderTestApp: the files option was given, but the app was created without one, so nothing reads files to put a source under.");
|
|
72
79
|
}
|
|
@@ -116,6 +123,14 @@ export class LambderTestApp {
|
|
|
116
123
|
get crashes() {
|
|
117
124
|
return this.crashList;
|
|
118
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* The summary of every API call the app answered since the last reset,
|
|
128
|
+
* in order (see LambderCallSummary): what its callSummary option would
|
|
129
|
+
* have been handed, collected here instead of written to stdout.
|
|
130
|
+
*/
|
|
131
|
+
get callSummaries() {
|
|
132
|
+
return this.callSummaryList;
|
|
133
|
+
}
|
|
119
134
|
/** The session manager, for tests that inspect or manipulate sessions directly. Throws when the app has no sessions. */
|
|
120
135
|
get sessionManager() {
|
|
121
136
|
return this.lambder.getSessionManager();
|
|
@@ -178,15 +193,16 @@ export class LambderTestApp {
|
|
|
178
193
|
}
|
|
179
194
|
/**
|
|
180
195
|
* Rewinds what accumulated: sessions, rate-limit counters and replay
|
|
181
|
-
* records in the stores this test app made, the crashes
|
|
182
|
-
* the cookies of every visitor it created
|
|
183
|
-
* time it is used). For a beforeEach. The
|
|
184
|
-
* is the app's to rewind.
|
|
196
|
+
* records in the stores this test app made, the crashes and call
|
|
197
|
+
* summaries it recorded, and the cookies of every visitor it created
|
|
198
|
+
* (each empties its jar the next time it is used). For a beforeEach. The
|
|
199
|
+
* app's own data (its database) is the app's to rewind.
|
|
185
200
|
*/
|
|
186
201
|
reset() {
|
|
187
202
|
for (const store of this.ownStores)
|
|
188
203
|
store.reset();
|
|
189
204
|
this.crashList.length = 0;
|
|
205
|
+
this.callSummaryList.length = 0;
|
|
190
206
|
this.resetCount += 1;
|
|
191
207
|
}
|
|
192
208
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import LambderCaller from "../client/LambderCaller.js";
|
|
1
|
+
import LambderCaller, { type LambderCallerGroupCalls, type LambderCallerMembers } from "../client/LambderCaller.js";
|
|
2
2
|
import type { LambderHttpEventFormat } from "../core/LambderContext.js";
|
|
3
3
|
import type { LambderHandler } from "../core/LambderCreateOptions.js";
|
|
4
4
|
import { type LambderLambdaHttpResult } from "../invoke/LambderLambdaEvent.js";
|
|
@@ -84,7 +84,8 @@ export type LambderTestVisitorArgs<TOptions, TProvidedGuards extends string> = [
|
|
|
84
84
|
*
|
|
85
85
|
* Created by `LambderTestApp.visitor()` and `signIn()`, not constructed.
|
|
86
86
|
*/
|
|
87
|
-
|
|
87
|
+
declare class LambderTestVisitorCore<TContract extends LambderApiContractShape = any, TSessionData = any, TProvidedGuards extends string = never> {
|
|
88
|
+
#private;
|
|
88
89
|
readonly host: string;
|
|
89
90
|
readonly clientIp: string;
|
|
90
91
|
/**
|
|
@@ -95,7 +96,7 @@ export declare class LambderTestVisitor<TContract extends LambderApiContractShap
|
|
|
95
96
|
*/
|
|
96
97
|
readonly caller: LambderCaller<TContract, TProvidedGuards>;
|
|
97
98
|
/** The payload on success, `undefined` on a failure: LambderCaller.api, through this visitor. */
|
|
98
|
-
readonly api:
|
|
99
|
+
readonly api: LambderCallerMembers<TContract, TProvidedGuards>["api"];
|
|
99
100
|
/**
|
|
100
101
|
* The full outcome, never throwing: LambderCaller.apiOutcome, through
|
|
101
102
|
* this visitor. Pair it with assertApiSuccess / assertApiFailure.
|
|
@@ -105,11 +106,7 @@ export declare class LambderTestVisitor<TContract extends LambderApiContractShap
|
|
|
105
106
|
* 500"; here that error's `cause` is what the app actually threw, stack
|
|
106
107
|
* included, so a failing test points at the line in the handler.
|
|
107
108
|
*/
|
|
108
|
-
readonly apiOutcome:
|
|
109
|
-
private readonly wiring;
|
|
110
|
-
private readonly headers;
|
|
111
|
-
private readonly cookieJar;
|
|
112
|
-
private seenResetCount;
|
|
109
|
+
readonly apiOutcome: LambderCallerMembers<TContract, TProvidedGuards>["apiOutcome"];
|
|
113
110
|
constructor(wiring: LambderTestVisitorWiring<TSessionData>, options: LambderTestVisitorOptions<TContract, TProvidedGuards> & {
|
|
114
111
|
host: string;
|
|
115
112
|
clientIp: string;
|
|
@@ -151,3 +148,14 @@ export declare class LambderTestVisitor<TContract extends LambderApiContractShap
|
|
|
151
148
|
ttlSeconds?: number;
|
|
152
149
|
}): Promise<LambderCreatedSession<TSessionData>>;
|
|
153
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* One simulated browser in front of a real Lambder app, with the app's
|
|
153
|
+
* endpoints by group as a caller has them: `visitor.orders.place(input)`,
|
|
154
|
+
* `visitor.orders.place.outcome(input)`.
|
|
155
|
+
*/
|
|
156
|
+
export type LambderTestVisitor<TContract extends LambderApiContractShape = any, TSessionData = any, TProvidedGuards extends string = never> = LambderTestVisitorCore<TContract, TSessionData, TProvidedGuards> & LambderCallerGroupCalls<TContract, TProvidedGuards>;
|
|
157
|
+
export declare const LambderTestVisitor: {
|
|
158
|
+
new <TContract extends LambderApiContractShape = any, TSessionData = any, TProvidedGuards extends string = never>(...args: ConstructorParameters<typeof LambderTestVisitorCore<TContract, TSessionData, TProvidedGuards>>): LambderTestVisitor<TContract, TSessionData, TProvidedGuards>;
|
|
159
|
+
readonly prototype: LambderTestVisitorCore<any, any, any>;
|
|
160
|
+
};
|
|
161
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import LambderCaller from "../client/LambderCaller.js";
|
|
2
|
+
import { withApiGroupCalls } from "../shared/wire/LambderApiGroupCalls.js";
|
|
2
3
|
import { lambderHandlerTransport } from "../invoke/lambderHandlerTransport.js";
|
|
3
4
|
import { decodeLambdaHttpResult, localLambdaContext, synthesizeLambdaHttpEvent, } from "../invoke/LambderLambdaEvent.js";
|
|
4
5
|
import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
|
|
@@ -15,7 +16,7 @@ import { DEFAULT_MAX_RESTORED_PAYLOAD_BYTES } from "../shared/wire/LambderReques
|
|
|
15
16
|
*
|
|
16
17
|
* Created by `LambderTestApp.visitor()` and `signIn()`, not constructed.
|
|
17
18
|
*/
|
|
18
|
-
|
|
19
|
+
class LambderTestVisitorCore {
|
|
19
20
|
host;
|
|
20
21
|
clientIp;
|
|
21
22
|
/**
|
|
@@ -37,19 +38,19 @@ export class LambderTestVisitor {
|
|
|
37
38
|
* included, so a failing test points at the line in the handler.
|
|
38
39
|
*/
|
|
39
40
|
apiOutcome;
|
|
40
|
-
wiring;
|
|
41
|
-
headers;
|
|
42
|
-
cookieJar;
|
|
43
|
-
seenResetCount;
|
|
41
|
+
#wiring;
|
|
42
|
+
#headers;
|
|
43
|
+
#cookieJar;
|
|
44
|
+
#seenResetCount;
|
|
44
45
|
constructor(wiring, options) {
|
|
45
|
-
this
|
|
46
|
+
this.#wiring = wiring;
|
|
46
47
|
this.host = options.host;
|
|
47
48
|
this.clientIp = options.clientIp;
|
|
48
|
-
this
|
|
49
|
-
this
|
|
50
|
-
this
|
|
49
|
+
this.#headers = options.headers ?? {};
|
|
50
|
+
this.#cookieJar = new LambderCookieJar({ host: this.host });
|
|
51
|
+
this.#seenResetCount = wiring.resetCount();
|
|
51
52
|
const handlerTransport = lambderHandlerTransport(wiring.handler, { host: this.host, clientIp: this.clientIp, eventFormat: wiring.eventFormat });
|
|
52
|
-
const withVisitorHeaders = (request) => handlerTransport({ ...request, headers: { ...this
|
|
53
|
+
const withVisitorHeaders = (request) => handlerTransport({ ...request, headers: { ...this.#headers, ...request.headers } });
|
|
53
54
|
this.caller = new LambderCaller({
|
|
54
55
|
apiPath: wiring.apiPath,
|
|
55
56
|
apiVersion: options.apiVersion,
|
|
@@ -69,13 +70,19 @@ export class LambderTestVisitor {
|
|
|
69
70
|
if (wiring.sessionCookieNames) {
|
|
70
71
|
this.caller.setSessionCookieKey(wiring.sessionCookieNames.tokenCookieKey, wiring.sessionCookieNames.csrfCookieKey);
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const { result: outcome, crash } = await wiring.watchCrash(() =>
|
|
73
|
+
const caller = this.caller;
|
|
74
|
+
const apiOutcome = async (apiName, ...callArgs) => {
|
|
75
|
+
const { result: outcome, crash } = await wiring.watchCrash(() => caller.apiOutcome(apiName, ...callArgs));
|
|
75
76
|
if (crash && !outcome.ok && "error" in outcome && outcome.error.cause === undefined)
|
|
76
77
|
outcome.error.cause = crash;
|
|
77
78
|
return outcome;
|
|
78
|
-
}
|
|
79
|
+
};
|
|
80
|
+
this.api = ((apiName, ...callArgs) => caller.api(apiName, ...callArgs));
|
|
81
|
+
this.apiOutcome = apiOutcome;
|
|
82
|
+
// Each group of the contract, as on a caller, through this visitor's
|
|
83
|
+
// own two calls: visitor.orders.place.outcome(input) carries the crash
|
|
84
|
+
// cause as visitor.apiOutcome does.
|
|
85
|
+
return withApiGroupCalls(this, (apiName, args) => caller.api(apiName, ...args), (apiName, args) => apiOutcome(apiName, ...args));
|
|
79
86
|
}
|
|
80
87
|
/**
|
|
81
88
|
* This visitor's cookies, to inspect or clear; every call and request
|
|
@@ -84,12 +91,12 @@ export class LambderTestVisitor {
|
|
|
84
91
|
* emptied store would read as signed in until an answer said otherwise.
|
|
85
92
|
*/
|
|
86
93
|
get jar() {
|
|
87
|
-
const resetCount = this
|
|
88
|
-
if (resetCount !== this
|
|
89
|
-
this
|
|
90
|
-
this
|
|
94
|
+
const resetCount = this.#wiring.resetCount();
|
|
95
|
+
if (resetCount !== this.#seenResetCount) {
|
|
96
|
+
this.#cookieJar.clear();
|
|
97
|
+
this.#seenResetCount = resetCount;
|
|
91
98
|
}
|
|
92
|
-
return this
|
|
99
|
+
return this.#cookieJar;
|
|
93
100
|
}
|
|
94
101
|
/**
|
|
95
102
|
* One HTTP request to the app that is not an API call, answered by
|
|
@@ -117,12 +124,12 @@ export class LambderTestVisitor {
|
|
|
117
124
|
path: pathname,
|
|
118
125
|
query,
|
|
119
126
|
host: this.host,
|
|
120
|
-
headers: { ...this
|
|
127
|
+
headers: { ...this.#headers, ...init.headers },
|
|
121
128
|
clientIp: this.clientIp,
|
|
122
129
|
cookies: this.jar.cookiePairs(cookieScope),
|
|
123
130
|
body: init.body,
|
|
124
|
-
}, { invoke: false, eventFormat: this
|
|
125
|
-
const result = await decodeLambdaHttpResult(await this
|
|
131
|
+
}, { invoke: false, eventFormat: this.#wiring.eventFormat });
|
|
132
|
+
const result = await decodeLambdaHttpResult(await this.#wiring.handler(event, localLambdaContext("lambder-test")), DEFAULT_MAX_RESTORED_PAYLOAD_BYTES);
|
|
126
133
|
if (result.cookies.length)
|
|
127
134
|
this.jar.storeSetCookies(result.cookies, cookieScope);
|
|
128
135
|
return result;
|
|
@@ -140,11 +147,11 @@ export class LambderTestVisitor {
|
|
|
140
147
|
* answer sessionExpired with nothing to say why.
|
|
141
148
|
*/
|
|
142
149
|
async signIn(sessionKey, data, options = {}) {
|
|
143
|
-
if (!this
|
|
150
|
+
if (!this.#wiring.sessionCookieNames)
|
|
144
151
|
throw new Error("LambderTestVisitor: signIn() needs an app with sessions. Pass the session option to create().");
|
|
145
|
-
const { created, setCookies } = await this
|
|
152
|
+
const { created, setCookies } = await this.#wiring.issueSession(this.host, sessionKey, data, options.ttlSeconds);
|
|
146
153
|
this.jar.storeSetCookies(setCookies, { host: this.host });
|
|
147
|
-
if (this.jar.get(this
|
|
154
|
+
if (this.jar.get(this.#wiring.sessionCookieNames.tokenCookieKey, { host: this.host, includeHttpOnly: true }) === undefined) {
|
|
148
155
|
throw new Error(`LambderTestVisitor: the session cookie did not stick for host "${this.host}", the way a browser on that host would drop it. ` +
|
|
149
156
|
"The app most likely scopes its session cookie to a domain this host is not under: " +
|
|
150
157
|
"give the test app or this visitor a `host` the cookie domain covers.");
|
|
@@ -152,3 +159,4 @@ export class LambderTestVisitor {
|
|
|
152
159
|
return created;
|
|
153
160
|
}
|
|
154
161
|
}
|
|
162
|
+
export const LambderTestVisitor = LambderTestVisitorCore;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "13.0.1",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
|
|
6
6
|
"keywords": [
|