diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index 30a426db..f8418424 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -11,11 +11,11 @@ export const seed = [ "userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" }, { - "title": "Shopping List", - "content": "Milk, eggs, bread, and fruits.", + "title": "Different User - scoping check", + "content": "Should not see this note with Test User", "createdAt": "2024-02-05T23:33:42.253Z", "updatedAt": "2024-02-05T23:33:42.253Z", - "userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" + "userID": "dcf89a7e-b941-4f17-bbe0-4e0c8b2cd272" }, { "title": "Recipe", diff --git a/backend/tests/service/notes.spec.ts b/backend/tests/service/notes.spec.ts index ef2b85e8..72b68c8e 100644 --- a/backend/tests/service/notes.spec.ts +++ b/backend/tests/service/notes.spec.ts @@ -54,12 +54,15 @@ describe("Authenticated Flows", () => { token = response.body.token; }); - test("Get the list of Notes", async () => { + test("Should only see notes for the given user ", async () => { getNoteResponse = await request(NOTES_URL) .get("/") .set("Authorization", `Bearer ${token}`); + expect(getNoteResponse.status).toBe(200); - expect(getNoteResponse.body).toHaveLength(9); + + /** Should see 8 notes instead of all 9 in the database */ + expect(getNoteResponse.body).toHaveLength(8); expect(getNoteResponse.body[getNoteResponse.body.length - 1]).toStrictEqual( { @@ -71,6 +74,9 @@ describe("Authenticated Flows", () => { userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272", } ); + + /** Should not see notes for a different user */ + expect(getNoteResponse.body.find((note: any) => note.title === "Different User - scoping check")).toBeUndefined(); }); test("Create a new Note", async () => { @@ -96,7 +102,7 @@ describe("Authenticated Flows", () => { .get("/") .set("Authorization", `Bearer ${token}`); expect(getNoteResponse.status).toBe(200); - expect(getNoteResponse.body).toHaveLength(10); + expect(getNoteResponse.body).toHaveLength(9); }); test("Update a Note", async () => { @@ -115,7 +121,7 @@ describe("Authenticated Flows", () => { .get("/") .set("Authorization", `Bearer ${token}`); expect(getNoteResponse.status).toBe(200); - expect(getNoteResponse.body).toHaveLength(10); + expect(getNoteResponse.body).toHaveLength(9); // Updated note should appear first in the list expect(getNoteResponse.body[0].id).toBe(createdID); @@ -131,7 +137,7 @@ describe("Authenticated Flows", () => { .get("/") .set("Authorization", `Bearer ${token}`); expect(getNoteResponse.status).toBe(200); - expect(getNoteResponse.body).toHaveLength(9); + expect(getNoteResponse.body).toHaveLength(8); }); test("Error handling: Attempt to Delete a Note with invalid ID", async () => { diff --git a/backend/tests/service/users.spec.ts b/backend/tests/service/users.spec.ts index cee3199c..89f52cb3 100644 --- a/backend/tests/service/users.spec.ts +++ b/backend/tests/service/users.spec.ts @@ -39,7 +39,7 @@ describe("Authenticated Flows", () => { expect(getUsersResponse.body.length).toBeGreaterThan(0); }); - test("Usernames should be unique", async () => { + test("Username's should be unique", async () => { const response = await request(USERS_URL) .post("/") .set("Authorization", `Bearer ${token}`) diff --git a/frontend/src/App.css b/frontend/src/App.css index 3be5c1a2..7aaf1c83 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -20,6 +20,10 @@ body { flex-direction: column; width: 100vw; } + + .action-header { + width: 40%; + } } .login-page { @@ -110,13 +114,22 @@ h2 { .app-header { display: flex; - flex-direction: column; + flex-direction: row; justify-content: center; align-items: center; margin-bottom: 20px; width: 100%; } +.action-header { + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: center; + margin-bottom: 5px; + width: 100% vw; +} + .note-form { display: flex; flex-direction: column; diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx new file mode 100644 index 00000000..5302e4b2 --- /dev/null +++ b/frontend/src/App.test.tsx @@ -0,0 +1,60 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import App from "./App"; + +describe("App", () => { + it("renders correctly", () => { + render(); + + expect(screen.getByTestId("app-logo")).toBeInTheDocument(); + expect(screen.getByText("Login")).toBeInTheDocument(); + + expect(localStorage.getItem("token")).toBeNull(); + + expect(screen.queryByTestId("note-app")).not.toBeInTheDocument(); + }); + + it("renders correctly when logged in", async () => { + render(); + + const username = screen.getByTestId("username"); + const password = screen.getByTestId("password"); + const loginButton = screen.getByText("Login"); + + userEvent.type(username, "test"); + userEvent.type(password, "pass"); + userEvent.click(loginButton); + + const noteApp = await screen.findByTestId("note-app"); + + expect(noteApp).toBeInTheDocument(); + + expect(localStorage.getItem("token")).not.toBeNull(); + }); + + it("renders correctly when logged out", async () => { + render(); + + const username = screen.getByTestId("username"); + const password = screen.getByTestId("password"); + const loginButton = screen.getByText("Login"); + + userEvent.type(username, "test"); + userEvent.type(password, "pass"); + userEvent.click(loginButton); + + const noteApp = await screen.findByTestId("note-app"); + + expect(noteApp).toBeInTheDocument(); + + const logoutButton = screen.getByText("Logout"); + + userEvent.click(logoutButton); + + expect(localStorage.getItem("token")).toBeNull(); + + expect(screen.queryByTestId("note-app")).not.toBeInTheDocument(); + + expect(screen.getByText("Login")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0711737f..db947c3e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,12 +13,17 @@ function App() { setLoggedIn(true); }; + const handleLogout = () => { + setLoggedIn(false); + localStorage.removeItem("token"); + }; + return (
: } /> - : } /> + : } /> ); diff --git a/frontend/src/NoteApp.test.tsx b/frontend/src/NoteApp.test.tsx index 4546f639..bbd79c71 100644 --- a/frontend/src/NoteApp.test.tsx +++ b/frontend/src/NoteApp.test.tsx @@ -6,7 +6,7 @@ import { errorHandlers } from "./mocks/handlers"; test("Notes NoteApp loads with notes", async () => { - render(); + render( {}} />); expect(await screen.findByTestId('spinner-container')).not.toBeInTheDocument(); @@ -17,7 +17,7 @@ test("Notes NoteApp loads with notes", async () => { }); test('User can select and update note', async () => { - render(); + render( {}} />); expect(await screen.findByText("Test Title Note 1")).toBeInTheDocument(); // Click on the first note @@ -42,7 +42,7 @@ test('User can select and update note', async () => { }); test('User can add a Add a note', async () => { - render(); + render( {}} />); const addButton = await screen.findByRole("button", { name: "Add a note" }); userEvent.click(addButton); @@ -65,7 +65,7 @@ test('User can add a Add a note', async () => { }); test('User can delete a note', async () => { - render(); + render( {}} />); // Click on the first note const noteTitle = await screen.findByText("Test Title Note 2"); @@ -90,14 +90,14 @@ test('User can delete a note', async () => { test('Connection Error is displayed on Notes fetch', async () => { mswServer.use(...errorHandlers); - render(); + render( {}} />); expect(await screen.findByRole("heading", { name: "Warning: API Connection Issue"})).toBeInTheDocument(); }); test('Connection Error is displayed on Create Note', async () => { // Render the NoteApp component - render(); + render( {}}/>); const addButton = await screen.findByRole("button", { name: "Add a note" }); userEvent.click(addButton); @@ -127,7 +127,7 @@ test('Connection Error is displayed on Create Note', async () => { // }); test('Connection Error is displayed on Update Note', async () => { - render(); + render( {}}/>); // Click on the first note const notes = await screen.findAllByTestId("note"); @@ -143,4 +143,14 @@ test('Connection Error is displayed on Update Note', async () => { userEvent.click(saveButton); expect(await screen.findByRole("heading", { name: "Warning: API Connection Issue"})).toBeInTheDocument(); +}); + +test('User can logout', async () => { + const onLogout = vitest.fn(); + render(); + + const logoutButton = await screen.findByRole("button", { name: "Logout" }); + userEvent.click(logoutButton); + + expect(onLogout).toHaveBeenCalledTimes(1); }); \ No newline at end of file diff --git a/frontend/src/NoteApp.tsx b/frontend/src/NoteApp.tsx index f3e584ff..ef46834d 100644 --- a/frontend/src/NoteApp.tsx +++ b/frontend/src/NoteApp.tsx @@ -7,7 +7,11 @@ import Spinner from "./components/Spinner"; import type NoteType from "./types/note"; import { postNote, patchNote, getNotes, removeNote } from "./api/apiService"; -function NoteApp() { +export interface LogoutProps { + onLogout: () => void; +} + +const NoteApp: React.FC = ({ onLogout }) => { const [notes, setNotes] = useState([]); const [selectedNote, setSelectedNote] = useState(null); const [connectionIssue, setConnectionIssue] = useState(false); @@ -81,8 +85,8 @@ function NoteApp() { }; return ( -
-
+
+
+
{connectionIssue && ( diff --git a/frontend/src/api/apiService.ts b/frontend/src/api/apiService.ts index 391cd7fd..8861dfe1 100644 --- a/frontend/src/api/apiService.ts +++ b/frontend/src/api/apiService.ts @@ -64,6 +64,5 @@ export const login = async (username: string, password: string) => { }, } ); - console.log(response); return response; }; diff --git a/frontend/src/components/Header.test.tsx b/frontend/src/components/Header.test.tsx new file mode 100644 index 00000000..4a1e3ca8 --- /dev/null +++ b/frontend/src/components/Header.test.tsx @@ -0,0 +1,9 @@ +import { render, screen } from '@testing-library/react'; +import Header from './Header'; + +describe('Header', () => { + it('renders correctly', () => { + render(
); + expect(screen.getByTestId('app-logo')).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index ebe36abd..dbd3d3bf 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,7 +1,7 @@ const Header = () => { return (
- note icon + note icon
); }; diff --git a/playwright/tests/note.spec.ts b/playwright/tests/note.spec.ts index 98b1a5bd..37596987 100644 --- a/playwright/tests/note.spec.ts +++ b/playwright/tests/note.spec.ts @@ -18,7 +18,7 @@ test.beforeAll(async ({ browser }, { timeout }) => { }); test('Notes App e2e flow', async () => { - await test.step('Should see the login form', async () => { + await test.step('Should be able to login with valid user credentials', async () => { await expect(page.getByPlaceholder('Username')).toBeVisible(); await expect(page.getByPlaceholder('Password')).toBeVisible(); @@ -96,5 +96,12 @@ test('Notes App e2e flow', async () => { await expect(page.getByTestId('note-content').first()).not.toHaveText( NOTE_CONTENT ); + // Close the modal + await page.locator('button[class*="modal-close"]').click(); + }); + + await test.step('Should be able to logout', async () => { + await page.getByRole('button', { name: 'Logout' }).click(); + await expect(page.getByPlaceholder('Username')).toBeVisible(); }); });