Skip to content

Commit b8895df

Browse files
authored
Merge pull request #1366 from bcgov/cypress/notifications
APS-4346 add e2e tests for email notifications
2 parents 3766ace + 523d4bb commit b8895df

File tree

8 files changed

+472
-2
lines changed

8 files changed

+472
-2
lines changed

.env.local

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ GWA_RES_SVR_CLIENT_ID=gwa-api
2020
GWA_RES_SVR_CLIENT_SECRET=18900468-3db1-43f7-a8af-e75f079eb742
2121
KEYCLOAK_AUTH_URL=http://keycloak.localtest.me:9081/auth
2222
KEYCLOAK_REALM=master
23-
EMAIL_ENABLED=false
23+
EMAIL_ENABLED=true
24+
EMAIL_HOST=mailpit.localtest.me
25+
EMAIL_PORT=1025
26+
EMAIL_SECURE=false
27+
EMAIL_FROM=noreply@api.gov.bc.ca
28+
EMAIL_USER=
29+
EMAIL_PASS=
2430
EXTERNAL_URL=http://oauth2proxy.localtest.me:4180
2531
OIDC_ISSUER=http://keycloak.localtest.me:9081/auth/realms/master
2632
LOCAL_ENV=true

docker-compose.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,5 +289,17 @@ services:
289289
profiles:
290290
- testsuite
291291

292+
mailpit:
293+
image: axllent/mailpit:latest
294+
container_name: mailpit
295+
restart: unless-stopped
296+
ports:
297+
- '1025:1025' # SMTP port for receiving emails
298+
- '8025:8025' # Web UI for viewing emails
299+
networks:
300+
aps-net:
301+
aliases:
302+
- mailpit.localtest.me
303+
292304
networks:
293305
aps-net: {}

e2e/cypress.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export default defineConfig({
4343
'./cypress/tests/18-*/*.ts',
4444
'./cypress/tests/19-*/*.ts',
4545
'./cypress/tests/20-*/*.ts',
46+
'./cypress/tests/21-*/*.ts',
4647
]
4748
return config
4849
},

e2e/cypress/support/e2e.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'cypress-xpath'
33
import './auth-commands'
44
import './prep-commands'
55
import './util-commands'
6+
import './mailpit-commands'
67
import '@cypress/code-coverage/support'
78
const _ = require('lodash')
89
const YAML = require('yamljs')

