Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BACKEND-7: Add edit user route #18

Merged
merged 1 commit into from
Sep 10, 2024
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
21 changes: 20 additions & 1 deletion src/users/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import {
Controller,
Example,
Get,
Path,
Post,
Put,
Route,
SuccessResponse,
} from "tsoa";
import { UserService } from "./services";
import { exampleUser, exampleUsers } from "./examples";
import { User } from "./models";
import { UserCreationParams } from "./types";
import { UserCreationParams, UserUpdateParams } from "./types";
import { Types } from "mongoose";

@Route("users")
export class UserController extends Controller {
Expand Down Expand Up @@ -45,4 +48,20 @@ export class UserController extends Controller {
this.setStatus(201);
return this.userService.insertUser(req);
}

/**
* Update a user with the given ID.
*
* @param userId The ID of the user to update.
* @param req The user data in the request body.
* @returns The updated User object.
*/
@Put("{userId}")
@Example(exampleUser)
public async updateUser(
@Path() userId: Types.ObjectId,
@Body() req: UserUpdateParams
): Promise<User> {
return this.userService.updateUser(userId, req);
}
}
32 changes: 31 additions & 1 deletion src/users/services.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Types } from "mongoose";
import { UserModel } from "./models";
import { UserCreationParams } from "./types";
import { UserCreationParams, UserUpdateParams } from "./types";
import { InvalidArgumentError } from "../utils/errors";

export class UserService {
/**
Expand All @@ -21,4 +23,32 @@ export class UserService {
public insertUser = async (userData: UserCreationParams) => {
return await UserModel.create(userData);
};

/**
* Update a user in the database.
*
* @param userId The ID of the user to update.
* @param userData The data for the updated user.
* @throws InvalidArgumentError when an invalid id is supplied.
* @returns A promise resolving to the updated user document or error.
*/
public updateUser = async (
userId: Types.ObjectId,
userData: UserUpdateParams
) => {
const updatedUser = await UserModel.findByIdAndUpdate(
userId,
{
email: userData.email,
imageUrl: userData.imageUrl,
isAdmin: userData.isAdmin,
name: userData.name,
},
{ new: true }
);
if (!updatedUser) {
throw new InvalidArgumentError("Invalid userId supplied");
}
return updatedUser;
};
}
7 changes: 7 additions & 0 deletions src/users/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@ export type UserCreationParams = {
isAdmin: boolean;
name: string;
};

export type UserUpdateParams = {
email?: string;
imageUrl?: string;
isAdmin?: boolean;
name?: string;
};
132 changes: 132 additions & 0 deletions tests/users.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { connectDB, disconnectDB } from "./utils/dbConnection";
import { UserService } from "../src/users/services";
import { UserModel } from "../src/users/models";
import UserFactory from "./mocks/UserFactory";
import { Types } from "mongoose";
import { InvalidArgumentError } from "../src/utils/errors";
import { faker } from "@faker-js/faker";

describe("getUsers", () => {
let userService: UserService;
Expand Down Expand Up @@ -98,3 +101,132 @@ describe("insertUser", () => {
await expect(insertRequest).rejects.toThrow();
});
});

describe("updateUser", () => {
let userService: UserService;

beforeAll(async () => {
await connectDB();
await UserModel.createCollection();
await UserModel.syncIndexes();
userService = new UserService();
});

afterAll(async () => {
await UserModel.deleteMany({});
await disconnectDB();
});

beforeEach(async () => {
await UserModel.deleteMany({});
});

it("should throw an error for invalid id", async () => {
// given
const mocks = await UserFactory.create(1);
const mockUser = mocks[0];
const mockId = new Types.ObjectId();
await UserModel.create(mocks);

// when
const updateRequest = async () =>
await userService.updateUser(mockId, {
email: mockUser.email,
name: mockUser.name,
});

// then
await expect(updateRequest).rejects.toThrow(InvalidArgumentError);

// when
const getResponse = await UserModel.find();

// then
expect(getResponse[0].email).toStrictEqual(mockUser.email);
expect(getResponse[0].imageUrl).toStrictEqual(mockUser.imageUrl);
expect(getResponse[0].isAdmin).toStrictEqual(mockUser.isAdmin);
expect(getResponse[0].name).toStrictEqual(mockUser.name);
});

it("should throw an error for duplicate email", async () => {
// given
const mocks = await UserFactory.create(2);
const email = mocks[0].email;
await UserModel.create(mocks[0]);
const mockUser2 = await UserModel.create(mocks[1]);

// when
const updateRequest = async () =>
await userService.updateUser(mockUser2._id, { email });

// then
await expect(updateRequest).rejects.toThrow();
});

it("should properly update a single field only", async () => {
// given
const mocks = await UserFactory.create(1);
const mockUser = mocks[0];
const newName = faker.string.alpha({ length: { min: 5, max: 10 } });
const insertResponse = await UserModel.create(mockUser);

// when
const updateRequest = await userService.updateUser(insertResponse._id, {
name: newName,
});

// then
expect(updateRequest._id).toStrictEqual(insertResponse._id);
expect(updateRequest.email).toStrictEqual(mockUser.email);
expect(updateRequest.imageUrl).toStrictEqual(mockUser.imageUrl);
expect(updateRequest.isAdmin).toStrictEqual(mockUser.isAdmin);
expect(updateRequest.name).toStrictEqual(newName);

// when
const getRequest = await UserModel.find();

// then
expect(getRequest[0]._id).toStrictEqual(insertResponse._id);
expect(getRequest[0].email).toStrictEqual(mockUser.email);
expect(getRequest[0].imageUrl).toStrictEqual(mockUser.imageUrl);
expect(getRequest[0].isAdmin).toStrictEqual(mockUser.isAdmin);
expect(getRequest[0].name).toStrictEqual(newName);
});

it("should properly update multiple fields", async () => {
// given
const mocks = await UserFactory.create(1);
const mockUser = mocks[0];
const newName = faker.string.alpha({ length: { min: 5, max: 10 } });
const newEmail = faker.string.alpha({ length: { min: 5, max: 10 } });
const newImageUrl = faker.image.url();
const newIsAdmin = false;

const insertResponse = await UserModel.create(mockUser);

// when
const updateRequest = await userService.updateUser(insertResponse._id, {
email: newEmail,
imageUrl: newImageUrl,
isAdmin: newIsAdmin,
name: newName,
});

// then
expect(updateRequest._id).toStrictEqual(insertResponse._id);
expect(updateRequest.email).toStrictEqual(newEmail);
expect(updateRequest.imageUrl).toStrictEqual(newImageUrl);
expect(updateRequest.isAdmin).toStrictEqual(newIsAdmin);
expect(updateRequest.name).toStrictEqual(newName);

// when
const getRequest = await UserModel.find();

// then
expect(getRequest[0]._id).toStrictEqual(insertResponse._id);
expect(getRequest[0].email).toStrictEqual(newEmail);
expect(getRequest[0].imageUrl).toStrictEqual(newImageUrl);
expect(getRequest[0].isAdmin).toStrictEqual(newIsAdmin);
expect(getRequest[0].name).toStrictEqual(newName);
});
});
Loading