From 1baaf36eca64f5efedf5c9b4960931f7098ab9e8 Mon Sep 17 00:00:00 2001 From: Dave <43282177+helloitsdave@users.noreply.github.com> Date: Mon, 27 May 2024 20:23:16 +0100 Subject: [PATCH] [pact] feat: Adding additional pact test (#49) * [pact] feat: Adding additional pact test --- backend/package-lock.json | 28 ++----- backend/package.json | 4 +- backend/src/routes/noteRoutes.ts | 5 ++ backend/tests/unit/notes.spec.ts | 28 +++++++ .../contract-tests/consumer.pact.spec.ts | 43 ++++++++++- .../pacts/NotesFEService-NotesBEService.json | 29 ++++++++ frontend/src/NoteApp.tsx | 4 +- frontend/src/api/apiService.ts | 74 ++++++++++--------- frontend/tsconfig.json | 2 +- frontend/vite.config.pact.js | 2 +- 10 files changed, 154 insertions(+), 65 deletions(-) rename frontend/{src => }/contract-tests/consumer.pact.spec.ts (78%) diff --git a/backend/package-lock.json b/backend/package-lock.json index f1c8595f..9b3ebfd3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -34,7 +34,6 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "globals": "^15.1.0", - "husky": "^9.0.11", "nodemon": "3.0.2", "nyc": "15.1.0", "prettier": "^3.2.5", @@ -2908,9 +2907,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001621", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz", - "integrity": "sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==", + "version": "1.0.30001623", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001623.tgz", + "integrity": "sha512-X/XhAVKlpIxWPpgRTnlgZssJrF0m6YtRA0QDWgsBNT12uZM6LPRydR7ip405Y3t1LamD8cP2TZFEDZFBf5ApcA==", "dev": true, "funding": [ { @@ -4857,21 +4856,6 @@ "node": ">=10.17.0" } }, - "node_modules/husky": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", - "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", - "dev": true, - "bin": { - "husky": "bin.mjs" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7049,9 +7033,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.0.tgz", + "integrity": "sha512-G3nn4N8SRaR9NsCqEUHfTlfTM/Fgza1yfb8JP2CEmzYuHtHWza5Uf+g7nuUQq96prwu0GiGyPgDw752+j4fzQQ==", "dev": true }, "node_modules/secure-json-parse": { diff --git a/backend/package.json b/backend/package.json index a0a5fd0e..961787fd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,8 +19,7 @@ "prisma": "npx prisma migrate dev --name init", "seed": "npx ts-node prisma/seed.ts", "lint": "eslint .", - "lint:fix": "eslint . --fix", - "prepare": "husky" + "lint:fix": "eslint . --fix" }, "author": "Dave Gordon", "license": "ISC", @@ -41,7 +40,6 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "globals": "^15.1.0", - "husky": "^9.0.11", "nodemon": "3.0.2", "nyc": "15.1.0", "prettier": "^3.2.5", diff --git a/backend/src/routes/noteRoutes.ts b/backend/src/routes/noteRoutes.ts index ffabf25e..62efd65e 100644 --- a/backend/src/routes/noteRoutes.ts +++ b/backend/src/routes/noteRoutes.ts @@ -43,6 +43,11 @@ router.put('/api/notes/:id', authenticateToken, async (req, res) => { return res.status(400).send({ error: 'title and content fields required' }); } + const note = await prisma.note.findUnique({ where: { id } }); + if (!note) { + return res.status(404).send({ error: 'Note not found' }); + } + try { const updatedNote = await prisma.note.update({ where: { id }, diff --git a/backend/tests/unit/notes.spec.ts b/backend/tests/unit/notes.spec.ts index 4c84c333..37ad95aa 100644 --- a/backend/tests/unit/notes.spec.ts +++ b/backend/tests/unit/notes.spec.ts @@ -117,6 +117,14 @@ describe('Create a note', () => { describe('Update a note', () => { test('PUT update note - success', async ({}) => { + prisma.note.findUnique.mockResolvedValue({ + title: 'Test', + content: 'Test', + id: 'a1b2c3d4-1234-5678-9abc-abcdef123457', + updatedAt: new Date('2024-02-05T23:33:42.252Z'), + createdAt: new Date('2024-02-05T23:33:42.252Z'), + userID: 'ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272', + }); prisma.note.update.mockResolvedValue({ title: 'Test update', content: 'Test', @@ -156,7 +164,27 @@ describe('Update a note', () => { .send({ title: 'Test', content: 'Test', id: 1 }); expect(response.status).toBe(404); }); + test('PUT with a 404 error - failure', async ({}) => { + prisma.note.update.mockImplementation(() => { + throw new Error('Test error'); + }); + const response = await request(app) + .put('/api/notes/a1b2c3d4-1234-5678-9abc-abcdef123457') + .send({ title: 'Test update', content: 'Test', id: 1 }); + expect(response.status).toBe(404); + expect(response.body).toStrictEqual({ + error: 'Note not found', + }); + }); test('PUT with a 500 error - failure', async ({}) => { + prisma.note.findUnique.mockResolvedValue({ + title: 'Test', + content: 'Test', + id: 'a1b2c3d4-1234-5678-9abc-abcdef123457', + updatedAt: new Date('2024-02-05T23:33:42.252Z'), + createdAt: new Date('2024-02-05T23:33:42.252Z'), + userID: 'ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272', + }); prisma.note.update.mockImplementation(() => { throw new Error('Test error'); }); diff --git a/frontend/src/contract-tests/consumer.pact.spec.ts b/frontend/contract-tests/consumer.pact.spec.ts similarity index 78% rename from frontend/src/contract-tests/consumer.pact.spec.ts rename to frontend/contract-tests/consumer.pact.spec.ts index 1f533bfd..886c2fbd 100644 --- a/frontend/src/contract-tests/consumer.pact.spec.ts +++ b/frontend/contract-tests/consumer.pact.spec.ts @@ -4,7 +4,8 @@ import { MatchersV3, SpecificationVersion, } from "@pact-foundation/pact"; -import * as API from "../api/apiService"; +import * as API from "../src/api/apiService"; +import { AxiosResponse } from "axios"; const { eachLike, like } = MatchersV3; const provider = new PactV3({ @@ -140,7 +141,7 @@ describe("Pact with NotesBEService", () => { content: "Updated Note Content", }); - expect(response.data).toStrictEqual( + expect((response as AxiosResponse).data).toStrictEqual( { "id": "a37f39bc-9e4f-45f2-b1d6-fe668bba2b55", "title": "Updated Note Title", @@ -148,7 +149,45 @@ describe("Pact with NotesBEService", () => { "createdAt": "2024-05-21T22:58:55.743Z", "updatedAt": "2024-05-26T19:43:58.742Z", "userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272" + }, + ); + }); + }); + it("update non-existant note", async () => { + provider.addInteraction({ + states: [{ description: "note does not exist" }], + uponReceiving: "a request to update a non-existant note", + withRequest: { + method: "PUT", + path: "/api/notes/b37f39bc-9e4f-45f2-b1d6-fe668bba2b55", + body: { + title: "Updated Note Title", + content: "Updated Note Content", + }, + }, + willRespondWith: { + status: 404, + headers: { + "Content-Type": "application/json" + }, + body: like ( { error: "Note not found" }), }, + }); + + await provider.executeTest(async (mockService) => { + const response = await API.patchNote({ + id: "b37f39bc-9e4f-45f2-b1d6-fe668bba2b55", + title: "Updated Note Title", + content: "Updated Note Content", + }); + + expect(response.status).toBe(404); + + expect(response).toStrictEqual( + { + error: "Note not found", + status: 404, + }, ); }); }); diff --git a/frontend/pacts/NotesFEService-NotesBEService.json b/frontend/pacts/NotesFEService-NotesBEService.json index 39f4c01b..bf70d795 100644 --- a/frontend/pacts/NotesFEService-NotesBEService.json +++ b/frontend/pacts/NotesFEService-NotesBEService.json @@ -3,6 +3,35 @@ "name": "NotesFEService" }, "interactions": [ + { + "description": "a request to update a non-existant note", + "providerState": "note does not exist", + "request": { + "body": { + "content": "Updated Note Content", + "title": "Updated Note Title" + }, + "headers": { + "Content-Type": "application/json" + }, + "method": "PUT", + "path": "/api/notes/b37f39bc-9e4f-45f2-b1d6-fe668bba2b55" + }, + "response": { + "body": { + "error": "Note not found" + }, + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "$.body": { + "match": "type" + } + }, + "status": 404 + } + }, { "description": "a request to create a note", "providerState": "note is added", diff --git a/frontend/src/NoteApp.tsx b/frontend/src/NoteApp.tsx index f162b941..ea394e02 100644 --- a/frontend/src/NoteApp.tsx +++ b/frontend/src/NoteApp.tsx @@ -53,7 +53,9 @@ const NoteApp: React.FC = ({ onLogout }) => { const updateNote = async (updatedNote: NoteType) => { try { const response = await patchNote(updatedNote); - await response.data; + if ('data' in response) { + await response.data; + } fetchNotes(); } catch (error) { console.error(error); diff --git a/frontend/src/api/apiService.ts b/frontend/src/api/apiService.ts index 9122c89e..5b279ed6 100644 --- a/frontend/src/api/apiService.ts +++ b/frontend/src/api/apiService.ts @@ -1,9 +1,8 @@ -import axios from "axios"; +import axios, { AxiosError } from "axios"; import type NoteType from "../types/note"; -const URL = - process.env.REACT_APP_API_BASE_URL || "http://localhost:5000/api/"; +const URL = process.env.REACT_APP_API_BASE_URL || "http://localhost:5000/api/"; const api = axios.create({ baseURL: URL, @@ -15,30 +14,39 @@ const api = axios.create({ api.interceptors.request.use((config) => { const token = localStorage.getItem("token"); - config.headers.Authorization = token ? `Bearer ${token}` : ''; + config.headers.Authorization = token ? `Bearer ${token}` : ""; return config; }); export const postNote = async (newNote: NoteType) => { - const response = await api.post( - 'notes', - { - title: newNote.title, - content: newNote.content, - } - ); + const response = await api.post("notes", { + title: newNote.title, + content: newNote.content, + }); return response; }; export const patchNote = async (updatedNote: NoteType) => { - const response = await api.put( - `notes/${updatedNote.id}`, - { + try { + const response = await api.put(`notes/${updatedNote.id}`, { title: updatedNote.title, content: updatedNote.content, + }); + return response; + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response && axiosError.response.status === 404) { + // Return custom response for 404 error + return { + status: 404, + error: 'Note not found', + }; + } else { + // Log and rethrow the error for non-404 errors + console.log(axiosError); + throw axiosError; } - ); - return response; + } }; export const removeNote = async (id: string) => { @@ -47,13 +55,13 @@ export const removeNote = async (id: string) => { }; export const getNotes = async () => { - const response = await api.get('notes'); + const response = await api.get("notes"); return response; }; export const login = async (username: string, password: string) => { const response = await api.post( - 'login', + "login", { username, password, @@ -67,28 +75,24 @@ export const login = async (username: string, password: string) => { return response; }; -export const createUser = async ( user: - { username: string, - email: string, - password: string } -) => { - const response = await api.post( - 'users', - user, - { - headers: { - "Content-Type": "application/json", - }, - } - ); +export const createUser = async (user: { + username: string; + email: string; + password: string; +}) => { + const response = await api.post("users", user, { + headers: { + "Content-Type": "application/json", + }, + }); return response; -} +}; export const deleteUser = async () => { - const response = await api.delete('users', { + const response = await api.delete("users", { headers: { "Content-Type": "application/json", }, }); return response; -} \ No newline at end of file +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a910e529..f5765543 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -23,5 +23,5 @@ }, "include": [ "src" - ] +, "contract-tests" ] } diff --git a/frontend/vite.config.pact.js b/frontend/vite.config.pact.js index 1dda14af..c0255c3f 100644 --- a/frontend/vite.config.pact.js +++ b/frontend/vite.config.pact.js @@ -4,7 +4,7 @@ import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], test: { - include: ["**/src/contract-tests/**",], + include: ["**/contract-tests/**",], globals: true, environment: "jsdom", },