Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/courses/courses.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { CourseVersion } from './entities/course-version.entity';
import { BulkOperation } from './entities/bulk-operation.entity';
import { PaginationService } from '../common/services/pagination.service';
import { User, UserRole } from '../users/entities/user.entity';
import { OutboxService } from '../common/events/outbox.service';
import { DataSource } from 'typeorm';

describe('CoursesController Pagination (#826)', () => {
let controller: CoursesController;
Expand Down Expand Up @@ -109,6 +111,15 @@ describe('CoursesController Pagination (#826)', () => {
}),
};

const mockOutboxService = {
enqueue: jest.fn(),
enqueueStandalone: jest.fn(),
};

const mockDataSource = {
transaction: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CoursesController],
Expand All @@ -119,6 +130,8 @@ describe('CoursesController Pagination (#826)', () => {
{ provide: getRepositoryToken(CourseReview), useValue: {} },
{ provide: getRepositoryToken(CourseVersion), useValue: {} },
{ provide: getRepositoryToken(BulkOperation), useValue: {} },
{ provide: OutboxService, useValue: mockOutboxService },
{ provide: DataSource, useValue: mockDataSource },
{ provide: EventEmitter2, useValue: { emit: jest.fn() } },
],
}).compile();
Expand Down
3 changes: 2 additions & 1 deletion src/courses/courses.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { PaginationQueryDto } from '../common/dto/pagination.dto';
import { OffsetPaginatedResponse } from '../common/interfaces/pagination.interface';

import { PaginationService } from '../common/services/pagination.service';
import { clampLimit } from '../common/utils/pagination.utils';
import { OutboxService } from '../common/events/outbox.service';
import { AnalyticsService } from '../analytics/analytics.service';
import { EventType } from '../analytics/entities/event.entity';
Expand Down Expand Up @@ -145,7 +146,7 @@ export class CoursesService {
requestingUser?: User,
query?: PaginationQueryDto,
): Promise<OffsetPaginatedResponse<Course>> {
const limit = query?.limit ?? 20;
const limit = clampLimit(query?.limit);
const isPrivileged = checkUserRole(requestingUser, ...PRIVILEGED_ROLES);

const qb = this.courseRepo.createQueryBuilder('course');
Expand Down
13 changes: 9 additions & 4 deletions src/messaging/message.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Controller, Post, Body, Get, Param, Patch, UseGuards } from '@nestjs/common';
import { Controller, Post, Body, Get, Param, Patch, UseGuards, Query, Req } from '@nestjs/common';
import { MessagingService } from './messaging.service';
import { CreateMessageDto } from './message.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { PaginationQueryDto } from '../common/dto/pagination.dto';

@UseGuards(JwtAuthGuard)
@Controller('messages')
Expand All @@ -17,10 +18,14 @@ export class MessagingController {
@Get('conversation/:otherUserId')
async getConversation(
@Param('otherUserId') otherUserId: string,
@Param('userId') userId: string,
@Req() req: any,
@Query() query?: PaginationQueryDto,
) {
// Assuming userId is retrieved from auth token via a custom decorator; placeholder for now
const conversation = await this.messagingService.getConversation(userId, otherUserId);
const conversation = await this.messagingService.getConversation(
req.user.id,
otherUserId,
query,
);
return { success: true, conversation };
}

Expand Down
2 changes: 2 additions & 0 deletions src/messaging/messaging.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Message } from './message.entity';
import { ConnectionSessionService } from './websocket-resilience/connection-session.service';
import { WebSocketResilienceService } from './websocket-resilience/websocket-resilience.service';
import { TracingService } from './tracing/tracing.service';
import { PaginationService } from '../common/services/pagination.service';

