Skip to content

Commit 91abbf9

Browse files
authored
Feat/7 following unfollowing homepage (#19)
* Create an unfollow write, refactor * Implement following and unfollowing users * Render posts on home page * Add tests, add minor tweaks * Refactor tests to work with new homepage * Create a followers/following view
1 parent 0b01d9e commit 91abbf9

40 files changed

+784
-148
lines changed

backend/requests/put-requests.rest

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# creating a user
2+
POST http://localhost:3001/api/users
3+
Content-Type: application/json
4+
5+
{
6+
"fullName": "Bob Dob",
7+
"email": "[email protected]",
8+
"username": "bobbydob",
9+
"password": "secret"
10+
}
11+
12+
###
13+
# creating a second user
14+
POST http://localhost:3001/api/users
15+
Content-Type: application/json
16+
17+
{
18+
"fullName": "Bob Dob",
19+
"email": "[email protected]",
20+
"username": "bobbydob2",
21+
"password": "secret"
22+
}
23+
24+
###
25+
#log in first user
26+
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWQiOiI2NWU0ZjZhZTNjMjg4Y2EzODVkYzJhNGEiLCJpYXQiOjE3MDk2MTEzMTAsImV4cCI6MTcwOTYxNDkxMH0.gmUZvHrjxO8ceZVOm0kTh3fYboJldmsCdxMPMyUmbH4
27+
POST http://localhost:3001/api/login
28+
Content-Type: application/json
29+
30+
{
31+
"username": "admin",
32+
"password": "admin"
33+
}
34+
35+
###
36+
# request to follow second user
37+
@id = 65e64e6a0fa35c92f120667b
38+
PUT http://localhost:3001/api/users/{{id}}/follow
39+
Authorization: bearer {{token}}
40+
41+
###
42+
# request to unfollow second user
43+
@id = 65e64e6a0fa35c92f120667b
44+
PUT http://localhost:3001/api/users/{{id}}/unfollow
45+
Authorization: bearer {{token}}

backend/src/routes/users.ts

+19-4
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,13 @@ router.delete('/:id/image', authenticator(), async (req, res, next) => {
8787
});
8888

8989
router.put('/:id/follow', authenticator(), async (req, res, next) => {
90-
if (req.params.id === req.userToken!.id) return res.status(400).send({ error: 'You can\'t follow yourself!' });
90+
if (req.params.id === req.userToken!.id) {
91+
return res.status(400).send({ error: 'You can\'t follow yourself!' });
92+
}
9193

9294
try {
93-
const followedUser = await userService.followUserById(req.userToken!.id, req.params.id);
94-
return res.status(200).send(followedUser);
95+
await userService.followUserById(req.userToken!.id, req.params.id);
96+
return res.status(200).end();
9597
} catch (error) {
9698
const errorMessage = logger.getErrorMessage(error);
9799
logger.error(errorMessage);
@@ -103,7 +105,20 @@ router.put('/:id/follow', authenticator(), async (req, res, next) => {
103105
return next(error);
104106
}
105107
});
108+
109+
router.put('/:id/unfollow', authenticator(), async (req, res, next) => {
110+
if (req.params.id === req.userToken!.id) {
111+
return res.status(400).send({ error: 'You can\'t unfollow yourself!' });
112+
}
113+
114+
try {
115+
await userService.unfollowUserById(req.userToken!.id, req.params.id);
116+
return res.status(200).end();
117+
} catch (error) {
118+
return next(error);
119+
}
120+
});
121+
106122
// TODO: write a delete route
107-
// TODO: write unfollow route?
108123

109124
export default router;

backend/src/services/user-service.ts

+42-8
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,29 @@ import { NewUser, UpdatedUserFields } from '../types';
55
import cloudinary from '../utils/cloudinary';
66

77
const getUsers = async () => {
8-
// TODO: decide which are and populate the fields that should be populated.
9-
// TODO: omit passwordHash from results.
108
const users = await User.find({})
11-
.populate('posts', { image: 1 });
9+
.select('-passwordHash')
10+
.populate({
11+
path: 'posts',
12+
select: 'image createdAt updatedAt creator caption',
13+
populate: {
14+
path: 'creator',
15+
select: 'username id image',
16+
},
17+
})
18+
.populate('followers', { username: 1 })
19+
.populate('following', { username: 1 });
1220

1321
return users;
1422
};
1523

1624
const getUserById = async (id: string) => {
1725
const user = await User.findById(id)
18-
.populate('posts', { image: 1 });
26+
.select('-passwordHash')
27+
.populate('posts', { image: 1 })
28+
.populate('followers', { username: 1 })
29+
.populate('following', { username: 1 });
30+
1931
return user;
2032
};
2133

@@ -57,18 +69,20 @@ const followUserById = async (followerId: string, followedUserId: string) => {
5769
const followedUser = await User.findById(followedUserId);
5870
const follower = await User.findById(followerId);
5971

60-
if (!followedUser) throw new Error('User not found.');
72+
if (!followedUser) {
73+
throw new Error('User not found.');
74+
}
6175

6276
// checking if user already follows the user they are requesting to follow
63-
if (follower.following.map((ids: ObjectId) => ids?.toString()).includes(followedUser.id)) throw new Error('You already follow that user!');
77+
if (follower.following.map((ids: ObjectId) => ids?.toString()).includes(followedUser.id)) {
78+
throw new Error('You already follow that user!');
79+
}
6480

6581
followedUser.followers = [...followedUser.followers, follower];
6682
follower.following = [...follower.following, followedUser.id];
6783

6884
await followedUser.save();
6985
await follower.save();
70-
71-
return followedUser;
7286
};
7387

7488
const deleteUserImage = async (id: string) => {
@@ -80,11 +94,31 @@ const deleteUserImage = async (id: string) => {
8094
return user;
8195
};
8296

97+
const unfollowUserById = async (followerId: string, followedUserId: string) => {
98+
const followedUser = await User.findById(followedUserId);
99+
const follower = await User.findById(followerId);
100+
101+
if (!followedUser) {
102+
throw new Error('User not found.');
103+
}
104+
105+
follower.following = follower.following.filter(
106+
(id: ObjectId) => id.toString() !== followedUser.id,
107+
);
108+
followedUser.followers = followedUser.followers.filter(
109+
(id: ObjectId) => id.toString() !== follower.id,
110+
);
111+
112+
await follower.save();
113+
await followedUser.save();
114+
};
115+
83116
export default {
84117
getUsers,
85118
getUserById,
86119
addUser,
87120
updateUserById,
88121
followUserById,
89122
deleteUserImage,
123+
unfollowUserById,
90124
};

backend/src/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export interface User {
3434
passwordHash: string,
3535
image?: Image,
3636
posts?: string[], // ref
37-
followers?: string[], // ref -> User
38-
following?: string[], // ref -> User
37+
followers?: { id: string, username: string }[], // ref -> User
38+
following?: { id: string, username: string }[], // ref -> User
3939
}
4040

4141
export interface NewUser {

backend/test/helpers/test-helpers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { User, Post } from '../../src/mongo';
33
import { NewUser } from '../../src/types';
44

55
const usersInDB = async () => {
6-
const users = await User.find({});
6+
const users = await User.find({}).select('-passwordHash');
77
return users.map((user) => user.toJSON());
88
};
99

backend/test/user-api.test.ts

+45-5
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('When there are multiple users in the database', () => {
5555
});
5656

5757
describe('When getting a single user by id', () => {
58-
test('Correct user is retured', async () => {
58+
test('Correct user is returned', async () => {
5959
const targetUser = (await testHelpers.usersInDB())[0];
6060
const response = await api
6161
.get(`/api/users/${targetUser.id}`)
@@ -277,15 +277,17 @@ describe('When there are multiple users in the database', () => {
277277
const users = (await testHelpers.usersInDB());
278278
const differentUser = users.find((user) => user.id !== targetUser.id);
279279

280-
const response = await api
280+
await api
281281
.put(`/api/users/${differentUser.id}/follow`)
282282
.set('Authorization', `bearer ${token}`)
283283
.expect(200);
284284

285-
const returnedUser = response.body;
285+
const followedUser = (await testHelpers.usersInDB()).find(
286+
(user) => user.id === differentUser.id,
287+
);
286288

287-
expect(returnedUser.followers).toHaveLength(1);
288-
expect(returnedUser.followers[0].id).toBe(targetUser.id);
289+
expect(followedUser.followers).toHaveLength(1);
290+
expect(followedUser.followers[0].toString()).toBe(targetUser.id.toString());
289291
});
290292

291293
test('following a user adds that user to the requesting user following list', async () => {
@@ -303,6 +305,44 @@ describe('When there are multiple users in the database', () => {
303305
expect(updatedTargetUser.following).toHaveLength(1);
304306
expect(updatedTargetUser.following[0].toString()).toBe(differentUser.id.toString());
305307
});
308+
309+
test('user can be unfollowed', async () => {
310+
const users = (await testHelpers.usersInDB());
311+
const differentUser = users.find((user) => user.id !== targetUser.id);
312+
const differentUserId = differentUser.id;
313+
314+
await api
315+
.put(`/api/users/${differentUserId}/follow`)
316+
.set('Authorization', `bearer ${token}`)
317+
.expect(200);
318+
319+
let updatedTargetUser = (await testHelpers.usersInDB()).find(
320+
(user) => user.id === targetUser.id,
321+
);
322+
let updatedDifferentUser = (await testHelpers.usersInDB()).find(
323+
(user) => user.id === differentUserId,
324+
);
325+
326+
expect(updatedTargetUser.following).toHaveLength(1);
327+
expect(updatedTargetUser.following[0].toString()).toBe(differentUserId.toString());
328+
expect(updatedDifferentUser.followers).toHaveLength(1);
329+
expect(updatedDifferentUser.followers[0].toString()).toBe(targetUser.id.toString());
330+
331+
await api
332+
.put(`/api/users/${differentUserId}/unfollow`)
333+
.set('Authorization', `bearer ${token}`)
334+
.expect(200);
335+
336+
updatedTargetUser = (await testHelpers.usersInDB()).find(
337+
(user) => user.id === targetUser.id,
338+
);
339+
updatedDifferentUser = (await testHelpers.usersInDB()).find(
340+
(user) => user.id === differentUserId,
341+
);
342+
343+
expect(updatedTargetUser.following).toHaveLength(0);
344+
expect(updatedDifferentUser.followers).toHaveLength(0);
345+
});
306346
});
307347

308348
// TODO: Write tests to check if comment fields are populated when fetching user/users.

frontend/cypress.json

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
"username": "admin",
99
"password": "secret",
1010
"email": "[email protected]"
11+
},
12+
"user2": {
13+
"fullName": "Superuser2",
14+
"username": "admin2",
15+
"password": "secret",
16+
"email": "[email protected]"
1117
}
1218
}
1319
}

frontend/cypress/integration/deleting-posts.spec.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,9 @@ it('user can delete their post in the post view', () => {
2525

2626
cy.contains(/post deleted/i).should('be.visible');
2727

28-
cy.reload();
29-
30-
cy.contains(/post/i).should('not.exist');
31-
3228
cy.contains(/posts/i)
3329
.prev()
3430
.should('contain.text', '0');
31+
32+
cy.get('[data-cy="user-profile-image-grid"] img').should('not.exist');
3533
});

0 commit comments

Comments
 (0)