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.
Files changed (35) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +4 -2
  3. package/dist/api/LambderApiIdempotency.d.ts +7 -0
  4. package/dist/api/LambderApiIdempotency.js +12 -0
  5. package/dist/api/LambderApiPipeline.d.ts +30 -1
  6. package/dist/api/LambderApiPipeline.js +14 -0
  7. package/dist/api/LambderApiPolicyEngine.d.ts +11 -0
  8. package/dist/api/LambderApiPolicyEngine.js +8 -0
  9. package/dist/api/LambderApiRateLimits.d.ts +7 -0
  10. package/dist/api/LambderApiRateLimits.js +12 -0
  11. package/dist/core/Lambder.d.ts +35 -1
  12. package/dist/core/Lambder.js +32 -1
  13. package/dist/core/LambderFiles.d.ts +7 -0
  14. package/dist/core/LambderFiles.js +12 -0
  15. package/dist/index.d.ts +1 -1
  16. package/dist/invoke/LambderLambdaEvent.d.ts +23 -9
  17. package/dist/invoke/LambderLambdaEvent.js +41 -16
  18. package/dist/invoke/lambderHandlerTransport.d.ts +3 -0
  19. package/dist/invoke/lambderHandlerTransport.js +1 -1
  20. package/dist/mock.d.ts +2 -0
  21. package/dist/mock.js +3 -0
  22. package/dist/session/LambderSessionManager.d.ts +12 -1
  23. package/dist/session/LambderSessionManager.js +19 -3
  24. package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
  25. package/dist/shared/util/LambderTestingDoors.js +29 -0
  26. package/dist/shared/wire/LambderApiContract.d.ts +50 -0
  27. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +80 -0
  28. package/dist/shared/wire/LambderOutcomeAssertions.js +113 -0
  29. package/dist/testing/LambderTestApp.d.ts +178 -0
  30. package/dist/testing/LambderTestApp.js +206 -0
  31. package/dist/testing/LambderTestVisitor.d.ts +155 -0
  32. package/dist/testing/LambderTestVisitor.js +154 -0
  33. package/dist/testing.d.ts +26 -0
  34. package/dist/testing.js +23 -0
  35. 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";
@@ -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.2.4",
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,