@Module({
imports: [
Expand All @@ -21,6 +22,7 @@ import { TracingService } from './tracing/tracing.service';
ConnectionSessionService,
WebSocketResilienceService,
TracingService,
PaginationService,
],
controllers: [MessagingController],
exports: [MessagingService],
Expand Down
74 changes: 74 additions & 0 deletions src/messaging/messaging.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MessagingService } from './messaging.service';
import { TracingService } from './tracing/tracing.service';
import { QUEUE_NAMES } from '../common/constants/queue.constants';
import { Message } from './message.entity';
import { PaginationService } from '../common/services/pagination.service';

const mockSpan = { end: jest.fn() };

Expand All @@ -22,6 +23,7 @@ const mockMessageRepo = {
save: jest.fn((msg) => Promise.resolve(msg)),
find: jest.fn().mockResolvedValue([]),
update: jest.fn().mockResolvedValue(undefined),
createQueryBuilder: jest.fn(),
};

const mockTracingService = {
Expand All @@ -33,12 +35,70 @@ describe('MessagingService', () => {
let service: MessagingService;

beforeEach(async () => {
const dataset = [
{
id: 'msg-1',
senderId: 'user-1',
recipientId: 'user-2',
content: 'one',
createdAt: new Date('2026-01-01T00:00:00Z'),
},
{
id: 'msg-2',
senderId: 'user-2',
recipientId: 'user-1',
content: 'two',
createdAt: new Date('2026-01-02T00:00:00Z'),
},
{
id: 'msg-3',
senderId: 'user-1',
recipientId: 'user-2',
content: 'three',
createdAt: new Date('2026-01-03T00:00:00Z'),
},
];

mockMessageRepo.createQueryBuilder.mockImplementation(() => {
let whereParams: any = null;
let skipVal = 0;
let takeVal = 20;
const qb: any = {
where: jest.fn().mockImplementation((_cond, params) => {
whereParams = params;
return qb;
}),
orderBy: jest.fn().mockReturnThis(),
addOrderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockImplementation((val) => {
skipVal = val;
return qb;
}),
take: jest.fn().mockImplementation((val) => {
takeVal = val;
return qb;
}),
getManyAndCount: jest.fn().mockImplementation(async () => {
const items = dataset.filter(
(item) =>
(item.senderId === whereParams.userId &&
item.recipientId === whereParams.otherUserId) ||
(item.senderId === whereParams.otherUserId &&
item.recipientId === whereParams.userId),
);
return [items.slice(skipVal, skipVal + takeVal), items.length];
}),
};
return qb;
});

const module: TestingModule = await Test.createTestingModule({
providers: [
MessagingService,
{ provide: getQueueToken(QUEUE_NAMES.MESSAGE_QUEUE), useValue: mockQueue },
{ provide: TracingService, useValue: mockTracingService },
{ provide: getRepositoryToken(Message), useValue: mockMessageRepo },
PaginationService,
],
}).compile();

Expand Down Expand Up @@ -95,4 +155,18 @@ describe('MessagingService', () => {
expect(status).toEqual({ waiting: 2, active: 1, completed: 3, failed: 0 });
});
});

describe('getConversation', () => {
it('should paginate conversation results and clamp the page size', async () => {
const result = await service.getConversation('user-1', 'user-2', {
page: 1,
limit: 999,
} as any);

expect(result.limit).toBe(100);
expect(result.data).toHaveLength(3);
expect(result.total).toBe(3);
expect(result.page).toBe(1);
});
});
});
28 changes: 20 additions & 8 deletions src/messaging/messaging.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { Queue, Job } from 'bull';
import { QUEUE_NAMES } from '../common/constants/queue.constants';
import { TracingService } from './tracing/tracing.service';
import { enrichWithCorrelation } from '../queues/utils/correlation-job.util';
import { PaginationService } from '../common/services/pagination.service';
import { PaginationQueryDto } from '../common/dto/pagination.dto';
import { clampLimit } from '../common/utils/pagination.utils';

/**
* Provides messaging operations.
Expand All @@ -21,6 +24,7 @@ export class MessagingService {
private readonly tracingService: TracingService,
@InjectRepository(Message)
private readonly messageRepo: Repository<Message>,
private readonly paginationService: PaginationService,
) {}

/**
Expand Down Expand Up @@ -50,16 +54,24 @@ export class MessagingService {
}
}

async getConversation(userId: string, otherUserId: string): Promise<Message[]> {
async getConversation(
userId: string,
otherUserId: string,
query?: PaginationQueryDto,
): Promise<any> {
const span = this.tracingService.startSpan('get-conversation');
try {
return await this.messageRepo.find({
where: [
{ senderId: userId, recipientId: otherUserId },
{ senderId: otherUserId, recipientId: userId },
],
order: { createdAt: 'ASC' },
});
const limit = clampLimit(query?.limit);
const offset =
query?.offset ?? (query?.cursor ? undefined : ((query?.page ?? 1) - 1) * limit);
const qb = this.messageRepo
.createQueryBuilder('message')
.where(
'(message.senderId = :userId AND message.recipientId = :otherUserId) OR (message.senderId = :otherUserId AND message.recipientId = :userId)',
{ userId, otherUserId },
);

return this.paginationService.paginate(qb, query?.cursor, limit, offset, 'createdAt');
} finally {
this.tracingService.endSpan(span);
}
Expand Down
4 changes: 4 additions & 0 deletions src/notifications/notifications.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
jest.mock('./templates/notification-template.service', () => ({
NotificationTemplateService: class NotificationTemplateService {},
}));

import { NotificationsService } from './notifications.service';
import { NotificationType } from './entities/notification.entity';

Expand Down
3 changes: 2 additions & 1 deletion src/notifications/notifications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CreateNotificationDto } from './dto/notification.dto';
import { SendTemplatedNotificationDto } from './dto/preferences.dto';
import { PreferencesService } from './preferences/preferences.service';
import { NotificationTemplateService } from './templates/notification-template.service';
import { clampLimit } from '../common/utils/pagination.utils';

@Injectable()
export class NotificationsService {
Expand Down Expand Up @@ -193,7 +194,7 @@ export class NotificationsService {
}

async findForUser(userId: string, query?: PaginationQueryDto) {
const limit = query?.limit ?? 20;
const limit = clampLimit(query?.limit);
const offset = query?.offset ?? (query?.cursor ? undefined : ((query?.page ?? 1) - 1) * limit);

const qb = this.notificationRepository
Expand Down
Loading