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(() => { })