diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1e578fc..a575b02 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -42,7 +42,13 @@ jobs:
- run:
name: Lint
command: |
- npm run lint
+ npm run lint
+ - run:
+ name: Run Tests
+ command: |
+ npm run test-server
+ npm run test-client
+ echo "Tests completed successfully."
- run:
name: Front-End Build
command: |
diff --git a/.gitignore b/.gitignore
index 85e149c..6e8b84f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,29 +1,27 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
# dependencies
/node_modules
-server/node_modules
-frontend/node_modules
+/server/node_modules
+/frontend/node_modules
/.pnp
.pnp.js
# testing
-/coverage
+**/coverage/
# production
-frontend/build
-server/dist
-server/dist.zip
+/frontend/build
+/server/dist
+/server/dist.zip
-# misc
-.DS_Store
-.env
-.env.development
-.env.test
-.env.production
+# environment variables
+.env*
cypress.env.json
.secrets
+# misc
+.DS_Store
+
+# logs
npm-debug.log*
yarn-debug.log*
-yarn-error.log*
+yarn-error.log*
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
index 12f542d..daf0733 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -6,7 +6,7 @@
"start": "react-scripts start --project ./frontend",
"build": "react-scripts build --project ./frontend",
"build-production": "REACT_APP_API_URL=https://employee-polling.us-east-2.elasticbeanstalk.com react-scripts build --project ./frontend",
- "test": "react-scripts test --transformIgnorePatterns \"node_modules/(?!@toolz/allow-react)/\" --env=jsdom",
+ "test": "react-scripts test --coverage --watchAll=false --transformIgnorePatterns \"node_modules/(?!@toolz/allow-react)/\"",
"eject": "react-scripts eject",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
diff --git a/frontend/src/features/pollSlice.ts b/frontend/src/features/pollSlice.ts
index e95cb8d..6951bca 100644
--- a/frontend/src/features/pollSlice.ts
+++ b/frontend/src/features/pollSlice.ts
@@ -28,7 +28,7 @@ export interface PollsState {
}
// Initial state for polls
-const initialState: PollsState = {
+export const initialState: PollsState = {
polls: {},
status: 'idle',
error: undefined,
diff --git a/frontend/src/tests/Home.test.tsx b/frontend/src/tests/Home.test.tsx
index ca5a3cb..ff20a19 100644
--- a/frontend/src/tests/Home.test.tsx
+++ b/frontend/src/tests/Home.test.tsx
@@ -80,4 +80,102 @@ describe('Home Page', () => {
expect(screen.queryByText('Show Answered Polls')).not.toBeInTheDocument();
expect(screen.getByText('Show Unanswered Polls')).toBeInTheDocument();
});
+
+ it('displays loading spinner when polls are loading', () => {
+ const store = mockStore({
+ users: {
+ currentUser: {
+ id: '123',
+ name: 'User',
+ username: 'User',
+ pollsCreated: 1,
+ pollsVotedOn: 0,
+ },
+ users: [],
+ status: 'succeeded',
+ error: null,
+ },
+ poll: {
+ polls: {},
+ status: 'loading',
+ error: null,
+ },
+ });
+
+ render(
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
+ });
+
+ it('displays error message when fetching polls fails', () => {
+ const store = mockStore({
+ users: {
+ currentUser: {
+ id: '123',
+ name: 'User',
+ username: 'User',
+ pollsCreated: 1,
+ pollsVotedOn: 0,
+ },
+ users: [],
+ status: 'succeeded',
+ error: null,
+ },
+ poll: {
+ polls: {},
+ status: 'failed',
+ error: 'Failed to fetch polls',
+ },
+ });
+
+ render(
+
+
+
+
+ ,
+ );
+
+ expect(
+ screen.getByText('Error: Failed to fetch polls'),
+ ).toBeInTheDocument();
+ });
+
+ it('displays no polls available message when there are no polls', () => {
+ const store = mockStore({
+ users: {
+ currentUser: {
+ id: '123',
+ name: 'User',
+ username: 'User',
+ pollsCreated: 1,
+ pollsVotedOn: 0,
+ },
+ users: [],
+ status: 'succeeded',
+ error: null,
+ },
+ poll: {
+ polls: {},
+ status: 'succeeded',
+ error: null,
+ },
+ });
+
+ render(
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByText('No polls available.')).toBeInTheDocument();
+ });
});
diff --git a/frontend/src/tests/Navbar.test.tsx b/frontend/src/tests/Navbar.test.tsx
deleted file mode 100644
index a19ac8c..0000000
--- a/frontend/src/tests/Navbar.test.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import { Provider } from 'react-redux';
-import { BrowserRouter as Router } from 'react-router-dom';
-import Navbar from '../components/Navbar';
-import configureStore from 'redux-mock-store';
-import { initialState } from '../features/usersSlice';
-
-const mockStore = configureStore();
-
-describe('Navbar Component', () => {
- it('should display logout link when user is authenticated', () => {
- const store = mockStore({
- users: { ...initialState, currentUser: { id: '123', name: 'User' } },
- });
- render(
-
-
-
-
- ,
- );
-
- const createPollLinks = screen.getAllByText('Create Poll');
- expect(createPollLinks.length).toBeGreaterThan(0); // Check if there's at least one
- createPollLinks.forEach(link => {
- expect(link).toBeInTheDocument();
- });
- const createLeaderboardLinks = screen.getAllByText('Leaderboard');
- expect(createLeaderboardLinks.length).toBeGreaterThan(0); // Check if there's at least one
- createPollLinks.forEach(link => {
- expect(link).toBeInTheDocument();
- });
- expect(screen.getByText('Logout')).toBeInTheDocument();
- expect(screen.queryByText('Login')).toBeNull();
- });
- it('should display login and signup links when user is not authenticated', () => {
- const store = mockStore({ users: { ...initialState, currentUser: null } });
- render(
-
-
-
-
- ,
- );
-
- expect(screen.getByText('Login/Signup')).toBeInTheDocument();
- expect(screen.queryByText('Logout')).toBeNull();
- });
-});
diff --git a/frontend/src/tests/PollList.test.tsx b/frontend/src/tests/PollList.test.tsx
new file mode 100644
index 0000000..8936978
--- /dev/null
+++ b/frontend/src/tests/PollList.test.tsx
@@ -0,0 +1,74 @@
+import { configureStore, Store } from '@reduxjs/toolkit';
+import pollReducer, {
+ fetchPolls,
+ addNewPoll,
+ PollsState,
+ initialState,
+} from '../features/pollSlice';
+import * as api from '../server/api';
+import { ThunkDispatch } from 'redux-thunk';
+import { RootState } from '../store/store';
+import { Action } from 'redux';
+
+jest.mock('../server/api');
+
+type AppThunkDispatch = ThunkDispatch;
+
+const mockStore = (state: PollsState) =>
+ configureStore({
+ reducer: {
+ poll: pollReducer,
+ },
+ preloadedState: {
+ poll: state,
+ },
+ });
+
+describe('pollSlice', () => {
+ let store: Store & { dispatch: AppThunkDispatch };
+
+ beforeEach(() => {
+ store = mockStore(initialState) as Store & {
+ dispatch: AppThunkDispatch;
+ };
+ jest.clearAllMocks();
+ });
+
+ it('should handle initial state', () => {
+ expect(store.getState().poll).toEqual(initialState);
+ });
+
+ it('should handle fetchPolls', async () => {
+ const polls = [
+ { id: '1', optionOne: 'Option 1', optionTwo: 'Option 2', votes: [] },
+ ];
+ (api.fetchPolls as jest.Mock).mockResolvedValue({ polls });
+
+ await store.dispatch(fetchPolls());
+
+ const state = store.getState().poll;
+ expect(state.polls['1']).toEqual(polls[0]);
+ expect(state.status).toEqual('succeeded');
+ expect(state.error).toBeUndefined();
+ });
+
+ it('should handle addNewPoll', async () => {
+ const newPoll = {
+ id: '2',
+ optionOne: 'Option 1',
+ optionTwo: 'Option 2',
+ userId: '1',
+ votes: [],
+ };
+ (api.createPoll as jest.Mock).mockResolvedValue(newPoll);
+
+ await store.dispatch(addNewPoll(newPoll));
+
+ const state = store.getState().poll;
+ expect(state.polls['2']).toEqual(newPoll);
+ expect(state.status).toEqual('succeeded');
+ expect(state.error).toBeUndefined();
+ });
+
+ // Add more tests for voteOnPoll
+});
diff --git a/frontend/src/tests/createPollForm.test.tsx b/frontend/src/tests/createPollForm.test.tsx
new file mode 100644
index 0000000..f1e5327
--- /dev/null
+++ b/frontend/src/tests/createPollForm.test.tsx
@@ -0,0 +1,48 @@
+import { render, fireEvent, screen, waitFor } from '@testing-library/react';
+import CreatePollForm from '../components/CreatePollForm';
+import { Provider } from 'react-redux';
+import configureStore from 'redux-mock-store';
+import { BrowserRouter as Router } from 'react-router-dom';
+
+const mockStore = configureStore();
+
+describe('CreatePollForm', () => {
+ let store: ReturnType;
+
+ beforeEach(() => {
+ store = mockStore({
+ users: {
+ currentUser: {
+ id: '123',
+ name: 'User',
+ username: 'User',
+ pollCount: 1,
+ voteCount: 0,
+ },
+ users: [],
+ status: 'succeeded',
+ error: undefined,
+ },
+ poll: {
+ polls: {},
+ status: 'idle',
+ error: undefined,
+ },
+ });
+ store.dispatch = jest.fn();
+ });
+
+ it('renders form inputs', async () => {
+ render(
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByLabelText(/Option One/i)).toBeInTheDocument();
+ expect(screen.getByLabelText(/Option Two/i)).toBeInTheDocument();
+ expect(screen.getByText(/Create Poll/i)).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/tests/docs/testing.md b/frontend/src/tests/docs/testing.md
deleted file mode 100644
index c293315..0000000
--- a/frontend/src/tests/docs/testing.md
+++ /dev/null
@@ -1,87 +0,0 @@
-# Testing Plan
-
-## Components
-
-### 1. Navbar
-
-- Verifies responsive behavior and the display of correct navigation links based on the user's authentication status.
-- Checks the mobile hamburger menu functionality.
-
-### 2. Home Page
-
-- Tests display of polls and conditional content for authenticated vs. unauthenticated users.
-- Ensures correct filtering and display of answered and unanswered polls.
-
-### 3. Login Form
-
-- Validates form submission handling and user feedback on errors. (!TODO!)
-- Tests navigation between Login and Signup forms. (!TODO!)
-
-### 4. Signup Form
-
-- Checks form validation, submission handling, and error feedback. (!TODO!)
-- Validates navigation between Signup and Login forms. (!TODO!)
-
-### 5. Poll List
-
-- Ensures correct fetching and rendering of polls. (!TODO!)
-
-### 6. Poll Details
-
-- Tests display of poll details, voting functionality, and results presentation. (!TODO!)
-
-### 7. Create Poll Form
-
-- Validates form submissions for creating new polls. (!TODO!)
-
-### 8. Leaderboard
-
-- Checks correct fetching and display of user statistics, sorted by activity. (!TODO!)
-
-## API Calls
-
-### 1. Fetch Users
-
-- Ensures users are retrieved correctly and handles errors.
-
-### 2. Login User
-
-- Tests successful user authentication and error handling.
-
-### 3. Register User
-
-- Validates user registration and error responses.
-
-### 4. Fetch Polls
-
-- Ensures polls are fetched correctly and handles possible errors. (!TODO!)
-
-### 5. Create Poll
-
-- Tests API response on creating a new poll and handles errors. (!TODO!)
-
-### 6. Vote on Poll
-
-- Ensures the voting mechanism works correctly and error conditions are handled. (!TODO!)
-
-## Testing Strategy
-
-### MVP Unit Testing with Jest
-
-#### Unit Tests
-
-- Test individual functions and components in isolation. (!TODO!) (In Progress)
-- Mock API calls using jest mocks to simulate server responses.
-- Use React Testing Library to render components and interact with them as users would. (!TODO!) (In Progress)
-
-### Comprehensive Testing with Playwright
-
-#### Integration Tests
-
-- Verify the interaction between components and the API. (!TODO!)
-- Test scenarios such as user registration, login, poll creation, and voting workflows. (!TODO!)
-
-#### End-to-End Tests
-
-- Simulate user journeys from start to finish. (!TODO!)
-- Use Playwright to automate browser interactions to test the complete flow of the application, including responsive behavior on different devices. (!TODO!)
diff --git a/frontend/src/tests/usersSlice.test.ts b/frontend/src/tests/usersSlice.test.tsx
similarity index 95%
rename from frontend/src/tests/usersSlice.test.ts
rename to frontend/src/tests/usersSlice.test.tsx
index dccc9cf..3bb2702 100644
--- a/frontend/src/tests/usersSlice.test.ts
+++ b/frontend/src/tests/usersSlice.test.tsx
@@ -1,6 +1,6 @@
import { fetchUsers } from '../features/usersSlice';
import { store } from '../store/store';
-import * as mockApi from '../tests/mocks/api';
+import * as mockApi from './mocks/api';
const testUsers = [
{
diff --git a/server/package.json b/server/package.json
index 2415bce..9a96207 100644
--- a/server/package.json
+++ b/server/package.json
@@ -10,7 +10,7 @@
"migrate": "sequelize-cli db:migrate",
"migrate:undo": "sequelize db:migrate:undo",
"migrate:all": "npm run build && sequelize db:migrate",
- "test": "cross-env NODE_ENV=test node -r dotenv/config ./node_modules/.bin/jest"
+ "test": "jest --coverage"
},
"dependencies": {
"bcrypt": "^5.1.1",
diff --git a/server/src/controllers/pollController.ts b/server/src/controllers/pollController.ts
index 0486728..84b8000 100644
--- a/server/src/controllers/pollController.ts
+++ b/server/src/controllers/pollController.ts
@@ -34,7 +34,7 @@ export const getPolls = async (req: Request, res: Response): Promise => {
res.json({ polls: polls.map(poll => poll.toJSON()) });
} catch (error) {
- console.error('Failed to fetch polls:', error);
+ console.log('Failed to fetch polls: return 500 server error');
res.status(500).json({ error: 'Error fetching polls' });
}
};
@@ -98,7 +98,7 @@ export const createPoll = async (
res.status(201).json(poll);
} catch (error) {
- console.error('Failed to create poll:', error);
+ console.log('Failed to create poll: return 500 server error');
res.status(500).json({ error: 'Error creating poll' });
}
};
@@ -199,7 +199,7 @@ export const voteOnPoll = async (
res.json({ message: 'Vote recorded successfully', vote });
} catch (error) {
- console.error('Failed to vote:', error);
+ console.log('Failed to vote: return 500 server error');
res.status(500).json({ error: 'Error voting' });
}
};
diff --git a/server/src/controllers/userController.ts b/server/src/controllers/userController.ts
index 1017e52..c25b26d 100644
--- a/server/src/controllers/userController.ts
+++ b/server/src/controllers/userController.ts
@@ -85,7 +85,7 @@ export const register = async (req: Request, res: Response) => {
res.status(201).json({ message: 'User registered successfully', token });
} catch (error) {
// Handle errors and send a 500 Internal Server Error response
- console.error('Registration failed:', error);
+ console.log('Failed to register user: Server Error 500');
res.status(500).json({ error: 'User registration failed' });
}
};
@@ -147,6 +147,7 @@ export const login = async (req: Request, res: Response) => {
res.json({ message: 'User logged in successfully', token });
} catch (error) {
// Handle errors and send a 500 Internal Server Error response
+ console.log('Failed to login: Server Error 500');
res.status(500).json({ error: 'Login failed' });
}
};
@@ -206,7 +207,7 @@ export const getAllUsers = async (
res.status(200).json(usersWithCounts);
} catch (error) {
- console.error('Failed to fetch users:', error);
+ console.log('Failed to get users: Server Error 500');
res.status(500).json({ error: 'Error fetching users' });
}
};
@@ -286,7 +287,7 @@ export const updateUser = async (req: Request, res: Response) => {
// Respond with success message and the user token
res.status(201).json({ message: 'User updated successfully', token });
} catch (error) {
- console.error('Failed to update user:', error);
+ console.log('Failed to update user: Server Error 500');
res.status(500).json({ error: 'Failed to update user' });
}
};
@@ -328,7 +329,7 @@ export const deleteUser = async (req: Request, res: Response) => {
console.log('User deleted successfully');
res.status(204).send();
} catch (error) {
- console.error('Failed to delete user:', error);
+ console.log('Failed to delete user: Server Error 500');
res.status(500).json({ error: 'Failed to delete user' });
}
};
diff --git a/server/src/middleware/authenticate.ts b/server/src/middleware/authenticate.ts
index 5360cb9..ba9d1ab 100644
--- a/server/src/middleware/authenticate.ts
+++ b/server/src/middleware/authenticate.ts
@@ -39,7 +39,7 @@ export const authenticate = (
}
} catch (error) {
// Handle any error related to token verification by sending a 401 Unauthorized status
- console.error('Invalid token', error);
+ console.log('Invalid token: Return 401 response');
res.status(401).json({ error: 'Invalid token' });
}
};
diff --git a/server/src/tests/authentication.test.ts b/server/src/tests/authentication.test.ts
new file mode 100644
index 0000000..38e479f
--- /dev/null
+++ b/server/src/tests/authentication.test.ts
@@ -0,0 +1,50 @@
+import { authenticate } from '../../src/middleware/authenticate';
+import { Request, Response, NextFunction } from 'express';
+import jwt from 'jsonwebtoken';
+
+jest.mock('jsonwebtoken');
+
+describe('Authentication Middleware', () => {
+ let req: Partial;
+ let res: Partial;
+ let next: NextFunction;
+
+ beforeEach(() => {
+ req = {
+ headers: {
+ authorization: 'Bearer token',
+ },
+ };
+ res = {
+ status: jest.fn().mockReturnThis(),
+ json: jest.fn(),
+ };
+ next = jest.fn();
+ });
+
+ it('should call next if token is valid', () => {
+ const userPayload = { id: '1', username: 'testuser' };
+ (jwt.verify as jest.Mock).mockReturnValue({ user: userPayload });
+
+ authenticate(req as Request, res as Response, next);
+ expect(next).toHaveBeenCalled();
+ expect(req.user).toEqual(userPayload);
+ });
+
+ it('should return 403 if no token is provided', () => {
+ req.headers = {};
+ authenticate(req as Request, res as Response, next);
+ expect(res.status).toHaveBeenCalledWith(403);
+ expect(res.json).toHaveBeenCalledWith({ error: 'No token provided' });
+ });
+
+ it('should return 401 if token is invalid', () => {
+ (jwt.verify as jest.Mock).mockReturnValue({});
+
+ authenticate(req as Request, res as Response, next);
+
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(res.json).toHaveBeenCalledWith({ error: 'Invalid token' });
+ expect(next).not.toHaveBeenCalled();
+ });
+});
diff --git a/server/src/tests/poll.test.ts b/server/src/tests/poll.test.ts
index de00740..04c286f 100644
--- a/server/src/tests/poll.test.ts
+++ b/server/src/tests/poll.test.ts
@@ -1,108 +1,218 @@
+import { Request, Response, NextFunction } from 'express';
import request from 'supertest';
-import { startRandomServer } from '../server';
-import sequelize from '../config/sequelize';
+import app from '../app';
import { Poll } from '../database/models/poll';
import { Vote } from '../database/models/vote';
-import { User } from '../database/models/user';
-
-describe('Poll API', () => {
- let serverInstance: { server: import('http').Server; port: number };
- let app: string;
- let userToken: string;
- let testUser: User;
-
- beforeAll(async () => {
- serverInstance = await startRandomServer();
- app = `http://localhost:${serverInstance.port}`;
-
- // Create a test user
- testUser = await User.create({
- username: `testuser_${Date.now()}`,
- password: '$2b$10$Tmh5BMmRudQ/zs4OsK5DluEkPuuoFtxglMKUY8/ug3mE6atADF3y2',
- name: 'Test User',
- });
-
- // Login to get a token
- const loginResponse = await request(app).post('/user/login').send({
- username: testUser.username,
- password: 'password123',
- });
- if (loginResponse.status !== 200) {
- console.error('Failed to login:', loginResponse.body);
- throw new Error('Failed to login test user');
- }
+// Mocking the models
+jest.mock('../../src/database/models/user');
+jest.mock('../../src/database/models/poll');
+jest.mock('../../src/database/models/vote');
- userToken = loginResponse.body.token;
+// Mocking middleware
+jest.mock('../../src/middleware/authenticate', () => ({
+ authenticate: (req: Request, res: Response, next: NextFunction) => next(),
+}));
- // Clear existing data
- await Poll.destroy({ where: {} });
- await Vote.destroy({ where: {} });
+describe('Poll Controller', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
});
- afterAll(async () => {
- // Cleanup users to avoid affecting other tests
- await User.destroy({ where: { id: testUser.id } });
- await sequelize.close();
- await serverInstance.server.close();
+ describe('GET /polls', () => {
+ it('should fetch all polls', async () => {
+ const polls = [
+ {
+ id: '1',
+ optionOne: 'Option One',
+ optionTwo: 'Option Two',
+ votes: [{ userId: '1', chosenOption: 1 }],
+ toJSON: jest.fn().mockReturnValue({
+ id: '1',
+ optionOne: 'Option One',
+ optionTwo: 'Option Two',
+ votes: [{ userId: '1', chosenOption: 1 }],
+ }),
+ },
+ ];
+ /*
+ Poll.findAll returns plain JavaScript objects, not instances of Sequelize models.
+ In Sequelize, toJSON() is a method available on model instances but not on plain objects.
+ To resolve this, we structure polls to return model-like objects.
+ */
+ (Poll.findAll as jest.Mock).mockResolvedValue(polls);
+
+ const response = await request(app).get('/polls');
+ expect(response.status).toBe(200);
+ expect(response.body).toEqual({
+ polls: polls.map(poll => poll.toJSON()),
+ });
+ expect(Poll.findAll).toHaveBeenCalledTimes(1);
+ });
+
+ it('should return 500 if fetching polls fails', async () => {
+ (Poll.findAll as jest.Mock).mockRejectedValue(
+ new Error('Failed to fetch polls'),
+ );
+
+ const response = await request(app).get('/polls');
+ expect(response.status).toBe(500);
+ expect(response.body).toEqual({ error: 'Error fetching polls' });
+ expect(Poll.findAll).toHaveBeenCalledTimes(1);
+ });
});
describe('POST /polls', () => {
it('should create a new poll', async () => {
- const res = await request(app)
- .post('/polls')
- .set('Authorization', `Bearer ${userToken}`)
- .send({
- optionOne: 'Option 1',
- optionTwo: 'Option 2',
- userId: testUser.id,
- })
- .expect(201);
-
- expect(res.body).toEqual(
- expect.objectContaining({
- optionOne: 'Option 1',
- optionTwo: 'Option 2',
- userId: testUser.id,
+ const poll = {
+ id: '1',
+ optionOne: 'Option One',
+ optionTwo: 'Option Two',
+ userId: '1',
+ toJSON: jest.fn().mockReturnValue({
+ id: '1',
+ optionOne: 'Option One',
+ optionTwo: 'Option Two',
+ userId: '1',
}),
- );
+ };
+ (Poll.create as jest.Mock).mockResolvedValue(poll);
+
+ const response = await request(app).post('/polls').send({
+ optionOne: 'Option One',
+ optionTwo: 'Option Two',
+ userId: '1',
+ });
+
+ expect(response.status).toBe(201);
+ expect(response.body).toEqual(poll.toJSON());
+ expect(Poll.create).toHaveBeenCalledTimes(1);
+ expect(Poll.create).toHaveBeenCalledWith({
+ optionOne: 'Option One',
+ optionTwo: 'Option Two',
+ userId: '1',
+ });
});
- it('should return 400 for missing poll details', async () => {
- const res = await request(app)
+ it('should return 400 if missing required poll details', async () => {
+ const response = await request(app)
.post('/polls')
- .set('Authorization', `Bearer ${userToken}`)
- .send({})
- .expect(400);
+ .send({ optionOne: 'Option One' });
+
+ expect(response.status).toBe(400);
+ expect(response.body).toEqual({ error: 'Missing required poll details' });
+ expect(Poll.create).not.toHaveBeenCalled();
+ });
+
+ it('should return 500 if creating poll fails', async () => {
+ (Poll.create as jest.Mock).mockRejectedValue(
+ new Error('Failed to create poll'),
+ );
+
+ const response = await request(app).post('/polls').send({
+ optionOne: 'Option One',
+ optionTwo: 'Option Two',
+ userId: '1',
+ });
- expect(res.body).toHaveProperty('error');
+ expect(response.status).toBe(500);
+ expect(response.body).toEqual({ error: 'Error creating poll' });
+ expect(Poll.create).toHaveBeenCalledTimes(1);
});
});
describe('POST /polls/:id/vote', () => {
- let pollId: string;
+ it('should record a vote on a poll', async () => {
+ const pollId = '1';
+ const userId = '1';
+ const vote = { pollId, userId, chosenOption: 1 };
+ const poll = { id: pollId };
- beforeAll(async () => {
- const poll = await Poll.create({
- userId: testUser.id,
- optionOne: 'Option 1',
- optionTwo: 'Option 2',
+ (Poll.findByPk as jest.Mock).mockResolvedValue(poll);
+ (Vote.findOne as jest.Mock).mockResolvedValue(null);
+ (Vote.create as jest.Mock).mockResolvedValue(vote);
+
+ const response = await request(app)
+ .post(`/polls/${pollId}/vote`)
+ .send({ userId, chosenOption: 1 });
+
+ expect(response.status).toBe(200);
+ expect(response.body).toEqual({
+ message: 'Vote recorded successfully',
+ vote,
+ });
+ expect(Poll.findByPk).toHaveBeenCalledTimes(1);
+ expect(Poll.findByPk).toHaveBeenCalledWith(pollId);
+ expect(Vote.findOne).toHaveBeenCalledTimes(1);
+ expect(Vote.findOne).toHaveBeenCalledWith({ where: { pollId, userId } });
+ expect(Vote.create).toHaveBeenCalledTimes(1);
+ expect(Vote.create).toHaveBeenCalledWith(vote);
+ });
+
+ it('should return 400 if missing vote information', async () => {
+ const response = await request(app)
+ .post('/polls/1/vote')
+ .send({ userId: '1' });
+
+ expect(response.status).toBe(400);
+ expect(response.body).toEqual({ error: 'Missing information' });
+ expect(Poll.findByPk).not.toHaveBeenCalled();
+ expect(Vote.findOne).not.toHaveBeenCalled();
+ expect(Vote.create).not.toHaveBeenCalled();
+ });
+
+ it('should return 404 if poll is not found', async () => {
+ (Poll.findByPk as jest.Mock).mockResolvedValue(null);
+
+ const response = await request(app)
+ .post('/polls/1/vote')
+ .send({ userId: '1', chosenOption: 1 });
+
+ expect(response.status).toBe(404);
+ expect(response.body).toEqual({ error: 'Poll not found' });
+ expect(Poll.findByPk).toHaveBeenCalledTimes(1);
+ expect(Vote.findOne).not.toHaveBeenCalled();
+ expect(Vote.create).not.toHaveBeenCalled();
+ });
+
+ it('should return 409 if user has already voted on this poll', async () => {
+ const pollId = '1';
+ const userId = '1';
+ const existingVote = { pollId, userId, chosenOption: 1 };
+
+ (Poll.findByPk as jest.Mock).mockResolvedValue({ id: pollId });
+ (Vote.findOne as jest.Mock).mockResolvedValue(existingVote);
+
+ const response = await request(app)
+ .post(`/polls/${pollId}/vote`)
+ .send({ userId, chosenOption: 1 });
+
+ expect(response.status).toBe(409);
+ expect(response.body).toEqual({
+ error: 'User has already voted on this poll',
});
- pollId = poll.id;
+ expect(Poll.findByPk).toHaveBeenCalledTimes(1);
+ expect(Vote.findOne).toHaveBeenCalledTimes(1);
+ expect(Vote.create).not.toHaveBeenCalled();
});
- it('should allow a user to vote on a poll', async () => {
- const res = await request(app)
+ it('should return 500 if voting fails', async () => {
+ const pollId = '1';
+ const userId = '1';
+
+ (Poll.findByPk as jest.Mock).mockResolvedValue({ id: pollId });
+ (Vote.findOne as jest.Mock).mockResolvedValue(null);
+ (Vote.create as jest.Mock).mockRejectedValue(new Error('Failed to vote'));
+
+ const response = await request(app)
.post(`/polls/${pollId}/vote`)
- .set('Authorization', `Bearer ${userToken}`)
- .send({
- pollId: pollId,
- userId: testUser.id,
- chosenOption: 1,
- })
- .expect(200);
-
- expect(res.body).toHaveProperty('message', 'Vote recorded successfully');
+ .send({ userId, chosenOption: 1 });
+
+ expect(response.status).toBe(500);
+ expect(response.body).toEqual({ error: 'Error voting' });
+ expect(Poll.findByPk).toHaveBeenCalledTimes(1);
+ expect(Vote.findOne).toHaveBeenCalledTimes(1);
+ expect(Vote.create).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/server/src/tests/user.test.ts b/server/src/tests/user.test.ts
index 3f23cd0..45a34be 100644
--- a/server/src/tests/user.test.ts
+++ b/server/src/tests/user.test.ts
@@ -1,152 +1,232 @@
+import { Request, Response, NextFunction } from 'express';
import request from 'supertest';
-import { startRandomServer } from '../server';
-import User from '../database/models/user';
-import Poll from '../database/models/poll';
-import Vote from '../database/models/vote';
-import type { UserDTO } from '../controllers/userController';
-
-describe('User API', () => {
- let serverInstance: { server: import('http').Server; port: number };
- let app: string;
- const hashedPass =
- '$2b$10$Tmh5BMmRudQ/zs4OsK5DluEkPuuoFtxglMKUY8/ug3mE6atADF3y2';
-
- beforeAll(async () => {
- try {
- serverInstance = await startRandomServer();
- app = `http://localhost:${serverInstance.port}`;
-
- await User.bulkCreate([
- {
- username: 'user1',
- password: hashedPass,
- name: 'User One',
- avatar_url: null,
- },
- {
- username: 'user2',
- password: hashedPass,
- name: 'User Two',
- avatar_url: null,
- },
- ]);
- const user1 = await User.findOne({ where: { username: 'user1' } });
- const user2 = await User.findOne({ where: { username: 'user2' } });
+import app from '../../src/app';
+import { User } from '../../src/database/models/user';
+import { Poll } from '../../src/database/models/poll';
+import { Vote } from '../../src/database/models/vote';
+import jwt from 'jsonwebtoken';
+import bcrypt from 'bcrypt';
+
+// Mocking the models
+jest.mock('../../src/database/models/user');
+jest.mock('../../src/database/models/poll');
+jest.mock('../../src/database/models/vote');
+
+// Mocking jwt and bcrypt
+jest.mock('jsonwebtoken');
+jest.mock('bcrypt');
+
+// Mocking middleware
+jest.mock('../../src/middleware/authenticate', () => ({
+ authenticate: (req: Request, res: Response, next: NextFunction) => next(),
+}));
+
+describe('User Controller', () => {
+ const userPayload = {
+ id: '1',
+ username: 'testuser',
+ password: 'testpass',
+ name: 'Test User',
+ };
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
- if (!user1 || !user2) {
- throw new Error('Test setup failed, required users not found');
- }
+ describe('POST /user/register', () => {
+ it('should register a new user', async () => {
+ (User.create as jest.Mock).mockResolvedValue(userPayload);
+ (jwt.sign as jest.Mock).mockReturnValue('token');
+
+ const response = await request(app).post('/user/register').send({
+ username: 'testuser',
+ password: 'testpass',
+ name: 'Test User',
+ });
- await Poll.create({
- userId: user1.id,
- optionOne: 'Option One',
- optionTwo: 'Option Two',
+ expect(response.status).toBe(201);
+ expect(response.body).toEqual({
+ message: 'User registered successfully',
+ token: 'token',
});
- const poll1 = await Poll.findOne({ where: { userId: user1.id } });
+ });
- if (!poll1) {
- throw new Error('Test setup failed, required poll not found');
- }
+ it('should return 500 if registration fails', async () => {
+ (User.create as jest.Mock).mockRejectedValue(
+ new Error('Registration failed'),
+ );
- await Vote.create({
- pollId: poll1.id,
- userId: user2.id,
- chosenOption: 1,
+ const response = await request(app).post('/user/register').send({
+ username: 'testuser',
+ password: 'testpass',
+ name: 'Test User',
});
- } catch (error) {
- console.error('Error inserting test user:', error);
- }
+
+ expect(response.status).toBe(500);
+ expect(response.body).toEqual({ error: 'User registration failed' });
+ });
});
- afterAll(async () => {
- try {
- await User.destroy({ where: {} });
- await Poll.destroy({ where: {} });
- await Vote.destroy({ where: {} });
+ describe('POST /user/login', () => {
+ it('should login a user', async () => {
+ (User.findOne as jest.Mock).mockResolvedValue({
+ ...userPayload,
+ password: 'hashedpass',
+ });
+ (jwt.sign as jest.Mock).mockReturnValue('token');
+ (bcrypt.compare as jest.Mock).mockResolvedValue(true);
+ (Poll.count as jest.Mock).mockResolvedValue(2);
+ (Vote.count as jest.Mock).mockResolvedValue(3);
+
+ const response = await request(app)
+ .post('/user/login')
+ .send({ username: 'testuser', password: 'testpass' });
- await serverInstance.server.close();
- } catch (error) {
- console.error('Error cleaning up test user:', error);
- }
+ expect(response.status).toBe(200);
+ expect(response.body).toEqual({
+ message: 'User logged in successfully',
+ token: 'token',
+ });
+ });
+
+ it('should return 401 if login fails due to credentials', async () => {
+ (User.findOne as jest.Mock).mockResolvedValue(null);
+
+ const response = await request(app)
+ .post('/user/login')
+ .send({ username: 'testuser', password: 'testpass' });
+
+ expect(response.status).toBe(401);
+ expect(response.body).toEqual({ error: 'Invalid credentials' });
+ });
+
+ it('should return 500 if login fails due to server', async () => {
+ (User.findOne as jest.Mock).mockRejectedValue(new Error('Login failed'));
+
+ const response = await request(app).post('/user/login').send({
+ username: 'testuser',
+ password: 'password123',
+ });
+
+ expect(response.status).toBe(500);
+ expect(response.body).toEqual({ error: 'Login failed' });
+ });
});
- describe('POST /register', () => {
- it('should register a new user and return a token', async () => {
- const res = await request(app)
- .post('/user/register')
- .send({
- username: 'user3',
- password: 'password123',
- name: 'New User',
- avatar_url: null,
- })
- .expect(201);
-
- expect(res.body).toEqual(
- expect.objectContaining({
- message: expect.any(String),
- token: expect.any(String),
- }),
- );
+ describe('GET /user/all', () => {
+ it('should fetch all users', async () => {
+ const users = [{ id: '1', username: 'testuser', name: 'Test User' }];
+ (User.findAll as jest.Mock).mockResolvedValue(users);
+ (Poll.count as jest.Mock).mockResolvedValue(2);
+ (Vote.count as jest.Mock).mockResolvedValue(3);
+
+ const response = await request(app).get('/user/all');
+
+ expect(response.status).toBe(200);
+ expect(response.body).toEqual([
+ {
+ id: '1',
+ username: 'testuser',
+ name: 'Test User',
+ pollCount: 2,
+ voteCount: 3,
+ },
+ ]);
});
- it('should handle missing username and return a 400 status', async () => {
- const res = await request(app)
- .post('/user/register')
- .send({
- password: 'password123',
- name: 'New User',
- })
- .expect(400);
-
- expect(res.body).toHaveProperty('errors');
- expect(res.body.errors).toBeInstanceOf(Array);
+ it('should return 500 if fetching users fails', async () => {
+ (User.findAll as jest.Mock).mockRejectedValue(
+ new Error('Error fetching users'),
+ );
+
+ const response = await request(app).get('/user/all');
+
+ expect(response.status).toBe(500);
+ expect(response.body).toEqual({ error: 'Error fetching users' });
});
});
- describe('POST /login', () => {
- it('should authenticate existing user and return a token', async () => {
- const res = await request(app)
- .post('/user/login')
- .send({
- username: 'user1',
- password: 'password123',
- })
- .expect(200);
+ describe('PUT /user/:id', () => {
+ it('should update a user', async () => {
+ (User.findByPk as jest.Mock).mockResolvedValue({
+ ...userPayload,
+ save: jest.fn().mockResolvedValue(true),
+ });
+ (jwt.sign as jest.Mock).mockReturnValue('token');
- expect(res.body).toEqual({
- message: 'User logged in successfully',
- token: expect.any(String),
+ const response = await request(app).put('/user/1').send({
+ username: 'updateduser',
+ name: 'Updated User',
+ password: 'new_pass',
+ });
+
+ expect(response.status).toBe(201);
+ expect(response.body).toEqual({
+ message: 'User updated successfully',
+ token: 'token',
});
});
- it('should return error for invalid password', async () => {
- const res = await request(app)
- .post('/user/login')
- .send({
- username: 'user1',
- password: 'wrongpassword',
- })
- .expect(401);
-
- expect(res.body).toEqual({
- error: 'Invalid credentials',
+ it('should return 404 if user is not found', async () => {
+ (User.findByPk as jest.Mock).mockResolvedValue(null);
+
+ const response = await request(app)
+ .put('/user/1')
+ .send({ username: 'updateduser', name: 'Updated User' });
+
+ expect(response.status).toBe(404);
+ expect(response.body).toEqual({ error: 'User not found' });
+ });
+
+ it('should return 500 if updating user fails', async () => {
+ (User.findByPk as jest.Mock).mockResolvedValue({
+ ...userPayload,
+ save: jest.fn().mockRejectedValue(new Error('Failed to update user')),
});
+
+ const response = await request(app)
+ .put('/user/1')
+ .send({ username: 'updateduser', name: 'Updated User' });
+
+ expect(response.status).toBe(500);
+ expect(response.body).toEqual({ error: 'Failed to update user' });
});
});
- describe('GET /user/all', () => {
- test('GET /users/all should fetch all users with their details', async () => {
- const res = await request(app).get('/user/all').expect(200);
- expect(res.body).toBeInstanceOf(Array);
- expect(res.body.length).toBeGreaterThan(0);
- res.body.forEach((user: UserDTO) => {
- expect(user).toHaveProperty('id');
- expect(user).toHaveProperty('username');
- expect(user).toHaveProperty('name');
- expect(user).toHaveProperty('pollCount');
- expect(user).toHaveProperty('voteCount');
+ describe('DELETE /user/:id', () => {
+ it('should delete a user', async () => {
+ (User.findByPk as jest.Mock).mockResolvedValue({
+ ...userPayload,
+ destroy: jest.fn().mockResolvedValue(true),
});
+ (Poll.update as jest.Mock).mockResolvedValue(true);
+
+ const response = await request(app).delete('/user/1');
+
+ expect(response.status).toBe(204);
+ });
+
+ it('should return 404 if user is not found', async () => {
+ (User.findByPk as jest.Mock).mockResolvedValue(null);
+
+ const response = await request(app).delete('/user/1');
+
+ expect(response.status).toBe(404);
+ expect(response.body).toEqual({ error: 'User not found' });
+ });
+
+ it('should return 500 if deleting user fails', async () => {
+ (User.findByPk as jest.Mock).mockResolvedValue({
+ ...userPayload,
+ destroy: jest
+ .fn()
+ .mockRejectedValue(new Error('Failed to delete user')),
+ });
+
+ const response = await request(app).delete('/user/1');
+
+ expect(response.status).toBe(500);
+ expect(response.body).toEqual({ error: 'Failed to delete user' });
});
});
});
diff --git a/server/tsconfig.json b/server/tsconfig.json
index a670cab..45d5703 100644
--- a/server/tsconfig.json
+++ b/server/tsconfig.json
@@ -11,13 +11,13 @@
"paths": {
"*": ["./node_modules/*", "./src/types/*"]
},
- "typeRoots": ["./node_modules/@types"],
- "types": ["jest", "node"],
+ "typeRoots": ["./node_modules/@types", "./types"],
+ "types": ["jest", "node", "express"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
- "include": ["src/**/*"],
+ "include": ["src/**/*", "types/**/*"],
"exclude": ["node_modules", "dist"]
}
diff --git a/server/types/index.d.ts b/server/types/index.d.ts
new file mode 100644
index 0000000..d311fa0
--- /dev/null
+++ b/server/types/index.d.ts
@@ -0,0 +1,7 @@
+import { User } from '../src/database/models/user';
+
+declare module 'express-serve-static-core' {
+ interface Request {
+ user?: User;
+ }
+}