lambder 7.2.4 → 7.3.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 +88 -0
- package/README.md +4 -2
- package/dist/api/LambderApiIdempotency.d.ts +7 -0
- package/dist/api/LambderApiIdempotency.js +12 -0
- package/dist/api/LambderApiPipeline.d.ts +30 -1
- package/dist/api/LambderApiPipeline.js +14 -0
- package/dist/api/LambderApiPolicyEngine.d.ts +11 -0
- package/dist/api/LambderApiPolicyEngine.js +8 -0
- package/dist/api/LambderApiRateLimits.d.ts +7 -0
- package/dist/api/LambderApiRateLimits.js +12 -0
- package/dist/core/Lambder.d.ts +35 -1
- package/dist/core/Lambder.js +32 -1
- package/dist/core/LambderFiles.d.ts +7 -0
- package/dist/core/LambderFiles.js +12 -0
- package/dist/index.d.ts +1 -1
- package/dist/invoke/LambderLambdaEvent.d.ts +23 -9
- package/dist/invoke/LambderLambdaEvent.js +41 -16
- package/dist/invoke/lambderHandlerTransport.d.ts +3 -0
- package/dist/invoke/lambderHandlerTransport.js +1 -1
- package/dist/mock.d.ts +2 -0
- package/dist/mock.js +3 -0
- package/dist/session/LambderSessionManager.d.ts +12 -1
- package/dist/session/LambderSessionManager.js +19 -3
- package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
- package/dist/shared/util/LambderTestingDoors.js +29 -0
- package/dist/shared/wire/LambderApiContract.d.ts +50 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +80 -0
- package/dist/shared/wire/LambderOutcomeAssertions.js +113 -0
- package/dist/testing/LambderTestApp.d.ts +178 -0
- package/dist/testing/LambderTestApp.js +206 -0
- package/dist/testing/LambderTestVisitor.d.ts +155 -0
- package/dist/testing/LambderTestVisitor.js +154 -0
- package/dist/testing.d.ts +26 -0
- package/dist/testing.js +23 -0
- package/package.json +9 -1
|
@@ -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
|
+
* One thing is added to what the caller hands back. When the app crashed
|
|
35
|
+
* answering the call, the outcome is the `server` failure any client
|
|
36
|
+
* would get, whose error says "Request failed: 500" and nothing else;
|
|
37
|
+
* here that error's `cause` is what the app actually threw, stack
|
|
38
|
+
* included, so a failing test points at the line in the handler.
|
|
39
|
+
*/
|
|
40
|
+
apiOutcome;
|
|
41
|
+
wiring;
|
|
42
|
+
headers;
|
|
43
|
+
cookieJar;
|
|
44
|
+
seenResetCount;
|
|
45
|
+
constructor(wiring, options) {
|
|
46
|
+
this.wiring = wiring;
|
|
47
|
+
this.host = options.host;
|
|
48
|
+
this.clientIp = options.clientIp;
|
|
49
|
+
this.headers = options.headers ?? {};
|
|
50
|
+
this.cookieJar = new LambderCookieJar({ host: this.host });
|
|
51
|
+
this.seenResetCount = wiring.resetCount();
|
|
52
|
+
const handlerTransport = lambderHandlerTransport(wiring.handler, { host: this.host, clientIp: this.clientIp, eventFormat: wiring.eventFormat });
|
|
53
|
+
const withVisitorHeaders = (request) => handlerTransport({ ...request, headers: { ...this.headers, ...request.headers } });
|
|
54
|
+
this.caller = new LambderCaller({
|
|
55
|
+
apiPath: wiring.apiPath,
|
|
56
|
+
isCorsEnabled: false,
|
|
57
|
+
apiVersion: options.apiVersion,
|
|
58
|
+
apiSignatures: options.apiSignatures,
|
|
59
|
+
guardInputsProvider: options.guardInputsProvider,
|
|
60
|
+
logListHandler: () => { },
|
|
61
|
+
// The jar is read per call rather than captured, so a call made
|
|
62
|
+
// after the test app was reset carries none of the old cookies.
|
|
63
|
+
transport: (request) => lambderCookieJarTransport(withVisitorHeaders, { jar: this.jar, host: this.host })(request),
|
|
64
|
+
});
|
|
65
|
+
// The caller names the CSRF cookie the jar transport fills its token
|
|
66
|
+
// from, so an app that renamed its session cookies has to be matched
|
|
67
|
+
// here or every session call would post an empty token.
|
|
68
|
+
if (wiring.sessionCookieNames) {
|
|
69
|
+
this.caller.setSessionCookieKey(wiring.sessionCookieNames.tokenCookieKey, wiring.sessionCookieNames.csrfCookieKey);
|
|
70
|
+
}
|
|
71
|
+
this.api = this.caller.api.bind(this.caller);
|
|
72
|
+
this.apiOutcome = (async (...callArgs) => {
|
|
73
|
+
const { result: outcome, crash } = await wiring.watchCrash(() => this.caller.apiOutcome(...callArgs));
|
|
74
|
+
if (crash && !outcome.ok && "error" in outcome && outcome.error.cause === undefined)
|
|
75
|
+
outcome.error.cause = crash;
|
|
76
|
+
return outcome;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* This visitor's cookies, to inspect or clear; every call and request
|
|
81
|
+
* reads and fills them. Emptied by the test app's reset(), which a
|
|
82
|
+
* visitor notices here, the next time anything asks for its cookies: a
|
|
83
|
+
* jar still holding the token of an emptied store would read as signed
|
|
84
|
+
* 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" }`) writes one
|
|
138
|
+
* this visitor's host is not under, a browser on that host would drop it,
|
|
139
|
+
* and so does the jar; left silent, every session call after it answers
|
|
140
|
+
* 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,26 @@
|
|
|
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 { LambderLocalFileSource } from "./stores/LambderLocalFileSource.js";
|
|
22
|
+
export { LambderCookieJar } from "./shared/transport/LambderCookieJar.js";
|
|
23
|
+
export { LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
|
|
24
|
+
export type { LambderLambdaHttpResult } from "./invoke/LambderLambdaEvent.js";
|
|
25
|
+
export type { LambderCreatedSession } from "./session/LambderSessionManager.js";
|
|
26
|
+
export type { LambderApiOutcome, LambderApiFailureReason } from "./shared/wire/LambderApiOutcome.js";
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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 { LambderLocalFileSource } from "./stores/LambderLocalFileSource.js";
|
|
22
|
+
export { LambderCookieJar } from "./shared/transport/LambderCookieJar.js";
|
|
23
|
+
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": "7.
|
|
3
|
+
"version": "7.3.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": [
|
|
@@ -49,6 +49,10 @@
|
|
|
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
|
+
},
|
|
52
56
|
"./package.json": "./package.json"
|
|
53
57
|
},
|
|
54
58
|
"typesVersions": {
|
|
@@ -58,10 +62,14 @@
|
|
|
58
62
|
],
|
|
59
63
|
"mock": [
|
|
60
64
|
"./dist/mock.d.ts"
|
|
65
|
+
],
|
|
66
|
+
"testing": [
|
|
67
|
+
"./dist/testing.d.ts"
|
|
61
68
|
]
|
|
62
69
|
}
|
|
63
70
|
},
|
|
64
71
|
"browser": {
|
|
72
|
+
"async_hooks": false,
|
|
65
73
|
"fs": false,
|
|
66
74
|
"path": false,
|
|
67
75
|
"zlib": false,
|