proxymail-qa 0.4.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 CHANGED
@@ -105,6 +105,13 @@ await qa.setPassword(address, { password: 'the-new-test-password' });
105
105
  // its link goes, a stated expiry and more. Verdicts only, never its content.
106
106
  const result = await qa.checkEmail(address, { type: 'link', expectHost: 'staging.example.com' });
107
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 });
108
115
  ```
109
116
 
110
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;
@@ -162,6 +163,37 @@ export interface NoEmail {
162
163
  since: string;
163
164
  until: string;
164
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
+ }
165
197
  export declare class ProxyMailQAError extends Error {
166
198
  readonly status: number;
167
199
  readonly code: string;
@@ -174,6 +206,7 @@ export declare class ProxyMailQA {
174
206
  private readonly apiKey;
175
207
  private readonly baseUrl;
176
208
  private readonly fetcher;
209
+ run: string | null;
177
210
  constructor(options?: ClientOptions);
178
211
  private request;
179
212
  project(): Promise<Record<string, unknown>>;
@@ -217,5 +250,19 @@ export declare class ProxyMailQA {
217
250
  label: string;
218
251
  secret: string;
219
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;
220
267
  private wait;
221
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 fromEnv = globalThis.process?.env?.PROXYMAIL_QA_API_KEY;
45
- const apiKey = options.apiKey ?? fromEnv;
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
- async request(method, path, body) {
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: { Authorization: `Bearer ${this.apiKey}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
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);
@@ -164,7 +175,8 @@ export class ProxyMailQA {
164
175
  for (;;) {
165
176
  const left = Math.max(0, Math.min(MAX_WAIT_SECONDS, Math.ceil((deadline - Date.now()) / 1000)));
166
177
  const query = countQuery({ ...options, since }, { timeout: String(left) });
167
- const result = await this.request('GET', `/addresses/${encodeURIComponent(address)}/none${query}`);
178
+ const more = Date.now() + left * 1000 < deadline - 500;
179
+ const result = await this.request('GET', `/addresses/${encodeURIComponent(address)}/none${query}`, undefined, more);
168
180
  // The server's since, so later requests watch from the same moment
169
181
  // whatever this machine's clock says.
170
182
  since = result.since;
@@ -187,6 +199,46 @@ export class ProxyMailQA {
187
199
  savedPassword(address, label = 'password') {
188
200
  return this.request('GET', `/addresses/${encodeURIComponent(address)}/password?label=${encodeURIComponent(label)}`);
189
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
+ }
190
242
  async wait(address, kind, options, type = '', app) {
191
243
  const total = options.timeout ?? MAX_WAIT_SECONDS;
192
244
  const deadline = Date.now() + total * 1000;
@@ -202,8 +254,9 @@ export class ProxyMailQA {
202
254
  query.set('type', type);
203
255
  if (app !== undefined)
204
256
  query.set('app', String(app));
257
+ const more = Date.now() + left * 1000 < deadline - 500;
205
258
  try {
206
- return await this.request('GET', `/addresses/${encodeURIComponent(address)}/${kind}?${query}`);
259
+ return await this.request('GET', `/addresses/${encodeURIComponent(address)}/${kind}?${query}`, undefined, more);
207
260
  }
208
261
  catch (error) {
209
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.4.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)",