diff --git a/.github/workflows/build-tests.yml b/.github/workflows/build-tests.yml index c463425..67b109a 100644 --- a/.github/workflows/build-tests.yml +++ b/.github/workflows/build-tests.yml @@ -9,13 +9,12 @@ jobs: steps: - uses: actions/checkout@v3 - name: Build Docker Image - run: docker build -t codehammers/cd-testv1:latest -f Dockerfile-dev . + run: docker build -t codehammers/ch-dev-dep-v2:latest -f Dockerfile-dev . - name: Install Root Dependencies - run: docker run codehammers/cd-testv1:latest npm install + run: docker run codehammers/ch-dev-dep-v2:latest npm install - name: Install Client Dependencies - run: docker run codehammers/cd-testv1:latest /bin/sh -c "cd client && npm install" - #- name: List node_modules - #run: docker run codehammers/cd-testv1:latest /bin/sh -c "ls node_modules && cd client && ls node_modules" + run: docker run codehammers/ch-dev-dep-v2:latest /bin/sh -c "cd client && npm install" + - run: LINT_COMMAND=lint docker-compose -f docker-compose-lint.yml up --abort-on-container-exit - run: docker-compose -f docker-compose-test.yml up --abort-on-container-exit env: JWT_SECRET: ${{ secrets.JWT_SECRET }} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..c794dcd --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +# These files should always be ignored by Prettier. + +build/ +coverage/ +dist/ +node_modules/ +package-lock.json +*.test.jsx.snap +*.test.tsx.snap \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..48f074b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": true, + "jsxSingleQuote": false, + "printWidth": 100, + "tabWidth": 2, + "trailingComma": "all" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ca9e953 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + } +} diff --git a/Dockerfile b/Dockerfile index dd07b46..0366321 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Set Node.js version -FROM node:18.17.1 as builder +FROM node:20.14.0 as builder # Set the working directory WORKDIR /usr/src/app @@ -22,7 +22,7 @@ RUN cd client && npm run build RUN npm run build # Set up the final image -FROM node:18.17.1 +FROM node:20.14.0 # Set the working directory WORKDIR /usr/src/app diff --git a/Dockerfile-dev b/Dockerfile-dev index 5e1c903..1c2fad0 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -1,5 +1,5 @@ # Set Node version -FROM node:18.17.1 +FROM node:20.14.0 # Install webpack globally RUN npm install webpack -g diff --git a/__tests__/db.test.ts b/__tests__/db.test.ts index cd35c38..fc11d44 100644 --- a/__tests__/db.test.ts +++ b/__tests__/db.test.ts @@ -1,56 +1,52 @@ -import mongoose from "mongoose"; -import connectDB from "../server/config/db"; +import mongoose from 'mongoose'; +import connectDB from '../server/config/db'; -jest.mock("mongoose", () => ({ +jest.mock('mongoose', () => ({ connect: jest.fn().mockImplementation(() => Promise.resolve({ - connection: { host: "test-host" }, - }) + connection: { host: 'test-host' }, + }), ), })); -describe("connectDB", () => { +describe('connectDB', () => { let mockExit: jest.SpyInstance; let mockConsoleError: jest.SpyInstance; - let mockConsoleLog: jest.SpyInstance; beforeEach(() => { - mockExit = jest - .spyOn(process, "exit") - .mockImplementation((_code) => undefined as never); - mockConsoleError = jest.spyOn(console, "error").mockImplementation(); - mockConsoleLog = jest.spyOn(console, "log").mockImplementation(); + mockExit = jest.spyOn(process, 'exit').mockImplementation((_code) => undefined as never); + mockConsoleError = jest.spyOn(console, 'error').mockImplementation(); }); afterEach(() => { jest.clearAllMocks(); }); - it("should call mongoose.connect with MONGO_URI", async () => { - process.env.MONGO_URI = "test-mongo-uri"; + it('should call mongoose.connect with MONGO_URI', async () => { + process.env.MONGO_URI = 'test-mongo-uri'; await connectDB(); - expect(mongoose.connect).toHaveBeenCalledWith("test-mongo-uri"); + expect(mongoose.connect).toHaveBeenCalledWith('test-mongo-uri'); }); - it("should log an error and exit the process if mongoose.connect fails", async () => { - process.env.MONGO_URI = "test-mongo-uri"; + it('should log an error and exit the process if mongoose.connect fails', async () => { + process.env.MONGO_URI = 'test-mongo-uri'; (mongoose.connect as jest.Mock).mockImplementationOnce(() => { - throw new Error("test error"); + throw new Error('test error'); }); await connectDB(); - expect(mockConsoleError).toHaveBeenCalledWith("test error"); + expect(mockConsoleError).toHaveBeenCalledWith('test error'); expect(mockExit).toHaveBeenCalledWith(1); }); - it("should throw an error if MONGO_URI is not defined", async () => { + it('should throw an error if MONGO_URI is not defined', async () => { delete process.env.MONGO_URI; await connectDB(); expect(mockConsoleError).toHaveBeenCalledWith( - "MONGO_URI must be defined in the environment variables." + 'MONGO_URI must be defined in the environment variables.', ); expect(mockExit).toHaveBeenCalledWith(1); }); diff --git a/__tests__/errorController.test.ts b/__tests__/errorController.test.ts index c39b24d..e723193 100644 --- a/__tests__/errorController.test.ts +++ b/__tests__/errorController.test.ts @@ -1,7 +1,7 @@ -import { Request, Response, NextFunction } from "express"; -import { notFound, errorHandler } from "../server/controllers/errorControllers"; +import { Request, Response, NextFunction } from 'express'; +import { notFound, errorHandler } from '../server/controllers/errorControllers'; -describe("Middleware Tests", () => { +describe('Middleware Tests', () => { let mockRequest: Partial; let mockResponse: Partial; let mockNext: NextFunction; @@ -15,29 +15,24 @@ describe("Middleware Tests", () => { mockNext = jest.fn(); }); - describe("notFound Middleware", () => { - it("should return 404 and the original URL", () => { + describe('notFound Middleware', () => { + it('should return 404 and the original URL', () => { notFound(mockRequest as Request, mockResponse as Response, mockNext); expect(mockResponse.status).toHaveBeenCalledWith(404); expect(mockNext).toHaveBeenCalled(); }); }); - describe("errorHandler Middleware", () => { - it("should handle the error correctly", () => { - const mockError = new Error("Some error"); - errorHandler( - mockError, - mockRequest as Request, - mockResponse as Response, - mockNext - ); + describe('errorHandler Middleware', () => { + it('should handle the error correctly', () => { + const mockError = new Error('Some error'); + errorHandler(mockError, mockRequest as Request, mockResponse as Response, mockNext); expect(mockResponse.status).toHaveBeenCalledWith(400); expect(mockResponse.json).toHaveBeenCalledWith( expect.objectContaining({ message: expect.anything(), stack: expect.any(String), - }) + }), ); }); }); diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index fdc88ba..f39a5eb 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -1,10 +1,12 @@ -import request from "supertest"; -import app, { startServer } from "../server/index"; -import { Server } from "http"; +import request from 'supertest'; +import app, { startServer } from '../server/index'; +import { Server } from 'http'; +import mongoose from 'mongoose'; -let server: Server; +// TODO +/*eslint jest/no-disabled-tests: "off"*/ -import mongoose from "mongoose"; +let server: Server; beforeEach(() => { server = startServer(); @@ -24,49 +26,47 @@ afterAll(async () => { await mongoose.connection.close(); }); -describe("API Endpoints", () => { - xit("should get the API Running message in development", async () => { - const res = await request(app).get("/api"); +describe('API Endpoints', () => { + xit('should get the API Running message in development', async () => { + const res = await request(app).get('/api'); expect(res.statusCode).toEqual(200); - expect(res.body).toHaveProperty("message", "API Running - Hazzah!"); + expect(res.body).toHaveProperty('message', 'API Running - Hazzah!'); }); - xit("should serve the frontend files in production", async () => { - process.env.NODE_ENV = "production"; + xit('should serve the frontend files in production', async () => { + process.env.NODE_ENV = 'production'; - const res = await request(app).get("/"); + const res = await request(app).get('/'); expect(res.statusCode).toEqual(200); - expect(res.headers["content-type"]).toContain("text/html"); + expect(res.headers['content-type']).toContain('text/html'); }); - xit("should catch all routes and serve the frontend in production", async () => { - process.env.NODE_ENV = "production"; - const res = await request(app).get("/nonexistentroute"); + xit('should catch all routes and serve the frontend in production', async () => { + process.env.NODE_ENV = 'production'; + const res = await request(app).get('/nonexistentroute'); expect(res.statusCode).toEqual(200); - expect(res.headers["content-type"]).toContain("text/html"); + expect(res.headers['content-type']).toContain('text/html'); }); }); -describe("Server Start-Up", () => { - it("should start up the server if required as main module", async () => { +describe('Server Start-Up', () => { + it('should start up the server if required as main module', async () => { const originalLog = console.log; const logCalls: string[] = []; - console.log = jest.fn((...args: any[]) => { - logCalls.push(args.join(" ")); + console.log = jest.fn((...args: string[]) => { + logCalls.push(args.join(' ')); }); jest.resetModules(); await new Promise((resolve) => { if (server) { - server.on("listening", resolve); + server.on('listening', resolve); } }); - const hasExpectedLog = logCalls.some((log) => - log.includes("Server running in") - ); + const hasExpectedLog = logCalls.some((log) => log.includes('Server running in')); expect(hasExpectedLog).toBe(true); console.log = originalLog; diff --git a/__tests__/profileController.test.ts b/__tests__/profileController.test.ts index 614e860..5081639 100644 --- a/__tests__/profileController.test.ts +++ b/__tests__/profileController.test.ts @@ -1,20 +1,23 @@ -import { Request, Response, NextFunction } from "express"; +import { Request, Response, NextFunction } from 'express'; import { createProfile, updateProfile, getAllProfiles, getProfileById, -} from "../server/controllers/profileController"; -import Profile from "../server/models/profileModel"; +} from '../server/controllers/profileController'; +import Profile from '../server/models/profileModel'; -jest.mock("../server/models/profileModel", () => ({ +// TODO +/*eslint jest/no-disabled-tests: "off"*/ + +jest.mock('../server/models/profileModel', () => ({ findOneAndUpdate: jest.fn(), findOne: jest.fn(), create: jest.fn(), find: jest.fn(), })); -describe("Profile Controller Tests", () => { +describe('Profile Controller Tests', () => { let mockRequest: Partial; let mockResponse: Partial; let mockNext: NextFunction = jest.fn(); @@ -28,81 +31,77 @@ describe("Profile Controller Tests", () => { }; }); - describe("createProfile function", () => { - xit("should handle profile creation", async () => { + describe('createProfile function', () => { + xit('should handle profile creation', async () => { (Profile.create as jest.Mock).mockResolvedValue({ - _id: "someId", - bio: "I am Code", + _id: 'someId', + bio: 'I am Code', job: { - title: "Senior Developer", - company: "ACME Corp.", - description: "Working on various projects...", - date: "2021-04-07T00:00:00.000Z", + title: 'Senior Developer', + company: 'ACME Corp.', + description: 'Working on various projects...', + date: '2021-04-07T00:00:00.000Z', }, socials: { - linkedIn: "https://www.linkedin.com/in/yourprofile", - github: "https://github.com/yourprofile", - twitter: "https://twitter.com/yourprofile", - facebook: "https://www.facebook.com/yourprofile", - instagram: "https://www.instagram.com/yourprofile", + linkedIn: 'https://www.linkedin.com/in/yourprofile', + github: 'https://github.com/yourprofile', + twitter: 'https://twitter.com/yourprofile', + facebook: 'https://www.facebook.com/yourprofile', + instagram: 'https://www.instagram.com/yourprofile', }, }); mockRequest.body = { - user: "65117c94f000c9930ef5c0ee", - bio: "I am Code", + user: '65117c94f000c9930ef5c0ee', + bio: 'I am Code', job: { - title: "Senior Developer", - company: "ACME Corp.", - description: "Working on various projects", - date: "2021-04-07T00:00:00.000Z", + title: 'Senior Developer', + company: 'ACME Corp.', + description: 'Working on various projects', + date: '2021-04-07T00:00:00.000Z', }, socials: { - linkedIn: "https://www.linkedin.com/in/yourprofile", - github: "https://github.com/yourprofile", - twitter: "https://twitter.com/yourprofile", - facebook: "https://www.facebook.com/yourprofile", - instagram: "https://www.instagram.com/yourprofile", + linkedIn: 'https://www.linkedin.com/in/yourprofile', + github: 'https://github.com/yourprofile', + twitter: 'https://twitter.com/yourprofile', + facebook: 'https://www.facebook.com/yourprofile', + instagram: 'https://www.instagram.com/yourprofile', }, }; - await createProfile( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await createProfile(mockRequest as Request, mockResponse as Response, mockNext); expect(mockResponse.json).toHaveBeenCalledWith( expect.objectContaining({ - _id: "someId", - bio: "I am Code", + _id: 'someId', + bio: 'I am Code', job: { - title: "Senior Developer", - company: "ACME Corp.", - description: "Working on various projects...", - date: "2021-04-07T00:00:00.000Z", + title: 'Senior Developer', + company: 'ACME Corp.', + description: 'Working on various projects...', + date: '2021-04-07T00:00:00.000Z', }, socials: { - linkedIn: "https://www.linkedin.com/in/yourprofile", - github: "https://github.com/yourprofile", - twitter: "https://twitter.com/yourprofile", - facebook: "https://www.facebook.com/yourprofile", - instagram: "https://www.instagram.com/yourprofile", + linkedIn: 'https://www.linkedin.com/in/yourprofile', + github: 'https://github.com/yourprofile', + twitter: 'https://twitter.com/yourprofile', + facebook: 'https://www.facebook.com/yourprofile', + instagram: 'https://www.instagram.com/yourprofile', }, - }) + }), ); }); }); - describe("updateProfile function", () => { + describe('updateProfile function', () => { beforeEach(() => { jest.clearAllMocks(); mockRequest = { - params: { userID: "65117c94f000c9930ef5c0ee" }, + params: { userID: '65117c94f000c9930ef5c0ee' }, body: { - firstName: "Bobby", - lastName: "Orr", - email: "test@test.com", + firstName: 'Bobby', + lastName: 'Orr', + email: 'test@test.com', }, }; mockResponse = { @@ -112,122 +111,96 @@ describe("Profile Controller Tests", () => { mockNext = jest.fn(); }); - it("should handle profile update", async () => { + it('should handle profile update', async () => { (Profile.findOneAndUpdate as jest.Mock).mockResolvedValue({ - _id: "65117c94f000c9930ef5c0ee", - firstName: "Bobby", - lastName: "Orr", - email: "test@test.com", + _id: '65117c94f000c9930ef5c0ee', + firstName: 'Bobby', + lastName: 'Orr', + email: 'test@test.com', }); - await updateProfile( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await updateProfile(mockRequest as Request, mockResponse as Response, mockNext); expect(Profile.findOneAndUpdate).toHaveBeenCalledWith( - { user: "65117c94f000c9930ef5c0ee" }, + { user: '65117c94f000c9930ef5c0ee' }, mockRequest.body, - { new: true } + { new: true }, ); expect(mockResponse.status).toHaveBeenCalledWith(200); expect(mockResponse.json).toHaveBeenCalledWith(expect.any(Object)); }); - it("should handle errors in profile updating", async () => { - (Profile.findOneAndUpdate as jest.Mock).mockRejectedValue( - new Error("Update failed") - ); + it('should handle errors in profile updating', async () => { + (Profile.findOneAndUpdate as jest.Mock).mockRejectedValue(new Error('Update failed')); - await updateProfile( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await updateProfile(mockRequest as Request, mockResponse as Response, mockNext); expect(mockNext).toHaveBeenCalledWith(expect.anything()); }); - it("should handle the case where no profile is found", async () => { + it('should handle the case where no profile is found', async () => { (Profile.findOneAndUpdate as jest.Mock).mockResolvedValue(null); - await updateProfile( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await updateProfile(mockRequest as Request, mockResponse as Response, mockNext); expect(mockNext).toHaveBeenCalledWith( expect.objectContaining({ - log: "Express error in updateProfile Middleware - NO PROFILE FOUND", + log: 'Express error in updateProfile Middleware - NO PROFILE FOUND', status: 404, - message: { err: "An error occurred during profile update" }, - }) + message: { err: 'An error occurred during profile update' }, + }), ); }); }); - describe("getAllProfiles function", () => { - xit("should handle successful retrieval of all profiles", async () => { + describe('getAllProfiles function', () => { + xit('should handle successful retrieval of all profiles', async () => { const mockProfiles = [ - { _id: "1", user: "user1", bio: "Bio 1" }, - { _id: "2", user: "user2", bio: "Bio 2" }, + { _id: '1', user: 'user1', bio: 'Bio 1' }, + { _id: '2', user: 'user2', bio: 'Bio 2' }, ]; (Profile.find as jest.Mock).mockResolvedValue(mockProfiles); - await getAllProfiles( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await getAllProfiles(mockRequest as Request, mockResponse as Response, mockNext); expect(mockResponse.status).toHaveBeenCalledWith(201); expect(mockResponse.json).toHaveBeenCalledWith(mockProfiles); }); - it("should handle no profiles found", async () => { + it('should handle no profiles found', async () => { (Profile.find as jest.Mock).mockResolvedValue([]); - await getAllProfiles( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await getAllProfiles(mockRequest as Request, mockResponse as Response, mockNext); expect(mockNext).toHaveBeenCalledWith( expect.objectContaining({ - log: "There are no profiles to retrieve", + log: 'There are no profiles to retrieve', status: 404, - message: { err: "There were no profiles to retrieve" }, - }) + message: { err: 'There were no profiles to retrieve' }, + }), ); }); - it("should handle errors during profile retrieval", async () => { - const errorMessage = { message: "Error finding profiles" }; + it('should handle errors during profile retrieval', async () => { + const errorMessage = { message: 'Error finding profiles' }; (Profile.find as jest.Mock).mockRejectedValue(errorMessage); - await getAllProfiles( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await getAllProfiles(mockRequest as Request, mockResponse as Response, mockNext); expect(mockNext).toHaveBeenCalledWith( expect.objectContaining({ - log: "Express error in getAllProfiles Middleware", + log: 'Express error in getAllProfiles Middleware', status: 500, - message: { err: "An error occurred during profile creation" }, - }) + message: { err: 'An error occurred during profile creation' }, + }), ); }); }); - describe("getProfileById function", () => { + describe('getProfileById function', () => { beforeEach(() => { jest.clearAllMocks(); - mockRequest = { params: { userID: "someUserId" } }; + mockRequest = { params: { userID: 'someUserId' } }; mockResponse = { status: jest.fn().mockReturnThis(), json: jest.fn(), @@ -235,58 +208,46 @@ describe("Profile Controller Tests", () => { mockNext = jest.fn(); }); - it("should handle successful profile retrieval", async () => { + it('should handle successful profile retrieval', async () => { const mockProfile = { - _id: "someUserId", - bio: "User Bio", + _id: 'someUserId', + bio: 'User Bio', //ABBRIEVIATED PROFILE OBJECT }; (Profile.findOne as jest.Mock).mockResolvedValue(mockProfile); - await getProfileById( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await getProfileById(mockRequest as Request, mockResponse as Response, mockNext); expect(mockResponse.status).toHaveBeenCalledWith(200); expect(mockResponse.json).toHaveBeenCalledWith(mockProfile); }); - it("should handle profile not found", async () => { + it('should handle profile not found', async () => { (Profile.findOne as jest.Mock).mockResolvedValue(null); - await getProfileById( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await getProfileById(mockRequest as Request, mockResponse as Response, mockNext); expect(mockNext).toHaveBeenCalledWith( expect.objectContaining({ - log: "Profile does not exist", + log: 'Profile does not exist', status: 404, - message: { err: "An error occurred during profile retrieval" }, - }) + message: { err: 'An error occurred during profile retrieval' }, + }), ); }); - it("should handle errors", async () => { - const errorMessage = { message: "Error finding profile" }; + it('should handle errors', async () => { + const errorMessage = { message: 'Error finding profile' }; (Profile.findOne as jest.Mock).mockRejectedValue(errorMessage); - await getProfileById( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await getProfileById(mockRequest as Request, mockResponse as Response, mockNext); expect(mockNext).toHaveBeenCalledWith( expect.objectContaining({ - log: "Express error in getProfileById Middleware", + log: 'Express error in getProfileById Middleware', status: 500, - message: { err: "An error occurred during profile retrieval" }, - }) + message: { err: 'An error occurred during profile retrieval' }, + }), ); }); }); diff --git a/__tests__/userController.tests.ts b/__tests__/userController.tests.ts index d5edf80..7c537c2 100644 --- a/__tests__/userController.tests.ts +++ b/__tests__/userController.tests.ts @@ -1,24 +1,24 @@ -import { Request, Response, NextFunction } from "express"; +import { Request, Response, NextFunction } from 'express'; import { registerUser, authUser, getUserById, deleteUserByEmail, -} from "../server/controllers/userController"; -import User from "../server/models/userModel"; +} from '../server/controllers/userController'; +import User from '../server/models/userModel'; -jest.mock("../server/models/userModel", () => ({ +jest.mock('../server/models/userModel', () => ({ findOne: jest.fn(), create: jest.fn(), findOneAndDelete: jest.fn(), })); -jest.mock("../server/utils/generateToken", () => () => "someFakeToken"); +jest.mock('../server/utils/generateToken', () => () => 'someFakeToken'); -describe("User Controller Tests", () => { +describe('User Controller Tests', () => { let mockRequest: Partial; let mockResponse: Partial; //TODO Add some error test for global error handler - let mockNext: NextFunction = jest.fn(); + const mockNext: NextFunction = jest.fn(); beforeEach(() => { mockRequest = {}; @@ -30,128 +30,112 @@ describe("User Controller Tests", () => { }; }); - describe("registerUser function", () => { - xit("should handle user registration", async () => { + describe('registerUser function', () => { + xit('should handle user registration', async () => { (User.findOne as jest.Mock).mockResolvedValue(null); (User.create as jest.Mock).mockResolvedValue({ - _id: "someId", - firstName: "John", - lastName: "Doh", - email: "john@example.com", - password: "hashedPassword", + _id: 'someId', + firstName: 'John', + lastName: 'Doh', + email: 'john@example.com', + password: 'hashedPassword', }); mockRequest.body = { - firstName: "John", - lastName: "Doh", - email: "john@example.com", - password: "password", + firstName: 'John', + lastName: 'Doh', + email: 'john@example.com', + password: 'password', }; - await registerUser( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await registerUser(mockRequest as Request, mockResponse as Response, mockNext); expect(mockResponse.json).toHaveBeenCalledWith({ - _id: "someId", - firstName: "John", - lastName: "Doh", - email: "john@example.com", + _id: 'someId', + firstName: 'John', + lastName: 'Doh', + email: 'john@example.com', }); expect(mockResponse.cookie).toHaveBeenCalledWith( - "token", - "someFakeToken", - expect.any(Object) + 'token', + 'someFakeToken', + expect.any(Object), ); }); }); - describe("authUser function", () => { - it("should handle user authentication", async () => { + describe('authUser function', () => { + it('should handle user authentication', async () => { (User.findOne as jest.Mock).mockResolvedValue({ - _id: "someId", - firstName: "John", - lastName: "Doh", - email: "john@example.com", - password: "hashedPassword", + _id: 'someId', + firstName: 'John', + lastName: 'Doh', + email: 'john@example.com', + password: 'hashedPassword', matchPassword: jest.fn().mockResolvedValue(true), }); - mockRequest.body = { email: "john@example.com", password: "password" }; + mockRequest.body = { email: 'john@example.com', password: 'password' }; - await authUser( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await authUser(mockRequest as Request, mockResponse as Response, mockNext); expect(mockResponse.json).toHaveBeenCalledWith({ - _id: "someId", - firstName: "John", - lastName: "Doh", - email: "john@example.com", + _id: 'someId', + firstName: 'John', + lastName: 'Doh', + email: 'john@example.com', }); expect(mockResponse.cookie).toHaveBeenCalledWith( - "token", - "someFakeToken", - expect.any(Object) + 'token', + 'someFakeToken', + expect.any(Object), ); }); }); - describe("getUserById function", () => { - it("should get a user by ID", async () => { + describe('getUserById function', () => { + it('should get a user by ID', async () => { (User.findOne as jest.Mock).mockResolvedValue({ - _id: "someId", - firstName: "John", - lastName: "Doh", - email: "john@example.com", + _id: 'someId', + firstName: 'John', + lastName: 'Doh', + email: 'john@example.com', }); - mockRequest.params = { userId: "someId" }; + mockRequest.params = { userId: 'someId' }; - await getUserById( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await getUserById(mockRequest as Request, mockResponse as Response, mockNext); expect(mockResponse.json).toHaveBeenCalledWith( expect.objectContaining({ - _id: "someId", - firstName: "John", - lastName: "Doh", - email: "john@example.com", - }) + _id: 'someId', + firstName: 'John', + lastName: 'Doh', + email: 'john@example.com', + }), ); }); }); - describe("deleteUserByEmail function", () => { - it("should delete a user by email", async () => { + describe('deleteUserByEmail function', () => { + it('should delete a user by email', async () => { (User.findOneAndDelete as jest.Mock).mockResolvedValue({ - _id: "someId", - firstName: "John", - lastName: "Doh", - email: "john@example.com", + _id: 'someId', + firstName: 'John', + lastName: 'Doh', + email: 'john@example.com', }); - mockRequest.params = { email: "john@example.com" }; + mockRequest.params = { email: 'john@example.com' }; - await deleteUserByEmail( - mockRequest as Request, - mockResponse as Response, - mockNext - ); + await deleteUserByEmail(mockRequest as Request, mockResponse as Response, mockNext); expect(mockResponse.status).toHaveBeenCalledWith(200); expect(mockResponse.json).toHaveBeenCalledWith( expect.objectContaining({ - msg: "User successfully deleted!", - }) + msg: 'User successfully deleted!', + }), ); }); }); diff --git a/client/.babelrc b/client/.babelrc index b57f614..fa82860 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -1,7 +1,7 @@ { - "presets": [ - "@babel/preset-env", - "@babel/preset-typescript" - ] - } - \ No newline at end of file + "presets": [ + "@babel/preset-env", + ["@babel/preset-react", { "runtime": "automatic" }], + "@babel/preset-typescript" + ] +} diff --git a/client/__mocks__/fileMock.js b/client/__mocks__/fileMock.js index 0a445d0..86059f3 100644 --- a/client/__mocks__/fileMock.js +++ b/client/__mocks__/fileMock.js @@ -1 +1 @@ -module.exports = "test-file-stub"; +module.exports = 'test-file-stub'; diff --git a/client/jest.config.js b/client/jest.config.js index 148bab9..2529d9c 100644 --- a/client/jest.config.js +++ b/client/jest.config.js @@ -1,17 +1,17 @@ -const path = require("path"); +const path = require('path'); module.exports = { - preset: "ts-jest", - testEnvironment: "jest-environment-jsdom", + preset: 'ts-jest', + testEnvironment: 'jest-environment-jsdom', moduleNameMapper: { - "\\.(css|less|scss|sass)$": "identity-obj-proxy", - "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": - "/__mocks__/fileMock.js", + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/__mocks__/fileMock.js', }, - setupFilesAfterEnv: [path.resolve(__dirname, "./setupTests.ts")], + setupFilesAfterEnv: [path.resolve(__dirname, './setupTests.ts')], transform: { - "^.+\\.tsx?$": "ts-jest", + '^.+\\.tsx?$': 'ts-jest', }, - testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", - moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], }; diff --git a/client/package.json b/client/package.json index 0558c84..1ecb31d 100644 --- a/client/package.json +++ b/client/package.json @@ -4,10 +4,10 @@ "description": "", "main": "webpack.config.js", "scripts": { - "test": "jest --coverage", + "test": "jest --colors --coverage", "type-check": "tsc --noEmit", - "start": "webpack serve --open --hot --mode development", - "build": "webpack --mode production" + "start": "webpack serve --open --hot --mode=development", + "build": "webpack --mode=production" }, "author": "", "license": "ISC", diff --git a/client/postcss.config.js b/client/postcss.config.js index 36b474a..2cb79e6 100644 --- a/client/postcss.config.js +++ b/client/postcss.config.js @@ -1,4 +1,4 @@ -const tailwindcss = require("tailwindcss"); +const tailwindcss = require('tailwindcss'); module.exports = { - plugins: ["postcss-preset-env", tailwindcss], + plugins: ['postcss-preset-env', tailwindcss], }; diff --git a/client/public/index.html b/client/public/index.html index 4ef4415..a35efcf 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -1,14 +1,11 @@ - + - + Code Hammers diff --git a/client/setupTests.ts b/client/setupTests.ts index d0de870..7b0828b 100644 --- a/client/setupTests.ts +++ b/client/setupTests.ts @@ -1 +1 @@ -import "@testing-library/jest-dom"; +import '@testing-library/jest-dom'; diff --git a/client/src/App.test.tsx b/client/src/App.test.tsx index 3f1086b..56650e8 100644 --- a/client/src/App.test.tsx +++ b/client/src/App.test.tsx @@ -1,45 +1,41 @@ -import React from "react"; -import { render, RenderResult, screen } from "@testing-library/react"; -import { MemoryRouter } from "react-router-dom"; -import App from "./App"; -import { Provider } from "react-redux"; -import configureStore from "redux-mock-store"; +import { ReactNode } from 'react'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import App from './App'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; const mockStore = configureStore([]); const initialState = { user: { userData: null, - status: "idle", + status: 'idle', error: null, }, }; // MOCK THE BROWSER ROUTER SO THAT WE CAN WRAP APP COMPONENT WITH MEMORYROUTER -jest.mock("react-router-dom", () => { - const originalModule = jest.requireActual("react-router-dom"); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); return { ...originalModule, - BrowserRouter: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), + BrowserRouter: ({ children }: { children: ReactNode }) =>
{children}
, }; }); -describe("App Component", () => { - let component: RenderResult; - +describe('App Component', () => { beforeEach(() => { const store = mockStore(initialState); - component = render( + render( - + - + , ); }); - test("renders LandingPage component at root path", () => { - expect(screen.getByText("Code Hammers")).toBeInTheDocument(); + test('renders LandingPage component at root path', () => { + expect(screen.getByText('Code Hammers')).toBeInTheDocument(); }); }); diff --git a/client/src/App.tsx b/client/src/App.tsx index 8e25d45..55ff068 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,11 +1,10 @@ -import React from "react"; -import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; -import LandingPage from "./pages/LandingPage"; -import NotFoundPage from "./pages/NotFoundPage/NotFoundPage"; -import AuthenticatedApp from "./AuthenticatedApp"; -import RegistrationPage from "./pages/RegistrationPage/RegistrationPage"; +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; +import LandingPage from './pages/LandingPage'; +import NotFoundPage from './pages/NotFoundPage/NotFoundPage'; +import AuthenticatedApp from './AuthenticatedApp'; +import RegistrationPage from './pages/RegistrationPage/RegistrationPage'; -const App = (): JSX.Element => { +const App = () => { return ( diff --git a/client/src/AuthenticatedApp.test.tsx b/client/src/AuthenticatedApp.test.tsx index e6651a9..c5ced58 100644 --- a/client/src/AuthenticatedApp.test.tsx +++ b/client/src/AuthenticatedApp.test.tsx @@ -1,45 +1,41 @@ -import React from "react"; -import { render, RenderResult, screen } from "@testing-library/react"; -import { MemoryRouter } from "react-router-dom"; -import { Provider } from "react-redux"; -import configureStore from "redux-mock-store"; -import AuthenticatedApp from "./AuthenticatedApp"; +import { ReactNode } from 'react'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import AuthenticatedApp from './AuthenticatedApp'; const mockStore = configureStore([]); const initialState = { user: { userData: null, - status: "idle", + status: 'idle', error: null, }, }; -jest.mock("react-router-dom", () => { - const originalModule = jest.requireActual("react-router-dom"); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); return { ...originalModule, - BrowserRouter: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), + BrowserRouter: ({ children }: { children: ReactNode }) =>
{children}
, useNavigate: () => jest.fn(), }; }); -describe("AuthenticatedApp Component", () => { - let component: RenderResult; - +describe('AuthenticatedApp Component', () => { beforeEach(() => { const store = mockStore(initialState); - component = render( + render( - + - + , ); }); - test("renders with the BANNER placeholder text", () => { - expect(screen.getByText("Code Hammers")).toBeInTheDocument(); + test('renders with the BANNER placeholder text', () => { + expect(screen.getByText('Code Hammers')).toBeInTheDocument(); }); }); diff --git a/client/src/AuthenticatedApp.tsx b/client/src/AuthenticatedApp.tsx index 11e9f44..0dbc03d 100644 --- a/client/src/AuthenticatedApp.tsx +++ b/client/src/AuthenticatedApp.tsx @@ -1,36 +1,33 @@ -import React, { useEffect, useState } from "react"; -import { Route, Routes } from "react-router-dom"; -import Header from "./components/Header/Header"; -import MainPage from "./pages/MainPage/MainPage"; -import Forums from "./pages/Forums/Forums"; -import Profiles from "./pages/Profiles/Profiles"; -import Profile from "./pages/Profile/Profile"; -import EditProfilePage from "./pages/EditProfilePage/EditProfilePage"; -import Directory from "./pages/DirectoryPage/DirectoryPage"; -import NotFoundPage from "./pages/NotFoundPage/NotFoundPage"; -import { useNavigate } from "react-router-dom"; +import { useEffect } from 'react'; +import { Route, Routes } from 'react-router-dom'; +import Header from './components/Header/Header'; +import MainPage from './pages/MainPage/MainPage'; +import Forums from './pages/Forums/Forums'; +import Profiles from './pages/Profiles/Profiles'; +import Profile from './pages/Profile/Profile'; +import EditProfilePage from './pages/EditProfilePage/EditProfilePage'; +import Directory from './pages/DirectoryPage/DirectoryPage'; +import NotFoundPage from './pages/NotFoundPage/NotFoundPage'; +import { useNavigate } from 'react-router-dom'; const AuthenticatedApp = () => { const navigate = useNavigate(); - const [isAuthenticated, setIsAuthenticated] = useState(false); useEffect(() => { const validateSession = async () => { try { - const response = await fetch("/api/auth/validate-session", { - method: "GET", - credentials: "include", + const response = await fetch('/api/auth/validate-session', { + method: 'GET', + credentials: 'include', }); const data = await response.json(); - if (response.ok && data.isAuthenticated) { - setIsAuthenticated(true); - } else { - navigate("/"); + if (!response.ok || !data.isAuthenticated) { + navigate('/'); } } catch (error) { - console.error("Session validation failed:", error); - navigate("/"); + console.error('Session validation failed:', error); + navigate('/'); } }; diff --git a/client/src/app/hooks.ts b/client/src/app/hooks.ts index 72d1476..4691088 100644 --- a/client/src/app/hooks.ts +++ b/client/src/app/hooks.ts @@ -1,5 +1,5 @@ -import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; -import { RootState, AppDispatch } from "./store"; +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import { RootState, AppDispatch } from './store'; export const useAppDispatch = () => useDispatch(); export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/client/src/app/store.ts b/client/src/app/store.ts index ec80f06..24ad46d 100644 --- a/client/src/app/store.ts +++ b/client/src/app/store.ts @@ -1,8 +1,8 @@ -import { configureStore } from "@reduxjs/toolkit"; -import userReducer from "../features/user/userSlice"; -import profilesReducer from "../features/profiles/profilesSlice"; -import userProfileReducer from "../features/userProfile/userProfileSlice"; -import alumniReducer from "../features/alumni/alumniSlice"; +import { configureStore } from '@reduxjs/toolkit'; +import userReducer from '../features/user/userSlice'; +import profilesReducer from '../features/profiles/profilesSlice'; +import userProfileReducer from '../features/userProfile/userProfileSlice'; +import alumniReducer from '../features/alumni/alumniSlice'; export const store = configureStore({ reducer: { diff --git a/client/src/components/Banner/Banner.test.tsx b/client/src/components/Banner/Banner.test.tsx index 7d7e4dd..4dba96c 100644 --- a/client/src/components/Banner/Banner.test.tsx +++ b/client/src/components/Banner/Banner.test.tsx @@ -1,28 +1,27 @@ -import React from "react"; -import { render, screen, fireEvent } from "@testing-library/react"; -import "@testing-library/jest-dom"; -import Banner from "./Banner"; -import { useAppDispatch, useAppSelector } from "../../app/hooks"; -import { logout } from "../../features/user/userSlice"; -import { useNavigate } from "react-router-dom"; - -jest.mock("../../app/hooks", () => ({ +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Banner from './Banner'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import { logout } from '../../features/user/userSlice'; +import { useNavigate } from 'react-router-dom'; + +jest.mock('../../app/hooks', () => ({ useAppDispatch: jest.fn(), useAppSelector: jest.fn(), })); -jest.mock("../../features/user/userSlice", () => ({ +jest.mock('../../features/user/userSlice', () => ({ logout: jest.fn(), })); -jest.mock("react-router-dom", () => ({ +jest.mock('react-router-dom', () => ({ useNavigate: jest.fn(), })); -describe("Banner Component", () => { +describe('Banner Component', () => { const mockDispatch = jest.fn(); const mockNavigate = jest.fn(); const mockUserData = { - id: "123", - name: "John Doe", + id: '123', + name: 'John Doe', }; beforeEach(() => { @@ -31,7 +30,7 @@ describe("Banner Component", () => { (useAppSelector as jest.Mock).mockImplementation((selector) => selector({ user: { userData: mockUserData }, - }) + }), ); }); @@ -39,56 +38,56 @@ describe("Banner Component", () => { jest.clearAllMocks(); }); - it("renders the logo image correctly", () => { + it('renders the logo image correctly', () => { render(); - const logo = screen.getByAltText("Code Hammers Logo"); + const logo = screen.getByAltText('Code Hammers Logo'); expect(logo).toBeInTheDocument(); }); - it("renders the title text correctly", () => { + it('renders the title text correctly', () => { render(); - const title = screen.getByText("Code Hammers"); + const title = screen.getByText('Code Hammers'); expect(title).toBeInTheDocument(); }); - it("renders the Options button", () => { + it('renders the Options button', () => { render(); - const optionsButton = screen.getByRole("button", { name: "Options" }); + const optionsButton = screen.getByRole('button', { name: 'Options' }); expect(optionsButton).toBeInTheDocument(); }); - it("opens the dropdown and shows options when Options button is clicked", () => { + it('opens the dropdown and shows options when Options button is clicked', () => { render(); - const optionsButton = screen.getByRole("button", { name: "Options" }); + const optionsButton = screen.getByRole('button', { name: 'Options' }); fireEvent.click(optionsButton); - const profileOption = screen.getByText("Edit Profile"); - const logoutOption = screen.getByText("Logout"); + const profileOption = screen.getByText('Edit Profile'); + const logoutOption = screen.getByText('Logout'); expect(profileOption).toBeInTheDocument(); expect(logoutOption).toBeInTheDocument(); }); - it("handles navigation to Profile on clicking Go to Profile", () => { + it('handles navigation to Profile on clicking Go to Profile', () => { render(); - const optionsButton = screen.getByRole("button", { name: "Options" }); + const optionsButton = screen.getByRole('button', { name: 'Options' }); fireEvent.click(optionsButton); - const profileOption = screen.getByText("Edit Profile"); + const profileOption = screen.getByText('Edit Profile'); fireEvent.click(profileOption); - expect(mockNavigate).toHaveBeenCalledWith("editProfile"); + expect(mockNavigate).toHaveBeenCalledWith('editProfile'); }); - it("handles logout on clicking Logout", () => { + it('handles logout on clicking Logout', () => { render(); - const optionsButton = screen.getByRole("button", { name: "Options" }); + const optionsButton = screen.getByRole('button', { name: 'Options' }); fireEvent.click(optionsButton); - const logoutOption = screen.getByText("Logout"); + const logoutOption = screen.getByText('Logout'); fireEvent.click(logoutOption); expect(mockDispatch).toHaveBeenCalledWith(logout()); - expect(mockNavigate).toHaveBeenCalledWith("/"); + expect(mockNavigate).toHaveBeenCalledWith('/'); }); }); diff --git a/client/src/components/Banner/Banner.tsx b/client/src/components/Banner/Banner.tsx index 3729191..9fe9d75 100644 --- a/client/src/components/Banner/Banner.tsx +++ b/client/src/components/Banner/Banner.tsx @@ -1,32 +1,29 @@ -import React, { useState } from "react"; -import logo from "../../assets/hammer.png"; -import { useAppDispatch, useAppSelector } from "../../app/hooks"; -import { logout } from "../../features/user/userSlice"; -import { useNavigate } from "react-router-dom"; +import { useState } from 'react'; +import logo from '../../assets/hammer.png'; +import { useAppDispatch } from '../../app/hooks'; +import { logout } from '../../features/user/userSlice'; +import { useNavigate } from 'react-router-dom'; -const Banner = (): JSX.Element => { - const user = useAppSelector((state) => state.user.userData); +const Banner = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); const [showDropdown, setShowDropdown] = useState(false); const handleLogout = () => { dispatch(logout()); - navigate("/"); + navigate('/'); //TODO CLEAR ALL STATE }; const goToEditProfile = () => { - navigate("editProfile"); + navigate('editProfile'); setShowDropdown(false); }; return (
Code Hammers Logo
-

- Code Hammers -

+

Code Hammers

); diff --git a/client/src/components/Forums/CreateThread/CreateThread.tsx b/client/src/components/Forums/CreateThread/CreateThread.tsx index 3d3efd2..03240c6 100644 --- a/client/src/components/Forums/CreateThread/CreateThread.tsx +++ b/client/src/components/Forums/CreateThread/CreateThread.tsx @@ -1,54 +1,41 @@ -import React, { useState, ChangeEvent, FormEvent } from "react"; -import axios from "axios"; +import { useState, ChangeEvent, FormEvent } from 'react'; +import axios from 'axios'; interface CreateThreadProps { forumId: string; onClose: () => void; } -const CreateThread: React.FC = ({ forumId, onClose }) => { +const CreateThread = ({ forumId, onClose }: CreateThreadProps) => { const [formData, setFormData] = useState<{ title: string; content: string }>({ - title: "", - content: "", + title: '', + content: '', }); const { title, content } = formData; - const handleChange = ( - e: ChangeEvent - ) => { + const handleChange = (e: ChangeEvent) => { setFormData({ ...formData, [e.target.name]: e.target.value }); }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); try { - const response = await axios.post( - `/api/forums/${forumId}/threads`, - formData, - { - withCredentials: true, - } - ); - console.log("Thread created:", response.data); + await axios.post(`/api/forums/${forumId}/threads`, formData, { + withCredentials: true, + }); onClose(); } catch (error) { - console.error("Failed to create thread:", error); + console.error('Failed to create thread:', error); //TODO userfeedback with errors } }; return (
-
+
-
-