lambder 7.2.5 → 8.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1021 -3
- package/README.md +43 -21
- package/dist/api/LambderApiAnswer.d.ts +18 -22
- package/dist/api/LambderApiAnswer.js +6 -7
- package/dist/api/LambderApiCallContext.d.ts +21 -8
- package/dist/api/LambderApiCallContext.js +22 -4
- package/dist/api/LambderApiDefinition.d.ts +4 -3
- package/dist/api/LambderApiEnvelope.d.ts +14 -9
- package/dist/api/LambderApiEnvelope.js +33 -34
- package/dist/api/LambderApiGuards.d.ts +78 -51
- package/dist/api/LambderApiGuards.js +34 -36
- package/dist/api/LambderApiIdempotency.d.ts +74 -61
- package/dist/api/LambderApiIdempotency.js +226 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +77 -39
- package/dist/api/LambderApiPipeline.js +135 -62
- package/dist/api/LambderApiRateLimits.d.ts +208 -54
- package/dist/api/LambderApiRateLimits.js +197 -108
- package/dist/api/LambderApiRequest.d.ts +27 -21
- package/dist/api/LambderApiRequest.js +26 -19
- package/dist/api/LambderApiSignature.d.ts +12 -15
- package/dist/api/LambderApiSignature.js +28 -51
- package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
- package/dist/api/LambderApiValidationRefusal.js +10 -10
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/writeApiSignatures.d.ts +109 -0
- package/dist/build/writeApiSignatures.js +222 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +8 -0
- package/dist/client/LambderCaller.d.ts +13 -44
- package/dist/client/LambderCaller.js +77 -84
- package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
- package/dist/client/LambderReloadLoopBreaker.js +90 -46
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +5 -3
- package/dist/client.js +2 -1
- package/dist/core/Lambder.d.ts +161 -69
- package/dist/core/Lambder.js +370 -226
- package/dist/core/LambderContext.d.ts +82 -15
- package/dist/core/LambderContext.js +107 -20
- package/dist/core/LambderCors.d.ts +21 -3
- package/dist/core/LambderCors.js +35 -16
- package/dist/core/LambderCrashHandling.d.ts +40 -0
- package/dist/core/LambderCrashHandling.js +97 -0
- package/dist/core/LambderCreateOptions.d.ts +151 -75
- package/dist/core/LambderCreateOptions.js +16 -23
- package/dist/core/LambderFiles.d.ts +28 -7
- package/dist/core/LambderFiles.js +73 -33
- package/dist/core/LambderIndexHtml.js +12 -11
- package/dist/core/LambderPolicyBuilders.d.ts +17 -5
- package/dist/core/LambderPolicyBuilders.js +17 -5
- package/dist/core/LambderPublicFiles.d.ts +11 -5
- package/dist/core/LambderPublicFiles.js +32 -4
- package/dist/core/LambderRequestPath.d.ts +43 -0
- package/dist/core/LambderRequestPath.js +63 -0
- package/dist/core/LambderResponse.d.ts +26 -5
- package/dist/core/LambderResponse.js +157 -70
- package/dist/core/LambderResponseBuilder.d.ts +49 -4
- package/dist/core/LambderResponseBuilder.js +64 -3
- package/dist/core/LambderRouting.d.ts +2 -3
- package/dist/core/LambderRouting.js +22 -7
- package/dist/core/LambderTemplatingEngine.js +211 -32
- package/dist/index.d.ts +15 -8
- package/dist/index.js +5 -4
- package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
- package/dist/invoke/LambderInvokeCaller.js +76 -66
- package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
- package/dist/invoke/LambderInvokeOutcome.js +9 -22
- package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
- package/dist/invoke/LambderLambdaEvent.js +80 -37
- package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
- package/dist/invoke/lambderHandlerTransport.js +16 -19
- package/dist/mock/LambderMockApp.d.ts +67 -83
- package/dist/mock/LambderMockApp.js +167 -153
- package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
- package/dist/mock/LambderMockBrowserCookies.js +24 -28
- package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
- package/dist/mock/LambderMockCallRecorder.js +19 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
- package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
- package/dist/mock/LambderMockEntryRegistry.js +24 -29
- package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
- package/dist/mock/LambderMockFailureInjector.js +3 -6
- package/dist/mock/LambderMockTypes.d.ts +78 -108
- package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
- package/dist/mock/lambderMockInvokeTransport.js +11 -10
- package/dist/mock/lambderMockMswHandler.d.ts +33 -29
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock.d.ts +3 -1
- package/dist/mock.js +5 -3
- package/dist/session/LambderSessionController.d.ts +108 -89
- package/dist/session/LambderSessionController.js +187 -168
- package/dist/session/LambderSessionCrypto.d.ts +16 -7
- package/dist/session/LambderSessionCrypto.js +26 -12
- package/dist/session/LambderSessionManager.d.ts +136 -47
- package/dist/session/LambderSessionManager.js +280 -139
- package/dist/shared/LambderHtml.d.ts +42 -3
- package/dist/shared/LambderHtml.js +127 -7
- package/dist/shared/LambderHtmlPositions.d.ts +173 -0
- package/dist/shared/LambderHtmlPositions.js +652 -0
- package/dist/shared/LambderI18n.d.ts +10 -11
- package/dist/shared/LambderI18n.js +33 -21
- package/dist/shared/contracts/LambderCache.d.ts +66 -0
- package/dist/shared/contracts/LambderCache.js +11 -0
- package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
- package/dist/shared/contracts/LambderFileSource.js +5 -8
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
- package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
- package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
- package/dist/shared/contracts/LambderRateLimiter.js +4 -5
- package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
- package/dist/shared/contracts/LambderSessionStore.js +5 -6
- package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
- package/dist/shared/transport/LambderApiTransport.js +7 -7
- package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
- package/dist/shared/transport/LambderCookieJar.js +54 -66
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
- package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
- package/dist/shared/util/LambderCallAbort.d.ts +5 -5
- package/dist/shared/util/LambderCallAbort.js +5 -5
- package/dist/shared/util/LambderClientIp.d.ts +27 -11
- package/dist/shared/util/LambderClientIp.js +96 -13
- package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
- package/dist/shared/util/LambderExpiringMap.js +41 -57
- package/dist/shared/util/LambderNodeModules.js +6 -7
- package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
- package/dist/shared/util/LambderOptionChecks.js +4 -4
- package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
- package/dist/shared/util/LambderResponseBrand.js +5 -5
- package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
- package/dist/shared/util/LambderTestingDoors.js +29 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
- package/dist/shared/util/LambderTypeUtilities.js +3 -3
- package/dist/shared/util/boundKeyField.d.ts +20 -0
- package/dist/shared/util/boundKeyField.js +34 -0
- package/dist/shared/util/canonicalJson.d.ts +11 -0
- package/dist/shared/util/canonicalJson.js +28 -0
- package/dist/shared/util/joinKeyFields.d.ts +20 -0
- package/dist/shared/util/joinKeyFields.js +22 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
- package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
- package/dist/shared/wire/LambderApiContract.d.ts +107 -32
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
- package/dist/shared/wire/LambderApiRefusal.js +36 -7
- package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
- package/dist/shared/wire/LambderApiSignature.js +16 -19
- package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
- package/dist/shared/wire/LambderCallOptions.js +9 -11
- package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
- package/dist/shared/wire/LambderCompressionCodec.js +31 -36
- package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
- package/dist/shared/wire/LambderCompressionOption.js +9 -9
- package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
- package/dist/shared/wire/LambderCrashDetail.js +12 -15
- package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
- package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
- package/dist/shared/wire/LambderInvokeApiId.js +27 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
- package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/stores/LambderCacheFiller.d.ts +48 -0
- package/dist/stores/LambderCacheFiller.js +119 -0
- package/dist/stores/LambderCacheKeys.d.ts +26 -0
- package/dist/stores/LambderCacheKeys.js +54 -0
- package/dist/stores/LambderCacheValues.d.ts +45 -0
- package/dist/stores/LambderCacheValues.js +74 -0
- package/dist/stores/LambderDdbCache.d.ts +121 -56
- package/dist/stores/LambderDdbCache.js +528 -225
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
- package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
- package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
- package/dist/stores/LambderDdbRateLimiter.js +151 -39
- package/dist/stores/LambderDdbSdk.d.ts +43 -31
- package/dist/stores/LambderDdbSdk.js +79 -33
- package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
- package/dist/stores/LambderDdbSessionStore.js +119 -47
- package/dist/stores/LambderHttpFileSource.d.ts +15 -6
- package/dist/stores/LambderHttpFileSource.js +15 -13
- package/dist/stores/LambderMemoryCache.d.ts +49 -0
- package/dist/stores/LambderMemoryCache.js +113 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
- package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
- package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
- package/dist/stores/LambderMemoryRateLimiter.js +14 -13
- package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
- package/dist/stores/LambderMemorySessionStore.js +38 -19
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/testing/LambderTestApp.d.ts +176 -0
- package/dist/testing/LambderTestApp.js +204 -0
- package/dist/testing/LambderTestVisitor.d.ts +153 -0
- package/dist/testing/LambderTestVisitor.js +154 -0
- package/dist/testing.d.ts +27 -0
- package/dist/testing.js +24 -0
- package/package.json +20 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
- package/dist/api/LambderApiPolicyEngine.js +0 -77
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- package/dist/shared/util/LambderKeyFields.js +0 -34
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
+
import { createContext } from "../core/LambderContext.js";
|
|
3
|
+
import { localLambdaContext, synthesizeLambdaHttpEvent } from "../invoke/LambderLambdaEvent.js";
|
|
4
|
+
import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
|
|
5
|
+
import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
|
|
6
|
+
import { LambderMemoryIdempotencyStore } from "../stores/LambderMemoryIdempotencyStore.js";
|
|
7
|
+
import { LambderMemoryRateLimiter } from "../stores/LambderMemoryRateLimiter.js";
|
|
8
|
+
import { LambderMemorySessionStore } from "../stores/LambderMemorySessionStore.js";
|
|
9
|
+
import { LambderTestVisitor } from "./LambderTestVisitor.js";
|
|
10
|
+
/**
|
|
11
|
+
* A real Lambder app under test: the instance an app already has, with memory
|
|
12
|
+
* stores put under it in place, and as many simulated browsers in front of it
|
|
13
|
+
* as a test needs. No HTTP, no AWS, and nothing in the app restructured.
|
|
14
|
+
*
|
|
15
|
+
* The app's own declarations all run as written: guards, named rate-limit
|
|
16
|
+
* policies, idempotency settings, session salt, cookie options and
|
|
17
|
+
* dataRefresh, hooks and error handlers. Only where things rest is replaced:
|
|
18
|
+
* once this is created, the stores the app was configured with are out of the
|
|
19
|
+
* instance's reach, so a test cannot touch a production table by mistake.
|
|
20
|
+
* What the app reaches on its own (its database, a mailer) is the app's to
|
|
21
|
+
* replace.
|
|
22
|
+
*
|
|
23
|
+
* The sibling of LambderMockApp, which serves a contract from mock handlers:
|
|
24
|
+
* the same verbs (`signIn`, `signOut`, `expireSessionData`, `reset`) over the
|
|
25
|
+
* real handlers.
|
|
26
|
+
*
|
|
27
|
+
* Time is not this class's: fake `Date` with the test runner
|
|
28
|
+
* (`vi.useFakeTimers({ toFake: ["Date"] })`), which moves the framework, the
|
|
29
|
+
* memory stores and the app's own handlers together.
|
|
30
|
+
*
|
|
31
|
+
* One test app per instance: a second one puts its own stores under the same
|
|
32
|
+
* instance and takes over its crash watch, so the first stops seeing either.
|
|
33
|
+
*/
|
|
34
|
+
export class LambderTestApp {
|
|
35
|
+
/** The instance's handler: what `event()` and every visitor call. */
|
|
36
|
+
handler;
|
|
37
|
+
/** The host visitors browse unless they name their own. */
|
|
38
|
+
host;
|
|
39
|
+
/** The session store now under the instance, for assertions; null when the app has no sessions. A LambderMemorySessionStore unless one was given. */
|
|
40
|
+
sessionStore;
|
|
41
|
+
/** The rate limiter now under the instance; null when the app declares no rate limits. */
|
|
42
|
+
rateLimiter;
|
|
43
|
+
/** The idempotency store now under the instance; null when the app has no idempotency. */
|
|
44
|
+
idempotencyStore;
|
|
45
|
+
lambder;
|
|
46
|
+
wiring;
|
|
47
|
+
/** The memory stores this test app made itself, which reset() empties. One the test supplied is the test's: nothing here knows what else holds it. */
|
|
48
|
+
ownStores = [];
|
|
49
|
+
visitorCount = 0;
|
|
50
|
+
resetCount = 0;
|
|
51
|
+
crashList = [];
|
|
52
|
+
/**
|
|
53
|
+
* The call a crash happened under. The app's 500 says nothing about the
|
|
54
|
+
* crash, so the error travels beside the answer, and with concurrent
|
|
55
|
+
* calls (a duplicate sent while the original is in flight is an ordinary
|
|
56
|
+
* idempotency test) only the async context says which call it belongs to.
|
|
57
|
+
*/
|
|
58
|
+
crashScope = new AsyncLocalStorage();
|
|
59
|
+
constructor(lambder, options = {}) {
|
|
60
|
+
// Only a Lambder instance from this same package copy answers to the
|
|
61
|
+
// key; saying so here beats a bare "is not a function".
|
|
62
|
+
if (typeof lambder?.[LAMBDER_BACKEND_SWAP] !== "function") {
|
|
63
|
+
throw new Error("lambderTestApp: expected a Lambder instance (what initLambder().create() returns), from the same installed copy of lambder as lambder/testing.");
|
|
64
|
+
}
|
|
65
|
+
const own = (store) => { this.ownStores.push(store); return store; };
|
|
66
|
+
const sessionStore = options.session?.store ?? own(new LambderMemorySessionStore());
|
|
67
|
+
const rateLimiter = options.rateLimits?.limiter ?? own(new LambderMemoryRateLimiter());
|
|
68
|
+
const idempotencyStore = options.idempotency?.store ?? own(new LambderMemoryIdempotencyStore());
|
|
69
|
+
const swap = lambder[LAMBDER_BACKEND_SWAP]({ sessionStore, rateLimiter, idempotencyStore, fileSource: options.files });
|
|
70
|
+
if (options.files && !swap.files) {
|
|
71
|
+
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
|
+
}
|
|
73
|
+
lambder[LAMBDER_CRASH_WATCH]((error) => {
|
|
74
|
+
this.crashList.push(error);
|
|
75
|
+
const scope = this.crashScope.getStore();
|
|
76
|
+
if (scope)
|
|
77
|
+
scope.crash = error;
|
|
78
|
+
});
|
|
79
|
+
this.lambder = lambder;
|
|
80
|
+
this.handler = lambder.getHandler();
|
|
81
|
+
this.host = options.host ?? "localhost";
|
|
82
|
+
this.sessionStore = swap.sessions ? sessionStore : null;
|
|
83
|
+
this.rateLimiter = swap.rateLimits ? rateLimiter : null;
|
|
84
|
+
this.idempotencyStore = swap.idempotency ? idempotencyStore : null;
|
|
85
|
+
this.wiring = {
|
|
86
|
+
handler: this.handler,
|
|
87
|
+
apiPath: lambder.apiPath,
|
|
88
|
+
eventFormat: options.eventFormat ?? "v2",
|
|
89
|
+
sessionCookieNames: swap.sessions,
|
|
90
|
+
resetCount: () => this.resetCount,
|
|
91
|
+
watchCrash: async (run) => {
|
|
92
|
+
const scope = { crash: null };
|
|
93
|
+
return { result: await this.crashScope.run(scope, run), crash: scope.crash };
|
|
94
|
+
},
|
|
95
|
+
issueSession: async (host, sessionKey, data, ttlSeconds) => {
|
|
96
|
+
// Through a session controller on a request context, as a
|
|
97
|
+
// login handler does, so the cookies are what the app's own
|
|
98
|
+
// cookie options produce for that host.
|
|
99
|
+
const event = synthesizeLambdaHttpEvent({ method: "GET", path: "/", host }, { invoke: false });
|
|
100
|
+
const ctx = createContext(event, localLambdaContext("lambder-test"), { apiPath: lambder.apiPath });
|
|
101
|
+
const created = await lambder.getSessionController(ctx).issueSession(sessionKey, data, ttlSeconds);
|
|
102
|
+
const headers = {};
|
|
103
|
+
ctx.responseHeaders.applyInto(headers);
|
|
104
|
+
return { created, setCookies: getAnswerHeader(headers, "Set-Cookie") ?? [] };
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Every error the app threw while answering a request since the last
|
|
110
|
+
* reset, in order: what reached its global error handler, or the
|
|
111
|
+
* framework's last-resort 500. The answers say nothing about what was
|
|
112
|
+
* thrown, so a test reads it here; `expect(app.crashes).toEqual([])`
|
|
113
|
+
* says nothing crashed. A refusal is not a crash, and neither is an
|
|
114
|
+
* error an `event()` rejects with, which the test already holds.
|
|
115
|
+
*/
|
|
116
|
+
get crashes() {
|
|
117
|
+
return this.crashList;
|
|
118
|
+
}
|
|
119
|
+
/** The session manager, for tests that inspect or manipulate sessions directly. Throws when the app has no sessions. */
|
|
120
|
+
get sessionManager() {
|
|
121
|
+
return this.lambder.getSessionManager();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A new simulated browser: its own cookie jar, and its own address unless
|
|
125
|
+
* one is given. A stranger until it signs in, through the app's own login
|
|
126
|
+
* API or through `signIn`.
|
|
127
|
+
*/
|
|
128
|
+
visitor(...[options]) {
|
|
129
|
+
this.visitorCount += 1;
|
|
130
|
+
return new LambderTestVisitor(this.wiring, {
|
|
131
|
+
...options,
|
|
132
|
+
host: options?.host ?? this.host,
|
|
133
|
+
// One private address per visitor, counted up and never handed out
|
|
134
|
+
// twice, so no two visitors share a per-ip counter by accident.
|
|
135
|
+
clientIp: options?.clientIp ?? `10.${(this.visitorCount >> 16) & 255}.${(this.visitorCount >> 8) & 255}.${this.visitorCount & 255}`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A new visitor, already signed in: `visitor()` followed by its
|
|
140
|
+
* `signIn()`. The session is minted by the app's own session model, so
|
|
141
|
+
* no login endpoint has to exist or be called.
|
|
142
|
+
*
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const admin = await app.signIn("user:ada", { userId: "ada", role: "admin" });
|
|
145
|
+
* expect(await admin.api("org.rename", { name })).toEqual({ ok: true });
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
async signIn(sessionKey, data, ...[options]) {
|
|
149
|
+
const { ttlSeconds, ...visitorOptions } = (options ?? {});
|
|
150
|
+
const visitor = this.visitor(...[visitorOptions]);
|
|
151
|
+
await visitor.signIn(sessionKey, data, { ttlSeconds });
|
|
152
|
+
return visitor;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Ends every session of the subject, the way "log out everywhere" does.
|
|
156
|
+
* A visitor signed in as that subject keeps its cookies, as a browser
|
|
157
|
+
* would, so its next call is what a real one's would be: answered
|
|
158
|
+
* sessionExpired, with the stale cookies evicted.
|
|
159
|
+
*/
|
|
160
|
+
async signOut(sessionKey) {
|
|
161
|
+
await this.sessionManager.deleteSessionAllByKey(sessionKey);
|
|
162
|
+
}
|
|
163
|
+
/** Marks the subject's session data stale, so the next read renews it through the app's dataRefresh. */
|
|
164
|
+
async expireSessionData(sessionKey) {
|
|
165
|
+
await this.sessionManager.expireSessionDataAllByKey(sessionKey);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Hands the handler an event that is not an HTTP request (a schedule, an
|
|
169
|
+
* SNS or SQS delivery), which is how an addAction handler runs, with a
|
|
170
|
+
* Lambda context filled in. Resolves to whatever the action returned.
|
|
171
|
+
*
|
|
172
|
+
* ```typescript
|
|
173
|
+
* await app.event({ source: "aws.events", "detail-type": "Scheduled Event" });
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
async event(event, context = {}) {
|
|
177
|
+
return await this.handler(event, localLambdaContext("lambder-test", context));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Rewinds what accumulated: sessions, rate-limit counters and replay
|
|
181
|
+
* records in the stores this test app made, the crashes it recorded, and
|
|
182
|
+
* the cookies of every visitor it created (each empties its jar the next
|
|
183
|
+
* time it is used). For a beforeEach. The app's own data (its database)
|
|
184
|
+
* is the app's to rewind.
|
|
185
|
+
*/
|
|
186
|
+
reset() {
|
|
187
|
+
for (const store of this.ownStores)
|
|
188
|
+
store.reset();
|
|
189
|
+
this.crashList.length = 0;
|
|
190
|
+
this.resetCount += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Puts a built Lambder instance under test. See LambderTestApp.
|
|
195
|
+
*
|
|
196
|
+
* ```typescript
|
|
197
|
+
* import { lambderTestApp } from "lambder/testing";
|
|
198
|
+
* import { lambder } from "../src/index.js";
|
|
199
|
+
*
|
|
200
|
+
* const app = lambderTestApp(lambder);
|
|
201
|
+
* beforeEach(() => app.reset());
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
export const lambderTestApp = (lambder, options = {}) => new LambderTestApp(lambder, options);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import LambderCaller from "../client/LambderCaller.js";
|
|
2
|
+
import type { LambderHttpEventFormat } from "../core/LambderContext.js";
|
|
3
|
+
import type { LambderHandler } from "../core/LambderCreateOptions.js";
|
|
4
|
+
import { type LambderLambdaHttpResult } from "../invoke/LambderLambdaEvent.js";
|
|
5
|
+
import type { LambderCreatedSession } from "../session/LambderSessionManager.js";
|
|
6
|
+
import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
|
|
7
|
+
import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.js";
|
|
8
|
+
import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
|
|
9
|
+
import type { LambderGuardInputsProviderOption } from "../shared/wire/LambderCallOptions.js";
|
|
10
|
+
/**
|
|
11
|
+
* How one visitor differs from the next. Everything is optional: a visitor
|
|
12
|
+
* nobody described is a stranger on the app's host, at an address of its own.
|
|
13
|
+
*/
|
|
14
|
+
export type LambderTestVisitorOptions<TContract, TProvidedGuards extends string = never> = {
|
|
15
|
+
/** The host this visitor browses (ctx.host), and the host its cookies are scoped to. Default: the test app's. */
|
|
16
|
+
host?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The address the gateway observed (ctx.ip). Default: one no other
|
|
19
|
+
* visitor of this test app has, so a `per: "ip"` rate limit counts each
|
|
20
|
+
* visitor apart, as it does for two people in production. Give two
|
|
21
|
+
* visitors the same one to test what a shared address does.
|
|
22
|
+
*/
|
|
23
|
+
clientIp?: string;
|
|
24
|
+
/** Headers every request of this visitor carries, under those a single call or request adds: what a CDN in front of the app writes (a country header), or a user agent. */
|
|
25
|
+
headers?: Record<string, string>;
|
|
26
|
+
/** Sent with every API call as `version`. Default: none, and a call naming no version is not judged by minApiVersion. */
|
|
27
|
+
apiVersion?: string;
|
|
28
|
+
/** Sent per API call as `signature`. Default: none, and a call carrying no signature is never gated; pass a map to test the gate itself. */
|
|
29
|
+
apiSignatures?: LambderApiSignatureMap;
|
|
30
|
+
} & LambderGuardInputsProviderOption<TContract, TProvidedGuards>;
|
|
31
|
+
/** What `request()` sends beside the method and the path. */
|
|
32
|
+
export type LambderTestRequestInit = {
|
|
33
|
+
/** Query parameters, merged over any the path itself carries. */
|
|
34
|
+
query?: Record<string, string>;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
/** Sent as is: a string is JSON unless a content-type header says otherwise, a Buffer is binary. */
|
|
37
|
+
body?: string | Buffer;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* What a test app hands each of its visitors: the handler they call, and the
|
|
41
|
+
* one thing a visitor cannot do alone, which is start a session without a
|
|
42
|
+
* login endpoint. The session model is the instance's, so the test app mints
|
|
43
|
+
* and the visitor keeps the cookies.
|
|
44
|
+
*/
|
|
45
|
+
export type LambderTestVisitorWiring<TSessionData> = {
|
|
46
|
+
handler: LambderHandler;
|
|
47
|
+
apiPath: string;
|
|
48
|
+
/** The gateway shape every event is synthesized in; the test app's, since a deployment sits behind one gateway. */
|
|
49
|
+
eventFormat: LambderHttpEventFormat;
|
|
50
|
+
/** The app's session cookie names, or null when it has no sessions. */
|
|
51
|
+
sessionCookieNames: {
|
|
52
|
+
tokenCookieKey: string;
|
|
53
|
+
csrfCookieKey: string;
|
|
54
|
+
} | null;
|
|
55
|
+
issueSession: (host: string, sessionKey: string, data: TSessionData, ttlSeconds?: number) => Promise<{
|
|
56
|
+
created: LambderCreatedSession<TSessionData>;
|
|
57
|
+
setCookies: string[];
|
|
58
|
+
}>;
|
|
59
|
+
/** How many times the test app was reset. A visitor compares it with the count it last saw, so the test app keeps no list of the visitors it made. */
|
|
60
|
+
resetCount: () => number;
|
|
61
|
+
/** Runs one call, and hands back beside its result the error the app threw while answering it, if it crashed. */
|
|
62
|
+
watchCrash: <TResult>(run: () => Promise<TResult>) => Promise<{
|
|
63
|
+
result: TResult;
|
|
64
|
+
crash: Error | null;
|
|
65
|
+
}>;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The options argument of `visitor()` and `signIn()`: optional, until the
|
|
69
|
+
* call names provided guards. Then the provider that supplies them is
|
|
70
|
+
* required, as it is on LambderCaller, since naming guards without one would
|
|
71
|
+
* send nothing for them.
|
|
72
|
+
*/
|
|
73
|
+
export type LambderTestVisitorArgs<TOptions, TProvidedGuards extends string> = [
|
|
74
|
+
TProvidedGuards
|
|
75
|
+
] extends [never] ? [options?: TOptions] : [options: TOptions];
|
|
76
|
+
/**
|
|
77
|
+
* One simulated browser in front of a real Lambder app: a cookie jar, an
|
|
78
|
+
* address and a host of its own, and two ways in. `api` / `apiOutcome` are a
|
|
79
|
+
* typed LambderCaller's, over the real handler in this process, so a call
|
|
80
|
+
* runs the whole pipeline (rate limits, session, replay, guards, validation)
|
|
81
|
+
* the way a browser's would. `request` is everything else a browser sends:
|
|
82
|
+
* pages, redirects, session routes, file requests. Both carry the same jar,
|
|
83
|
+
* so a session started through one is the session the other presents.
|
|
84
|
+
*
|
|
85
|
+
* Created by `LambderTestApp.visitor()` and `signIn()`, not constructed.
|
|
86
|
+
*/
|
|
87
|
+
export declare class LambderTestVisitor<TContract extends LambderApiContractShape = any, TSessionData = any, TProvidedGuards extends string = never> {
|
|
88
|
+
readonly host: string;
|
|
89
|
+
readonly clientIp: string;
|
|
90
|
+
/**
|
|
91
|
+
* The typed caller `api` and `apiOutcome` run on, for code under test
|
|
92
|
+
* that takes a LambderCaller itself (a frontend store, a shared client
|
|
93
|
+
* module): handed this one, it talks to the real server in this process.
|
|
94
|
+
* An answer's logList is not printed; it is on the outcome.
|
|
95
|
+
*/
|
|
96
|
+
readonly caller: LambderCaller<TContract, TProvidedGuards>;
|
|
97
|
+
/** The payload on success, `undefined` on a failure: LambderCaller.api, through this visitor. */
|
|
98
|
+
readonly api: LambderCaller<TContract, TProvidedGuards>["api"];
|
|
99
|
+
/**
|
|
100
|
+
* The full outcome, never throwing: LambderCaller.apiOutcome, through
|
|
101
|
+
* this visitor. Pair it with assertApiSuccess / assertApiFailure.
|
|
102
|
+
*
|
|
103
|
+
* When the app crashed answering the call, the outcome is the `server`
|
|
104
|
+
* failure any client would get, whose error says only "Request failed:
|
|
105
|
+
* 500"; here that error's `cause` is what the app actually threw, stack
|
|
106
|
+
* included, so a failing test points at the line in the handler.
|
|
107
|
+
*/
|
|
108
|
+
readonly apiOutcome: LambderCaller<TContract, TProvidedGuards>["apiOutcome"];
|
|
109
|
+
private readonly wiring;
|
|
110
|
+
private readonly headers;
|
|
111
|
+
private readonly cookieJar;
|
|
112
|
+
private seenResetCount;
|
|
113
|
+
constructor(wiring: LambderTestVisitorWiring<TSessionData>, options: LambderTestVisitorOptions<TContract, TProvidedGuards> & {
|
|
114
|
+
host: string;
|
|
115
|
+
clientIp: string;
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* This visitor's cookies, to inspect or clear; every call and request
|
|
119
|
+
* reads and fills them. The test app's reset() empties them, noticed here
|
|
120
|
+
* the next time anything asks: a jar still holding the token of an
|
|
121
|
+
* emptied store would read as signed in until an answer said otherwise.
|
|
122
|
+
*/
|
|
123
|
+
get jar(): LambderCookieJar;
|
|
124
|
+
/**
|
|
125
|
+
* One HTTP request to the app that is not an API call, answered by
|
|
126
|
+
* whatever answers it in production: a route, a session route, the public
|
|
127
|
+
* files, the index page, a fallback. The answer comes back decoded
|
|
128
|
+
* (decompressed, headers lowercased), redirects are not followed, and
|
|
129
|
+
* its Set-Cookie headers land in this visitor's jar.
|
|
130
|
+
*
|
|
131
|
+
* ```typescript
|
|
132
|
+
* const page = await visitor.request("GET", "/orders?page=2");
|
|
133
|
+
* expect(page.statusCode).toBe(200);
|
|
134
|
+
* expect(page.text()).toContain("Your orders");
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
request(method: string, path: string, init?: LambderTestRequestInit): Promise<LambderLambdaHttpResult>;
|
|
138
|
+
/**
|
|
139
|
+
* Starts a session for this visitor without a login endpoint: minted by
|
|
140
|
+
* the app's own session model, under its own cookie options, and planted
|
|
141
|
+
* in this visitor's jar, so its next call is signed in. Returns the raw
|
|
142
|
+
* tokens too, as LambderMockApp.signIn does.
|
|
143
|
+
*
|
|
144
|
+
* Throws when the cookies do not stick. An app that scopes its session
|
|
145
|
+
* cookie to a domain (`cookie: { domain: ".example.com" }`) this visitor's
|
|
146
|
+
* host is not under writes a cookie a browser on that host would drop, and
|
|
147
|
+
* the jar drops it too; left silent, every later session call would
|
|
148
|
+
* answer sessionExpired with nothing to say why.
|
|
149
|
+
*/
|
|
150
|
+
signIn(sessionKey: string, data: TSessionData, options?: {
|
|
151
|
+
ttlSeconds?: number;
|
|
152
|
+
}): Promise<LambderCreatedSession<TSessionData>>;
|
|
153
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import LambderCaller from "../client/LambderCaller.js";
|
|
2
|
+
import { lambderHandlerTransport } from "../invoke/lambderHandlerTransport.js";
|
|
3
|
+
import { decodeLambdaHttpResult, localLambdaContext, synthesizeLambdaHttpEvent, } from "../invoke/LambderLambdaEvent.js";
|
|
4
|
+
import { LambderCookieJar } from "../shared/transport/LambderCookieJar.js";
|
|
5
|
+
import { lambderCookieJarTransport } from "../shared/transport/lambderCookieJarTransport.js";
|
|
6
|
+
import { DEFAULT_MAX_RESTORED_PAYLOAD_BYTES } from "../shared/wire/LambderRequestPayload.js";
|
|
7
|
+
/**
|
|
8
|
+
* One simulated browser in front of a real Lambder app: a cookie jar, an
|
|
9
|
+
* address and a host of its own, and two ways in. `api` / `apiOutcome` are a
|
|
10
|
+
* typed LambderCaller's, over the real handler in this process, so a call
|
|
11
|
+
* runs the whole pipeline (rate limits, session, replay, guards, validation)
|
|
12
|
+
* the way a browser's would. `request` is everything else a browser sends:
|
|
13
|
+
* pages, redirects, session routes, file requests. Both carry the same jar,
|
|
14
|
+
* so a session started through one is the session the other presents.
|
|
15
|
+
*
|
|
16
|
+
* Created by `LambderTestApp.visitor()` and `signIn()`, not constructed.
|
|
17
|
+
*/
|
|
18
|
+
export class LambderTestVisitor {
|
|
19
|
+
host;
|
|
20
|
+
clientIp;
|
|
21
|
+
/**
|
|
22
|
+
* The typed caller `api` and `apiOutcome` run on, for code under test
|
|
23
|
+
* that takes a LambderCaller itself (a frontend store, a shared client
|
|
24
|
+
* module): handed this one, it talks to the real server in this process.
|
|
25
|
+
* An answer's logList is not printed; it is on the outcome.
|
|
26
|
+
*/
|
|
27
|
+
caller;
|
|
28
|
+
/** The payload on success, `undefined` on a failure: LambderCaller.api, through this visitor. */
|
|
29
|
+
api;
|
|
30
|
+
/**
|
|
31
|
+
* The full outcome, never throwing: LambderCaller.apiOutcome, through
|
|
32
|
+
* this visitor. Pair it with assertApiSuccess / assertApiFailure.
|
|
33
|
+
*
|
|
34
|
+
* When the app crashed answering the call, the outcome is the `server`
|
|
35
|
+
* failure any client would get, whose error says only "Request failed:
|
|
36
|
+
* 500"; here that error's `cause` is what the app actually threw, stack
|
|
37
|
+
* included, so a failing test points at the line in the handler.
|
|
38
|
+
*/
|
|
39
|
+
apiOutcome;
|
|
40
|
+
wiring;
|
|
41
|
+
headers;
|
|
42
|
+
cookieJar;
|
|
43
|
+
seenResetCount;
|
|
44
|
+
constructor(wiring, options) {
|
|
45
|
+
this.wiring = wiring;
|
|
46
|
+
this.host = options.host;
|
|
47
|
+
this.clientIp = options.clientIp;
|
|
48
|
+
this.headers = options.headers ?? {};
|
|
49
|
+
this.cookieJar = new LambderCookieJar({ host: this.host });
|
|
50
|
+
this.seenResetCount = wiring.resetCount();
|
|
51
|
+
const handlerTransport = lambderHandlerTransport(wiring.handler, { host: this.host, clientIp: this.clientIp, eventFormat: wiring.eventFormat });
|
|
52
|
+
const withVisitorHeaders = (request) => handlerTransport({ ...request, headers: { ...this.headers, ...request.headers } });
|
|
53
|
+
this.caller = new LambderCaller({
|
|
54
|
+
apiPath: wiring.apiPath,
|
|
55
|
+
apiVersion: options.apiVersion,
|
|
56
|
+
apiSignatures: options.apiSignatures,
|
|
57
|
+
guardInputsProvider: options.guardInputsProvider,
|
|
58
|
+
logListHandler: () => { },
|
|
59
|
+
// The jar is read per call rather than captured, so a call made
|
|
60
|
+
// after the test app was reset carries none of the old cookies.
|
|
61
|
+
// The visitor's session lives in that jar, so a CSRF token the
|
|
62
|
+
// caller read from a page's document.cookie (a test with a DOM)
|
|
63
|
+
// is dropped and the jar's own is posted.
|
|
64
|
+
transport: (request) => lambderCookieJarTransport(withVisitorHeaders, { jar: this.jar, host: this.host })({ ...request, token: "" }),
|
|
65
|
+
});
|
|
66
|
+
// The caller names the CSRF cookie the jar transport fills its token
|
|
67
|
+
// from, so an app with custom session cookie names has to be matched
|
|
68
|
+
// here or every session call would post an empty token.
|
|
69
|
+
if (wiring.sessionCookieNames) {
|
|
70
|
+
this.caller.setSessionCookieKey(wiring.sessionCookieNames.tokenCookieKey, wiring.sessionCookieNames.csrfCookieKey);
|
|
71
|
+
}
|
|
72
|
+
this.api = this.caller.api.bind(this.caller);
|
|
73
|
+
this.apiOutcome = (async (...callArgs) => {
|
|
74
|
+
const { result: outcome, crash } = await wiring.watchCrash(() => this.caller.apiOutcome(...callArgs));
|
|
75
|
+
if (crash && !outcome.ok && "error" in outcome && outcome.error.cause === undefined)
|
|
76
|
+
outcome.error.cause = crash;
|
|
77
|
+
return outcome;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* This visitor's cookies, to inspect or clear; every call and request
|
|
82
|
+
* reads and fills them. The test app's reset() empties them, noticed here
|
|
83
|
+
* the next time anything asks: a jar still holding the token of an
|
|
84
|
+
* emptied store would read as signed in until an answer said otherwise.
|
|
85
|
+
*/
|
|
86
|
+
get jar() {
|
|
87
|
+
const resetCount = this.wiring.resetCount();
|
|
88
|
+
if (resetCount !== this.seenResetCount) {
|
|
89
|
+
this.cookieJar.clear();
|
|
90
|
+
this.seenResetCount = resetCount;
|
|
91
|
+
}
|
|
92
|
+
return this.cookieJar;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* One HTTP request to the app that is not an API call, answered by
|
|
96
|
+
* whatever answers it in production: a route, a session route, the public
|
|
97
|
+
* files, the index page, a fallback. The answer comes back decoded
|
|
98
|
+
* (decompressed, headers lowercased), redirects are not followed, and
|
|
99
|
+
* its Set-Cookie headers land in this visitor's jar.
|
|
100
|
+
*
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const page = await visitor.request("GET", "/orders?page=2");
|
|
103
|
+
* expect(page.statusCode).toBe(200);
|
|
104
|
+
* expect(page.text()).toContain("Your orders");
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
async request(method, path, init = {}) {
|
|
108
|
+
const queryStart = path.indexOf("?");
|
|
109
|
+
const pathname = queryStart === -1 ? path : path.slice(0, queryStart);
|
|
110
|
+
const query = {
|
|
111
|
+
...(queryStart === -1 ? {} : Object.fromEntries(new URLSearchParams(path.slice(queryStart + 1)))),
|
|
112
|
+
...init.query,
|
|
113
|
+
};
|
|
114
|
+
const cookieScope = { host: this.host, path: pathname };
|
|
115
|
+
const event = synthesizeLambdaHttpEvent({
|
|
116
|
+
method,
|
|
117
|
+
path: pathname,
|
|
118
|
+
query,
|
|
119
|
+
host: this.host,
|
|
120
|
+
headers: { ...this.headers, ...init.headers },
|
|
121
|
+
clientIp: this.clientIp,
|
|
122
|
+
cookies: this.jar.cookiePairs(cookieScope),
|
|
123
|
+
body: init.body,
|
|
124
|
+
}, { invoke: false, eventFormat: this.wiring.eventFormat });
|
|
125
|
+
const result = await decodeLambdaHttpResult(await this.wiring.handler(event, localLambdaContext("lambder-test")), DEFAULT_MAX_RESTORED_PAYLOAD_BYTES);
|
|
126
|
+
if (result.cookies.length)
|
|
127
|
+
this.jar.storeSetCookies(result.cookies, cookieScope);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Starts a session for this visitor without a login endpoint: minted by
|
|
132
|
+
* the app's own session model, under its own cookie options, and planted
|
|
133
|
+
* in this visitor's jar, so its next call is signed in. Returns the raw
|
|
134
|
+
* tokens too, as LambderMockApp.signIn does.
|
|
135
|
+
*
|
|
136
|
+
* Throws when the cookies do not stick. An app that scopes its session
|
|
137
|
+
* cookie to a domain (`cookie: { domain: ".example.com" }`) this visitor's
|
|
138
|
+
* host is not under writes a cookie a browser on that host would drop, and
|
|
139
|
+
* the jar drops it too; left silent, every later session call would
|
|
140
|
+
* answer sessionExpired with nothing to say why.
|
|
141
|
+
*/
|
|
142
|
+
async signIn(sessionKey, data, options = {}) {
|
|
143
|
+
if (!this.wiring.sessionCookieNames)
|
|
144
|
+
throw new Error("LambderTestVisitor: signIn() needs an app with sessions. Pass the session option to create().");
|
|
145
|
+
const { created, setCookies } = await this.wiring.issueSession(this.host, sessionKey, data, options.ttlSeconds);
|
|
146
|
+
this.jar.storeSetCookies(setCookies, { host: this.host });
|
|
147
|
+
if (this.jar.get(this.wiring.sessionCookieNames.tokenCookieKey, { host: this.host, includeHttpOnly: true }) === undefined) {
|
|
148
|
+
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
|
+
"The app most likely scopes its session cookie to a domain this host is not under: " +
|
|
150
|
+
"give the test app or this visitor a `host` the cookie domain covers.");
|
|
151
|
+
}
|
|
152
|
+
return created;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing entry point (`import ... from "lambder/testing"`).
|
|
3
|
+
*
|
|
4
|
+
* A real Lambder app under test, in this process: the instance an app
|
|
5
|
+
* already has, memory stores put under it in place, and simulated browsers
|
|
6
|
+
* in front of it, each with a typed caller and a cookie jar of its own.
|
|
7
|
+
* Server-only, like the root entry, and never part of a deployment by
|
|
8
|
+
* construction: nothing else imports it.
|
|
9
|
+
*
|
|
10
|
+
* The sibling of `lambder/mock`, which serves a contract from mock handlers
|
|
11
|
+
* for frontend work. This one runs the real handlers.
|
|
12
|
+
*/
|
|
13
|
+
export { lambderTestApp } from "./testing/LambderTestApp.js";
|
|
14
|
+
export type { LambderTestApp, LambderTestAppOptions, LambderTestedInstance } from "./testing/LambderTestApp.js";
|
|
15
|
+
export type { LambderTestVisitor, LambderTestVisitorOptions, LambderTestRequestInit } from "./testing/LambderTestVisitor.js";
|
|
16
|
+
export { assertApiSuccess, assertApiFailure } from "./shared/wire/LambderOutcomeAssertions.js";
|
|
17
|
+
export type { LambderExpectedFailure } from "./shared/wire/LambderOutcomeAssertions.js";
|
|
18
|
+
export { LambderMemorySessionStore } from "./stores/LambderMemorySessionStore.js";
|
|
19
|
+
export { LambderMemoryRateLimiter } from "./stores/LambderMemoryRateLimiter.js";
|
|
20
|
+
export { LambderMemoryIdempotencyStore } from "./stores/LambderMemoryIdempotencyStore.js";
|
|
21
|
+
export { LambderMemoryCache } from "./stores/LambderMemoryCache.js";
|
|
22
|
+
export { LambderLocalFileSource } from "./stores/LambderLocalFileSource.js";
|
|
23
|
+
export { LambderCookieJar } from "./shared/transport/LambderCookieJar.js";
|
|
24
|
+
export { LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
|
|
25
|
+
export type { LambderLambdaHttpResult } from "./invoke/LambderLambdaEvent.js";
|
|
26
|
+
export type { LambderCreatedSession } from "./session/LambderSessionManager.js";
|
|
27
|
+
export type { LambderApiOutcome, LambderApiFailureReason } from "./shared/wire/LambderApiOutcome.js";
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing entry point (`import ... from "lambder/testing"`).
|
|
3
|
+
*
|
|
4
|
+
* A real Lambder app under test, in this process: the instance an app
|
|
5
|
+
* already has, memory stores put under it in place, and simulated browsers
|
|
6
|
+
* in front of it, each with a typed caller and a cookie jar of its own.
|
|
7
|
+
* Server-only, like the root entry, and never part of a deployment by
|
|
8
|
+
* construction: nothing else imports it.
|
|
9
|
+
*
|
|
10
|
+
* The sibling of `lambder/mock`, which serves a contract from mock handlers
|
|
11
|
+
* for frontend work. This one runs the real handlers.
|
|
12
|
+
*/
|
|
13
|
+
export { lambderTestApp } from "./testing/LambderTestApp.js";
|
|
14
|
+
export { assertApiSuccess, assertApiFailure } from "./shared/wire/LambderOutcomeAssertions.js";
|
|
15
|
+
// What a test reaches for beside the test app: the stores to inspect or to
|
|
16
|
+
// hand it, a file source over fixtures, the refusal codes to assert on, and
|
|
17
|
+
// the types of what a visitor and its session hand back.
|
|
18
|
+
export { LambderMemorySessionStore } from "./stores/LambderMemorySessionStore.js";
|
|
19
|
+
export { LambderMemoryRateLimiter } from "./stores/LambderMemoryRateLimiter.js";
|
|
20
|
+
export { LambderMemoryIdempotencyStore } from "./stores/LambderMemoryIdempotencyStore.js";
|
|
21
|
+
export { LambderMemoryCache } from "./stores/LambderMemoryCache.js";
|
|
22
|
+
export { LambderLocalFileSource } from "./stores/LambderLocalFileSource.js";
|
|
23
|
+
export { LambderCookieJar } from "./shared/transport/LambderCookieJar.js";
|
|
24
|
+
export { LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.2",
|
|
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": [
|
|
@@ -49,6 +49,14 @@
|
|
|
49
49
|
"types": "./dist/mock.d.ts",
|
|
50
50
|
"default": "./dist/mock.js"
|
|
51
51
|
},
|
|
52
|
+
"./testing": {
|
|
53
|
+
"types": "./dist/testing.d.ts",
|
|
54
|
+
"default": "./dist/testing.js"
|
|
55
|
+
},
|
|
56
|
+
"./build": {
|
|
57
|
+
"types": "./dist/build.d.ts",
|
|
58
|
+
"default": "./dist/build.js"
|
|
59
|
+
},
|
|
52
60
|
"./package.json": "./package.json"
|
|
53
61
|
},
|
|
54
62
|
"typesVersions": {
|
|
@@ -58,14 +66,23 @@
|
|
|
58
66
|
],
|
|
59
67
|
"mock": [
|
|
60
68
|
"./dist/mock.d.ts"
|
|
69
|
+
],
|
|
70
|
+
"testing": [
|
|
71
|
+
"./dist/testing.d.ts"
|
|
72
|
+
],
|
|
73
|
+
"build": [
|
|
74
|
+
"./dist/build.d.ts"
|
|
61
75
|
]
|
|
62
76
|
}
|
|
63
77
|
},
|
|
64
78
|
"browser": {
|
|
79
|
+
"async_hooks": false,
|
|
80
|
+
"child_process": false,
|
|
65
81
|
"fs": false,
|
|
66
82
|
"path": false,
|
|
67
83
|
"zlib": false,
|
|
68
|
-
"crypto": false
|
|
84
|
+
"crypto": false,
|
|
85
|
+
"url": false
|
|
69
86
|
},
|
|
70
87
|
"files": [
|
|
71
88
|
"dist",
|
|
@@ -93,7 +110,7 @@
|
|
|
93
110
|
"tough-cookie": "^6.0.2"
|
|
94
111
|
},
|
|
95
112
|
"peerDependencies": {
|
|
96
|
-
"@aws-sdk/client-dynamodb": "^3.
|
|
113
|
+
"@aws-sdk/client-dynamodb": "^3.868.0",
|
|
97
114
|
"@aws-sdk/client-lambda": "^3.574.0",
|
|
98
115
|
"@aws-sdk/client-s3": "^3.574.0",
|
|
99
116
|
"@aws-sdk/lib-dynamodb": "^3.574.0",
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { LambderApiRequest } from "./LambderApiRequest.js";
|
|
2
|
-
import type { LambderApiCallContext, LambderApiCallTrace } from "./LambderApiCallContext.js";
|
|
3
|
-
import type { LambderApiAnswer } from "./LambderApiAnswer.js";
|
|
4
|
-
import type { LambderApiDefinition } from "./LambderApiDefinition.js";
|
|
5
|
-
import { type LambderApiGuard } from "./LambderApiGuards.js";
|
|
6
|
-
import { type LambderApiRateLimitPolicyConfig, type LambderApiRateLimitsConfig } from "./LambderApiRateLimits.js";
|
|
7
|
-
import { type LambderApiIdempotencyConfig } from "./LambderApiIdempotency.js";
|
|
8
|
-
/**
|
|
9
|
-
* Runtime side of the declarative API options: composes the three policy
|
|
10
|
-
* subsystems (rate limits in ./LambderApiRateLimits.ts, guards in
|
|
11
|
-
* ./LambderApiGuards.ts, idempotency in ./LambderApiIdempotency.ts), asserts
|
|
12
|
-
* registrations against them at startup, and executes them around handlers
|
|
13
|
-
* at request time. Owned by LambderApiPipeline; apps interact through the
|
|
14
|
-
* create() options (rateLimits, guards, idempotency) and the per-API
|
|
15
|
-
* options.
|
|
16
|
-
*/
|
|
17
|
-
export declare class LambderApiPolicyEngine {
|
|
18
|
-
private rateLimits;
|
|
19
|
-
private guards;
|
|
20
|
-
private idempotency;
|
|
21
|
-
/** True once any of the three subsystems was configured. */
|
|
22
|
-
get isConfigured(): boolean;
|
|
23
|
-
configureRateLimits(config: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig>>): void;
|
|
24
|
-
configureGuards(guards: Record<string, LambderApiGuard<any, any, any>>): void;
|
|
25
|
-
configureIdempotency(config: LambderApiIdempotencyConfig): void;
|
|
26
|
-
/** Startup validation of one API's declarative options. */
|
|
27
|
-
assertRegistration(definition: LambderApiDefinition): void;
|
|
28
|
-
/** The rate-limit policies that can be checked before the session is read: see LambderApiRateLimitsEngine.run. */
|
|
29
|
-
runSessionlessRateLimits(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition): Promise<void>;
|
|
30
|
-
/** The remaining rate limits, then guards, in declared order. Refusals throw; the trace records each guard as it runs. */
|
|
31
|
-
runPreflight(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition, trace: LambderApiCallTrace): Promise<void>;
|
|
32
|
-
/** Idempotency replay fast path, run before the preflight: see LambderApiIdempotencyEngine.findReplay. */
|
|
33
|
-
findReplay(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition, trace: LambderApiCallTrace): Promise<LambderApiAnswer | null>;
|
|
34
|
-
/** Idempotency claim/replay wrapper around handler execution: see LambderApiIdempotencyEngine.withIdempotency. */
|
|
35
|
-
withIdempotency(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition, trace: LambderApiCallTrace, exec: () => Promise<LambderApiAnswer>): Promise<LambderApiAnswer>;
|
|
36
|
-
}
|