diff --git a/backend/prisma/migrations/20240316221424_user_model/migration.sql b/backend/prisma/migrations/20240316221424_user_model/migration.sql new file mode 100644 index 00000000..0e89f4e2 --- /dev/null +++ b/backend/prisma/migrations/20240316221424_user_model/migration.sql @@ -0,0 +1,23 @@ +-- AlterTable +ALTER TABLE "Note" ADD COLUMN "user_id" INTEGER; + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "username" TEXT NOT NULL, + "password" TEXT NOT NULL, + "email" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- AddForeignKey +ALTER TABLE "Note" ADD CONSTRAINT "Note_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20240316224317_init/migration.sql b/backend/prisma/migrations/20240316224317_init/migration.sql new file mode 100644 index 00000000..249e903f --- /dev/null +++ b/backend/prisma/migrations/20240316224317_init/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- DropForeignKey +ALTER TABLE "Note" DROP CONSTRAINT "Note_user_id_fkey"; + +-- AlterTable +ALTER TABLE "Note" ALTER COLUMN "user_id" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "User" DROP CONSTRAINT "User_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "User_id_seq"; + +-- AddForeignKey +ALTER TABLE "Note" ADD CONSTRAINT "Note_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 497a9c2a..1c683b0a 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -17,4 +17,16 @@ model Note { content String createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @default(now()) @map("updated_at") + userID String? @map("user_id") + author User? @relation(fields: [userID], references: [id]) +} + +model User { + id String @id @default(uuid()) + username String @unique + password String + email String? @unique + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @map("updated_at") + notes Note[] } \ No newline at end of file diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index 4f9da588..419bb6ed 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -8,59 +8,80 @@ export const seed = [ "content": "Discussed project timelines and goals.", "createdAt": "2024-02-05T23:33:42.252Z", "updatedAt": "2024-02-05T23:33:42.252Z", + "userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" }, { "title": "Shopping List", "content": "Milk, eggs, bread, and fruits.", "createdAt": "2024-02-05T23:33:42.253Z", "updatedAt": "2024-02-05T23:33:42.253Z", + "userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" }, { "title": "Recipe", "content": "Ingredients: Chicken, tomatoes, onions, garlic.", "createdAt": "2024-02-05T23:33:42.254Z", "updatedAt": "2024-02-05T23:33:42.254Z", + "userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" }, { "title": "Ideas", "content": "Brainstorming ideas for the next feature release. 🚀", "createdAt": "2024-02-05T23:33:42.255Z", "updatedAt": "2024-02-05T23:33:42.255Z", + "userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" }, { "title": "Personal Goals", "content": "Exercise for 30 minutes daily. Read a book every week.", "createdAt": "2024-02-05T23:33:42.256Z", "updatedAt": "2024-02-05T23:33:42.256Z", + "userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" }, { "title": "Fête d'anniversaire", "content": "Préparer une surprise pour la fête d'anniversaire.", "createdAt": "2024-02-05T23:33:42.257Z", "updatedAt": "2024-02-05T23:33:42.257Z", + "userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" }, { "title": "日本旅行", "content": "計画: 東京、京都、大阪を訪れる。", "createdAt": "2024-02-05T23:33:42.258Z", "updatedAt": "2024-02-05T23:33:42.258Z", + "userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" }, { "title": "Семейный ужин", "content": "Приготовить вкусный ужин для всей семьи.", "createdAt": "2024-02-05T23:33:42.259Z", "updatedAt": "2024-02-05T23:33:42.259Z", + "userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" }, { "title": "Coding Project", "content": "Implement new features using React and Express.", "createdAt": "2024-02-05T23:33:42.260Z", "updatedAt": "2024-02-05T23:33:42.260Z", + "userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" } ]; async function main() { - // Seed data here + // Seed user data + await prisma.user.create({ + data: { + id : 'ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272', + email: 'helloitsdave@hotmail.com', + password: 'n0te$App!23', + username: 'Test User', + createdAt: "2024-02-05T23:33:42.260Z", + updatedAt: "2024-02-05T23:33:42.260Z", + } + }); + + // Seed note data await prisma.note.createMany({ data: seed, }); diff --git a/backend/src/index.ts b/backend/src/index.ts index e9419127..a61988f6 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -79,6 +79,38 @@ app.put("/api/notes/:id", async (req, res) => { } }); + app.get("/api/users", async (req, res) => { + try { + const users = await prisma.user.findMany(); + + const usersWithPasswordsRemoved = users.map((user) => { + delete user.password; + return user; + }); + res.json(usersWithPasswordsRemoved); + } catch (error) { + res.status(500).send({ "error": "Oops, something went wrong"}); + } + }); + + app.post("/api/users", async (req, res) => { + const { email, password, username } = req.body; + + if (!email || !password || !username) { + return res.status(400).send({ "error": "email, password, and username fields required"}); + } + + try { + const user = await prisma.user.create({ + data: { email, password, username }, + }); + delete user.password; + res.json(user); + } catch (error) { + res.status(500).send({ "error": "Oops, something went wrong"}); + } + }); + app.listen(PORT, () => { console.log("server running on localhost", PORT); }); diff --git a/backend/tests/service/api.spec.ts b/backend/tests/service/api.spec.ts index 20af52d3..b7acf9cf 100644 --- a/backend/tests/service/api.spec.ts +++ b/backend/tests/service/api.spec.ts @@ -26,6 +26,7 @@ test("Get the list of Notes", async () => { id: 1, title: "Meeting Notes", updatedAt: "2024-02-05T23:33:42.252Z", + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272", }); }); diff --git a/backend/tests/unit/app.spec.ts b/backend/tests/unit/app.spec.ts index d9a8afeb..154478b2 100644 --- a/backend/tests/unit/app.spec.ts +++ b/backend/tests/unit/app.spec.ts @@ -2,76 +2,12 @@ import { test, describe, expect, vi } from "vitest"; import request from "supertest"; import app from "../../src/index"; import prisma from "../../src/__mocks__/prisma"; +import { noteSeed } from "./mocks/notes.mock"; +import { userSeed } from "./mocks/users.mock" // Mock the prisma client vi.mock("../../src/prisma"); -const seed = [ - { - id: 1, - title: "Meeting Notes", - content: "Discussed project timelines and goals.", - updatedAt: new Date("2024-02-05T23:33:42.252Z"), - createdAt: new Date("2024-02-05T23:43:42.252Z") - }, - { - id: 2, - title: "Shopping List", - content: "Milk, eggs, bread, and fruits.", - updatedAt: new Date("2024-02-05T23:33:42.252Z"), - createdAt: new Date("2024-02-05T23:43:42.252Z") - }, - { - id: 3, - title: "Recipe", - content: "Ingredients: Chicken, tomatoes, onions, garlic.", - updatedAt: new Date("2024-02-05T23:33:42.252Z"), - createdAt: new Date("2024-02-05T23:43:42.252Z") - }, - { - id: 4, - title: "Ideas", - content: "Brainstorming ideas for the next feature release. 🚀", - updatedAt: new Date("2024-02-05T23:33:42.252Z"), - createdAt: new Date("2024-02-05T23:43:42.252Z") - }, - { - id: 5, - title: "Personal Goals", - content: "Exercise for 30 minutes daily. Read a book every week.", - updatedAt: new Date("2024-02-05T23:33:42.252Z"), - createdAt: new Date("2024-02-05T23:43:42.252Z") - }, - { - id: 6, - title: "Fête d'anniversaire", - content: "Préparer une surprise pour la fête d'anniversaire.", - updatedAt: new Date("2024-02-05T23:33:42.252Z"), - createdAt: new Date("2024-02-05T23:43:42.252Z") - }, - { - id: 7, - title: "日本旅行", - content: "計画: 東京、京都、大阪を訪れる。", - updatedAt: new Date("2024-02-05T23:33:42.252Z"), - createdAt: new Date("2024-02-05T23:43:42.252Z") - }, - { - id: 8, - title: "Семейный ужин", - content: "Приготовить вкусный ужин для всей семьи.", - updatedAt: new Date("2024-02-05T23:33:42.252Z"), - createdAt: new Date("2024-02-05T23:43:42.252Z") - }, - { - id: 9, - title: "Coding Project", - content: "Implement new features using React and Express.", - updatedAt: new Date("2024-02-05T23:33:42.252Z"), - createdAt: new Date("2024-02-05T23:43:42.252Z") - }, -]; - describe("View notes", () => { test("No notes returned - success", async ({}) => { prisma.note.findMany.mockResolvedValue([]); @@ -80,7 +16,7 @@ describe("View notes", () => { expect(response.body).toStrictEqual([]); }); test("Single note returned - success", async ({}) => { - prisma.note.findMany.mockResolvedValue([seed[0]]); + prisma.note.findMany.mockResolvedValue([noteSeed[0]]); const response = await request(app).get("/api/notes"); expect(response.status).toBe(200); expect(response.body).toEqual([ @@ -90,21 +26,22 @@ describe("View notes", () => { content: "Discussed project timelines and goals.", createdAt: "2024-02-05T23:43:42.252Z", updatedAt: "2024-02-05T23:33:42.252Z", + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272", }, ]); }); test("Many notes returned - success", async ({}) => { - - prisma.note.findMany.mockResolvedValue(seed); + prisma.note.findMany.mockResolvedValue(noteSeed); const response = await request(app).get("/api/notes"); expect(response.status).toBe(200); - const expectedResult = seed.map(item => ({ + const expectedResult = noteSeed.map((item) => ({ ...item, createdAt: new Date(item.createdAt).toISOString(), - updatedAt: new Date(item.updatedAt).toISOString() + updatedAt: new Date(item.updatedAt).toISOString(), + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272", })); - + expect(response.body).toEqual(expectedResult); }); test("500 error - failure", async ({}) => { @@ -124,6 +61,7 @@ describe("Create a note", () => { id: 1, updatedAt: new Date("2024-02-05T23:33:42.252Z"), createdAt: new Date("2024-02-05T23:33:42.252Z"), + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272", }); const response = await request(app) .post("/api/notes") @@ -173,6 +111,7 @@ describe("Update a note", () => { id: 1, updatedAt: new Date("2024-02-05T23:33:42.252Z"), createdAt: new Date("2024-02-05T23:33:42.252Z"), + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272", }); const response = await request(app) .put("/api/notes/1") @@ -183,7 +122,8 @@ describe("Update a note", () => { content: "Test", createdAt: "2024-02-05T23:33:42.252Z", id: 1, - updatedAt: "2024-02-05T23:33:42.252Z" + updatedAt: "2024-02-05T23:33:42.252Z", + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272", }); }); test("PUT without title - failure", async ({}) => { @@ -258,10 +198,103 @@ describe("Delete a note", () => { }); }); -describe("Health check", () => { +describe("Health check", () => { test("GET /health", async ({}) => { const response = await request(app).get("/health"); expect(response.status).toBe(200); expect(response.body).toStrictEqual({ status: "ok" }); }); }); + +describe("Get Users", () => { + test("No Users returned", async ({}) => { + prisma.user.findMany.mockResolvedValue([]); + const response = await request(app).get("/api/users"); + expect(response.status).toBe(200); + expect(response.body).toStrictEqual([]); + }); + + test("Should get many users returned", async () => { + prisma.user.findMany.mockResolvedValue(userSeed); + + const response = await request(app).get("/api/users"); + expect(response.status).toBe(200); + + const expectedResult = userSeed.map((item) => ({ + ...item, + createdAt: new Date(item.createdAt).toISOString(), + updatedAt: new Date(item.updatedAt).toISOString(), + })); + + expect(response.body[0]).not.toHaveProperty('password') + + expect(response.body).toEqual(expectedResult); + }); + + test("Network Error", async ({}) => { + prisma.user.findMany.mockImplementation(() => { + throw new Error("Test error"); + }); + const response = await request(app).get("/api/users"); + expect(response.status).toBe(500); + expect(response.body).toStrictEqual({ error: "Oops, something went wrong"}); + }); +}); + +describe("Create User", () => { + test("POST with email, username and password", async ({}) => { + prisma.user.create.mockResolvedValue({ + id: "gcf89a7e-b941-4f17-bbe0-4e0c8b2cd272", + email: 'test@email.com', + username: 'Dave', + password: 'check', + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:33:42.252Z"), + }); + const response = await request(app) + .post("/api/users") + .send({ email: "email", username: "Dave", password: 'check' }); + expect(response.status).toBe(200); + }); + + test("POST without email", async ({}) => { + const response = await request(app) + .post("/api/users") + .send({ username: "Dave", password: 'check' }); + expect(response.status).toBe(400); + expect(response.body).toStrictEqual({ + error: "email, password, and username fields required", + }); + }); + + test("POST without username", async ({}) => { + const response = await request(app) + .post("/api/users") + .send({ email: "Dave@hotmail.com", password: 'check' }); + expect(response.status).toBe(400); + expect(response.body).toStrictEqual({ + error: "email, password, and username fields required", + }); + }); + + test("POST without password", async ({}) => { + const response = await request(app) + .post("/api/users") + .send({ email: "Dave@hotmail.com", username: 'check' }); + expect(response.status).toBe(400); + expect(response.body).toStrictEqual({ + error: "email, password, and username fields required", + }); + }); + + test("POST with error", async ({}) => { + prisma.user.create.mockImplementation(() => { + throw new Error("Test error"); + }); + const response = await request(app) + .post("/api/users") + .send({ email: "email", username: "Dave", password: 'check' }); + expect(response.status).toBe(500); + expect(response.body).toStrictEqual({ error: "Oops, something went wrong"}); + }); +}); diff --git a/backend/tests/unit/mocks/notes.mock.ts b/backend/tests/unit/mocks/notes.mock.ts new file mode 100644 index 00000000..648d3796 --- /dev/null +++ b/backend/tests/unit/mocks/notes.mock.ts @@ -0,0 +1,74 @@ +export const noteSeed = [ + { + id: 1, + title: "Meeting Notes", + content: "Discussed project timelines and goals.", + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:43:42.252Z"), + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" + }, + { + id: 2, + title: "Shopping List", + content: "Milk, eggs, bread, and fruits.", + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:43:42.252Z"), + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" + }, + { + id: 3, + title: "Recipe", + content: "Ingredients: Chicken, tomatoes, onions, garlic.", + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:43:42.252Z"), + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" + }, + { + id: 4, + title: "Ideas", + content: "Brainstorming ideas for the next feature release. 🚀", + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:43:42.252Z"), + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" + }, + { + id: 5, + title: "Personal Goals", + content: "Exercise for 30 minutes daily. Read a book every week.", + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:43:42.252Z"), + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" + }, + { + id: 6, + title: "Fête d'anniversaire", + content: "Préparer une surprise pour la fête d'anniversaire.", + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:43:42.252Z"), + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" + }, + { + id: 7, + title: "日本旅行", + content: "計画: 東京、京都、大阪を訪れる。", + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:43:42.252Z"), + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" + }, + { + id: 8, + title: "Семейный ужин", + content: "Приготовить вкусный ужин для всей семьи.", + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:43:42.252Z"), + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" + }, + { + id: 9, + title: "Coding Project", + content: "Implement new features using React and Express.", + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:43:42.252Z"), + userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" + }, + ]; \ No newline at end of file diff --git a/backend/tests/unit/mocks/users.mock.ts b/backend/tests/unit/mocks/users.mock.ts new file mode 100644 index 00000000..4291e07a --- /dev/null +++ b/backend/tests/unit/mocks/users.mock.ts @@ -0,0 +1,26 @@ +export const userSeed = [ + { + id: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272", + username: "Dave@notes.app", + email: "dave@test.com", + password: "shouldnotappear", + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:43:42.252Z"), + }, + { + id: "dcf89a7e-b941-4f17-bbe0-4e0c8b2cd272", + username: "Jim O'Brien", + email: "jim@test.com", + password: "shouldnotappear", + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:43:42.252Z"), + }, + { + id: "dcf89a7e-b941-4f17-bbe0-4e0c8b2cd272", + username: "Jim", + email: "email.email@email.io", + password: "shouldnotappear", + updatedAt: new Date("2024-02-05T23:33:42.252Z"), + createdAt: new Date("2024-02-05T23:43:42.252Z"), + }, + ]; \ No newline at end of file diff --git a/backend/tests/unit/primsa.spec.ts b/backend/tests/unit/primsa.spec.ts index eeec33e2..a8532a93 100644 --- a/backend/tests/unit/primsa.spec.ts +++ b/backend/tests/unit/primsa.spec.ts @@ -1,6 +1,10 @@ import { test, expect } from "vitest"; import prisma from "../../src/prisma"; -test("Ensure Prisma Singleton contains note schemaq", async () => { +test("Ensure Prisma Singleton contains note schema", async () => { expect(prisma).toHaveProperty('note'); }); + +test("Ensure Prisma Singleton contains note schema", async () => { + expect(prisma).toHaveProperty('user'); +});