diff --git a/backend/src/controllers/application-controller.ts b/backend/src/controllers/application-controller.ts index cf2e71c8..51819287 100644 --- a/backend/src/controllers/application-controller.ts +++ b/backend/src/controllers/application-controller.ts @@ -250,7 +250,6 @@ export class ApplicationController { @Authorized(UserRole.User) public async getAllTeams(): Promise { const teams = await this._teams.getAllTeams(); - // TODO test member emails not exposed return teams.map((team) => convertBetweenEntityAndDTO(team, TeamDTO)); } diff --git a/backend/src/services/rating-service.ts b/backend/src/services/rating-service.ts index 8ab1c928..298efbcb 100644 --- a/backend/src/services/rating-service.ts +++ b/backend/src/services/rating-service.ts @@ -83,7 +83,6 @@ export class RatingService implements IRatingService { projectId: number, user: User, ): Promise { - // TODO test return this._database.getRepository(Rating).find({ where: { project: { diff --git a/backend/src/services/team-service.ts b/backend/src/services/team-service.ts index e1b25fde..bbb72d65 100644 --- a/backend/src/services/team-service.ts +++ b/backend/src/services/team-service.ts @@ -176,12 +176,10 @@ export class TeamService implements ITeamService { team.teamImg = placeholder_img[randomIndex]; } - // TODO test team.owner = user; const createdTeam = await this._teams.save(team); - // TODO test user.team = createdTeam; await this._users.save(user); @@ -289,7 +287,6 @@ export class TeamService implements ITeamService { const isAdmin = requestedBy.role === UserRole.Root; const isOwner = team.owner?.id === requestedBy.id; if (!isAdmin && !isOwner) { - // TODO test only admins or owners may accept requests throw new Error("You are not the owner of this team"); } @@ -322,12 +319,10 @@ export class TeamService implements ITeamService { const isOwner = team.owner?.id === requestedBy.id; const isAdmin = requestedBy.role === UserRole.Root; if (!isOwner && !isAdmin && userId !== requestedBy.id) { - // TODO test removing oneself should work throw new Error("Only the owner may remove other users from a team"); } if (team.owner?.id === userId) { - // TODO test throw new Error("Make someone else owner of the team first"); } @@ -335,7 +330,6 @@ export class TeamService implements ITeamService { throw new Error(`user ${userId} is not part of the team ${teamId}`); } - // TODO test success await this._users.update({ id: userId }, { team: null, teamRequest: null }); return Promise.resolve(); @@ -361,12 +355,10 @@ export class TeamService implements ITeamService { const isAdmin = requestedBy.role === UserRole.Root; const isOwner = team.owner?.id === requestedBy.id; if (!isAdmin && !isOwner) { - // TODO test throw new Error("Only the owner may change the owner"); } if (!team.userIds().includes(userId)) { - // TODO test throw new Error(`User ${userId} is not part of the team ${teamId}`); } @@ -376,7 +368,6 @@ export class TeamService implements ITeamService { throw new Error(`User ${userId} not found`); } - // TODO test success await this._teams.update({ id: teamId }, { owner: newOwner }); return Promise.resolve(); diff --git a/backend/test/controllers/application-controller.spec.ts b/backend/test/controllers/application-controller.spec.ts new file mode 100644 index 00000000..68141a96 --- /dev/null +++ b/backend/test/controllers/application-controller.spec.ts @@ -0,0 +1,70 @@ +import { classToPlain } from "class-transformer"; +import { ApplicationController } from "../../src/controllers/application-controller"; +import { Team } from "../../src/entities/team"; +import { User } from "../../src/entities/user"; +import { UserRole } from "../../src/entities/user-role"; +import { IApplicationService } from "../../src/services/application-service"; +import { ITeamService } from "../../src/services/team-service"; +import { IUserService } from "../../src/services/user-service"; +import { MockedService } from "../services/mock"; +import { MockApplicationService } from "../services/mock/mock-application-service"; +import { MockTeamsService } from "../services/mock/mock-teams-service"; +import { MockUserService } from "../services/mock/mock-user-service"; + +describe("ApplicationController", () => { + let applicationService: MockedService; + let userService: MockedService; + let teamService: MockedService; + let controller: ApplicationController; + + beforeEach(() => { + applicationService = new MockApplicationService(); + userService = new MockUserService(); + teamService = new MockTeamsService(); + controller = new ApplicationController( + applicationService.instance, + userService.instance, + teamService.instance, + ); + }); + + describe("getAllTeams", () => { + it("does not expose member email addresses", async () => { + expect.assertions(2); + + const member = Object.assign(new User(), { + id: 1, + firstName: "Jane", + lastName: "Doe", + email: "jane@example.com", + role: UserRole.User, + password: "", + verifyToken: "", + tokenSecret: "", + forgotPasswordToken: "", + team: null, + teamRequest: null, + }); + + const mockTeam = Object.assign(new Team(), { + id: 1, + title: "Test Team", + teamImg: "", + description: "A team", + owner: member, + users: [member], + requests: [], + }); + + teamService.mocks.getAllTeams.mockResolvedValue([mockTeam]); + + const teams = await controller.getAllTeams(); + + // Simulate the ResponseInterceptor which serializes with excludeAll + const serialized = classToPlain(teams, { strategy: "excludeAll" }) as any[]; + + expect(serialized).toHaveLength(1); + expect(serialized[0].users[0]).not.toHaveProperty("email"); + }); + }); +}); diff --git a/backend/test/services/rating-service.spec.ts b/backend/test/services/rating-service.spec.ts index 3c8c54ff..2b1ba59e 100644 --- a/backend/test/services/rating-service.spec.ts +++ b/backend/test/services/rating-service.spec.ts @@ -109,6 +109,95 @@ describe("RatingService", () => { await ratingService.bootstrap(); }); + describe("getUsersRatingsForProject", () => { + it("returns ratings for the specified project and user", async () => { + expect.assertions(2); + + settingsService.mocks.getSettings.mockResolvedValue({ + project: { allowRatingProjects: true }, + } as any); + + const rating = Object.assign(new Rating(), { + project: mockProject, + user: ratingUser, + criterion: mockCriterion, + rating: 4, + }); + await ratingService.upsertRating(rating, ratingUser); + + const results = await ratingService.getUsersRatingsForProject( + mockProject.id, + ratingUser, + ); + + expect(results).toHaveLength(1); + expect(results[0].rating).toBe(4); + }); + + it("returns an empty list when no ratings exist for the project", async () => { + expect.assertions(1); + + const results = await ratingService.getUsersRatingsForProject( + mockProject.id, + ratingUser, + ); + + expect(results).toHaveLength(0); + }); + + it("does not return ratings belonging to other users", async () => { + expect.assertions(1); + + await ratingRepo.save( + Object.assign(new Rating(), { + project: mockProject, + user: teamMember, + criterion: mockCriterion, + rating: 3, + }), + ); + + const results = await ratingService.getUsersRatingsForProject( + mockProject.id, + ratingUser, + ); + + expect(results).toHaveLength(0); + }); + + it("does not return ratings for other projects", async () => { + expect.assertions(1); + + settingsService.mocks.getSettings.mockResolvedValue({ + project: { allowRatingProjects: true }, + } as any); + + const otherProject = await projectRepo.save( + Object.assign(new Project(), { + team: mockTeam, + title: "Other Project", + description: "", + allowRating: true, + }), + ); + + const rating = Object.assign(new Rating(), { + project: otherProject, + user: ratingUser, + criterion: mockCriterion, + rating: 3, + }); + await ratingService.upsertRating(rating, ratingUser); + + const results = await ratingService.getUsersRatingsForProject( + mockProject.id, + ratingUser, + ); + + expect(results).toHaveLength(0); + }); + }); + describe("checkPermission", () => { describe("via upsertRating", () => { it("throws ForbiddenError if user is not admitted", async () => { diff --git a/backend/test/services/team-service.spec.ts b/backend/test/services/team-service.spec.ts index 34253b96..47d02687 100644 --- a/backend/test/services/team-service.spec.ts +++ b/backend/test/services/team-service.spec.ts @@ -1,3 +1,4 @@ +import { Repository } from "typeorm"; import { Project } from "../../src/entities/project"; import { Team } from "../../src/entities/team"; import { User } from "../../src/entities/user"; @@ -8,6 +9,31 @@ import { UserRole } from "../../src/entities/user-role"; describe("TeamService", () => { let teamService: ITeamService; let database: TestDatabaseService; + let userRepo: Repository; + let teamRepo: Repository; + + const makeUser = (email: string, role = UserRole.User): User => { + const user = new User(); + user.firstName = "Test"; + user.lastName = "User"; + user.email = email; + user.password = ""; + user.role = role; + user.verifyToken = ""; + user.tokenSecret = ""; + user.forgotPasswordToken = ""; + user.team = null; + user.teamRequest = null; + return user; + }; + + const makeTeam = (title = "Test Team"): Team => { + const team = new Team(); + team.title = title; + team.teamImg = ""; + team.description = "A test team"; + return team; + }; beforeAll(async () => { database = new TestDatabaseService(); @@ -18,36 +44,187 @@ describe("TeamService", () => { await database.nuke(); teamService = new TeamService(database); await teamService.bootstrap(); + userRepo = database.getRepository(User); + teamRepo = database.getRepository(Team); }); describe("createTeam", () => { it("creates a default project", async () => { const projectRepo = database.getRepository(Project); - const userRepo = database.getRepository(User); expect(await projectRepo.count()).toEqual(0); - const user = new User(); - user.firstName = "Regular"; - user.lastName = "User"; - user.email = "user@test.com"; - user.password = ""; - user.role = UserRole.User; - user.verifyToken = ""; - user.tokenSecret = ""; - user.forgotPasswordToken = ""; - user.team = null; // The team will be assigned in createTeam - user.teamRequest = null; - await userRepo.save(user); - - const team = new Team(); - team.title = "Team 1"; - team.teamImg = ""; - team.description = "Team 1 description"; + const user = await userRepo.save(makeUser("user@test.com")); + + const team = makeTeam("Team 1"); await teamService.createTeam(team, user); const projects = await projectRepo.find(); expect(projects).toHaveLength(1); expect(projects[0].team.title).toEqual(team.title); }); + + it("sets the creator as the team owner", async () => { + expect.assertions(1); + + const user = await userRepo.save(makeUser("owner@test.com")); + const createdTeam = await teamService.createTeam(makeTeam(), user); + + const foundTeam = await teamRepo.findOne({ + where: { id: createdTeam.id }, + relations: ["owner"], + }); + + expect(foundTeam!.owner.id).toEqual(user.id); + }); + + it("assigns the newly created team to the user", async () => { + expect.assertions(1); + + const user = await userRepo.save(makeUser("member@test.com")); + const createdTeam = await teamService.createTeam(makeTeam(), user); + + const updatedUser = await userRepo.findOne({ where: { id: user.id } }); + expect(updatedUser!.team!.id).toEqual(createdTeam.id); + }); + }); + + describe("acceptUserToTeam", () => { + it("throws when the requester is neither owner nor admin", async () => { + expect.assertions(1); + + const owner = await userRepo.save(makeUser("owner@test.com")); + const requestingUser = await userRepo.save(makeUser("req@test.com")); + const randomUser = await userRepo.save(makeUser("random@test.com")); + + const createdTeam = await teamService.createTeam(makeTeam(), owner); + + await userRepo.save({ ...requestingUser, teamRequest: createdTeam }); + + await expect( + teamService.acceptUserToTeam(createdTeam.id, requestingUser.id, randomUser), + ).rejects.toThrow("You are not the owner of this team"); + }); + + it("allows the team owner to accept a join request", async () => { + expect.assertions(1); + + const owner = await userRepo.save(makeUser("owner@test.com")); + const requestingUser = await userRepo.save(makeUser("req@test.com")); + + const createdTeam = await teamService.createTeam(makeTeam(), owner); + await userRepo.save({ ...requestingUser, teamRequest: createdTeam }); + + await teamService.acceptUserToTeam(createdTeam.id, requestingUser.id, owner); + + const acceptedUser = await userRepo.findOne({ where: { id: requestingUser.id } }); + expect(acceptedUser!.team!.id).toEqual(createdTeam.id); + }); + + it("allows an admin to accept a join request", async () => { + expect.assertions(1); + + const owner = await userRepo.save(makeUser("owner@test.com")); + const admin = await userRepo.save(makeUser("admin@test.com", UserRole.Root)); + const requestingUser = await userRepo.save(makeUser("req@test.com")); + + const createdTeam = await teamService.createTeam(makeTeam(), owner); + await userRepo.save({ ...requestingUser, teamRequest: createdTeam }); + + await teamService.acceptUserToTeam(createdTeam.id, requestingUser.id, admin); + + const acceptedUser = await userRepo.findOne({ where: { id: requestingUser.id } }); + expect(acceptedUser!.team!.id).toEqual(createdTeam.id); + }); + }); + + describe("removeUserFromTeam", () => { + it("allows a user to remove themselves from a team", async () => { + expect.assertions(1); + + const owner = await userRepo.save(makeUser("owner@test.com")); + const member = await userRepo.save(makeUser("member@test.com")); + + const createdTeam = await teamService.createTeam(makeTeam(), owner); + await userRepo.save({ ...member, team: createdTeam }); + + await teamService.removeUserFromTeam(createdTeam.id, member.id, member); + + const updatedMember = await userRepo.findOne({ where: { id: member.id } }); + expect(updatedMember!.team).toBeNull(); + }); + + it("throws when trying to remove the team owner", async () => { + expect.assertions(1); + + const owner = await userRepo.save(makeUser("owner@test.com")); + const createdTeam = await teamService.createTeam(makeTeam(), owner); + + await expect( + teamService.removeUserFromTeam(createdTeam.id, owner.id, owner), + ).rejects.toThrow("Make someone else owner of the team first"); + }); + + it("removes a member from the team successfully", async () => { + expect.assertions(1); + + const owner = await userRepo.save(makeUser("owner@test.com")); + const member = await userRepo.save(makeUser("member@test.com")); + + const createdTeam = await teamService.createTeam(makeTeam(), owner); + await userRepo.save({ ...member, team: createdTeam }); + + await teamService.removeUserFromTeam(createdTeam.id, member.id, owner); + + const updatedMember = await userRepo.findOne({ where: { id: member.id } }); + expect(updatedMember!.team).toBeNull(); + }); + }); + + describe("setOwner", () => { + it("throws when the requester is neither owner nor admin", async () => { + expect.assertions(1); + + const owner = await userRepo.save(makeUser("owner@test.com")); + const member = await userRepo.save(makeUser("member@test.com")); + const randomUser = await userRepo.save(makeUser("random@test.com")); + + const createdTeam = await teamService.createTeam(makeTeam(), owner); + await userRepo.save({ ...member, team: createdTeam }); + + await expect( + teamService.setOwner(createdTeam.id, member.id, randomUser), + ).rejects.toThrow("Only the owner may change the owner"); + }); + + it("throws when the target user is not in the team", async () => { + expect.assertions(1); + + const owner = await userRepo.save(makeUser("owner@test.com")); + const outsider = await userRepo.save(makeUser("outsider@test.com")); + + const createdTeam = await teamService.createTeam(makeTeam(), owner); + + await expect( + teamService.setOwner(createdTeam.id, outsider.id, owner), + ).rejects.toThrow(`User ${outsider.id} is not part of the team ${createdTeam.id}`); + }); + + it("sets the new owner successfully", async () => { + expect.assertions(1); + + const owner = await userRepo.save(makeUser("owner@test.com")); + const member = await userRepo.save(makeUser("member@test.com")); + + const createdTeam = await teamService.createTeam(makeTeam(), owner); + await userRepo.save({ ...member, team: createdTeam }); + + await teamService.setOwner(createdTeam.id, member.id, owner); + + const updatedTeam = await teamRepo.findOne({ + where: { id: createdTeam.id }, + relations: ["owner"], + }); + expect(updatedTeam!.owner.id).toEqual(member.id); + }); }); });