Skip to content
Open
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
8 changes: 8 additions & 0 deletions BackendAcademy/src/social/dto/follow.dto.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

export class FollowDto {
@ApiProperty({ example: 'learner-1', description: 'The user initiating the follow request' })
@IsString()
@IsNotEmpty()
followerId: string;

@ApiProperty({ example: 'tutor-1', description: 'The target user to follow (must not be self)' })
@IsString()
@IsNotEmpty()
followeeId: string;
}

export interface SelfFollowError {
code: 'CANNOT_FOLLOW_SELF';
message: string;
}
29 changes: 29 additions & 0 deletions BackendAcademy/src/social/follow.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BadRequestException } from '@nestjs/common';
import { FollowDto } from './dto/follow.dto';
import { FollowService } from './follow.service';
import { DEFAULT_RANK_WEIGHT, FOLLOW_RANK_WEIGHT } from './social.types';

Expand Down Expand Up @@ -152,4 +153,32 @@ describe('FollowService', () => {
expect(service.getFeed('nobody')).toEqual([]);
});
});

// ── BA-053: Prevent users from following themselves ──────────────────────

describe('BA-053 — Prevent users from following themselves', () => {
it('rejects self-follow with structured CANNOT_FOLLOW_SELF error code', () => {
let caught: BadRequestException | undefined;
try {
service.follow('learner-self', 'learner-self');
} catch (err) {
caught = err as BadRequestException;
}

expect(caught).toBeInstanceOf(BadRequestException);
expect(caught!.getStatus()).toBe(400);
const res = caught!.getResponse() as Record<string, unknown>;
expect(res.code).toBe('CANNOT_FOLLOW_SELF');
expect(res.message).toBe('An account cannot follow itself');
});

it('rejects self-follow passed via DTO object', () => {
const dto: FollowDto = { followerId: 'user-99', followeeId: 'user-99' };
expect(() => service.follow(dto)).toThrow(BadRequestException);
});

it('rejects self-follow with trailing or leading whitespace', () => {
expect(() => service.follow('user-trim ', ' user-trim')).toThrow(BadRequestException);
});
});
});
20 changes: 17 additions & 3 deletions BackendAcademy/src/social/follow.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { FollowDto, SelfFollowError } from './dto/follow.dto';
import {
DEFAULT_RANK_WEIGHT,
FeedItem,
Expand Down Expand Up @@ -35,9 +36,22 @@ export class FollowService {
return `${followerId}|${followeeId}`;
}

follow(followerId: string, followeeId: string): FollowEdge {
if (followerId === followeeId) {
throw new BadRequestException('An account cannot follow itself');
follow(followerId: string, followeeId: string): FollowEdge;
follow(dto: FollowDto): FollowEdge;
follow(followerOrDto: string | FollowDto, maybeFolloweeId?: string): FollowEdge {
const followerId = typeof followerOrDto === 'object' ? followerOrDto.followerId : followerOrDto;
const followeeId = typeof followerOrDto === 'object' ? followerOrDto.followeeId : maybeFolloweeId!;

if (!followerId || !followeeId) {
throw new BadRequestException('Follower and followee IDs must be provided');
}

if (followerId === followeeId || followerId.trim() === followeeId.trim()) {
const errorPayload: SelfFollowError = {
code: 'CANNOT_FOLLOW_SELF',
message: 'An account cannot follow itself',
};
throw new BadRequestException(errorPayload);
}

const key = FollowService.edgeKey(followerId, followeeId);
Expand Down