Skip to content

Commit

Permalink
Part 2 (#2)
Browse files Browse the repository at this point in the history
* Implement Part 2.2

* Implement Part 2.2

* Minor change

* Implement 2.3
  • Loading branch information
AdamRolander authored Dec 30, 2024
1 parent ceb8338 commit 2920549
Show file tree
Hide file tree
Showing 22 changed files with 463 additions and 51 deletions.
2 changes: 2 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import cors from "cors";
import { isHttpError } from "http-errors";
import taskRoutes from "src/routes/task";
import tasksRoutes from "src/routes/tasks";
import userRoutes from "src/routes/user";

const app = express();

Expand All @@ -27,6 +28,7 @@ app.use(

app.use("/api/task", taskRoutes);
app.use("/api/tasks", tasksRoutes);
app.use("/api/user", userRoutes);

/**
* Error handler; all errors thrown by server are handled here.
Expand Down
32 changes: 23 additions & 9 deletions backend/src/controllers/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import { RequestHandler } from "express";
import mongoose from "mongoose";
import createHttpError from "http-errors";
import { validationResult } from "express-validator";
import TaskModel from "src/models/task";
Expand Down Expand Up @@ -30,7 +31,7 @@ export const getTask: RequestHandler = async (req, res, next) => {

try {
// if the ID doesn't exist, then findById returns null
const task = await TaskModel.findById(id);
const task = await TaskModel.findById(id).populate("assignee");

if (task === null) {
throw createHttpError(404, "Task not found.");
Expand All @@ -49,7 +50,7 @@ export const getTask: RequestHandler = async (req, res, next) => {
export const createTask: RequestHandler = async (req, res, next) => {
// extract any errors that were found by the validator
const errors = validationResult(req);
const { title, description, isChecked } = req.body;
const { title, description, isChecked, assignee } = req.body;

try {
// if there are errors, then this function throws an exception
Expand All @@ -60,11 +61,14 @@ export const createTask: RequestHandler = async (req, res, next) => {
description: description,
isChecked: isChecked,
dateCreated: Date.now(),
assignee: assignee,
});

const populatedTask = await TaskModel.findById(task._id).populate("assignee");

// 201 means a new resource has been created successfully
// the newly created task is sent back to the user
res.status(201).json(task);
res.status(201).json(populatedTask);
} catch (error) {
next(error);
}
Expand All @@ -88,14 +92,24 @@ export const updateTask: RequestHandler = async (req, res, next) => {
try {
validationErrorParser(errors);

if (req.body._id != req.params.id) {
res.status(400);
// Check if assignee is a valid ObjectId
if (req.body.assignee && !mongoose.Types.ObjectId.isValid(req.body.assignee)) {
res.status(400).json({ error: "Invalid assignee ID." });
return;
}

const result = await TaskModel.findByIdAndUpdate(req.params.id, req.body, { new: true });

if (result === null) {
res.status(404);
// Update logic
const result = await TaskModel.findByIdAndUpdate(
req.params.id,
{
...req.body, // Spread operator to include the new assignee if present
},
{ new: true },
).populate("assignee");

if (!result) {
res.status(404).json({ error: "Task not found." });
return;
}

res.status(200).json(result);
Expand Down
2 changes: 1 addition & 1 deletion backend/src/controllers/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import TaskModel from "src/models/task";

export const getAllTasks: RequestHandler = async (req, res, next) => {
try {
const task = await TaskModel.find({}).sort({ dateCreated: -1 });
const task = await TaskModel.find({}).sort({ dateCreated: -1 }).populate("assignee");

res.status(200).json(task);
} catch (error) {
Expand Down
31 changes: 31 additions & 0 deletions backend/src/controllers/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { RequestHandler } from "express";
import createHttpError from "http-errors";
import UserModel from "src/models/user";

export const createUser: RequestHandler = async (req, res, next) => {
const { name, profilePictureURL } = req.body;

try {
const user = await UserModel.create({
name: name,
profilePictureURL: profilePictureURL,
});
res.status(201).json(user);
} catch (error) {
next(error);
}
};

export const getUser: RequestHandler = async (req, res, next) => {
const { id } = req.params;

try {
const user = await UserModel.findById(id);
if (user === null) {
throw createHttpError(404, "User not found.");
}
res.status(200).json(user);
} catch (error) {
next(error);
}
};
1 change: 1 addition & 0 deletions backend/src/models/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const taskSchema = new Schema({
// When we send a Task object in the JSON body of an API response, the date
// will automatically get "serialized" into a standard date string.
dateCreated: { type: Date, required: true },
assignee: { type: Schema.Types.ObjectId, ref: "User" },
});

type Task = InferSchemaType<typeof taskSchema>;
Expand Down
9 changes: 9 additions & 0 deletions backend/src/models/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { InferSchemaType, Schema, model } from "mongoose";

const userSchema = new Schema({
name: { type: String, required: true },
profilePictureURL: { type: String, required: false },
});

type User = InferSchemaType<typeof userSchema>;
export default model<User>("User", userSchema);
11 changes: 11 additions & 0 deletions backend/src/routes/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import express from "express";

import * as UserController from "src/controllers/user";
import * as UserValidator from "src/validators/user";

const router = express.Router();

router.post("/", UserValidator.createUser, UserController.createUser);
router.get("/:id", UserController.getUser);

export default router;
17 changes: 17 additions & 0 deletions backend/src/validators/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { body } from "express-validator";

const makeNameValidator = () =>
body("title")
.exists()
.withMessage("title is required")
.bail()
.isString()
.withMessage("title must be a string")
.bail()
.notEmpty()
.withMessage("title cannot be empty");

const makeURLValidator = () =>
body("profilePictureURL").optional().isString().withMessage("description must be a string");

export const createUser = [makeNameValidator(), makeURLValidator()];
10 changes: 10 additions & 0 deletions frontend/public/userDefault.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HelmetProvider } from "react-helmet-async";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { About, Home } from "src/pages";
import { About, Home, TaskDetail } from "src/pages";
import "src/globals.css";

export default function App() {
Expand All @@ -10,6 +10,7 @@ export default function App() {
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/task/:id" element={<TaskDetail />} />
</Routes>
</BrowserRouter>
</HelmetProvider>
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/api/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { get, handleAPIError, post, put } from "src/api/requests";

import type { APIResult } from "src/api/requests";
import type { User } from "src/api/users";

/**
* Defines the "shape" of a Task object (what fields are present and their types) for
Expand All @@ -13,6 +14,7 @@ export interface Task {
description?: string;
isChecked: boolean;
dateCreated: Date;
assignee?: User;
}

/**
Expand All @@ -30,6 +32,7 @@ interface TaskJSON {
description?: string;
isChecked: boolean;
dateCreated: string;
assignee?: User;
}

/**
Expand All @@ -46,6 +49,7 @@ function parseTask(task: TaskJSON): Task {
description: task.description,
isChecked: task.isChecked,
dateCreated: new Date(task.dateCreated),
assignee: task.assignee,
};
}

Expand All @@ -57,6 +61,7 @@ function parseTask(task: TaskJSON): Task {
export interface CreateTaskRequest {
title: string;
description?: string;
assignee?: string;
}

/**
Expand All @@ -69,6 +74,7 @@ export interface UpdateTaskRequest {
description?: string;
isChecked: boolean;
dateCreated: Date;
assignee?: string;
}

/**
Expand Down Expand Up @@ -113,7 +119,8 @@ export async function getAllTasks(): Promise<APIResult<Task[]>> {

export async function updateTask(task: UpdateTaskRequest): Promise<APIResult<Task>> {
try {
const response = await put(`/api/task/${task._id}`, task);
const newTask = { ...task, assignee: task.assignee };
const response = await put(`/api/task/${task._id}`, newTask);
const json = (await response.json()) as TaskJSON;
return { success: true, data: parseTask(json) };
} catch (error) {
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/api/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { get, handleAPIError } from "src/api/requests";

import type { APIResult } from "src/api/requests";

// function parseUser(user: UserJSON): User {
// return {
// _id: user._id,
// name: user.name,
// profilePictureURL: user.profilePictureURL,
// };
// }

export interface User {
_id: string;
name: string;
profilePictureURL: string;
}

// interface UserJSON {
// _id: string;
// name: string,
// profilePictureURL: string,
// }

export async function getUser(id: string): Promise<APIResult<User>> {
try {
const response = await get(`/api/user/${id}`);
const json = await response.json();

return { success: true, data: json };
} catch (error) {
return handleAPIError(error);
}
}
20 changes: 13 additions & 7 deletions frontend/src/components/TaskForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { createTask } from "src/api/tasks";
import { createTask, updateTask } from "src/api/tasks";
import { TaskForm } from "src/components/TaskForm";
import { afterEach, describe, expect, it, vi } from "vitest";

import type { CreateTaskRequest, Task } from "src/api/tasks";
import type { CreateTaskRequest, Task, UpdateTaskRequest } from "src/api/tasks";
import type { TaskFormProps } from "src/components/TaskForm";

const TITLE_INPUT_ID = "task-title-input";
Expand Down Expand Up @@ -35,6 +35,7 @@ vi.mock("src/api/tasks", () => ({
* See https://vitest.dev/guide/mocking#functions for more info about mock functions.
*/
createTask: vi.fn((_params: CreateTaskRequest) => Promise.resolve({ success: true })),
updateTask: vi.fn((_params: UpdateTaskRequest) => Promise.resolve({ success: true })),
}));

