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,178 @@
1
+ import type { Context } from "aws-lambda";
2
+ import type Lambder from "../core/Lambder.js";
3
+ import { type LambderHttpEventFormat } from "../core/LambderContext.js";
4
+ import type { LambderHandler } from "../core/LambderCreateOptions.js";
5
+ import type LambderSessionManager from "../session/LambderSessionManager.js";
6
+ import type { LambderFileSource } from "../shared/contracts/LambderFileSource.js";
7
+ import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
8
+ import type { LambderRateLimiter } from "../shared/contracts/LambderRateLimiter.js";
9
+ import type { LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
10
+ import type { LambderApiContractShape } from "../shared/wire/LambderApiContract.js";
11
+ import { LambderTestVisitor, type LambderTestVisitorArgs, type LambderTestVisitorOptions } from "./LambderTestVisitor.js";
12
+ /**
13
+ * What a test app may be told. Nothing is required: `lambderTestApp(lambder)`
14
+ * is a working test app over memory stores.
15
+ *
16
+ * The stores sit where create() takes them (`session.store`,
17
+ * `rateLimits.limiter`, `idempotency.store`, `files`), naming only the part a
18
+ * test replaces; everything else the app configured there stays in force.
19
+ */
20
+ export type LambderTestAppOptions = {
21
+ /** The host visitors browse unless they name their own. Default: "localhost". An app that scopes its session cookie to a domain needs a host under it. */
22
+ host?: string;
23
+ /**
24
+ * The gateway shape the handler is called with: "v2" (an HTTP API, a
25
+ * Function URL) or "v1" (a REST API). Default: "v2". A handler answers
26
+ * both alike through its context, so this matters to code that reads the
27
+ * raw `ctx.event`, and to anyone who wants the suite to run on exactly
28
+ * what production delivers.
29
+ */
30
+ eventFormat?: LambderHttpEventFormat;
31
+ /** Default: a fresh LambderMemorySessionStore. Pass your own to run the suite over another store (DynamoDB Local, say). */
32
+ session?: {
33
+ store?: LambderSessionStore<any>;
34
+ };
35
+ /** Default: a fresh LambderMemoryRateLimiter. */
36
+ rateLimits?: {
37
+ limiter?: LambderRateLimiter;
38
+ };
39
+ /** Default: a fresh LambderMemoryIdempotencyStore. */
40
+ idempotency?: {
41
+ store?: LambderIdempotencyStore;
42
+ };
43
+ /** Default: the app's own source, which suits one that reads a local folder. Pass a LambderLocalFileSource over fixtures for an app whose production source is S3 or HTTP. */
44
+ files?: LambderFileSource;
45
+ };
46
+ /**
47
+ * A Lambder instance as a test app takes it: any instance, read for its
48
+ * session data type and, through the ApiContract property rather than the
49
+ * class parameter, for its contract. The property is what lets a large app
50
+ * name its flattened contract interface explicitly
51
+ * (`lambderTestApp<SessionData, ApiContractType>(lambder)`) and keep the
52
+ * cheap type check that interface exists for.
53
+ */
54
+ export type LambderTestedInstance<TSessionData, TContract> = Lambder<TSessionData, any, any, any, any, any, any, any> & {
55
+ readonly ApiContract: TContract;
56
+ };
57
+ /**
58
+ * A real Lambder app under test: the instance an app already has, with memory
59
+ * stores put under it in place, and as many simulated browsers in front of it
60
+ * as a test needs. No HTTP, no AWS, and nothing in the app restructured.
61
+ *
62
+ * The app's own declarations all run as written: its guards, its named
63
+ * rate-limit policies, its idempotency settings, its session salt, cookie
64
+ * options and dataRefresh, its hooks and error handlers. Only where things
65
+ * rest is replaced, and from the moment this is created the stores the app
66
+ * was configured with are out of the instance's reach, so a test cannot touch
67
+ * a production table even by mistake. What the app reaches on its own (its
68
+ * database, a mailer) is the app's to replace.
69
+ *
70
+ * The sibling of LambderMockApp, which serves a contract from mock handlers:
71
+ * the same verbs (`signIn`, `signOut`, `expireSessionData`, `reset`) over the
72
+ * real handlers instead.
73
+ *
74
+ * Time is not this class's: fake `Date` with the test runner
75
+ * (`vi.useFakeTimers({ toFake: ["Date"] })`), which moves the framework, the
76
+ * memory stores and the app's own handlers together.
77
+ *
78
+ * One test app per instance: a second one puts its own stores under the same
79
+ * instance and takes over its crash watch, so the first stops seeing either.
80
+ */
81
+ export declare class LambderTestApp<TContract extends LambderApiContractShape = any, TSessionData = any> {
82
+ /** The instance's handler: what `event()` and every visitor call. */
83
+ readonly handler: LambderHandler;
84
+ /** The host visitors browse unless they name their own. */
85
+ readonly host: string;
86
+ /** The session store now under the instance, for assertions; null when the app has no sessions. A LambderMemorySessionStore unless one was given. */
87
+ readonly sessionStore: LambderSessionStore<TSessionData> | null;
88
+ /** The rate limiter now under the instance; null when the app declares no rate limits. */
89
+ readonly rateLimiter: LambderRateLimiter | null;
90
+ /** The idempotency store now under the instance; null when the app has no idempotency. */
91
+ readonly idempotencyStore: LambderIdempotencyStore | null;
92
+ private readonly lambder;
93
+ private readonly wiring;
94
+ /** 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. */
95
+ private readonly ownStores;
96
+ private visitorCount;
97
+ private resetCount;
98
+ private readonly crashList;
99
+ /**
100
+ * The call a crash happened under. The app answers a crash with a 500
101
+ * that says nothing about it, so the error has to travel beside the
102
+ * answer, and with calls running concurrently (a duplicate sent while
103
+ * the original is in flight is an ordinary idempotency test) only the
104
+ * async context says which call a crash belongs to.
105
+ */
106
+ private readonly crashScope;
107
+ constructor(lambder: LambderTestedInstance<TSessionData, TContract>, options?: LambderTestAppOptions);
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 themselves say nothing about
112
+ * what was thrown, so this is where a test reads it, and
113
+ * `expect(app.crashes).toEqual([])` is how one says nothing crashed.
114
+ * A refusal is not a crash, and neither is an error an `event()` rejects
115
+ * with, which the test already holds.
116
+ */
117
+ get crashes(): readonly Error[];
118
+ /** The session manager, for tests that inspect or manipulate sessions directly. Throws when the app has no sessions. */
119
+ get sessionManager(): LambderSessionManager<TSessionData>;
120
+ /**
121
+ * A new simulated browser: its own cookie jar, and its own address unless
122
+ * one is given. A stranger until it signs in, through the app's own login
123
+ * API or through `signIn`.
124
+ */
125
+ visitor<TProvidedGuards extends string = never>(...[options]: LambderTestVisitorArgs<LambderTestVisitorOptions<TContract, TProvidedGuards>, TProvidedGuards>): LambderTestVisitor<TContract, TSessionData, TProvidedGuards>;
126
+ /**
127
+ * A new visitor, already signed in: `visitor()` followed by its
128
+ * `signIn()`. The session is minted by the app's own session model, so
129
+ * no login endpoint has to exist or be called.
130
+ *
131
+ * ```typescript
132
+ * const admin = await app.signIn("user:ada", { userId: "ada", role: "admin" });
133
+ * expect(await admin.api("org.rename", { name })).toEqual({ ok: true });
134
+ * ```
135
+ */
136
+ signIn<TProvidedGuards extends string = never>(sessionKey: string, data: TSessionData, ...[options]: LambderTestVisitorArgs<LambderTestVisitorOptions<TContract, TProvidedGuards> & {
137
+ ttlSeconds?: number;
138
+ }, TProvidedGuards>): Promise<LambderTestVisitor<TContract, TSessionData, TProvidedGuards>>;
139
+ /**
140
+ * Ends every session of the subject, the way "log out everywhere" does.
141
+ * A visitor signed in as that subject keeps its cookies, as a browser
142
+ * would, so its next call is what a real one's would be: answered
143
+ * sessionExpired, with the stale cookies evicted.
144
+ */
145
+ signOut(sessionKey: string): Promise<void>;
146
+ /** Marks the subject's session data stale, so the next read renews it through the app's dataRefresh. */
147
+ expireSessionData(sessionKey: string): Promise<void>;
148
+ /**
149
+ * Hands the handler an event that is not an HTTP request (a schedule, an
150
+ * SNS or SQS delivery), which is how an addAction handler runs, with a
151
+ * Lambda context filled in. Resolves to whatever the action returned.
152
+ *
153
+ * ```typescript
154
+ * await app.event({ source: "aws.events", "detail-type": "Scheduled Event" });
155
+ * ```
156
+ */
157
+ event(event: unknown, context?: Partial<Context>): Promise<unknown>;
158
+ /**
159
+ * Rewinds what accumulated: sessions, rate-limit counters and replay
160
+ * records in the stores this test app made, the crashes it recorded, and
161
+ * the cookies of every visitor it created (each empties its jar the next
162
+ * time it is used). For a beforeEach. The app's own data (its database)
163
+ * is the app's to rewind.
164
+ */
165
+ reset(): void;
166
+ }
167
+ /**
168
+ * Puts a built Lambder instance under test. See LambderTestApp.
169
+ *
170
+ * ```typescript
171
+ * import { lambderTestApp } from "lambder/testing";
172
+ * import { lambder } from "../src/index.js";
173
+ *
174
+ * const app = lambderTestApp(lambder);
175
+ * beforeEach(() => app.reset());
176
+ * ```
177
+ */
178
+ export declare const lambderTestApp: <TSessionData = any, TContract extends LambderApiContractShape = any>(lambder: LambderTestedInstance<TSessionData, TContract>, options?: LambderTestAppOptions) => LambderTestApp<TContract, TSessionData>;
@@ -0,0 +1,206 @@
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: its guards, its named
16
+ * rate-limit policies, its idempotency settings, its session salt, cookie
17
+ * options and dataRefresh, its hooks and error handlers. Only where things
18
+ * rest is replaced, and from the moment this is created the stores the app
19
+ * was configured with are out of the instance's reach, so a test cannot touch
20
+ * a production table even by mistake. What the app reaches on its own (its
21
+ * database, a mailer) is the app's to 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 instead.
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 answers a crash with a 500
54
+ * that says nothing about it, so the error has to travel beside the
55
+ * answer, and with calls running concurrently (a duplicate sent while
56
+ * the original is in flight is an ordinary idempotency test) only the
57
+ * async context says which call a crash belongs to.
58
+ */
59
+ crashScope = new AsyncLocalStorage();
60
+ constructor(lambder, options = {}) {
61
+ // Only a Lambder instance of this same package copy answers to the
62
+ // key, so the failure is said here rather than as "is not a function".
63
+ if (typeof lambder?.[LAMBDER_BACKEND_SWAP] !== "function") {
64
+ throw new Error("lambderTestApp: expected a Lambder instance (what initLambder().create() returns), from the same installed copy of lambder as lambder/testing.");
65
+ }
66
+ const own = (store) => { this.ownStores.push(store); return store; };
67
+ const sessionStore = options.session?.store ?? own(new LambderMemorySessionStore());
68
+ const rateLimiter = options.rateLimits?.limiter ?? own(new LambderMemoryRateLimiter());
69
+ const idempotencyStore = options.idempotency?.store ?? own(new LambderMemoryIdempotencyStore());
70
+ const swap = lambder[LAMBDER_BACKEND_SWAP]({ sessionStore, rateLimiter, idempotencyStore, fileSource: options.files });
71
+ if (options.files && !swap.files) {
72
+ 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.");
73
+ }
74
+ lambder[LAMBDER_CRASH_WATCH]((error) => {
75
+ this.crashList.push(error);
76
+ const scope = this.crashScope.getStore();
77
+ if (scope)
78
+ scope.crash = error;
79
+ });
80
+ this.lambder = lambder;
81
+ this.handler = lambder.getHandler();
82
+ this.host = options.host ?? "localhost";
83
+ this.sessionStore = swap.sessions ? sessionStore : null;
84
+ this.rateLimiter = swap.rateLimits ? rateLimiter : null;
85
+ this.idempotencyStore = swap.idempotency ? idempotencyStore : null;
86
+ this.wiring = {
87
+ handler: this.handler,
88
+ apiPath: lambder.apiPath,
89
+ eventFormat: options.eventFormat ?? "v2",
90
+ sessionCookieNames: swap.sessions,
91
+ resetCount: () => this.resetCount,
92
+ watchCrash: async (run) => {
93
+ const scope = { crash: null };
94
+ return { result: await this.crashScope.run(scope, run), crash: scope.crash };
95
+ },
96
+ issueSession: async (host, sessionKey, data, ttlSeconds) => {
97
+ // Through a session controller on a request context, the way a
98
+ // login handler does it, so the cookies are the ones the app's
99
+ // own cookie options produce for that host.
100
+ const event = synthesizeLambdaHttpEvent({ method: "GET", path: "/", host }, { invoke: false });
101
+ const ctx = createContext(event, localLambdaContext("lambder-test"), lambder.apiPath);
102
+ const created = await lambder.getSessionController(ctx).issueSession(sessionKey, data, ttlSeconds);
103
+ const headers = {};
104
+ ctx.responseHeaders.applyInto(headers);
105
+ return { created, setCookies: getAnswerHeader(headers, "Set-Cookie") ?? [] };
106
+ },
107
+ };
108
+ }
109
+ /**
110
+ * Every error the app threw while answering a request since the last
111
+ * reset, in order: what reached its global error handler, or the
112
+ * framework's last-resort 500. The answers themselves say nothing about
113
+ * what was thrown, so this is where a test reads it, and
114
+ * `expect(app.crashes).toEqual([])` is how one says nothing crashed.
115
+ * A refusal is not a crash, and neither is an error an `event()` rejects
116
+ * with, which the test already holds.
117
+ */
118
+ get crashes() {
119
+ return this.crashList;
120
+ }
121
+ /** The session manager, for tests that inspect or manipulate sessions directly. Throws when the app has no sessions. */
122
+ get sessionManager() {
123
+ return this.lambder.getSessionManager();
124
+ }
125
+ /**
126
+ * A new simulated browser: its own cookie jar, and its own address unless
127
+ * one is given. A stranger until it signs in, through the app's own login
128
+ * API or through `signIn`.
129
+ */
130
+ visitor(...[options]) {
131
+ this.visitorCount += 1;
132
+ return new LambderTestVisitor(this.wiring, {
133
+ ...options,
134
+ host: options?.host ?? this.host,
135
+ // One private address per visitor, counted up and never handed out
136
+ // twice, so no two visitors share a per-ip counter by accident.
137
+ clientIp: options?.clientIp ?? `10.${(this.visitorCount >> 16) & 255}.${(this.visitorCount >> 8) & 255}.${this.visitorCount & 255}`,
138
+ });
139
+ }
140
+ /**
141
+ * A new visitor, already signed in: `visitor()` followed by its
142
+ * `signIn()`. The session is minted by the app's own session model, so
143
+ * no login endpoint has to exist or be called.
144
+ *
145
+ * ```typescript
146
+ * const admin = await app.signIn("user:ada", { userId: "ada", role: "admin" });
147
+ * expect(await admin.api("org.rename", { name })).toEqual({ ok: true });
148
+ * ```
149
+ */
150
+ async signIn(sessionKey, data, ...[options]) {
151
+ const { ttlSeconds, ...visitorOptions } = (options ?? {});
152
+ const visitor = this.visitor(...[visitorOptions]);
153
+ await visitor.signIn(sessionKey, data, { ttlSeconds });
154
+ return visitor;
155
+ }
156
+ /**
157
+ * Ends every session of the subject, the way "log out everywhere" does.
158
+ * A visitor signed in as that subject keeps its cookies, as a browser
159
+ * would, so its next call is what a real one's would be: answered
160
+ * sessionExpired, with the stale cookies evicted.
161
+ */
162
+ async signOut(sessionKey) {
163
+ await this.sessionManager.deleteSessionAllByKey(sessionKey);
164
+ }
165
+ /** Marks the subject's session data stale, so the next read renews it through the app's dataRefresh. */
166
+ async expireSessionData(sessionKey) {
167
+ await this.sessionManager.expireSessionDataAllByKey(sessionKey);
168
+ }
169
+ /**
170
+ * Hands the handler an event that is not an HTTP request (a schedule, an
171
+ * SNS or SQS delivery), which is how an addAction handler runs, with a
172
+ * Lambda context filled in. Resolves to whatever the action returned.
173
+ *
174
+ * ```typescript
175
+ * await app.event({ source: "aws.events", "detail-type": "Scheduled Event" });
176
+ * ```
177
+ */
178
+ async event(event, context = {}) {
179
+ return await this.handler(event, localLambdaContext("lambder-test", context));
180
+ }
181
+ /**
182
+ * Rewinds what accumulated: sessions, rate-limit counters and replay
183
+ * records in the stores this test app made, the crashes it recorded, and
184
+ * the cookies of every visitor it created (each empties its jar the next
185
+ * time it is used). For a beforeEach. The app's own data (its database)
186
+ * is the app's to rewind.
187
+ */
188
+ reset() {
189
+ for (const store of this.ownStores)
190
+ store.reset();
191
+ this.crashList.length = 0;
192
+ this.resetCount += 1;
193
+ }
194
+ }
195
+ /**
196
+ * Puts a built Lambder instance under test. See LambderTestApp.
197
+ *
198
+ * ```typescript
199
+ * import { lambderTestApp } from "lambder/testing";
200
+ * import { lambder } from "../src/index.js";
201
+ *
202
+ * const app = lambderTestApp(lambder);
203
+ * beforeEach(() => app.reset());
204
+ * ```
205
+ */
206
+ export const lambderTestApp = (lambder, options = {}) => new LambderTestApp(lambder, options);
@@ -0,0 +1,155 @@
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
+ * One thing is added to what the caller hands back. When the app crashed
104
+ * answering the call, the outcome is the `server` failure any client
105
+ * would get, whose error says "Request failed: 500" and nothing else;
106
+ * here that error's `cause` is what the app actually threw, stack
107
+ * included, so a failing test points at the line in the handler.
108
+ */
109
+ readonly apiOutcome: LambderCaller<TContract, TProvidedGuards>["apiOutcome"];
110
+ private readonly wiring;
111
+ private readonly headers;
112
+ private readonly cookieJar;
113
+ private seenResetCount;
114
+ constructor(wiring: LambderTestVisitorWiring<TSessionData>, options: LambderTestVisitorOptions<TContract, TProvidedGuards> & {
115
+ host: string;
116
+ clientIp: string;
117
+ });
118
+ /**
119
+ * This visitor's cookies, to inspect or clear; every call and request
120
+ * reads and fills them. Emptied by the test app's reset(), which a
121
+ * visitor notices here, the next time anything asks for its cookies: a
122
+ * jar still holding the token of an emptied store would read as signed
123
+ * in until an answer said otherwise.
124
+ */
125
+ get jar(): LambderCookieJar;
126
+ /**
127
+ * One HTTP request to the app that is not an API call, answered by
128
+ * whatever answers it in production: a route, a session route, the public
129
+ * files, the index page, a fallback. The answer comes back decoded
130
+ * (decompressed, headers lowercased), redirects are not followed, and
131
+ * its Set-Cookie headers land in this visitor's jar.
132
+ *
133
+ * ```typescript
134
+ * const page = await visitor.request("GET", "/orders?page=2");
135
+ * expect(page.statusCode).toBe(200);
136
+ * expect(page.text()).toContain("Your orders");
137
+ * ```
138
+ */
139
+ request(method: string, path: string, init?: LambderTestRequestInit): Promise<LambderLambdaHttpResult>;
140
+ /**
141
+ * Starts a session for this visitor without a login endpoint: minted by
142
+ * the app's own session model, under its own cookie options, and planted
143
+ * in this visitor's jar, so its next call is signed in. Returns the raw
144
+ * tokens too, as LambderMockApp.signIn does.
145
+ *
146
+ * Throws when the cookies do not stick. An app that scopes its session
147
+ * cookie to a domain (`cookie: { domain: ".example.com" }`) writes one
148
+ * this visitor's host is not under, a browser on that host would drop it,
149
+ * and so does the jar; left silent, every session call after it answers
150
+ * sessionExpired with nothing to say why.
151
+ */
152
+ signIn(sessionKey: string, data: TSessionData, options?: {
153
+ ttlSeconds?: number;
154
+ }): Promise<LambderCreatedSession<TSessionData>>;
155
+ }