cypress-mailisk 2.1.1 → 2.2.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.
package/README.md CHANGED
@@ -1,17 +1,32 @@
1
1
  # cypress-mailisk
2
2
 
3
- ## Install with npm
3
+ Mailisk is an end-to-end email and SMS testing platform. It allows you to receive emails and SMS messages with code to automate tests.
4
+
5
+ - Get a unique subdomain and unlimited email addresses for free.
6
+ - Easily automate E2E password reset and account verification by catching emails.
7
+ - Receive SMS messages and automate SMS tests.
8
+ - Virtual SMTP and SMS support to test without 3rd party clients.
9
+
10
+ ## Get started
11
+
12
+ For a more step-by-step walkthrough see the [Cypress Guide](https://docs.mailisk.com/guides/cypress.html).
13
+
14
+ ### Installation
15
+
16
+ #### Install with npm
4
17
 
5
18
  ```shell
6
19
  npm install --save-dev cypress-mailisk
7
20
  ```
8
21
 
9
- ## Install with Yarn
22
+ #### Install with Yarn
10
23
 
11
24
  ```shell
12
25
  yarn add cypress-mailisk --dev
13
26
  ```
14
27
 
28
+ #### Add to Cypress
29
+
15
30
  After installing the package add the following in your project's `cypress/support/e2e.js`:
16
31
 
17
32
  ```js
@@ -36,38 +51,83 @@ The cypress-mailisk plugin provides additional commands which can be accessed on
36
51
 
37
52
  ### cy.mailiskSearchInbox
38
53
 
39
- This is the main command to interact with Mailisk, it wraps the [Search Inbox](/api-reference/search-inbox) endpoint.
54
+ This is the main command to interact with Mailisk, it wraps the [Search Inbox](/api-reference/search-inbox) endpoint. See the reference documentation for the full list of filters and their description.
40
55
 
41
56
  ```js
42
- cy.mailiskSearchInbox('yournamespace', { to_addr_prefix: 'test.user@' }).then((response) => {
43
- const emails = response.data;
44
- // ...
45
- });
57
+ cy.mailiskSearchInbox('yournamespace', { to_addr_prefix: 'test.user@', subject_includes: 'register' }).then(
58
+ (response) => {
59
+ const emails = response.data;
60
+ expect(emails).to.not.be.empty;
61
+ },
62
+ );
46
63
  ```
47
64
 
48
- This Cypress command does a few extra things out of the box compared to calling the raw API directly:
65
+ This command does a few extra things out of the box:
49
66
 
50
- - By default it uses the `wait` flag. This means the call won't return until at least one email is received. Disabling this flag via `wait: false` can cause it to return an empty response immediately.
51
- - The request timeout is adjustable by passing `timeout` in the request options. By default it uses a timeout of 5 minutes.
52
- - By default it returns emails in the last 15 minutes. This ensures that only new emails are returned. This can be overriden by passing the `from_timestamp` parameter (`from_timestamp: 0` will disable filtering by email age).
67
+ - Waits until at least one new email arrives (override with `wait: false`).
68
+ - Times out after 5 minutes if nothing shows up (adjust via `requestOptions.timeout`).
69
+ - Ignores messages older than 15 minutes to avoid picking up leftovers from previous tests (change via `from_timestamp`).
70
+
71
+ #### Quick examples
53
72
 
54
73
  ```js
55
- // timeout of 5 minute
74
+ // wait up to the default 5 min for *any* new mail
56
75
  cy.mailiskSearchInbox(namespace);
57
- // timeout of 1 minute
76
+ // custom 60-second timeout
58
77
  cy.mailiskSearchInbox(namespace, {}, { timeout: 1000 * 60 });
59
- // returns immediately, even if the result would be empty
78
+ // polling pattern — return immediately, even if inbox is empty
60
79
  cy.mailiskSearchInbox(namespace, { wait: false });
80
+ // returns the last 20 emails in the namespace immediately
81
+ cy.mailiskSearchInbox(namespace, { wait: false, from_timestamp: 0, limit: 20 });
82
+ ```
83
+
84
+ #### Filter by destination address
85
+
86
+ A common pattern is to wait for the email your UI just triggered (e.g. password-reset).
87
+ Pass `to_addr_prefix` so you don’t pick up stale messages:
88
+
89
+ ```js
90
+ cy.mailiskSearchInbox(namespace, {
91
+ to_addr_prefix: `test.user@${namespace}.mailisk.net`,
92
+ }).then((response) => {
93
+ const emails = response.data;
94
+ expect(emails).to.not.be.empty;
95
+ });
61
96
  ```
62
97
 
63
- For the full list of filters and their description see the [Search Inbox](/api-reference/search-inbox#request-1) endpoint reference.
98
+ #### Filter by sender address
99
+
100
+ Use `from_addr_includes` to narrow results to a sender or domain. This is useful when multiple systems write to the same namespace but only one sender matters for the test.
101
+
102
+ ```js
103
+ cy.mailiskSearchInbox(namespace, {
104
+ from_addr_includes: '@example.com',
105
+ }).then((response) => {
106
+ const emails = response.data;
107
+ expect(emails).to.not.be.empty;
108
+ });
109
+ ```
110
+
111
+ #### Filter by subject contents
112
+
113
+ The `subject_includes` filter helps when the UI sends different notification types from the same sender. Matching is case-insensitive and only requires the provided string to be present.
114
+
115
+ ```js
116
+ cy.mailiskSearchInbox(namespace, {
117
+ to_addr_prefix: `test.user@${namespace}.mailisk.net`,
118
+ subject_includes: 'password reset',
119
+ }).then((response) => {
120
+ const emails = response.data;
121
+ expect(emails).to.not.be.empty;
122
+ });
123
+ ```
64
124
 
65
125
  ### cy.mailiskGetAttachment
66
126
 
67
127
  This command retrieves attachment metadata including download URL, filename, content type, and size.
68
128
 
69
129
  ```js
70
- cy.mailiskGetAttachment('yournamespace', 'attachment-id').then((attachment) => {
130
+ cy.mailiskGetAttachment('attachment-id').then((attachment) => {
71
131
  console.log(attachment.data.filename);
72
132
  console.log(attachment.data.content_type);
73
133
  console.log(attachment.data.size);
@@ -79,60 +139,72 @@ cy.mailiskGetAttachment('yournamespace', 'attachment-id').then((attachment) => {
79
139
  This command downloads the actual attachment content as a Buffer. It first retrieves the attachment metadata, then downloads the file from the provided download URL.
80
140
 
81
141
  ```js
82
- cy.mailiskDownloadAttachment('yournamespace', 'attachment-id').then((buffer) => {
83
- // Save attachment to file
142
+ cy.mailiskDownloadAttachment('attachment-id').then((buffer) => {
84
143
  cy.writeFile('downloads/attachment.pdf', buffer);
85
144
  });
86
145
  ```
87
146
 
88
- Both commands accept optional request options as a third parameter:
147
+ ### cy.mailiskSearchSms
148
+
149
+ This is the main command to interact with Mailisk SMS, it wraps the [Search SMS](/api-reference/search-sms) endpoint. Use a phone number that is registered to your account.
89
150
 
90
151
  ```js
91
- cy.mailiskGetAttachment('yournamespace', 'attachment-id', { timeout: 30000 });
92
- cy.mailiskDownloadAttachment('yournamespace', 'attachment-id', { timeout: 60000 });
152
+ cy.mailiskSearchSms('phoneNumber', { from_date: new Date('2023-01-01T00:00:00Z'), limit: 5 }).then((response) => {
153
+ const smsMessages = response.data;
154
+ });
93
155
  ```
94
156
 
95
- ### Filter by TO address
157
+ This Cypress command does a few extra things out of the box compared to calling the raw API directly:
96
158
 
97
- The `to_addr_prefix` option allows filtering by the email's TO address. Specifically the TO address has to start with this.
159
+ - Waits until at least one SMS matches the filters (override with `wait: false`).
160
+ - Times out after 5 minutes if nothing shows up (adjust via `requestOptions.timeout`).
161
+ - Ignores SMS older than 15 minutes by default (override with `from_date`).
98
162
 
99
- For example, if someone sends an email to `my-user-1@yournamespace.mailisk.net`, you can filter it by using `my-user-1@`:
163
+ #### Quick examples
100
164
 
101
165
  ```js
102
- cy.mailiskSearchInbox(namespace, {
103
- to_addr_prefix: 'my-user-1@',
104
- });
166
+ // wait up to the default 5 min for *any* new SMS sent to the phone number
167
+ cy.mailiskSearchSms('phoneNumber');
168
+ // custom 60-second timeout
169
+ cy.mailiskSearchSms('phoneNumber', {}, { timeout: 1000 * 60 });
170
+ // polling pattern — return immediately, even if inbox is empty
171
+ cy.mailiskSearchSms('phoneNumber', { wait: false });
172
+ // returns the last 20 SMS messages for the phone number immediately
173
+ cy.mailiskSearchSms('phoneNumber', { wait: false, from_date: new Date(0), limit: 20 });
105
174
  ```
106
175
 
107
- ### Filter by FROM address
108
-
109
- The `from_addr_includes` option allows filtering by the email's FROM address. Specifically the TO address has to include this. Note that this is different from the to address as it is _includes_ not _prefix_.
176
+ #### Filter by sender phone number
110
177
 
111
- For example, if someone sends an email from the `example.com` domain we could filter like so:
178
+ Use `from_number` to narrow results to a specific phone number.
112
179
 
113
180
  ```js
114
- cy.mailiskSearchInbox(namespace, {
115
- from_addr_includes: '@example.com',
181
+ cy.mailiskSearchSms('phoneNumber', {
182
+ from_number: '1234567890',
183
+ }).then((response) => {
184
+ const smsMessages = response.data;
116
185
  });
117
186
  ```
118
187
 
119
- If we know a specific email address we want to listen to we can do this:
188
+ #### Filter by body contents
189
+
190
+ Use `body` to narrow results to a specific message body.
120
191
 
121
192
  ```js
122
- cy.mailiskSearchInbox(namespace, {
123
- from_addr_includes: 'no-reply@example.com',
193
+ cy.mailiskSearchSms('phoneNumber', {
194
+ body: 'Here is your code:',
195
+ }).then((response) => {
196
+ const smsMessages = response.data;
124
197
  });
125
198
  ```
126
199
 
127
- ### Filter by Subject
200
+ ### cy.mailiskListSmsNumbers
128
201
 
129
- The `subject_includes` option allows filtering by the email's Subject. Specifically the Subject has to include this (case-insensitive).
130
-
131
- If we're testing password reset that sends an email with the subject `Password reset request`. We could filter by something like this:
202
+ This command lists the SMS numbers available to the current API key.
132
203
 
133
204
  ```js
134
- cy.mailiskSearchInbox(namespace, {
135
- subject_includes: 'password reset request',
205
+ cy.mailiskListSmsNumbers().then((response) => {
206
+ const smsNumbers = response.data;
207
+ expect(smsNumbers).to.not.be.empty;
136
208
  });
137
209
  ```
138
210
 
@@ -154,21 +226,21 @@ describe('Test email attachments', () => {
154
226
  }).then((response) => {
155
227
  expect(response.data).to.not.be.empty;
156
228
  const email = response.data[0];
157
-
229
+
158
230
  // Check if email has attachments
159
231
  expect(email.attachments).to.not.be.empty;
160
232
  const attachment = email.attachments[0];
161
-
233
+
162
234
  // Get attachment metadata
163
235
  cy.mailiskGetAttachment(attachment.id).then((attachmentData) => {
164
236
  expect(attachmentData.data.filename).to.contain('.pdf');
165
237
  expect(attachmentData.data.content_type).to.equal('application/pdf');
166
-
238
+
167
239
  // Download the attachment
168
240
  cy.mailiskDownloadAttachment(attachment.id).then((buffer) => {
169
241
  // Save to downloads folder
170
242
  cy.writeFile(`downloads/${attachmentData.data.filename}`, buffer);
171
-
243
+
172
244
  // Verify file was downloaded
173
245
  cy.readFile(`downloads/${attachmentData.data.filename}`).should('exist');
174
246
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cypress-mailisk",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
4
4
  "description": "Mailisk library for Cypress",
5
5
  "keywords": [
6
6
  "mailisk",
@@ -43,10 +43,48 @@ export interface Email {
43
43
  expires_timestamp: number;
44
44
  /** The spam score as reported by SpamAssassin */
45
45
  spam_score?: number;
46
+ /** The headers of the email */
47
+ headers?: Record<string, string>;
46
48
  /** The attachments of the email */
47
49
  attachments?: EmailAttachment[];
48
50
  }
49
51
 
52
+ export interface SmsMessage {
53
+ /** Unique identifier for the message */
54
+ id: string;
55
+ /** Unique identifier for the SMS phone number */
56
+ sms_phone_number_id: string;
57
+ /** Body of the message */
58
+ body: string;
59
+ /** From number of the message */
60
+ from_number: string;
61
+ /** To number of the message */
62
+ to_number: string;
63
+ /** Provider message ID */
64
+ provider_message_id?: string;
65
+ /** Date and time the message was created */
66
+ created_at: string;
67
+ /** Direction of the message */
68
+ direction: 'inbound' | 'outbound';
69
+ }
70
+
71
+ export interface SmsNumber {
72
+ /** Unique identifier for the SMS number */
73
+ id: string;
74
+ /** Unique identifier for the organisation */
75
+ organisation_id: string;
76
+ /** Status of the SMS number */
77
+ status: 'requested' | 'active' | 'disabled';
78
+ /** Country of the SMS number */
79
+ country: string;
80
+ /** SMS Phone number */
81
+ phone_number?: string;
82
+ /** Date and time the SMS number was created */
83
+ created_at: string;
84
+ /** Date and time the SMS number was updated */
85
+ updated_at: string;
86
+ }
87
+
50
88
  export interface SearchInboxParams {
51
89
  /**
52
90
  * The maximum number of emails that can be returned in this request, used alongside `offset` for pagination.
@@ -105,6 +143,15 @@ export interface SearchInboxResponse {
105
143
  data: Email[];
106
144
  }
107
145
 
146
+ export interface SmtpSettings {
147
+ data: {
148
+ host: string;
149
+ port: number;
150
+ username: string;
151
+ password: string;
152
+ };
153
+ }
154
+
108
155
  export interface GetAttachmentResponse {
109
156
  data: {
110
157
  id: string;
@@ -116,9 +163,67 @@ export interface GetAttachmentResponse {
116
163
  };
117
164
  }
118
165
 
166
+ export interface ListNamespacesResponse {
167
+ total_count: number;
168
+ data: { id: string; namespace: string }[];
169
+ }
170
+
171
+ export interface SearchSmsMessagesParams {
172
+ /**
173
+ * The maximum number of SMS messages returned (1-100), used alongside `offset` for pagination.
174
+ */
175
+ limit?: number;
176
+ /**
177
+ * The number of SMS messages to skip/ignore, used alongside `limit` for pagination.
178
+ */
179
+ offset?: number;
180
+ /**
181
+ * Filter messages by body contents (case insensitive).
182
+ */
183
+ body?: string;
184
+ /**
185
+ * Filter messages by sender phone number prefix.
186
+ */
187
+ from_number?: string;
188
+ /**
189
+ * Filter messages created on or after this date (Date object or ISO 8601 string).
190
+ */
191
+ from_date?: Date | string;
192
+ /**
193
+ * Filter messages created on or before this date (Date object or ISO 8601 string).
194
+ */
195
+ to_date?: Date | string;
196
+ /**
197
+ * When true, keep the request open until at least one SMS is returned.
198
+ */
199
+ wait?: boolean;
200
+ }
201
+
202
+ export interface SearchSmsMessagesResponse {
203
+ total_count: number;
204
+ options: SearchSmsMessagesParams;
205
+ data: SmsMessage[];
206
+ }
207
+
208
+ export interface ListSmsNumbersResponse {
209
+ total_count: number;
210
+ data: SmsNumber[];
211
+ }
212
+
213
+ export interface SendVirtualSmsParams {
214
+ /** The phone number to send the SMS from */
215
+ from_number: string;
216
+ /** The phone number to send the SMS to */
217
+ to_number: string;
218
+ /** The body of the SMS message */
219
+ body: string;
220
+ }
221
+
119
222
  declare global {
120
223
  namespace Cypress {
121
224
  interface Chainable {
225
+ mailiskListNamespaces(): Cypress.Chainable<ListNamespacesResponse>;
226
+
122
227
  mailiskSearchInbox(
123
228
  /**
124
229
  * The unique namespace to search.
@@ -161,6 +266,32 @@ declare global {
161
266
  */
162
267
  options?: Partial<Cypress.RequestOptions>,
163
268
  ): Cypress.Chainable<Buffer>;
269
+
270
+ mailiskSearchSms(
271
+ /**
272
+ * The phone number to search.
273
+ */
274
+ phoneNumber: string,
275
+ /**
276
+ * Search parameters.
277
+ */
278
+ params?: SearchSmsMessagesParams,
279
+ /**
280
+ * Request options.
281
+ *
282
+ * See https://docs.cypress.io/api/commands/request#Arguments
283
+ */
284
+ options?: Partial<Cypress.RequestOptions>,
285
+ ): Cypress.Chainable<SearchSmsMessagesResponse>;
286
+
287
+ mailiskListSmsNumbers(
288
+ /**
289
+ * Request options.
290
+ *
291
+ * See https://docs.cypress.io/api/commands/request#Arguments
292
+ */
293
+ options?: Partial<Cypress.RequestOptions>,
294
+ ): Cypress.Chainable<ListSmsNumbersResponse>;
164
295
  }
165
296
  }
166
297
  }
@@ -2,23 +2,31 @@ const Request = require('./request');
2
2
 
3
3
  class MailiskCommands {
4
4
  static get cypressCommands() {
5
- return ['mailiskSetApiKey', 'mailiskListNamespaces', 'mailiskSearchInbox', 'mailiskGetAttachment', 'mailiskDownloadAttachment'];
5
+ return [
6
+ 'mailiskSetApiKey',
7
+ 'mailiskListNamespaces',
8
+ 'mailiskSearchInbox',
9
+ 'mailiskGetAttachment',
10
+ 'mailiskDownloadAttachment',
11
+ 'mailiskSearchSms',
12
+ 'mailiskListSmsNumbers',
13
+ ];
6
14
  }
7
15
 
8
16
  constructor() {
9
- const defaultApiKey = Cypress.env('MAILISK_API_KEY');
10
- this.mailiskSetApiKey(defaultApiKey);
17
+ const apiKey = Cypress.env('MAILISK_API_KEY');
18
+ this.mailiskSetApiKey(apiKey);
11
19
  }
12
20
 
13
21
  mailiskSetApiKey(apiKey) {
14
- this.request = new Request({ apiKey, baseUrl: Cypress.env('MAILISK_API_URL') });
22
+ this.request = new Request({ apiKey, apiUrl: Cypress.env('MAILISK_API_URL') });
15
23
  }
16
24
 
17
25
  mailiskListNamespaces() {
18
26
  return this.request.get('api/namespaces');
19
27
  }
20
28
 
21
- _mailiskSearchAction(namespace, _options, urlParams, startTime, nextTimeout) {
29
+ _mailiskSearchInboxAction(namespace, _options, urlParams, startTime, nextTimeout) {
22
30
  return this.request
23
31
  .get(`api/emails/${namespace}/inbox?${urlParams.toString()}`, { ..._options, timeout: nextTimeout })
24
32
  .then((response) => {
@@ -27,27 +35,27 @@ class MailiskCommands {
27
35
  }
28
36
  const timeout = Math.max(_options.timeout - (Date.now() - startTime), 1);
29
37
  cy.wait(Math.min(timeout, 9000), { log: false });
30
- return this._mailiskSearchAction(namespace, _options, urlParams, startTime, timeout);
38
+ return this._mailiskSearchInboxAction(namespace, _options, urlParams, startTime, timeout);
31
39
  });
32
40
  }
33
41
 
34
- mailiskSearchInbox(namespace, params, options = {}) {
42
+ mailiskSearchInbox(namespace, params = {}, options = {}) {
35
43
  let _params = { ...params };
36
44
 
37
45
  // default from_timestamp, 15 minutes before starting this request
38
- if (params.from_timestamp == null) {
39
- _params.from_timestamp = Math.floor(new Date().getTime() / 1000) - 15 * 60;
46
+ if (_params.from_timestamp == null) {
47
+ _params.from_timestamp = Math.floor(Date.now() / 1000) - 15 * 60;
40
48
  }
41
49
 
42
50
  // by default wait for email
43
- if (params.wait !== false) {
51
+ if (_params.wait !== false) {
44
52
  _params.wait = true;
45
53
  }
46
54
 
47
55
  const urlParams = new URLSearchParams();
48
56
  for (const key in _params) {
49
57
  const value = _params[key];
50
- if (value) urlParams.set(key, value.toString());
58
+ if (value !== undefined && value !== null) urlParams.set(key, value.toString());
51
59
  }
52
60
 
53
61
  let _options = { ...options };
@@ -61,7 +69,7 @@ class MailiskCommands {
61
69
  if (_params.wait) {
62
70
  urlParams.delete('wait');
63
71
  const startTime = Date.now();
64
- return this._mailiskSearchAction(namespace, _options, urlParams, startTime, _options.timeout);
72
+ return this._mailiskSearchInboxAction(namespace, _options, urlParams, startTime, _options.timeout);
65
73
  } else {
66
74
  return this.request.get(`api/emails/${namespace}/inbox?${urlParams.toString()}`, _options);
67
75
  }
@@ -76,6 +84,66 @@ class MailiskCommands {
76
84
  return this.request.getBinary(attachment.data.download_url, options);
77
85
  });
78
86
  }
87
+
88
+ _mailiskSearchSmsAction(phoneNumber, _options, urlParams, startTime, nextTimeout) {
89
+ return this.request
90
+ .get(`api/sms/${phoneNumber}/messages?${urlParams.toString()}`, { ..._options, timeout: nextTimeout })
91
+ .then((response) => {
92
+ if (response.total_count !== 0) {
93
+ return response;
94
+ }
95
+ const timeout = Math.max(_options.timeout - (Date.now() - startTime), 1);
96
+ cy.wait(Math.min(timeout, 9000), { log: false });
97
+ return this._mailiskSearchSmsAction(phoneNumber, _options, urlParams, startTime, timeout);
98
+ });
99
+ }
100
+
101
+ mailiskSearchSms(phoneNumber, params = {}, options = {}) {
102
+ let _params = { ...params };
103
+
104
+ // default from_date, 15 minutes before starting this request
105
+ if (_params.from_date == null) {
106
+ _params.from_date = new Date(Date.now() - 15 * 60 * 1000);
107
+ }
108
+
109
+ // by default wait for email
110
+ if (params.wait !== false) {
111
+ _params.wait = true;
112
+ }
113
+
114
+ if (_params.from_date instanceof Date) {
115
+ _params.from_date = _params.from_date.toISOString();
116
+ }
117
+ if (_params.to_date instanceof Date) {
118
+ _params.to_date = _params.to_date.toISOString();
119
+ }
120
+
121
+ const urlParams = new URLSearchParams();
122
+ for (const key in _params) {
123
+ const value = _params[key];
124
+ if (value !== undefined && value !== null) urlParams.set(key, value.toString());
125
+ }
126
+
127
+ let _options = { ...options };
128
+
129
+ // by default wait 5 minutes for emails
130
+ if (_params.wait && !options.timeout) {
131
+ _options.timeout = 1000 * 60 * 5;
132
+ }
133
+
134
+ // temporary workaround due cypress not supporting overriding maxRedirects
135
+ if (_params.wait) {
136
+ urlParams.delete('wait');
137
+ const startTime = Date.now();
138
+ return this._mailiskSearchSmsAction(phoneNumber, _options, urlParams, startTime, _options.timeout);
139
+ } else {
140
+ return this.request.get(`api/sms/${phoneNumber}/messages?${urlParams.toString()}`, _options);
141
+ }
142
+ }
143
+
144
+ mailiskListSmsNumbers(options = {}) {
145
+ return this.request.get(`api/sms/numbers`, options);
146
+ }
79
147
  }
80
148
 
81
149
  module.exports = MailiskCommands;