/**
Expand All @@ -45,7 +46,7 @@ const mockTask: Task = {
title: "My task",
description: "Very important",
isChecked: false,
dateCreated: new Date(),
dateCreated: new Date("2024-12-30T00:04:06.391Z"),
};

/**
Expand Down Expand Up @@ -121,22 +122,27 @@ describe("TaskForm", () => {
});

it("calls submit handler with edited fields", async () => {
// sometimes a test needs to be asynchronous, for example if we need to wait
// for component state updates
mountComponent({
mode: "edit",
task: mockTask,
});

fireEvent.change(screen.getByTestId(TITLE_INPUT_ID), { target: { value: "Updated title" } });
fireEvent.change(screen.getByTestId(DESCRIPTION_INPUT_ID), {
target: { value: "Updated description" },
});

const saveButton = screen.getByTestId(SAVE_BUTTON_ID);
fireEvent.click(saveButton);
expect(createTask).toHaveBeenCalledTimes(1);
expect(createTask).toHaveBeenCalledWith({

expect(updateTask).toHaveBeenCalledTimes(1); // Expect the correct number of calls
expect(updateTask).toHaveBeenCalledWith({
_id: "task123",
title: "Updated title",
dateCreated: new Date("2024-12-30T00:04:06.391Z"),
description: "Updated description",
isChecked: false, // Ensure this is passed correctly
assignee: undefined, // Handle the assignee field as undefined or a string
});
await waitFor(() => {
// If the test ends before all state updates and rerenders occur, we'll
Expand Down
Loading

0 comments on commit 2920549

Please sign in to comment.