feat: add NotificationsController
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsInt, IsOptional, Min } from 'class-validator';
|
||||
|
||||
export class NotificationQueryDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
limit?: number;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Patch,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { TeamAccessService } from '../teams/team-access.service';
|
||||
import { NotificationQueryDto } from './dto/notification-query.dto';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
|
||||
@ApiTags('Notifications')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Controller({ path: 'teams', version: '1' })
|
||||
export class NotificationsController {
|
||||
constructor(
|
||||
private readonly service: NotificationsService,
|
||||
private readonly access: TeamAccessService,
|
||||
) {}
|
||||
|
||||
@Get(':teamId/notifications')
|
||||
async list(
|
||||
@Req() req,
|
||||
@Param('teamId', ParseIntPipe) teamId: number,
|
||||
@Query() query: NotificationQueryDto,
|
||||
) {
|
||||
await this.access.assertMember(Number(req.user.id), teamId);
|
||||
return this.service.listForUser(Number(req.user.id), teamId, query.page ?? 1, query.limit ?? 20);
|
||||
}
|
||||
|
||||
@Get(':teamId/notifications/unread-count')
|
||||
async unreadCount(@Req() req, @Param('teamId', ParseIntPipe) teamId: number) {
|
||||
await this.access.assertMember(Number(req.user.id), teamId);
|
||||
return { count: await this.service.getUnreadCount(Number(req.user.id), teamId) };
|
||||
}
|
||||
|
||||
@Patch(':teamId/notifications/:id/read')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async markRead(
|
||||
@Req() req,
|
||||
@Param('teamId', ParseIntPipe) teamId: number,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
) {
|
||||
await this.access.assertMember(Number(req.user.id), teamId);
|
||||
await this.service.markRead(id, Number(req.user.id));
|
||||
}
|
||||
|
||||
@Patch(':teamId/notifications/read-all')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async markAllRead(@Req() req, @Param('teamId', ParseIntPipe) teamId: number) {
|
||||
await this.access.assertMember(Number(req.user.id), teamId);
|
||||
await this.service.markAllRead(Number(req.user.id), teamId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
INestApplication,
|
||||
UnauthorizedException,
|
||||
ValidationPipe,
|
||||
VersioningType,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import validationOptions from '../utils/validation-options';
|
||||
import { NotificationsController } from './notifications.controller';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
import { TeamAccessService } from '../teams/team-access.service';
|
||||
|
||||
describe('notifications HTTP boundary', () => {
|
||||
let app: INestApplication;
|
||||
const service = {
|
||||
listForUser: jest.fn(),
|
||||
getUnreadCount: jest.fn(),
|
||||
markRead: jest.fn(),
|
||||
markAllRead: jest.fn(),
|
||||
};
|
||||
const access = { assertMember: jest.fn() };
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
controllers: [NotificationsController],
|
||||
providers: [
|
||||
{ provide: NotificationsService, useValue: service },
|
||||
{ provide: TeamAccessService, useValue: access },
|
||||
],
|
||||
})
|
||||
.overrideGuard(AuthGuard('jwt'))
|
||||
.useValue({
|
||||
canActivate(context) {
|
||||
const httpRequest = context.switchToHttp().getRequest();
|
||||
if (httpRequest.headers.authorization !== 'Bearer user') {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
httpRequest.user = { id: 42, role: { id: 2 } };
|
||||
return true;
|
||||
},
|
||||
})
|
||||
.compile();
|
||||
app = module.createNestApplication();
|
||||
app.setGlobalPrefix('api');
|
||||
app.enableVersioning({ type: VersioningType.URI });
|
||||
app.useGlobalPipes(new ValidationPipe(validationOptions));
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(() => app.close());
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('requires authentication', async () => {
|
||||
await request(app.getHttpServer()).get('/api/v1/teams/10/notifications').expect(401);
|
||||
});
|
||||
|
||||
it('lists notifications for the authenticated user after checking membership', async () => {
|
||||
access.assertMember.mockResolvedValue(undefined);
|
||||
service.listForUser.mockResolvedValue({ data: [], page: 2, limit: 5, total: 0, hasNextPage: false });
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get('/api/v1/teams/10/notifications?page=2&limit=5')
|
||||
.set('Authorization', 'Bearer user')
|
||||
.expect(200);
|
||||
|
||||
expect(access.assertMember).toHaveBeenCalledWith(42, 10);
|
||||
expect(service.listForUser).toHaveBeenCalledWith(42, 10, 2, 5);
|
||||
});
|
||||
|
||||
it('defaults to page 1 and limit 20 when not provided', async () => {
|
||||
access.assertMember.mockResolvedValue(undefined);
|
||||
service.listForUser.mockResolvedValue({ data: [], page: 1, limit: 20, total: 0, hasNextPage: false });
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get('/api/v1/teams/10/notifications')
|
||||
.set('Authorization', 'Bearer user')
|
||||
.expect(200);
|
||||
|
||||
expect(service.listForUser).toHaveBeenCalledWith(42, 10, 1, 20);
|
||||
});
|
||||
|
||||
it('returns the unread count', async () => {
|
||||
access.assertMember.mockResolvedValue(undefined);
|
||||
service.getUnreadCount.mockResolvedValue(4);
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/api/v1/teams/10/notifications/unread-count')
|
||||
.set('Authorization', 'Bearer user')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual({ count: 4 });
|
||||
});
|
||||
|
||||
it('marks a single notification as read', async () => {
|
||||
access.assertMember.mockResolvedValue(undefined);
|
||||
service.markRead.mockResolvedValue(undefined);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.patch('/api/v1/teams/10/notifications/7/read')
|
||||
.set('Authorization', 'Bearer user')
|
||||
.expect(200);
|
||||
|
||||
expect(service.markRead).toHaveBeenCalledWith(7, 42);
|
||||
});
|
||||
|
||||
it('marks all notifications as read', async () => {
|
||||
access.assertMember.mockResolvedValue(undefined);
|
||||
service.markAllRead.mockResolvedValue(undefined);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.patch('/api/v1/teams/10/notifications/read-all')
|
||||
.set('Authorization', 'Bearer user')
|
||||
.expect(200);
|
||||
|
||||
expect(service.markAllRead).toHaveBeenCalledWith(42, 10);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { Player } from 'src/players/entities/player.entity';
|
||||
import { TeamsModule } from 'src/teams/teams.module';
|
||||
import { Notification } from './entities/notification.entity';
|
||||
import { NotificationRecipient } from './entities/notification-recipient.entity';
|
||||
import { NotificationsController } from './notifications.controller';
|
||||
import { NotificationsListener } from './notifications.listener';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
|
||||
@@ -14,6 +15,7 @@ import { NotificationsService } from './notifications.service';
|
||||
LoggingModule,
|
||||
TeamsModule,
|
||||
],
|
||||
controllers: [NotificationsController],
|
||||
providers: [NotificationsService, NotificationsListener],
|
||||
})
|
||||
export class NotificationsModule {}
|
||||
|
||||
Reference in New Issue
Block a user