Skip to content

Commit

Permalink
Merge pull request #75 from Arquisoft/userservice
Browse files Browse the repository at this point in the history
Leaderboard endpoint and tests
  • Loading branch information
carlosmndzg authored Mar 15, 2024
2 parents ad90098 + dbe6b7d commit dd9f2b9
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 24 deletions.
14 changes: 13 additions & 1 deletion gatewayservice/src/controllers/history-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ const getHistory = async (req: Request, res: Response, next: NextFunction) => {
}
};

const getLeaderboard = async (req: Request, res: Response, next: NextFunction) => {
try {
const authResponse = await axios.get(HISTORY_SERVICE_URL + '/history/leaderboard', {
params: req.query,
});

res.json(authResponse.data);
} catch (error: any) {
next(error);
}
};

const updateHistory = async (
req: Request,
res: Response,
Expand Down Expand Up @@ -58,4 +70,4 @@ const incrementHistory = async (
}
};

export { getHistory, updateHistory, incrementHistory };
export { getHistory, updateHistory, incrementHistory, getLeaderboard };
2 changes: 2 additions & 0 deletions gatewayservice/src/routes/history-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import {
getHistory,
updateHistory,
incrementHistory,
getLeaderboard
} from '../controllers/history-controller';

const router = express.Router();

router.get('/history', getHistory);
router.post('/history', updateHistory);
router.get('/history/leaderboard', getLeaderboard);
router.post('/history/increment', incrementHistory);

export default router;
60 changes: 43 additions & 17 deletions users/userservice/src/controllers/history-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,51 @@ const getHistory = async (req: Request, res: Response) => {
}
};

const DEFAULT_LEADERBOARD_SIZE = 10;

const getLeaderboard = async (req: Request, res: Response) => {
try {
const sizeParam = req.query.size;
let size = DEFAULT_LEADERBOARD_SIZE; // Default size if no parameter is received
if (sizeParam) {
size = parseInt(sizeParam as string, 10);
if (size <= 0) {
throw new Error('The size must be a positive value.');
}
}

const leaderboard = await User.find({})
.sort({ 'history.points': -1 }) // Sort in descending order of points
.limit(size) // Only take the first (size) users
.select('username history'); // Select only username and history (no password, date, etc.)

res.json({
status: 'success',
data: {
leaderboard
},
});
} catch (error: any) {
res.status(400).json({
status: 'fail',
data: {
error: error.message,
},
});
}
};

