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
Empty file added -H
Empty file.
1 change: 1 addition & 0 deletions -d
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"error":"User validation failed: name: Path `name` is required."}{"error":"User validation failed: name: Path `name` is required."}
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"; // add this line
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); // add this line
app.use("/api/user", userRoutes);

/**
* Error handler; all errors thrown by server are handled here.
Expand Down
15 changes: 10 additions & 5 deletions backend/src/controllers/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,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,22 +49,25 @@ 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
validationErrorParser(errors);

const task = await TaskModel.create({
title: title,
description: description,
isChecked: isChecked,
dateCreated: Date.now(),
assignee,
});
console.log("Task returned from DB:", task);
//await task.populate("assignee");
const populatedTask = await task.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 @@ -91,7 +94,9 @@ export const updateTask: RequestHandler = async (req, res, next) => {
if (req.params.id !== req.body._id) {
res.status(400);
}
const result = await TaskModel.findByIdAndUpdate(req.params.id, req.body, { new: true });
const result = await TaskModel.findByIdAndUpdate(req.params.id, req.body, {
new: true,
}).populate("assignee");
if (!result) {
throw createHttpError(404, "Task not found");
}
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 @@ -8,7 +8,7 @@ export const getAllTasks: RequestHandler = async (req, res, next) => {
try {
// your code here

const taskSet = await TaskModel.find(tasks).sort({ dateCreated: -1 });
const taskSet = await TaskModel.find(tasks).sort({ dateCreated: -1 }).populate("assignee");

if (!taskSet || taskSet.length == 0) {
throw createHttpError(404, "Task not found");
Expand Down
47 changes: 47 additions & 0 deletions backend/src/controllers/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { RequestHandler } from "express";
import createHttpError from "http-errors";
import User from "../models/user";

export const createUser: RequestHandler = async (req, res, next) => {
// ...
// extract any errors that were found by the validator
// extract any errors that were found by the validator
//const errors = validationResult(req);
//const { _id, name, profilePictureURL } = req.body;

try {
// if there are errors, then this function throws an exception
const { name, _id, profilePictureURL } = req.body;
//validationErrorParser(errors);
const user = new User({ name, _id, profilePictureURL });

await user.save();

// 201 means a new resource has been created successfully
// the newly created task is sent back to the user
res.status(201).json(user);
} catch (error) {
next(error);
}
};

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

try {
// if the ID doesn't exist, then findById returns null
const user = await User.findById(id);

if (user === null) {
throw createHttpError(404, "User not found.");
}

// Set the status code (200) and body (the task object as JSON) of the response.
// Note that you don't need to return anything, but you can still use a return
// statement to exit the function early.
res.status(200).json(user);
} catch (error) {
// pass errors to the error handler
next(error);
}
};
5 changes: 5 additions & 0 deletions backend/src/models/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ 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",
required: false,
},
});

type Task = InferSchemaType<typeof taskSchema>;
Expand Down
10 changes: 10 additions & 0 deletions backend/src/models/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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);
8 changes: 8 additions & 0 deletions backend/src/routes/tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import express from "express";
import * as TasksController from "src/controllers/tasks";

const router = express.Router();

router.get("/", TasksController.getAllTasks);

export default router;
26 changes: 26 additions & 0 deletions backend/src/routes/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Task route requests.
*/

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

const router = express.Router();

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

/**
* TaskValidator.createTask serves as middleware for this route. This means
* that instead of immediately serving up the route when the request is made,
* Express firsts passes the request to TaskValidator.createTask.
* TaskValidator.createTask processes the request and determines whether the
* request should be sent through or an error should be thrown.
*/

//router.put("/:id", UserValidator.updateTask, UserController.updateTask);

router.post("/", UserValidator.createUser, UserController.createUser);
//router.delete("/:id", UserController.removeTask);

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

/*const makeIDValidator = () =>
body("_id")
.exists()
.withMessage("_id is required")
.bail()
.isMongoId()
.withMessage("_id must be a MongoDB object ID");
*/ //Not needed because it seems like the HTTP request doesn't have an ID as an input but rather
//the backend itself is the one assigning an ID to the user.
const makeNameValidator = () =>
body("name").notEmpty().isString().isLength({ min: 1, max: 50 }).trim();
const makeProfileValidator = () =>
body("profilePictureURL")
.optional()
.isURL()
.withMessage("Must be a valid URL")
.matches(/\.(jpg|jpeg|png|gif|bmp|svg)$/i)
.withMessage("URL must point to an image (e.g., .jpg, .png")
.trim();

export const createUser = [
// ...
makeNameValidator(),
makeProfileValidator(),
];
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
6 changes: 6 additions & 0 deletions frontend/src/api/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { get, handleAPIError, post, put } from "src/api/requests";
import { User } from "src/api/users";

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

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
33 changes: 33 additions & 0 deletions frontend/src/api/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { get, handleAPIError } from "src/api/requests";

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

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

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

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

export async function getUser(id: string): Promise<APIResult<User>> {
try {
const response = await get(`/api/user/${id}`);
const json = (await response.json()) as UserJSON;
return { success: true, data: parseUser(json) };
} catch (error) {
return handleAPIError(error);
}
}
13 changes: 9 additions & 4 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 @@ -34,6 +34,7 @@ vi.mock("src/api/tasks", () => ({
*
* See https://vitest.dev/guide/mocking#functions for more info about mock functions.
*/
updateTask: vi.fn((_params: UpdateTaskRequest) => Promise.resolve({ success: true })),
createTask: vi.fn((_params: CreateTaskRequest) => Promise.resolve({ success: true })),
}));

Expand Down Expand Up @@ -133,10 +134,14 @@ describe("TaskForm", () => {
});
const saveButton = screen.getByTestId(SAVE_BUTTON_ID);
fireEvent.click(saveButton);
expect(createTask).toHaveBeenCalledTimes(1);
expect(createTask).toHaveBeenCalledWith({
expect(updateTask).toHaveBeenCalledTimes(1);
expect(updateTask).toHaveBeenCalledWith({
_id: "task123", // or `mockTask._id`
title: "Updated title",
description: "Updated description",
//assignee: "", // or whatever you're passing
isChecked: false, // from `mockTask`
dateCreated: mockTask.dateCreated, // or appropriate fallback
});
await waitFor(() => {
// If the test ends before all state updates and rerenders occur, we'll
Expand Down
Loading