proxymail-qa 0.3.0 → 0.5.0
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/README.md +12 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +71 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,6 +100,18 @@ const { delivered, invalid } = await qa.variants(address);
|
|
|
100
100
|
|
|
101
101
|
// After the app changes the test account's password, keep the saved one in step.
|
|
102
102
|
await qa.setPassword(address, { password: 'the-new-test-password' });
|
|
103
|
+
|
|
104
|
+
// Check the email that arrived: SPF, DKIM, DMARC, local or http links, where
|
|
105
|
+
// its link goes, a stated expiry and more. Verdicts only, never its content.
|
|
106
|
+
const result = await qa.checkEmail(address, { type: 'link', expectHost: 'staging.example.com' });
|
|
107
|
+
expect(result.failed).toEqual([]);
|
|
108
|
+
|
|
109
|
+
// A run: this client's calls add what they saw, for a report on the pull
|
|
110
|
+
// request. In GitHub Actions, the Proxy Mail QA action does this for you and
|
|
111
|
+
// the client picks the run up from PROXYMAIL_QA_RUN.
|
|
112
|
+
await qa.startRun({ name: 'Sign-up tests' });
|
|
113
|
+
// …the tests…
|
|
114
|
+
await qa.finishRun({ failed: testsFailed });
|
|
103
115
|
```
|
|
104
116
|
|
|
105
117
|
When a wait comes back empty, the error says why as far as Proxy Mail QA can
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export interface ClientOptions {
|
|
|
2
2
|
apiKey?: string;
|
|
3
3
|
baseUrl?: string;
|
|
4
4
|
fetch?: typeof fetch;
|
|
5
|
+
run?: string | null;
|
|
5
6
|
}
|
|
6
7
|
export interface Persona {
|
|
7
8
|
first_name: string | null;
|
|
@@ -132,11 +133,67 @@ export interface CountOptions {
|
|
|
132
133
|
subtype?: CountSubtype | CountSubtype[];
|
|
133
134
|
}
|
|
134
135
|
export type CountSubtype = 'otp' | 'coupon' | 'verify' | 'magic_signin' | 'invite' | 'reset' | 'other' | 'any';
|
|
136
|
+
export type CheckId = 'spf' | 'dkim' | 'dmarc' | 'local_links' | 'plain_http' | 'expected_host' | 'expiry' | 'plain_text' | 'size' | 'images_only';
|
|
137
|
+
export interface EmailCheck {
|
|
138
|
+
check: CheckId;
|
|
139
|
+
status: 'pass' | 'fail' | 'warn' | 'skip';
|
|
140
|
+
detail: string;
|
|
141
|
+
}
|
|
142
|
+
export interface EmailChecks {
|
|
143
|
+
address: string;
|
|
144
|
+
email: {
|
|
145
|
+
id: string;
|
|
146
|
+
received_at: string;
|
|
147
|
+
service: string | null;
|
|
148
|
+
service_domain: string | null;
|
|
149
|
+
type: 'code' | 'link' | null;
|
|
150
|
+
subtype: string | null;
|
|
151
|
+
};
|
|
152
|
+
passed: boolean;
|
|
153
|
+
failed: CheckId[];
|
|
154
|
+
warnings: CheckId[];
|
|
155
|
+
checks: EmailCheck[];
|
|
156
|
+
}
|
|
157
|
+
export interface CheckOptions extends CountOptions {
|
|
158
|
+
emailId?: string;
|
|
159
|
+
expectHost?: string;
|
|
160
|
+
}
|
|
135
161
|
export interface NoEmail {
|
|
136
162
|
address: string;
|
|
137
163
|
since: string;
|
|
138
164
|
until: string;
|
|
139
165
|
}
|
|
166
|
+
export interface RunInput {
|
|
167
|
+
name?: string;
|
|
168
|
+
source?: 'ci' | 'agent' | 'api';
|
|
169
|
+
meta?: {
|
|
170
|
+
repository?: string;
|
|
171
|
+
branch?: string;
|
|
172
|
+
commit?: string;
|
|
173
|
+
pull_request?: number;
|
|
174
|
+
workflow?: string;
|
|
175
|
+
job_url?: string;
|
|
176
|
+
agent?: string;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export interface RunEvent {
|
|
180
|
+
at: string;
|
|
181
|
+
kind: 'address' | 'email' | 'missing' | 'none' | 'count' | 'checks' | 'note';
|
|
182
|
+
address: string | null;
|
|
183
|
+
failed: boolean;
|
|
184
|
+
detail: Record<string, unknown>;
|
|
185
|
+
}
|
|
186
|
+
export interface Run {
|
|
187
|
+
id: string;
|
|
188
|
+
name: string;
|
|
189
|
+
source: 'ci' | 'agent' | 'api';
|
|
190
|
+
status: 'running' | 'passed' | 'failed';
|
|
191
|
+
meta: NonNullable<RunInput['meta']>;
|
|
192
|
+
started_at: string;
|
|
193
|
+
finished_at: string | null;
|
|
194
|
+
url: string;
|
|
195
|
+
events?: RunEvent[];
|
|
196
|
+
}
|
|
140
197
|
export declare class ProxyMailQAError extends Error {
|
|
141
198
|
readonly status: number;
|
|
142
199
|
readonly code: string;
|
|
@@ -149,6 +206,7 @@ export declare class ProxyMailQA {
|
|
|
149
206
|
private readonly apiKey;
|
|
150
207
|
private readonly baseUrl;
|
|
151
208
|
private readonly fetcher;
|
|
209
|
+
run: string | null;
|
|
152
210
|
constructor(options?: ClientOptions);
|
|
153
211
|
private request;
|
|
154
212
|
project(): Promise<Record<string, unknown>>;
|
|
@@ -171,6 +229,7 @@ export declare class ProxyMailQA {
|
|
|
171
229
|
limit?: number;
|
|
172
230
|
}): Promise<Record<string, unknown>[]>;
|
|
173
231
|
countEmails(address: string, options?: CountOptions): Promise<EmailCount>;
|
|
232
|
+
checkEmail(address: string, options?: CheckOptions): Promise<EmailChecks>;
|
|
174
233
|
expectNoEmail(address: string, options?: CountOptions & {
|
|
175
234
|
timeout?: number;
|
|
176
235
|
}): Promise<NoEmail>;
|
|
@@ -191,5 +250,19 @@ export declare class ProxyMailQA {
|
|
|
191
250
|
label: string;
|
|
192
251
|
secret: string;
|
|
193
252
|
}>;
|
|
253
|
+
startRun(input?: RunInput): Promise<Run>;
|
|
254
|
+
getRun(id?: string): Promise<Run>;
|
|
255
|
+
runReport(id?: string): Promise<string>;
|
|
256
|
+
listRuns(limit?: number): Promise<Run[]>;
|
|
257
|
+
addRunNote(text: string, options?: {
|
|
258
|
+
failed?: boolean;
|
|
259
|
+
address?: string;
|
|
260
|
+
run?: string;
|
|
261
|
+
}): Promise<void>;
|
|
262
|
+
finishRun(options?: {
|
|
263
|
+
failed?: boolean;
|
|
264
|
+
run?: string;
|
|
265
|
+
}): Promise<Run>;
|
|
266
|
+
private requireRun;
|
|
194
267
|
private wait;
|
|
195
268
|
}
|
package/dist/index.js
CHANGED
|
@@ -40,23 +40,34 @@ export class ProxyMailQA {
|
|
|
40
40
|
apiKey;
|
|
41
41
|
baseUrl;
|
|
42
42
|
fetcher;
|
|
43
|
+
// The run this client's calls add to, if any.
|
|
44
|
+
run;
|
|
43
45
|
constructor(options = {}) {
|
|
44
|
-
const
|
|
45
|
-
const apiKey = options.apiKey ??
|
|
46
|
+
const env = globalThis.process?.env;
|
|
47
|
+
const apiKey = options.apiKey ?? env?.PROXYMAIL_QA_API_KEY;
|
|
46
48
|
if (!apiKey)
|
|
47
49
|
throw new Error('Pass apiKey or set PROXYMAIL_QA_API_KEY. Create a key in Project settings → API keys.');
|
|
48
50
|
this.apiKey = apiKey;
|
|
49
51
|
this.baseUrl = (options.baseUrl ?? 'https://api.proxymail.qa').replace(/\/+$/, '');
|
|
50
52
|
this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
53
|
+
this.run = options.run === undefined ? env?.PROXYMAIL_QA_RUN?.trim() || null : options.run;
|
|
51
54
|
}
|
|
52
|
-
|
|
55
|
+
// retry: this call is asked again if nothing arrives, so the run only
|
|
56
|
+
// records the last try's "nothing arrived".
|
|
57
|
+
async request(method, path, body, retry = false) {
|
|
53
58
|
const response = await this.fetcher(`${this.baseUrl}/v1${path}`, {
|
|
54
59
|
method,
|
|
55
|
-
headers: {
|
|
60
|
+
headers: {
|
|
61
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
62
|
+
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
63
|
+
...(this.run ? { 'Proxy-Mail-QA-Run': this.run, ...(retry ? { 'Proxy-Mail-QA-Run-Retry': 'true' } : {}) } : {}),
|
|
64
|
+
},
|
|
56
65
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
57
66
|
});
|
|
58
67
|
if (response.status === 204)
|
|
59
68
|
return undefined;
|
|
69
|
+
if ((response.headers.get('Content-Type') ?? '').startsWith('text/markdown') && response.ok)
|
|
70
|
+
return (await response.text());
|
|
60
71
|
const payload = (await response.json().catch(() => ({})));
|
|
61
72
|
if (!response.ok) {
|
|
62
73
|
throw new ProxyMailQAError(response.status, payload.error ?? 'error', payload.message ?? `Request failed with ${response.status}`, payload.hint, payload.refused, payload.help);
|
|
@@ -141,6 +152,18 @@ export class ProxyMailQA {
|
|
|
141
152
|
countEmails(address, options = {}) {
|
|
142
153
|
return this.request('GET', `/addresses/${encodeURIComponent(address)}/count${countQuery(options)}`);
|
|
143
154
|
}
|
|
155
|
+
// Checks one email the address received: SPF, DKIM and DMARC, links to
|
|
156
|
+
// localhost or plain http, where its link goes (expectHost), a stated
|
|
157
|
+
// expiry, a plain-text part, size and image-only emails. Doesn't wait: wait
|
|
158
|
+
// for the email first. Throws ProxyMailQAError (404) when none matches.
|
|
159
|
+
checkEmail(address, options = {}) {
|
|
160
|
+
const extra = {};
|
|
161
|
+
if (options.emailId)
|
|
162
|
+
extra.email_id = options.emailId;
|
|
163
|
+
if (options.expectHost)
|
|
164
|
+
extra.expect_host = options.expectHost;
|
|
165
|
+
return this.request('GET', `/addresses/${encodeURIComponent(address)}/checks${countQuery(options, extra)}`);
|
|
166
|
+
}
|
|
144
167
|
// Resolves when no matching email arrives within timeout seconds (default
|
|
145
168
|
// 30; longer repeats the request), for checks such as "an unsubscribed user
|
|
146
169
|
// gets nothing". Throws ProxyMailQAError (status 409, code email_arrived)
|
|
@@ -152,7 +175,8 @@ export class ProxyMailQA {
|
|
|
152
175
|
for (;;) {
|
|
153
176
|
const left = Math.max(0, Math.min(MAX_WAIT_SECONDS, Math.ceil((deadline - Date.now()) / 1000)));
|
|
154
177
|
const query = countQuery({ ...options, since }, { timeout: String(left) });
|
|
155
|
-
const
|
|
178
|
+
const more = Date.now() + left * 1000 < deadline - 500;
|
|
179
|
+
const result = await this.request('GET', `/addresses/${encodeURIComponent(address)}/none${query}`, undefined, more);
|
|
156
180
|
// The server's since, so later requests watch from the same moment
|
|
157
181
|
// whatever this machine's clock says.
|
|
158
182
|
since = result.since;
|
|
@@ -175,6 +199,46 @@ export class ProxyMailQA {
|
|
|
175
199
|
savedPassword(address, label = 'password') {
|
|
176
200
|
return this.request('GET', `/addresses/${encodeURIComponent(address)}/password?label=${encodeURIComponent(label)}`);
|
|
177
201
|
}
|
|
202
|
+
// Starts a run: a CI job or agent session whose calls add what they saw
|
|
203
|
+
// (addresses, emails and how long after since, codes, a link's site and
|
|
204
|
+
// path, checks, and emails that never came). This client's later calls add
|
|
205
|
+
// to it; to add calls from other processes, pass them its id as
|
|
206
|
+
// PROXYMAIL_QA_RUN. The GitHub Action starts and finishes one for you.
|
|
207
|
+
async startRun(input = {}) {
|
|
208
|
+
const run = await this.request('POST', '/runs', input);
|
|
209
|
+
this.run = run.id;
|
|
210
|
+
return run;
|
|
211
|
+
}
|
|
212
|
+
getRun(id = this.requireRun()) {
|
|
213
|
+
return this.request('GET', `/runs/${encodeURIComponent(id)}`);
|
|
214
|
+
}
|
|
215
|
+
// The report in Markdown, for a pull request comment or a job summary.
|
|
216
|
+
runReport(id = this.requireRun()) {
|
|
217
|
+
return this.request('GET', `/runs/${encodeURIComponent(id)}?format=markdown`);
|
|
218
|
+
}
|
|
219
|
+
listRuns(limit) {
|
|
220
|
+
return this.request('GET', `/runs${limit ? `?limit=${limit}` : ''}`).then((r) => r.runs);
|
|
221
|
+
}
|
|
222
|
+
// A line of your own in the run, such as a test that failed for a reason
|
|
223
|
+
// of its own. failed: true fails the run.
|
|
224
|
+
addRunNote(text, options = {}) {
|
|
225
|
+
const id = options.run ?? this.requireRun();
|
|
226
|
+
return this.request('POST', `/runs/${encodeURIComponent(id)}/notes`, { text, failed: options.failed ?? false, address: options.address });
|
|
227
|
+
}
|
|
228
|
+
// Passed unless an email was missing, came when none should have, failed
|
|
229
|
+
// a check, or failed says your tests failed.
|
|
230
|
+
async finishRun(options = {}) {
|
|
231
|
+
const id = options.run ?? this.requireRun();
|
|
232
|
+
const run = await this.request('POST', `/runs/${encodeURIComponent(id)}/finish`, { failed: options.failed ?? false });
|
|
233
|
+
if (this.run === id)
|
|
234
|
+
this.run = null;
|
|
235
|
+
return run;
|
|
236
|
+
}
|
|
237
|
+
requireRun() {
|
|
238
|
+
if (!this.run)
|
|
239
|
+
throw new Error('No run: call startRun first, or set PROXYMAIL_QA_RUN.');
|
|
240
|
+
return this.run;
|
|
241
|
+
}
|
|
178
242
|
async wait(address, kind, options, type = '', app) {
|
|
179
243
|
const total = options.timeout ?? MAX_WAIT_SECONDS;
|
|
180
244
|
const deadline = Date.now() + total * 1000;
|
|
@@ -190,8 +254,9 @@ export class ProxyMailQA {
|
|
|
190
254
|
query.set('type', type);
|
|
191
255
|
if (app !== undefined)
|
|
192
256
|
query.set('app', String(app));
|
|
257
|
+
const more = Date.now() + left * 1000 < deadline - 500;
|
|
193
258
|
try {
|
|
194
|
-
return await this.request('GET', `/addresses/${encodeURIComponent(address)}/${kind}?${query}
|
|
259
|
+
return await this.request('GET', `/addresses/${encodeURIComponent(address)}/${kind}?${query}`, undefined, more);
|
|
195
260
|
}
|
|
196
261
|
catch (error) {
|
|
197
262
|
const timedOut = error instanceof ProxyMailQAError && error.status === 404 && TIMED_OUT.test(error.message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proxymail-qa",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Disposable test inboxes for Playwright, Cypress and scripts: create an address, then wait for its verification code or link.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Proxy Mail QA <hello@proxymail.qa> (https://proxymail.qa)",
|