const updateHistory = async (req: Request, res: Response) => {
try {
const user = req.user;

if (!user) {
throw new Error('Unknown error. User does not appear.');
}

validateRequiredFields(req, ['history']);
validateHistoryBody(req, user);
validateHistoryBody(req, user!);

user.history = { ...user.history, ...req.body.history };
await user.save();
user!.history = { ...user!.history, ...req.body.history };
await user!.save();

res.json({ status: 'success', data: user.history });
res.json({ status: 'success', data: user!.history });
} catch (error) {
res.status(400).json({
status: 'fail',
Expand All @@ -63,20 +93,16 @@ const incrementHistory = async (req: Request, res: Response) => {
try {
const user = req.user;

if (!user) {
throw new Error('Unknown error. User does not appear.');
}

validateRequiredFields(req, ['history']);
validateHistoryBody(req, user);
validateHistoryBody(req, user!);

Object.keys(req.body.history).forEach(key => {
(user.history as any)[key] += req.body.history[key];
(user!.history as any)[key] += req.body.history[key];
});

await user.save();
await user!.save();

res.json({ status: 'success', data: user.history });
res.json({ status: 'success', data: user!.history });
} catch (error) {
res.status(400).json({
status: 'fail',
Expand All @@ -87,4 +113,4 @@ const incrementHistory = async (req: Request, res: Response) => {
}
};

export { getHistory, updateHistory, incrementHistory };
export { getHistory, updateHistory, incrementHistory, getLeaderboard };
6 changes: 1 addition & 5 deletions users/userservice/src/middlewares/protect-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ const protect = async (req: Request, res: Response, next: NextFunction) => {
const { userId } = decoded;
const user = await User.findById(userId);

if (user === null) {
throw new Error("User does not exist. Log in again.'");
}

req.user = user;
req.user = user!;
next();
} catch (error: any) {
res.status(400).json({
Expand Down
3 changes: 2 additions & 1 deletion users/userservice/src/routes/history-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import express from 'express';
import {
getHistory,
updateHistory,
incrementHistory,
incrementHistory, getLeaderboard,
} from '../controllers/history-controller';
import { protect } from '../middlewares/protect-middleware';

const router = express.Router();

router.get('/history', getHistory);
router.post('/history', protect, updateHistory);
router.get('/history/leaderboard', getLeaderboard);
router.post('/history/increment', protect, incrementHistory);

export default router;
176 changes: 176 additions & 0 deletions users/userservice/test/user-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const request = require('supertest');
import mongoose from 'mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
import app from '../src/app';
import {validateHistoryBody} from "../src/utils/history-body-validation";
import { Request } from 'express';
import {validateNotEmpty, validateRequiredLength} from "../src/utils/field-validations";
import {verifyJWT} from "../src/utils/async-verification";

let mongoServer: MongoMemoryServer;

Expand Down Expand Up @@ -194,4 +198,176 @@ describe('User Service', () => {
expect(getResponse.statusCode).toBe(200);
expect(JSON.stringify(getResponse.body.data.history)).toMatch(JSON.stringify(expectedHistory.history));
});

// GET /history/leaderboard with size param
it('should obtain n users with the highest scores', async () => {
const newUserData = {
username: 'highestscoreuser',
password: 'testpassword',
};
// Add a new user to test leaderboard
await request(app).post('/adduser').send(newUserData);

// The new user will have 100000 points
const increment = {
history: {
points: 100000,
},
};
const newUser = await User.findOne({ username:'highestscoreuser' });
// Generates a temporary token for this test
const testToken2 = jwt.sign({ userId: newUser!._id }, 'your-secret-key', {
expiresIn: '2m',
});

await request(app)
.post('/history/increment')
.send(increment)
.set('Authorization', `Bearer ${testToken2}`);

// The old user will have 99999 points
const newHistory = {
history: {
points: 99999,
},
};
await request(app)
.post('/history')
.send(newHistory)
.set('Authorization', `Bearer ${testToken}`);

const response = await request(app)
.get('/history/leaderboard?size=2');

expect(response.statusCode).toBe(200);
expect(response.body.data.leaderboard.length).toBe(2);
expect(response.body.data.leaderboard[0].username).toEqual('highestscoreuser');
expect(response.body.data.leaderboard[0].history.points).toBe(100000);
expect(response.body.data.leaderboard[1].username).toEqual('testuser');
expect(response.body.data.leaderboard[1].history.points).toBe(99999);
});

// GET /history/leaderboard without param
it('should obtain users with the highest scores', async () => {
// If a request is made without the parameter it will return an amount of users
// specified by a constant in the controller (DEFAULT_LEADERBOARD_SIZE)
const response = await request(app)
.get('/history/leaderboard');

expect(response.statusCode).toBe(200);
expect(response.body.data.leaderboard).not.toBeUndefined();
});

// GET /history/leaderboard negative param
it('should obtain users with the highest scores', async () => {
// If a request is made with a negative size, it will throw an exception
const response = await request(app)
.get('/history/leaderboard?size=-1');

expect(response.statusCode).toBe(400);
expect(response.body.data.leaderboard).toBeUndefined();
});

// Body validation util, not part of the user history
it('should get an error when including a parameter that is not in the model', async () => {
const mockRequest = {
body: {
history: {
nonexistent: 1
}
}
} as Request;
const user = await User.find({ username:'testuser' });

try {
validateHistoryBody(mockRequest, user[0]);
fail('Should get an error in the previous call');
} catch (error) {
}
});

// Body validation util, non-numeric
it('should get an error when using non-numerical values', async () => {
const mockRequest = {
body: {
history: {
gamesPlayed: 'test'
}
}
} as Request;
const user = await User.find({ username:'testuser' });

try {
validateHistoryBody(mockRequest, user[0]);
fail('Should get an error in the previous call');
} catch (error) {
}
});

// Body validation util, negative
it('should get an error when using negative values', async () => {
const mockRequest = {
body: {
history: {
gamesPlayed: -1
}
}
} as Request;
const user = await User.find({ username:'testuser' });

try {
validateHistoryBody(mockRequest, user[0]);
fail('Should get an error in the previous call');
} catch (error) {
}
});

// Empty field validation
it('should get an error when passing an empty parameter', async () => {
const mockRequest = {
body: {}
} as Request;
mockRequest.body['history'] = '';

try {
validateNotEmpty(mockRequest, ['history']);
fail('Should get an error in the previous call');
} catch (error) {
}
// Should also get an error when the field does not exist
try {
validateNotEmpty(mockRequest, ['nonexistent']);
fail('Should get an error in the previous call');
} catch (error) {
}
});

// Empty field validation
it('should get an error when passing a parameter without the expected length', async () => {
const mockRequest = {
body: {}
} as Request;
mockRequest.body['test'] = '123456789';

try {
validateRequiredLength(mockRequest, ['test'], 10);
fail('Should get an error in the previous call');
} catch (error) {
}
// Should also get an error when the field does not exist
try {
validateRequiredLength(mockRequest, ['nonexistent'], 10);
fail('Should get an error in the previous call');
} catch (error) {
}
});

// Token validator
it('should get an error when invoking the function with an invalid token', async () => {
try {
await verifyJWT('invalidtoken');
fail('Should get an error in the previous call');
} catch (error) {
}
});
});

0 comments on commit dd9f2b9

Please sign in to comment.