-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhelpers.ts
More file actions
150 lines (150 loc) · 4.36 KB
/
Copy pathhelpers.ts
File metadata and controls
150 lines (150 loc) · 4.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import request from 'supertest';
import {
DevRandom,
DevTime,
EmailDev,
} from '../../packages/pgstencil/src/index.ts';
import { createTestContext } from '../../packages/pgstencil/src/testing.ts';
import { startApp, type AppConfig } from '../../examples/login/src/app.ts';
import type {
CapturedEmail,
EmailSender,
} from '../../packages/pgstencil/src/email.ts';
const SECRET = 'pgstencil-test-secret-at-least-32-characters';
/**
* What driving a login flow actually needs: somewhere to send requests, the
* Origin those requests claim, and the inbox that receives the mail. Both a
* full fixture and a bare second app instance satisfy it.
*/
export interface LoginTarget {
client: request.Agent;
origin: string;
email: { next(timeoutMs?: number): Promise<CapturedEmail> };
}
type AppOptions = Pick<
AppConfig,
'oauth' | 'oauthFetch' | 'publicOrigin' | 'secureCookies'
>;
export async function fixture({
seed,
email,
...options
}: { seed?: string; email?: EmailSender } & AppOptions = {}) {
const context = await createTestContext(seed ? { seed } : {});
try {
const app = await startApp({
databaseUrl: context.database.url,
time: context.time,
random: context.random,
email: email ?? context.email,
devInbox: context.email,
secret: SECRET,
development: true,
...options,
});
return {
...context,
app,
client: request.agent(app.origin),
origin: app.publicOrigin,
async close() {
await app.close();
await context.close();
},
};
} catch (error) {
await context.close();
throw error;
}
}
export type Fixture = Awaited<ReturnType<typeof fixture>>;
/**
* A second server on an existing database, for proving that state shared
* through Postgres is shared. It borrows the database, so it leases nothing.
*/
export async function secondApp(
f: Fixture,
seed: string,
options: AppOptions = {},
) {
const time = new DevTime();
const random = new DevRandom(seed);
const email = new EmailDev(time);
const app = await startApp({
databaseUrl: f.database.url,
time,
random,
email,
devInbox: email,
secret: SECRET,
development: true,
...options,
});
return {
app,
time,
random,
email,
client: request.agent(app.origin),
origin: app.publicOrigin,
async close() {
await app.close();
email.close();
},
};
}
export function field(html: string, name: string): string {
const match = html.match(new RegExp(`name="${name}" value="([^"]*)"`));
if (!match) throw new Error(`Missing field ${name}`);
return match[1]!;
}
export function cookies(response: request.Response): string {
const header = response.headers['set-cookie'] as string[] | undefined;
return (header ?? []).map((value) => value.split(';')[0]).join('; ');
}
/** The sign-in email prints the code in two groups; tests want the digits. */
export function codeFrom(message: { text: string }): string {
const match = message.text.match(/code is (\d{4}) (\d{4})/);
if (!match) throw new Error('No sign-in code in email');
return match.slice(1).join('');
}
export function sessionCookie(f: Fixture, response: request.Response): string {
return cookies(response)
.split('; ')
.find((c) => c.startsWith(`${f.app.sessionName}=`))!;
}
export function post(
target: LoginTarget,
path: string,
body: Record<string, string>,
cookie?: string,
) {
const req = target.client
.post(path)
.set('origin', target.origin)
.type('form')
.send(body);
if (cookie) req.set('Cookie', cookie);
return req;
}
export async function begin(target: LoginTarget, email = 'alice@example.test') {
const login = await target.client.get('/login').expect(200);
const csrf = field(login.text, 'csrf');
const pendingCookie = cookies(login);
await post(target, '/login', { csrf, email }, pendingCookie).expect(303);
const message = await target.email.next();
const link = new URL(
message.html.match(/href="([^"]+)"/)![1]!.replaceAll('&', '&'),
);
return { csrf, pendingCookie, message, code: codeFrom(message), link, login };
}
export async function login(f: Fixture) {
const flow = await begin(f);
const response = await post(
f,
'/login/code',
{ csrf: flow.csrf, code: flow.code },
flow.pendingCookie,
).expect(303);
return { ...flow, response, sessionCookie: sessionCookie(f, response) };
}