Skip to content

Commit 54e3557

Browse files
Merge branch 'next' into feat(api-service)-extend-query-parser-by-json-logic-js
2 parents 594e1d0 + c11f113 commit 54e3557

File tree

2 files changed

+362
-0
lines changed

2 files changed

+362
-0
lines changed

apps/api/e2e/setup.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,26 @@ function isResponseValidationError(error: unknown): error is {
244244
);
245245
}
246246

247+
function isValidationErrorDto(error: unknown): error is Error & {
248+
name: string;
249+
statusCode: number;
250+
path: string;
251+
timestamp: string;
252+
errors: Record<string, { messages: string[] }>;
253+
body?: string;
254+
} {
255+
return (
256+
typeof error === 'object' &&
257+
error !== null &&
258+
'name' in error &&
259+
error.name === 'ValidationErrorDto' &&
260+
'statusCode' in error &&
261+
'errors' in error &&
262+
'path' in error &&
263+
typeof (error as { errors: unknown }).errors === 'object'
264+
);
265+
}
266+
247267
/*
248268
* poc for logging errors in e2e tests where the context is not available
249269
* if it's adding unnecessary noise, we can remove it
@@ -262,6 +282,24 @@ function logE2EFailure(error: unknown): void {
262282
return;
263283
}
264284

285+
if (isValidationErrorDto(error)) {
286+
console.error('\n[Validation error]');
287+
console.error(`Status: ${error.statusCode} ${error.path}`);
288+
console.error(`Timestamp: ${error.timestamp}`);
289+
console.error('Validation errors:');
290+
for (const [field, fieldError] of Object.entries(error.errors)) {
291+
console.error(` ${field}:`);
292+
for (const message of fieldError.messages) {
293+
console.error(` - ${message}`);
294+
}
295+
}
296+
if (error.body) {
297+
console.error(`\nFull response body: ${error.body}`);
298+
}
299+
300+
return;
301+
}
302+
265303
const typedError = error as Error & { cause?: unknown };
266304
if (typedError.cause instanceof ZodError) {
267305
console.error('\n[Zod validation error]');
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
import { Novu } from '@novu/api';
2+
import { DetailEnum } from '@novu/application-generic';
3+
import {
4+
ExecutionDetailsRepository,
5+
MessageRepository,
6+
NotificationTemplateEntity,
7+
NotificationTemplateRepository,
8+
PreferencesRepository,
9+
SubscriberEntity,
10+
} from '@novu/dal';
11+
import { PreferencesTypeEnum, StepTypeEnum, WorkflowCreationSourceEnum } from '@novu/shared';
12+
import { SubscribersService, UserSession } from '@novu/testing';
13+
import { expect } from 'chai';
14+
import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper';
15+
16+
describe('Trigger event with preferences - /v1/events/trigger (POST) #novu-v2', () => {
17+
let session: UserSession;
18+
let template: NotificationTemplateEntity;
19+
let subscriber: SubscriberEntity;
20+
let subscriberService: SubscribersService;
21+
const messageRepository = new MessageRepository();
22+
const executionDetailsRepository = new ExecutionDetailsRepository();
23+
const preferencesRepository = new PreferencesRepository();
24+
const notificationTemplateRepository = new NotificationTemplateRepository();
25+
let novuClient: Novu;
26+
27+
beforeEach(async () => {
28+
session = new UserSession();
29+
await session.initialize();
30+
subscriberService = new SubscribersService(session.organization._id, session.environment._id);
31+
subscriber = await subscriberService.createSubscriber();
32+
novuClient = initNovuClassSdk(session);
33+
});
34+
35+
it('should deliver in-app notification when subscriber preferences allow it', async () => {
36+
const { result: workflow } = await novuClient.workflows.create({
37+
name: 'Test Workflow - Allow Preferences',
38+
workflowId: `test-workflow-allow-${Date.now()}`,
39+
source: WorkflowCreationSourceEnum.EDITOR,
40+
active: true,
41+
steps: [
42+
{
43+
name: 'In-App Step',
44+
type: StepTypeEnum.IN_APP,
45+
controlValues: {
46+
body: 'Test in-app notification content',
47+
},
48+
},
49+
],
50+
});
51+
52+
template = (await notificationTemplateRepository.findById(
53+
workflow.id,
54+
session.environment._id
55+
)) as NotificationTemplateEntity;
56+
57+
await novuClient.trigger({
58+
workflowId: workflow.workflowId,
59+
to: [subscriber.subscriberId],
60+
payload: {
61+
message: 'Test message',
62+
},
63+
});
64+
65+
await session.waitForJobCompletion(template._id);
66+
67+
const messages = await messageRepository.find({
68+
_environmentId: session.environment._id,
69+
_subscriberId: subscriber._id,
70+
channel: StepTypeEnum.IN_APP,
71+
});
72+
73+
expect(messages.length).to.equal(1);
74+
expect(messages[0].content).to.equal('Test in-app notification content');
75+
76+
const executionDetailsFiltered = await executionDetailsRepository.find({
77+
_environmentId: session.environment._id,
78+
_notificationTemplateId: template._id,
79+
detail: DetailEnum.STEP_FILTERED_BY_SUBSCRIBER_WORKFLOW_PREFERENCES,
80+
});
81+
82+
expect(executionDetailsFiltered.length).to.equal(0);
83+
});
84+
85+
it('should skip in-app notification when subscriber disables in-app channel for workflow', async () => {
86+
const { result: workflow } = await novuClient.workflows.create({
87+
name: 'Test Workflow - Disable Preferences',
88+
workflowId: `test-workflow-disable-${Date.now()}`,
89+
source: WorkflowCreationSourceEnum.EDITOR,
90+
active: true,
91+
steps: [
92+
{
93+
name: 'In-App Step',
94+
type: StepTypeEnum.IN_APP,
95+
controlValues: {
96+
body: 'Test in-app notification content',
97+
},
98+
},
99+
],
100+
});
101+
102+
template = (await notificationTemplateRepository.findById(
103+
workflow.id,
104+
session.environment._id
105+
)) as NotificationTemplateEntity;
106+
107+
await novuClient.subscribers.preferences.update(
108+
{
109+
workflowId: workflow.workflowId,
110+
channels: {
111+
inApp: false,
112+
},
113+
},
114+
subscriber.subscriberId
115+
);
116+
117+
await novuClient.trigger({
118+
workflowId: workflow.workflowId,
119+
to: [subscriber.subscriberId],
120+
payload: {
121+
message: 'Test message',
122+
},
123+
});
124+
125+
await session.waitForJobCompletion(template._id);
126+
127+
const messages = await messageRepository.find({
128+
_environmentId: session.environment._id,
129+
_subscriberId: subscriber._id,
130+
channel: StepTypeEnum.IN_APP,
131+
});
132+
133+
expect(messages.length).to.equal(0);
134+
135+
const executionDetails = await executionDetailsRepository.find({
136+
_environmentId: session.environment._id,
137+
_notificationTemplateId: template._id,
138+
detail: DetailEnum.STEP_FILTERED_BY_SUBSCRIBER_WORKFLOW_PREFERENCES,
139+
});
140+
141+
expect(executionDetails.length).to.equal(1);
142+
});
143+
144+
it('should deliver in-app notification when subscriber enables channel despite workflow having all channels disabled by default', async () => {
145+
const { result: workflow } = await novuClient.workflows.create({
146+
name: 'Test Workflow - Disabled Defaults',
147+
workflowId: `test-workflow-disabled-${Date.now()}`,
148+
source: WorkflowCreationSourceEnum.EDITOR,
149+
active: true,
150+
steps: [
151+
{
152+
name: 'In-App Step',
153+
type: StepTypeEnum.IN_APP,
154+
controlValues: {
155+
body: 'Test in-app notification with disabled workflow defaults',
156+
},
157+
},
158+
],
159+
preferences: {
160+
user: {
161+
all: {
162+
enabled: false,
163+
readOnly: false,
164+
},
165+
channels: {
166+
in_app: {
167+
enabled: false,
168+
},
169+
email: {
170+
enabled: false,
171+
},
172+
sms: {
173+
enabled: false,
174+
},
175+
push: {
176+
enabled: false,
177+
},
178+
chat: {
179+
enabled: false,
180+
},
181+
},
182+
},
183+
},
184+
});
185+
186+
template = (await notificationTemplateRepository.findById(
187+
workflow.id,
188+
session.environment._id
189+
)) as NotificationTemplateEntity;
190+
191+
await novuClient.subscribers.preferences.update(
192+
{
193+
workflowId: workflow.workflowId,
194+
channels: {
195+
inApp: true,
196+
},
197+
},
198+
subscriber.subscriberId
199+
);
200+
201+
const subscriberWorkflowPreference = await preferencesRepository.findOne({
202+
_environmentId: session.environment._id,
203+
_organizationId: session.organization._id,
204+
_subscriberId: subscriber._id,
205+
_templateId: template._id,
206+
type: PreferencesTypeEnum.SUBSCRIBER_WORKFLOW,
207+
});
208+
209+
expect(subscriberWorkflowPreference).to.exist;
210+
211+
await preferencesRepository.update(
212+
{
213+
_id: subscriberWorkflowPreference!._id,
214+
_environmentId: session.environment._id,
215+
_organizationId: session.organization._id,
216+
},
217+
{
218+
$unset: { 'preferences.all': '' },
219+
}
220+
);
221+
222+
await novuClient.trigger({
223+
workflowId: workflow.workflowId,
224+
to: [subscriber.subscriberId],
225+
payload: {
226+
message: 'Test message',
227+
},
228+
});
229+
230+
await session.waitForJobCompletion(template._id);
231+
232+
const messages = await messageRepository.find({
233+
_environmentId: session.environment._id,
234+
_subscriberId: subscriber._id,
235+
channel: StepTypeEnum.IN_APP,
236+
});
237+
238+
expect(messages.length).to.equal(1);
239+
expect(messages[0].content).to.equal('Test in-app notification with disabled workflow defaults');
240+
241+
const executionDetailsFiltered = await executionDetailsRepository.find({
242+
_environmentId: session.environment._id,
243+
_notificationTemplateId: template._id,
244+
detail: DetailEnum.STEP_FILTERED_BY_SUBSCRIBER_WORKFLOW_PREFERENCES,
245+
});
246+
247+
expect(executionDetailsFiltered.length).to.equal(0);
248+
});
249+
250+
it('should not deliver in-app notification when workflow has all channels disabled by default and no subscriber overrides', async () => {
251+
const { result: workflow } = await novuClient.workflows.create({
252+
name: 'Test Workflow - Disabled Defaults No Override',
253+
workflowId: `test-workflow-disabled-no-override-${Date.now()}`,
254+
source: WorkflowCreationSourceEnum.EDITOR,
255+
active: true,
256+
steps: [
257+
{
258+
name: 'In-App Step',
259+
type: StepTypeEnum.IN_APP,
260+
controlValues: {
261+
body: 'Test in-app notification that should not be delivered',
262+
},
263+
},
264+
],
265+
preferences: {
266+
user: {
267+
all: {
268+
enabled: false,
269+
readOnly: false,
270+
},
271+
channels: {
272+
in_app: {
273+
enabled: false,
274+
},
275+
email: {
276+
enabled: false,
277+
},
278+
sms: {
279+
enabled: false,
280+
},
281+
push: {
282+
enabled: false,
283+
},
284+
chat: {
285+
enabled: false,
286+
},
287+
},
288+
},
289+
},
290+
});
291+
292+
template = (await notificationTemplateRepository.findById(
293+
workflow.id,
294+
session.environment._id
295+
)) as NotificationTemplateEntity;
296+
297+
await novuClient.trigger({
298+
workflowId: workflow.workflowId,
299+
to: [subscriber.subscriberId],
300+
payload: {
301+
message: 'Test message',
302+
},
303+
});
304+
305+
await session.waitForJobCompletion(template._id);
306+
307+
const messages = await messageRepository.find({
308+
_environmentId: session.environment._id,
309+
_subscriberId: subscriber._id,
310+
channel: StepTypeEnum.IN_APP,
311+
});
312+
313+
expect(messages.length).to.equal(0);
314+
315+
const executionDetails = await executionDetailsRepository.find({
316+
_environmentId: session.environment._id,
317+
_notificationTemplateId: template._id,
318+
_subscriberId: subscriber._id,
319+
detail: DetailEnum.STEP_FILTERED_BY_USER_WORKFLOW_PREFERENCES,
320+
});
321+
322+
expect(executionDetails.length).to.equal(1);
323+
});
324+
});

0 commit comments

Comments
 (0)