Skip to content

Commit 6fe99ce

Browse files
committed
fix(demo,nestjs-notifications): resolve e2e test warnings and enforce auth
- Replace deprecated package.json#prisma config with prisma.config.ts - Remove deprecated prismaSchemaFolder preview feature from base.prisma - Register JwtAuthGuard as global APP_GUARD so protected routes require auth - Add conditional BullMQ queue providers in notification module async path to fix "Email queue is not available" errors - Update e2e tests to expect 401 on unauthenticated protected routes and clean 200 on notification preferences PUT
1 parent f800472 commit 6fe99ce

7 files changed

Lines changed: 85 additions & 29 deletions

File tree

apps/demo/package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,6 @@
4444
"reflect-metadata": "^0.2.0",
4545
"rxjs": "^7.8.0"
4646
},
47-
"prisma": {
48-
"schema": "prisma/schema"
49-
},
5047
"devDependencies": {
5148
"@nestjs/cli": "^10.0.0",
5249
"@nestjs/schematics": "^10.0.0",

apps/demo/prisma.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import path from 'node:path';
2+
import { defineConfig } from 'prisma/config';
3+
import dotenv from 'dotenv';
4+
5+
dotenv.config();
6+
7+
export default defineConfig({
8+
schema: path.join(__dirname, 'prisma', 'schema'),
9+
});

apps/demo/prisma/schema/base.prisma

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
generator client {
2-
provider = "prisma-client-js"
3-
previewFeatures = ["prismaSchemaFolder"]
2+
provider = "prisma-client-js"
43
}
54

65
datasource db {

apps/demo/src/app.module.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Module } from '@nestjs/common';
2+
import { APP_GUARD } from '@nestjs/core';
23
import { ConfigModule, ConfigService } from '@nestjs/config';
34
import { EventEmitterModule } from '@nestjs/event-emitter';
45
import { PrismaModule } from '@bbv/nestjs-prisma';
5-
import { AuthModule } from '@bbv/nestjs-auth';
6+
import { AuthModule, JwtAuthGuard } from '@bbv/nestjs-auth';
67
import { OtpModule } from '@bbv/nestjs-otp';
78
import { StorageModule } from '@bbv/nestjs-storage';
89
import { NotificationModule, AuthNotificationModule } from '@bbv/nestjs-notifications';
@@ -164,6 +165,12 @@ import * as path from 'path';
164165

165166
ItemsModule,
166167
],
168+
providers: [
169+
{
170+
provide: APP_GUARD,
171+
useClass: JwtAuthGuard,
172+
},
173+
],
167174
controllers: [AppController],
168175
})
169176
export class AppModule {}

apps/demo/test/app.e2e-spec.ts

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -255,15 +255,11 @@ describe('Demo App (e2e)', () => {
255255
itemId = res.body.data.id;
256256
});
257257