e2e/cypress/support/global.d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,5 +196,23 @@ declare namespace Cypress {
196196
makeAPIRequestForScanResult(scanID: string): Chainable<Cypress.Response<any>>
197197

198198
buildOrgGatewayDatasetAndProduct(): Chainable<Cypress.Response<any>>
199+
200+
// Mailpit commands for email testing
201+
mailpitGetMessages(): Chainable<any>
202+
203+
mailpitGetMessage(messageId: string): Chainable<any>
204+
205+
mailpitDeleteAllMessages(): Chainable<void>
206+
207+
mailpitSearchMessages(query: string): Chainable<any>
208+
209+
mailpitWaitForEmail(query: string, timeout?: number): Chainable<any>
210+
211+
mailpitAssertEmail(criteria: {
212+
to?: string
213+
from?: string
214+
subject?: string
215+
contains?: string
216+
}): Chainable<any>
199217
}
200218
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// Mailpit API utility commands for email testing
2+
// Mailpit API docs: https://github.com/axllent/mailpit/wiki/API
3+
4+
const MAILPIT_API_URL = 'http://mailpit.localtest.me:8025/api/v1'
5+
6+
export interface MailpitMessage {
7+
ID: string
8+
MessageID: string
9+
Read: boolean
10+
From: {
11+
Name: string
12+
Address: string
13+
}
14+
To: Array<{
15+
Name: string
16+
Address: string
17+
}>
18+
Cc: Array<{
19+
Name: string
20+
Address: string
21+
}>
22+
Bcc: Array<{
23+
Name: string
24+
Address: string
25+
}>
26+
ReplyTo: Array<{
27+
Name: string
28+
Address: string
29+
}>
30+
Subject: string
31+
Created: string
32+
Size: number
33+
Attachments: number
34+
Snippet: string
35+
}
36+
37+
export interface MailpitMessageSummary {
38+
total: number
39+
unread: number
40+
tagged: number
41+
messages: MailpitMessage[]
42+
}
43+
44+
export interface MailpitMessageDetails extends MailpitMessage {
45+
Text: string
46+
HTML: string
47+
Headers: Record<string, string[]>
48+
}
49+
50+
declare global {
51+
namespace Cypress {
52+
interface Chainable {
53+
/**
54+
* Get all messages from Mailpit inbox
55+
* @example cy.mailpitGetMessages()
56+
*/
57+
mailpitGetMessages(): Chainable<MailpitMessageSummary>
58+
59+
/**
60+
* Get a specific message by ID
61+
* @example cy.mailpitGetMessage('message-id')
62+
*/
63+
mailpitGetMessage(messageId: string): Chainable<MailpitMessageDetails>
64+
65+
/**
66+
* Delete all messages from Mailpit inbox
67+
* @example cy.mailpitDeleteAllMessages()
68+
*/
69+
mailpitDeleteAllMessages(): Chainable<void>
70+
71+
/**
72+
* Search for messages by query
73+
* @example cy.mailpitSearchMessages('to:user@example.com')
74+
*/
75+
mailpitSearchMessages(query: string): Chainable<MailpitMessageSummary>
76+
77+
/**
78+
* Wait for an email to arrive matching the query
79+
* @example cy.mailpitWaitForEmail('to:user@example.com', 10000)
80+
*/
81+
mailpitWaitForEmail(query: string, timeout?: number): Chainable<MailpitMessage>
82+
83+
/**
84+
* Assert that an email exists with specific criteria
85+
* @example cy.mailpitAssertEmail({ to: 'user@example.com', subject: 'Welcome' })
86+
*/
87+
mailpitAssertEmail(criteria: {
88+
to?: string
89+
from?: string
90+
subject?: string
91+
contains?: string
92+
}): Chainable<MailpitMessage>
93+
}
94+
}
95+
}
96+
97+
Cypress.Commands.add('mailpitGetMessages', () => {
98+
cy.request({
99+
method: 'GET',
100+
url: `${MAILPIT_API_URL}/messages`,
101+
headers: {
102+
Accept: 'application/json',
103+
},
104+
}).then((response) => {
105+
expect(response.status).to.eq(200)
106+
return response.body as MailpitMessageSummary
107+
})
108+
})
109+
110+
Cypress.Commands.add('mailpitGetMessage', (messageId: string) => {
111+
cy.request({
112+
method: 'GET',
113+
url: `${MAILPIT_API_URL}/message/${messageId}`,
114+
headers: {
115+
Accept: 'application/json',
116+
},
117+
}).then((response) => {
118+
expect(response.status).to.eq(200)
119+
return response.body as MailpitMessageDetails
120+
})
121+
})
122+
123+
Cypress.Commands.add('mailpitDeleteAllMessages', () => {
124+
cy.request({
125+
method: 'DELETE',
126+
url: `${MAILPIT_API_URL}/messages`,
127+
}).then((response) => {
128+
expect(response.status).to.be.oneOf([200, 204])
129+
})
130+
})
131+
132+
Cypress.Commands.add('mailpitSearchMessages', (query: string) => {
133+
cy.request({
134+
method: 'GET',
135+
url: `${MAILPIT_API_URL}/search`,
136+
qs: {
137+
query: query,
138+
},
139+
headers: {
140+
Accept: 'application/json',
141+
},
142+
}).then((response) => {
143+
expect(response.status).to.eq(200)
144+
return response.body as MailpitMessageSummary
145+
})
146+
})
147+
148+
Cypress.Commands.add(
149+
'mailpitWaitForEmail',
150+
(query: string, timeout: number = 10000) => {
151+
const startTime = Date.now()
152+
153+
const checkForEmail = (): Cypress.Chainable<MailpitMessage> => {
154+
return cy.mailpitSearchMessages(query).then((result) => {
155+
if (result.messages.length > 0) {
156+
return cy.wrap(result.messages[0])
157+
}
158+
159+
if (Date.now() - startTime > timeout) {
160+
throw new Error(
161+
`Timed out waiting for email matching query: ${query}`
162+
)
163+
}
164+
165+
// Wait 500ms and try again
166+
return cy.wait(500).then(() => checkForEmail())
167+
})
168+
}
169+
170+
return checkForEmail()
171+
}
172+
)
173+
174+
Cypress.Commands.add('mailpitAssertEmail', (criteria) => {
175+
let query = ''
176+
177+
if (criteria.to) {
178+
query += `to:${criteria.to} `
179+
}
180+
if (criteria.from) {
181+
query += `from:${criteria.from} `
182+
}
183+
if (criteria.subject) {
184+
query += `subject:"${criteria.subject}" `
185+
}
186+
if (criteria.contains) {
187+
query += `${criteria.contains} `
188+
}
189+
190+
query = query.trim()
191+
192+
return cy.mailpitSearchMessages(query).then((result) => {
193+
expect(
194+
result.messages.length,
195+
`Expected to find email matching: ${JSON.stringify(criteria)}`
196+
).to.be.greaterThan(0)
197+
198+
const message = result.messages[0]
199+
200+
if (criteria.to) {
201+
const toAddresses = message.To.map((t) => t.Address)
202+
expect(
203+
toAddresses,
204+
`Email should be sent to ${criteria.to}`
205+
).to.include(criteria.to)
206+
}
207+
208+
if (criteria.from) {
209+
expect(message.From.Address).to.eq(criteria.from)
210+
}
211+
212+
if (criteria.subject) {
213+
expect(message.Subject).to.include(criteria.subject)
214+
}
215+
216+
return cy.wrap(message)
217+
})
218+
})

0 commit comments

Comments
 (0)