From f76a8f7076e66c2b5e9942bf5f3fbdb3baeb09c3 Mon Sep 17 00:00:00 2001 From: Marti Herms Date: Tue, 20 Aug 2024 10:20:22 +0200 Subject: [PATCH] implement followers and following getters logic and testing in api #84 --- .../project/V-HUB/app/src/home/Footer.jsx | 2 +- .../project/V-HUB/app/src/home/Library.jsx | 10 +- .../project/V-HUB/app/src/home/Profile.jsx | 17 +++- .../project/V-HUB/app/src/home/UserList.jsx | 15 +++ .../project/V-HUB/app/src/home/index.jsx | 2 + .../app/src/library/NavigationButton.jsx | 4 +- .../project/V-HUB/core/data/models.js | 4 + .../V-HUB/core/logic/getUserFollowers.js | 39 ++++++++ .../V-HUB/core/logic/getUserFollowers.spec.js | 96 +++++++++++++++++++ .../V-HUB/core/logic/getUserFollowers.test.js | 10 ++ .../V-HUB/core/logic/getUserFollowing.js | 39 ++++++++ .../V-HUB/core/logic/getUserFollowing.spec.js | 96 +++++++++++++++++++ .../V-HUB/core/logic/getUserFollowing.test.js | 10 ++ .../V-HUB/core/logic/getUserLibrary.spec.js | 2 - .../V-HUB/core/logic/toggleFollowUser.js | 2 + 15 files changed, 338 insertions(+), 10 deletions(-) create mode 100644 staff/marti-herms/project/V-HUB/app/src/home/UserList.jsx create mode 100644 staff/marti-herms/project/V-HUB/core/logic/getUserFollowers.js create mode 100644 staff/marti-herms/project/V-HUB/core/logic/getUserFollowers.spec.js create mode 100644 staff/marti-herms/project/V-HUB/core/logic/getUserFollowers.test.js create mode 100644 staff/marti-herms/project/V-HUB/core/logic/getUserFollowing.js create mode 100644 staff/marti-herms/project/V-HUB/core/logic/getUserFollowing.spec.js create mode 100644 staff/marti-herms/project/V-HUB/core/logic/getUserFollowing.test.js diff --git a/staff/marti-herms/project/V-HUB/app/src/home/Footer.jsx b/staff/marti-herms/project/V-HUB/app/src/home/Footer.jsx index 232f46f5f..f5d8839d2 100644 --- a/staff/marti-herms/project/V-HUB/app/src/home/Footer.jsx +++ b/staff/marti-herms/project/V-HUB/app/src/home/Footer.jsx @@ -14,7 +14,7 @@ export default function Footer({ makeReviewVisibility, onSearchGame, onAddGame, {role === 'dev' && Add Game} Search } /> - HOME} /> + HOME} /> HOME} /> HOME} /> diff --git a/staff/marti-herms/project/V-HUB/app/src/home/Library.jsx b/staff/marti-herms/project/V-HUB/app/src/home/Library.jsx index c3843be7e..39e6fe0bd 100644 --- a/staff/marti-herms/project/V-HUB/app/src/home/Library.jsx +++ b/staff/marti-herms/project/V-HUB/app/src/home/Library.jsx @@ -4,12 +4,14 @@ import logic from '../../logic' import useContext from '../context.js' import GameBanner from './GameBanner.jsx' +import Button from '../library/Button.jsx' + import extractPayloadFromToken from '../../util/extractPayloadFromToken.js' export default function Library({ onGameClick, user }) { const { alert } = useContext() - const { userId, role } = extractPayloadFromToken(sessionStorage.token) + const { sub: userId, role } = extractPayloadFromToken(sessionStorage.token) const [games, setGames] = useState([]) const [libraryVisibility, setLibraryVisibility] = useState(false) @@ -90,12 +92,12 @@ export default function Library({ onGameClick, user }) { return
{((!user && role) || user.role === 'dev') && <> - + {devGamesVisibility && games.map(game => )} } - + {libraryVisibility && games.map(game => )} - + {favsVisibility && games.map(game => )}
} \ No newline at end of file diff --git a/staff/marti-herms/project/V-HUB/app/src/home/Profile.jsx b/staff/marti-herms/project/V-HUB/app/src/home/Profile.jsx index 15a3f3f89..e8e9eeb25 100644 --- a/staff/marti-herms/project/V-HUB/app/src/home/Profile.jsx +++ b/staff/marti-herms/project/V-HUB/app/src/home/Profile.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { useParams } from 'react-router-dom' +import { useParams, useNavigate, useLocation } from 'react-router-dom' import logic from '../../logic' @@ -20,6 +20,9 @@ import defaultAvatar from '../../images/defaultAvatar.svg' export default function Profile({ refreshStamp, onChange, onGameClick }) { const { alert } = useContext() + const navigate = useNavigate() + const location = useLocation() + const { userId } = useParams() const { sub: loggedInUserId } = extractPayloadFromToken(sessionStorage.token) @@ -119,6 +122,14 @@ export default function Profile({ refreshStamp, onChange, onGameClick }) { setEditAvatarVisibility(false) } + const handleFollowers = () => { + navigate(location.pathname + '/followers') + } + + const handleFollowing = () => { + navigate(location.pathname + '/following') + } + return <> {user && @@ -153,5 +164,9 @@ export default function Profile({ refreshStamp, onChange, onGameClick }) { {user && } + {user && + + + } } \ No newline at end of file diff --git a/staff/marti-herms/project/V-HUB/app/src/home/UserList.jsx b/staff/marti-herms/project/V-HUB/app/src/home/UserList.jsx new file mode 100644 index 000000000..93333f968 --- /dev/null +++ b/staff/marti-herms/project/V-HUB/app/src/home/UserList.jsx @@ -0,0 +1,15 @@ +import { useParams } from 'react-router-dom' + +import useContext from '../context' + +import Container from '../library/Container' + +export default function UserList({ userList }) { + const { alert } = useContext() + + const { userId } = useParams() + + return + + +} \ No newline at end of file diff --git a/staff/marti-herms/project/V-HUB/app/src/home/index.jsx b/staff/marti-herms/project/V-HUB/app/src/home/index.jsx index 46435c13f..9c668fd87 100644 --- a/staff/marti-herms/project/V-HUB/app/src/home/index.jsx +++ b/staff/marti-herms/project/V-HUB/app/src/home/index.jsx @@ -78,6 +78,8 @@ export default function Home({ onLogout }) { } /> } /> + Hello

} /> + Hello

} /> } /> } /> } /> diff --git a/staff/marti-herms/project/V-HUB/app/src/library/NavigationButton.jsx b/staff/marti-herms/project/V-HUB/app/src/library/NavigationButton.jsx index e82621566..d079b963d 100644 --- a/staff/marti-herms/project/V-HUB/app/src/library/NavigationButton.jsx +++ b/staff/marti-herms/project/V-HUB/app/src/library/NavigationButton.jsx @@ -1,3 +1,3 @@ -export default function Button({ children, ...nextProps }) { - return +export default function NavigationButton({ children, ...nextProps }) { + return } \ No newline at end of file diff --git a/staff/marti-herms/project/V-HUB/core/data/models.js b/staff/marti-herms/project/V-HUB/core/data/models.js index 79b6d85c6..d29ee4e46 100644 --- a/staff/marti-herms/project/V-HUB/core/data/models.js +++ b/staff/marti-herms/project/V-HUB/core/data/models.js @@ -39,6 +39,10 @@ const user = new Schema({ following: { type: [ObjectId], ref: 'User' + }, + followers: { + type: [ObjectId], + ref: 'User' } }) diff --git a/staff/marti-herms/project/V-HUB/core/logic/getUserFollowers.js b/staff/marti-herms/project/V-HUB/core/logic/getUserFollowers.js new file mode 100644 index 000000000..47c85e6f5 --- /dev/null +++ b/staff/marti-herms/project/V-HUB/core/logic/getUserFollowers.js @@ -0,0 +1,39 @@ +import { User } from '../data/models.js' + +import { validate, errors } from 'com' + +const { NotFoundError, SystemError } = errors + +export default (userId, targetUserId) => { + validate.string(userId, 'userId') + validate.string(targetUserId, 'targetUserId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return User.findById(targetUserId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) throw new NotFoundError('targetUser not found') + + return User.find({ _id: { $in: user.followers } }) + .catch(error => { throw new SystemError(error.message) }) + .then(users => { + const promises = users.map(user => { + user.id = user._id.toString() + + delete user._id + + delete user.password + + return user + }) + + return Promise.all(promises) + }) + }) + }) +} \ No newline at end of file diff --git a/staff/marti-herms/project/V-HUB/core/logic/getUserFollowers.spec.js b/staff/marti-herms/project/V-HUB/core/logic/getUserFollowers.spec.js new file mode 100644 index 000000000..89de037d6 --- /dev/null +++ b/staff/marti-herms/project/V-HUB/core/logic/getUserFollowers.spec.js @@ -0,0 +1,96 @@ +import 'dotenv/config' +import getUserFollowers from './getUserFollowers.js' +import toggleFollowUser from './toggleFollowUser.js' +import mongoose from 'mongoose' + +import { expect } from 'chai' +import { User } from '../data/models.js' + +import { errors } from 'com' + +const { NotFoundError, ValidationError } = errors + +describe('getUserFollowers', () => { + before(() => mongoose.connect(process.env.MONGODB_URI)) + + beforeEach(() => User.deleteMany()) + + it('succeeds on existing user returning all followers', () => { + return User.create({ username: 'monoloco', email: 'mono@loco.com', password: '123123123' }) + .then(user => + User.create({ username: 'eden', email: 'marti@herms.com', password: '123123123' }) + .then(_user => + toggleFollowUser(user.id, _user.id) + .then(() => getUserFollowers(_user.id, _user.id)) + .then(users => { + expect(users).to.be.an('array') + expect(users[0].id).to.equal(user.id) + }) + ) + ) + }) + + it('succeeds on existing user and returning empty array ', () => { + return User.create({ username: 'monoloco', email: 'mono@loco.com', password: '123123123' }) + .then(user => + getUserFollowers(user.id, user.id) + .then(users => { + expect(users).to.be.an('array') + expect(users.length).to.equal(0) + }) + ) + }) + + it('fails on non-existing user', () => { + let _error + + return getUserFollowers('66ba007f874aa7b84ec54491', '66ba007f874aa7b84ec54491') + .catch(error => _error = error) + .finally(() => { + expect(_error).to.be.instanceOf(NotFoundError) + expect(_error.message).to.equal('user not found') + }) + }) + + it('fails on non-existing targetUser', () => { + let _error + + return User.create({ name: 'Mono', surname: 'Loco', email: 'mono@loco.com', username: 'monoloco', password: '123123123' }) + .then(user => getUserFollowers(user.id, '66ba007f874aa7b84ec54491')) + .catch(error => _error = error) + .finally(() => { + expect(_error).to.be.instanceOf(NotFoundError) + expect(_error.message).to.equal('targetUser not found') + }) + }) + + it('fails on non-string userId', () => { + let error + + try { + getUserFollowers(123, '66ba007f874aa7b84ec54491') + } catch (_error) { + error = _error + } finally { + expect(error).to.be.instanceOf(ValidationError) + expect(error.message).to.equal('userId is not a string') + } + }) + + it('fails on non-string targetUserId', () => { + let error + + try { + getUserFollowers('66ba007f874aa7b84ec54491', 123) + } catch (_error) { + error = _error + } finally { + expect(error).to.be.instanceOf(ValidationError) + expect(error.message).to.equal('targetUserId is not a string') + } + }) + + afterEach(() => User.deleteMany()) + + after(() => mongoose.disconnect()) +}) \ No newline at end of file diff --git a/staff/marti-herms/project/V-HUB/core/logic/getUserFollowers.test.js b/staff/marti-herms/project/V-HUB/core/logic/getUserFollowers.test.js new file mode 100644 index 000000000..58b95e9c4 --- /dev/null +++ b/staff/marti-herms/project/V-HUB/core/logic/getUserFollowers.test.js @@ -0,0 +1,10 @@ +import 'dotenv/config' +import mongoose from 'mongoose' + +import getUserFollowers from './getUserFollowers.js' + +mongoose.connect(process.env.MONGODB_URI) + .then(() => getUserFollowers('66acb2b1730b0f09da259589', '66acb2b1730b0f09da259589')) + .then(games => console.log(games)) + .catch(error => console.error(error)) + .finally(() => mongoose.disconnect()) \ No newline at end of file diff --git a/staff/marti-herms/project/V-HUB/core/logic/getUserFollowing.js b/staff/marti-herms/project/V-HUB/core/logic/getUserFollowing.js new file mode 100644 index 000000000..ef3e22044 --- /dev/null +++ b/staff/marti-herms/project/V-HUB/core/logic/getUserFollowing.js @@ -0,0 +1,39 @@ +import { User } from '../data/models.js' + +import { validate, errors } from 'com' + +const { NotFoundError, SystemError } = errors + +export default (userId, targetUserId) => { + validate.string(userId, 'userId') + validate.string(targetUserId, 'targetUserId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return User.findById(targetUserId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) throw new NotFoundError('targetUser not found') + + return User.find({ _id: { $in: user.following } }) + .catch(error => { throw new SystemError(error.message) }) + .then(users => { + const promises = users.map(user => { + user.id = user._id.toString() + + delete user._id + + delete user.password + + return user + }) + + return Promise.all(promises) + }) + }) + }) +} \ No newline at end of file diff --git a/staff/marti-herms/project/V-HUB/core/logic/getUserFollowing.spec.js b/staff/marti-herms/project/V-HUB/core/logic/getUserFollowing.spec.js new file mode 100644 index 000000000..19890c213 --- /dev/null +++ b/staff/marti-herms/project/V-HUB/core/logic/getUserFollowing.spec.js @@ -0,0 +1,96 @@ +import 'dotenv/config' +import getUserFollowing from './getUserFollowing.js' +import toggleFollowUser from './toggleFollowUser.js' +import mongoose from 'mongoose' + +import { expect } from 'chai' +import { User } from '../data/models.js' + +import { errors } from 'com' + +const { NotFoundError, ValidationError } = errors + +describe('getUserFollowing', () => { + before(() => mongoose.connect(process.env.MONGODB_URI)) + + beforeEach(() => User.deleteMany()) + + it('succeeds on existing user returning all following', () => { + return User.create({ username: 'monoloco', email: 'mono@loco.com', password: '123123123' }) + .then(user => + User.create({ username: 'eden', email: 'marti@herms.com', password: '123123123' }) + .then(_user => + toggleFollowUser(user.id, _user.id) + .then(() => getUserFollowing(user.id, user.id)) + .then(users => { + expect(users).to.be.an('array') + expect(users[0].id).to.equal(_user.id) + }) + ) + ) + }) + + it('succeeds on existing user and returning empty array ', () => { + return User.create({ username: 'monoloco', email: 'mono@loco.com', password: '123123123' }) + .then(user => + getUserFollowing(user.id, user.id) + .then(users => { + expect(users).to.be.an('array') + expect(users.length).to.equal(0) + }) + ) + }) + + it('fails on non-existing user', () => { + let _error + + return getUserFollowing('66ba007f874aa7b84ec54491', '66ba007f874aa7b84ec54491') + .catch(error => _error = error) + .finally(() => { + expect(_error).to.be.instanceOf(NotFoundError) + expect(_error.message).to.equal('user not found') + }) + }) + + it('fails on non-existing targetUser', () => { + let _error + + return User.create({ name: 'Mono', surname: 'Loco', email: 'mono@loco.com', username: 'monoloco', password: '123123123' }) + .then(user => getUserFollowing(user.id, '66ba007f874aa7b84ec54491')) + .catch(error => _error = error) + .finally(() => { + expect(_error).to.be.instanceOf(NotFoundError) + expect(_error.message).to.equal('targetUser not found') + }) + }) + + it('fails on non-string userId', () => { + let error + + try { + getUserFollowing(123, '66ba007f874aa7b84ec54491') + } catch (_error) { + error = _error + } finally { + expect(error).to.be.instanceOf(ValidationError) + expect(error.message).to.equal('userId is not a string') + } + }) + + it('fails on non-string targetUserId', () => { + let error + + try { + getUserFollowing('66ba007f874aa7b84ec54491', 123) + } catch (_error) { + error = _error + } finally { + expect(error).to.be.instanceOf(ValidationError) + expect(error.message).to.equal('targetUserId is not a string') + } + }) + + afterEach(() => User.deleteMany()) + + after(() => mongoose.disconnect()) +}) \ No newline at end of file diff --git a/staff/marti-herms/project/V-HUB/core/logic/getUserFollowing.test.js b/staff/marti-herms/project/V-HUB/core/logic/getUserFollowing.test.js new file mode 100644 index 000000000..4fafa839b --- /dev/null +++ b/staff/marti-herms/project/V-HUB/core/logic/getUserFollowing.test.js @@ -0,0 +1,10 @@ +import 'dotenv/config' +import mongoose from 'mongoose' + +import getUserFollowing from './getUserFollowing.js' + +mongoose.connect(process.env.MONGODB_URI) + .then(() => getUserFollowing('66acb2b1730b0f09da259589', '66acb2b1730b0f09da259589')) + .then(games => console.log(games)) + .catch(error => console.error(error)) + .finally(() => mongoose.disconnect()) \ No newline at end of file diff --git a/staff/marti-herms/project/V-HUB/core/logic/getUserLibrary.spec.js b/staff/marti-herms/project/V-HUB/core/logic/getUserLibrary.spec.js index 641af18b9..a4d450137 100644 --- a/staff/marti-herms/project/V-HUB/core/logic/getUserLibrary.spec.js +++ b/staff/marti-herms/project/V-HUB/core/logic/getUserLibrary.spec.js @@ -54,7 +54,6 @@ describe('getUserLibrary', () => { .then(game => getUserLibrary('66ba007f874aa7b84ec54491', '66ba007f874aa7b84ec54491')) .catch(error => _error = error) .finally(() => { - console.log(_error) expect(_error).to.be.instanceOf(NotFoundError) expect(_error.message).to.equal('user not found') }) @@ -68,7 +67,6 @@ describe('getUserLibrary', () => { .then(game => getUserLibrary(user.id, '66ba007f874aa7b84ec54491'))) .catch(error => _error = error) .finally(() => { - console.log(_error) expect(_error).to.be.instanceOf(NotFoundError) expect(_error.message).to.equal('targetUser not found') }) diff --git a/staff/marti-herms/project/V-HUB/core/logic/toggleFollowUser.js b/staff/marti-herms/project/V-HUB/core/logic/toggleFollowUser.js index aab2d842d..d3d510129 100644 --- a/staff/marti-herms/project/V-HUB/core/logic/toggleFollowUser.js +++ b/staff/marti-herms/project/V-HUB/core/logic/toggleFollowUser.js @@ -21,9 +21,11 @@ export default (userId, targetUserId) => { if (user.following.some(userObjectId => userObjectId.toString() === targetUserId)) return User.findByIdAndUpdate(userId, { $pull: { following: targetUser._id } }) .catch(error => { throw new SystemError(error.message) }) + .then(() => User.findByIdAndUpdate(targetUserId, { $pull: { followers: user._id } })) else return User.findByIdAndUpdate(userId, { $push: { following: targetUser._id } }) .catch(error => { throw new SystemError(error.message) }) + .then(() => User.findByIdAndUpdate(targetUserId, { $push: { followers: user._id } })) }) }) .then(() => { })