258-
it('POST /items without auth should still create (no global guard on items)', async () => {
259-
const res = await request(httpServer)
258+
it('POST /items without auth should be rejected', async () => {
259+
await request(httpServer)
260260
.post('/items')
261261
.send({ name: 'No Auth Item' })
262-
.expect(201);
263-
264-
expect(res.body.success).toBe(true);
265-
expect(res.body.data.name).toBe('No Auth Item');
266-
expect(res.body.data.createdBy).toBeNull();
262+
.expect(401);
267263
});
268264

269265
it('GET /items/:id should return single item (public)', async () => {
@@ -309,10 +305,8 @@ describe('Demo App (e2e)', () => {
309305
expect(res.body.data.count).toBeDefined();
310306
});
311307

312-
it('GET /notifications without auth returns empty (no global guard)', async () => {
313-
const res = await request(httpServer).get('/notifications').expect(200);
314-
315-
expect(res.body.success).toBe(true);
308+
it('GET /notifications without auth should be rejected', async () => {
309+
await request(httpServer).get('/notifications').expect(401);
316310
});
317311
});
318312

@@ -329,22 +323,12 @@ describe('Demo App (e2e)', () => {
329323
});
330324

331325
it('PUT /notification-preferences should upsert preference (authenticated)', async () => {
332-
// The preference controller reads userId from req.user which is set
333-
// by the JWT strategy. Without the global APP_GUARD, userId may be
334-
// undefined for unauthenticated requests, causing a Prisma error.
335-
// With a valid token, it should work if the controller properly
336-
// extracts the user ID.
337326
const res = await request(httpServer)
338327
.put('/notification-preferences')
339328
.set('Authorization', `Bearer ${accessToken}`)
340329
.send({ channel: 'email', type: 'marketing', enabled: false });
341330

342-
// This endpoint reads userId from req.user which requires the
343-
// JwtAuthGuard or a global guard to populate. Since the demo app
344-
// doesn't apply a global guard to this controller, userId may be
345-
// undefined even with a valid bearer token, causing a 500.
346-
// We accept either 200 (working) or 500 (known limitation).
347-
expect([200, 500]).toContain(res.status);
331+
expect(res.status).toBe(200);
348332
});
349333
});
350334

packages/nestjs-notifications/src/notification.module.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ jest.mock('./channels/push/providers/firebase.provider', () => ({
1414
.fn()
1515
.mockImplementation(() => ({ send: jest.fn() })),
1616
}));
17+
jest.mock('bullmq', () => ({
18+
Queue: jest.fn().mockImplementation(() => ({})),
19+
}));
1720
jest.mock('@nestjs/bullmq', () => ({
1821
BullModule: {
1922
registerQueue: jest.fn().mockReturnValue({
@@ -25,6 +28,7 @@ jest.mock('@nestjs/bullmq', () => ({
2528
InjectQueue: () => () => {},
2629
Processor: () => () => {},
2730
WorkerHost: class WorkerHost {},
31+
getQueueToken: (name: string) => `BullQueue_${name}`,
2832
}));
2933

3034
import { NotificationModule } from './notification.module';
@@ -314,6 +318,9 @@ describe('NotificationModule', () => {
314318
expect(providerTokens).toContain(EMAIL_PROVIDER);
315319
expect(providerTokens).toContain(SMS_PROVIDER);
316320
expect(providerTokens).toContain(PUSH_PROVIDER);
321+
expect(providerTokens).toContain('BullQueue_notifications-email');
322+
expect(providerTokens).toContain('BullQueue_notifications-sms');
323+
expect(providerTokens).toContain('BullQueue_notifications-push');
317324

318325
expect(result.controllers).toContain(InAppController);
319326
expect(result.controllers).toContain(DeviceTokenController);

packages/nestjs-notifications/src/notification.module.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { DynamicModule, Module, Provider, Type } from '@nestjs/common';
2-
import { BullModule } from '@nestjs/bullmq';
2+
import { BullModule, getQueueToken } from '@nestjs/bullmq';
3+
import { Queue } from 'bullmq';
34
import {
45
NOTIFICATION_MODULE_OPTIONS,
56
EMAIL_PROVIDER,
@@ -203,6 +204,58 @@ export class NotificationModule {
203204
},
204205
inject: [NOTIFICATION_MODULE_OPTIONS],
205206
},
207+
// Conditional BullMQ queue providers — creates Queue instances only
208+
// when the channel is enabled and Redis config is present.
209+
// We avoid BullModule.registerQueueAsync here to prevent @Processor
210+
// workers from being created for disabled channels.
211+
{
212+
provide: getQueueToken('notifications-email'),
213+
useFactory: (options: NotificationModuleOptions) => {
214+
if (!options.channels.email?.enabled || !options.queue?.redis) {
215+
return null;
216+
}
217+
return new Queue('notifications-email', {
218+
connection: {
219+
host: options.queue.redis.host,
220+
port: options.queue.redis.port ?? 6379,
221+
password: options.queue.redis.password,
222+
},
223+
});
224+
},
225+
inject: [NOTIFICATION_MODULE_OPTIONS],
226+
},
227+
{
228+
provide: getQueueToken('notifications-sms'),
229+
useFactory: (options: NotificationModuleOptions) => {
230+
if (!options.channels.sms?.enabled || !options.queue?.redis) {
231+
return null;
232+
}
233+
return new Queue('notifications-sms', {
234+
connection: {
235+
host: options.queue.redis.host,
236+
port: options.queue.redis.port ?? 6379,
237+
password: options.queue.redis.password,
238+
},
239+
});
240+
},
241+
inject: [NOTIFICATION_MODULE_OPTIONS],
242+
},
243+
{
244+
provide: getQueueToken('notifications-push'),
245+
useFactory: (options: NotificationModuleOptions) => {
246+
if (!options.channels.push?.enabled || !options.queue?.redis) {
247+
return null;
248+
}
249+
return new Queue('notifications-push', {
250+
connection: {
251+
host: options.queue.redis.host,
252+
port: options.queue.redis.port ?? 6379,
253+
password: options.queue.redis.password,
254+
},
255+
});
256+
},
257+
inject: [NOTIFICATION_MODULE_OPTIONS],
258+
},
206259
];
207260

208261
const controllers: Type[] = [InAppController, DeviceTokenController, PreferenceController];

0 commit comments

Comments
 (0)