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
1 change: 0 additions & 1 deletion src/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,6 @@ export class AppService {
user_identifier: string,
file: Express.Multer.File
): Promise<UploadFileResponseDto> {
//eslint-disable-next-line
if (!file || !file.buffer) {
throw new BadRequestException(ERROR_MESSAGES.FILE_NOT_FOUND);
}
Expand Down
82 changes: 74 additions & 8 deletions src/background-jobs/notifications/clear/clear.processor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ describe('ClearProcessor', () => {
);
});

it('should log console message when clearing notifications', async () => {
const console_spy = jest.spyOn(console, 'log').mockImplementation();
it('should log success message when clearing notifications', async () => {
const logger_spy = jest.spyOn(processor['logger'], 'log').mockImplementation();

const job_data: ClearBackGroundNotificationJobDTO = {
user_id: 'user-123',
Expand All @@ -197,14 +197,11 @@ describe('ClearProcessor', () => {
mock_job as Job<ClearBackGroundNotificationJobDTO>
);

expect(console_spy).toHaveBeenCalledWith(
'Clearing notifications for user:',
'user-123',
'Tweet IDs:',
['tweet-1', 'tweet-2']
expect(logger_spy).toHaveBeenCalledWith(
'Successfully cleared 2 notification(s) by tweet IDs for user user-123'
);

console_spy.mockRestore();
logger_spy.mockRestore();
});

it('should log success message after clearing notifications', async () => {
Expand Down Expand Up @@ -232,5 +229,74 @@ describe('ClearProcessor', () => {

logger_log_spy.mockRestore();
});

it('should handle database errors gracefully', async () => {
const db_error = new Error('Database connection failed');
mock_notifications_service.deleteNotificationsByTweetIds.mockRejectedValue(db_error);

const job_data: ClearBackGroundNotificationJobDTO = {
user_id: 'user-123',
tweet_ids: ['tweet-1'],
};

const mock_job = {
id: 'job-error',
data: job_data,
} as Job<ClearBackGroundNotificationJobDTO>;

const logger_error_spy = jest.spyOn(Logger.prototype, 'error').mockImplementation();

await expect(processor.handleClearNotification(mock_job)).rejects.toThrow(
'Database connection failed'
);

expect(logger_error_spy).toHaveBeenCalledWith(
'Error processing clear notification job job-error:',
db_error
);

logger_error_spy.mockRestore();
});

it('should handle empty tweet_ids array as invalid', async () => {
const job_data: ClearBackGroundNotificationJobDTO = {
user_id: 'user-123',
tweet_ids: [],
};

const mock_job = {
id: 'job-empty',
data: job_data,
} as Job<ClearBackGroundNotificationJobDTO>;

const logger_spy = jest.spyOn(Logger.prototype, 'warn').mockImplementation();

await processor.handleClearNotification(mock_job);

expect(mock_notifications_service.deleteNotificationsByTweetIds).not.toHaveBeenCalled();
expect(logger_spy).toHaveBeenCalled();

logger_spy.mockRestore();
});

it('should handle large arrays of tweet IDs', async () => {
const large_tweet_ids = Array.from({ length: 100 }, (_, i) => `tweet-${i}`);
const job_data: ClearBackGroundNotificationJobDTO = {
user_id: 'user-123',
tweet_ids: large_tweet_ids,
};

const mock_job = {
id: 'job-large',
data: job_data,
} as Job<ClearBackGroundNotificationJobDTO>;

await processor.handleClearNotification(mock_job);

expect(mock_notifications_service.deleteNotificationsByTweetIds).toHaveBeenCalledWith(
'user-123',
large_tweet_ids
);
});
});
});
2 changes: 0 additions & 2 deletions src/background-jobs/notifications/clear/clear.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,13 @@ export class ClearProcessor {
}

if (tweet_ids?.length) {
console.log('Clearing notifications for user:', user_id, 'Tweet IDs:', tweet_ids);
await this.notifications_service.deleteNotificationsByTweetIds(user_id, tweet_ids);
this.logger.log(
`Successfully cleared ${tweet_ids.length} notification(s) by tweet IDs for user ${user_id}`
);
}

if (user_ids?.length) {
console.log('Clearing notifications for user:', user_id, 'User IDs:', user_ids);
await this.notifications_service.cleanupNotificationsByUserIds(user_id, user_ids);
this.logger.log(
`Successfully cleared ${user_ids.length} notification(s) by user IDs for user ${user_id}`
Expand Down
2 changes: 0 additions & 2 deletions src/background-jobs/notifications/follow/follow.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,11 @@ export class FollowProcessor {
const { followed_id, follower_id, action } = job.data;

if (action === 'remove') {
// Remove the notification from MongoDB
const notification_id = await this.notifications_service.removeFollowNotification(
followed_id,
follower_id
);

// Only send socket notification if deletion succeeded
if (notification_id) {
this.notifications_service.sendNotificationOnly(
NotificationType.FOLLOW,
Expand Down
1 change: 0 additions & 1 deletion src/background-jobs/notifications/like/like.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export class LikeProcessor {
const { like_to, liked_by, tweet, action, tweet_id } = job.data;

if (action === 'remove') {
// Remove the notification from MongoDB
let notification_id: string | null = null;
if (tweet_id) {
notification_id = await this.notifications_service.removeLikeNotification(
Expand Down
36 changes: 36 additions & 0 deletions src/background-jobs/notifications/like/like.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,41 @@ describe('LikeJobService', () => {
const result = await service.queueLikeNotification(mock_like_dto);
expect(result).toEqual({ success: false, error: 'Queue error' });
});

it('should handle different tweet object structures', async () => {
const dto_with_complex_tweet: LikeBackGroundNotificationJobDTO = {
like_to: 'user-123',
liked_by: 'user-456',
tweet: {
tweet_id: 'tweet-789',
content: 'Complex tweet',
user: { id: 'user-123', username: 'testuser' },
} as any,
};

const result = await service.queueLikeNotification(dto_with_complex_tweet);

expect(mock_queue.add).toHaveBeenCalledWith(
JOB_NAMES.NOTIFICATION.LIKE,
dto_with_complex_tweet,
expect.any(Object)
);
expect(result.success).toBe(true);
});

it('should queue job with action parameter', async () => {
const dto_with_action = {
...mock_like_dto,
action: 'add' as const,
};

await service.queueLikeNotification(dto_with_action);

expect(mock_queue.add).toHaveBeenCalledWith(
JOB_NAMES.NOTIFICATION.LIKE,
dto_with_action,
expect.any(Object)
);
});
});
});
11 changes: 1 addition & 10 deletions src/background-jobs/notifications/mention/mention.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,8 @@ export class MentionProcessor {
} = job.data;

if (action === 'remove') {
// For remove action, we need usernames to find user IDs
if (!mentioned_user_ids || mentioned_user_ids.length === 0 || !tweet_id) return;

// Queue removal for each mentioned user
for (const user_id of mentioned_user_ids) {
if (user_id === mentioned_by) continue;

Expand All @@ -71,11 +69,7 @@ export class MentionProcessor {
if (!tweet) {
this.logger.warn(`Tweet data not provided in job ${job.id}.`);
return;
}

// For add action with usernames (batch processing)
else if (mentioned_user_ids && mentioned_user_ids.length > 0) {
// Process mention for each user
} else if (mentioned_user_ids && mentioned_user_ids.length > 0) {
for (const user_id of mentioned_user_ids) {
if (user_id === mentioned_by) continue;

Expand Down Expand Up @@ -114,15 +108,13 @@ export class MentionProcessor {

mentioner.id = mentioned_by;

// Build payload
const payload: any = {
type: NotificationType.MENTION,
mentioned_by: mentioner,
tweet_type,
};

if (tweet_type === 'quote' && parent_tweet) {
// Use parent_tweet from DTO (already formatted)
const quote = plainToInstance(
TweetQuoteResponseDTO,
{
Expand All @@ -133,7 +125,6 @@ export class MentionProcessor {
);
payload.tweet = quote;
} else {
// For normal tweets or replies
payload.tweet = plainToInstance(TweetResponseDTO, tweet, {
excludeExtraneousValues: true,
});
Expand Down
44 changes: 44 additions & 0 deletions src/background-jobs/notifications/mention/mention.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,49 @@ describe('MentionJobService', () => {

expect(result).toEqual({ success: true, job_id: 'job-empty' });
});

it('should handle mention in quote tweet', async () => {
const dto: MentionBackGroundNotificationJobDTO = {
mentioned_usernames: ['user9'],
mentioned_by: 'author-quote',
tweet_id: 'tweet-quote',
tweet: { tweet_id: 'tweet-quote' } as any,
parent_tweet: { tweet_id: 'quoted-tweet' } as any,
tweet_type: 'quote',
action: 'add',
};

const mock_job = { id: 'job-quote' };
queue.add.mockResolvedValue(mock_job as any);

const result = await service.queueMentionNotification(dto);

expect(result).toEqual({ success: true, job_id: 'job-quote' });
});

it('should handle mention with default priority and delay', async () => {
const dto: MentionBackGroundNotificationJobDTO = {
mentioned_usernames: ['user10'],
mentioned_by: 'author-default',
tweet_id: 'tweet-default',
tweet_type: 'tweet',
action: 'add',
};

const mock_job = { id: 'job-default' };
queue.add.mockResolvedValue(mock_job as any);

const result = await service.queueMentionNotification(dto);

expect(queue.add).toHaveBeenCalledWith(
expect.any(String),
dto,
expect.objectContaining({
attempts: 3,
backoff: expect.any(Object),
})
);
expect(result.success).toBe(true);
});
});
});
52 changes: 52 additions & 0 deletions src/background-jobs/notifications/quote/quote.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,57 @@ describe('QuoteJobService', () => {

expect(result).toEqual({ success: true, job_id: 'job-remove' });
});

it('should handle quote with complex tweet structures', async () => {
const dto: QuoteBackGroundNotificationJobDTO = {
quote_to: 'author-complex',
quoted_by: 'quoter-complex',
quote_tweet_id: 'quote-complex',
quote_tweet: {
tweet_id: 'quote-complex',
content: 'Complex quote with media',
media: [{ url: 'image.jpg' }],
user: { id: 'quoter-complex', username: 'quoter' },
} as any,
parent_tweet: {
tweet_id: 'parent-complex',
content: 'Original complex tweet',
} as any,
action: 'add',
};

const mock_job = { id: 'job-complex' };
queue.add.mockResolvedValue(mock_job as any);

const result = await service.queueQuoteNotification(dto);

expect(result).toEqual({ success: true, job_id: 'job-complex' });
});

it('should apply default job options correctly', async () => {
const dto: QuoteBackGroundNotificationJobDTO = {
quote_to: 'author-defaults',
quoted_by: 'quoter-defaults',
quote_tweet_id: 'quote-defaults',
action: 'add',
};

const mock_job = { id: 'job-defaults' };
queue.add.mockResolvedValue(mock_job as any);

const result = await service.queueQuoteNotification(dto);

expect(queue.add).toHaveBeenCalledWith(
expect.any(String),
dto,
expect.objectContaining({
attempts: 3,
backoff: expect.any(Object),
removeOnComplete: 10,
removeOnFail: 5,
})
);
expect(result.success).toBe(true);
});
});
});
Loading