Skip to content

Commit

Permalink
fix comment delete on user delete
Browse files Browse the repository at this point in the history
  • Loading branch information
sspenst committed May 16, 2024
1 parent 7e3edc7 commit 6129ede
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 79 deletions.
2 changes: 1 addition & 1 deletion components/level/reviews/commentThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function CommentThread({ className, comment, mutateComments, onSe
</div>
{(comment.author._id.toString() === user?._id.toString() || (user?._id === comment.target)) && (
<button
className='text-xs text-white font-bold p-1 rounded-lg text-sm disabled:opacity-25 '
className='text-white font-bold p-1 rounded-lg text-sm disabled:opacity-25 '
disabled={isUpdating}
onClick={onDeleteComment}
>
Expand Down
87 changes: 49 additions & 38 deletions pages/api/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default withAuth({
} = req.body;

if (password) {
const user = await UserModel.findById(req.userId, '+password');
const user = await UserModel.findById(req.user._id, '+password');

if (!(await bcrypt.compare(currentPassword, user.password))) {
return res.status(401).json({
Expand Down Expand Up @@ -137,7 +137,7 @@ export default withAuth({
}

try {
const newUser = await UserModel.findOneAndUpdate({ _id: req.userId }, { $set: setObj }, { runValidators: true, new: true, projection: { _id: 1, email: 1, name: 1, emailConfirmationToken: 1 } });
const newUser = await UserModel.findOneAndUpdate({ _id: req.user._id }, { $set: setObj }, { runValidators: true, new: true, projection: { _id: 1, email: 1, name: 1, emailConfirmationToken: 1 } });

if (setObj['email']) {
newUser.emailConfirmationToken = getEmailConfirmationToken();
Expand All @@ -162,7 +162,7 @@ export default withAuth({
try {
await session.withTransaction(async () => {
const levels = await LevelModel.find({
userId: req.userId,
userId: req.user._id,
isDeleted: { $ne: true },
gameId: req.gameId,
}, '_id name', { session: session }).lean<Level[]>();
Expand All @@ -175,7 +175,7 @@ export default withAuth({

// Do the same for collections
const collections = await CollectionModel.find({
userId: req.userId,
userId: req.user._id,
gameId: req.gameId,
}, '_id name', { session: session }).lean<Collection[]>();

Expand Down Expand Up @@ -215,7 +215,7 @@ export default withAuth({
try {
await session.withTransaction(async () => {
const levels = await LevelModel.find<Level>({
userId: req.userId,
userId: req.user._id,
isDeleted: { $ne: true },
isDraft: false,
gameId: req.gameId,
Expand All @@ -227,39 +227,22 @@ export default withAuth({
// TODO: promise.all this?
await LevelModel.updateOne({ _id: level._id }, { $set: {
userId: new Types.ObjectId(TestId.ARCHIVE),
archivedBy: req.userId,
archivedBy: req.user._id,
archivedTs: ts,
slug: slug,
} }, { session: session });
}

await Promise.all([
AchievementModel.deleteMany({ userId: req.userId }, { session: session }),
CollectionModel.deleteMany({ userId: req.userId }, { session: session }),
DeviceModel.deleteMany({ userId: req.userId }, { session: session }),
GraphModel.deleteMany({ $or: [{ source: req.userId }, { target: req.userId }] }, { session: session }),
// delete in keyvaluemodel where key contains userId
KeyValueModel.deleteMany({ key: { $regex: `.*${req.userId}.*` } }, { session: session }),
NotificationModel.deleteMany({ $or: [
{ source: req.userId },
{ target: req.userId },
{ userId: req.userId },
] }, { session: session }),
UserConfigModel.deleteMany({ userId: req.userId }, { session: session }),
UserModel.deleteOne({ _id: req.userId }, { session: session }), // TODO, should make this soft delete...
]);

// delete all comments posted on this user's profile, and all their replies
await CommentModel.aggregate([
const commentAgg = await CommentModel.aggregate([
{
$match: { $or: [
{ author: req.userId, deletedAt: null },
{ target: req.userId, deletedAt: null },
] },
},
{
$set: {
deletedAt: deletedAt,
$match: {
deletedAt: null,
targetModel: 'User',
$or: [
{ author: req.user._id },
{ target: req.user._id },
],
},
},
{
Expand All @@ -270,19 +253,47 @@ export default withAuth({
as: 'children',
pipeline: [
{
$match: {
deletedAt: null,
},
},
{
$set: {
deletedAt: deletedAt,
$project: {
_id: 1,
},
},
],
},
},
{
$project: {
_id: 1,
children: 1,
},
},
], { session: session });

const commentIdsToDelete = [];

for (const comment of commentAgg) {
commentIdsToDelete.push(comment._id);

for (const child of comment.children) {
commentIdsToDelete.push(child._id);
}
}

await Promise.all([
AchievementModel.deleteMany({ userId: req.user._id }, { session: session }),
CollectionModel.deleteMany({ userId: req.user._id }, { session: session }),
CommentModel.updateMany({ _id: { $in: commentIdsToDelete } }, { $set: { deletedAt: deletedAt } }, { session: session }),
DeviceModel.deleteMany({ userId: req.user._id }, { session: session }),
GraphModel.deleteMany({ $or: [{ source: req.user._id }, { target: req.user._id }] }, { session: session }),
// delete in keyvaluemodel where key contains userId
KeyValueModel.deleteMany({ key: { $regex: `.*${req.user._id}.*` } }, { session: session }),
NotificationModel.deleteMany({ $or: [
{ source: req.user._id },
{ target: req.user._id },
{ userId: req.user._id },
] }, { session: session }),
UserConfigModel.deleteMany({ userId: req.user._id }, { session: session }),
UserModel.deleteOne({ _id: req.user._id }, { session: session }), // TODO, should make this soft delete...
]);
});
session.endSession();
} catch (err) {
Expand Down
80 changes: 46 additions & 34 deletions server/scripts/delete-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,43 +153,55 @@ async function deleteUser(userName: string) {

await deleteReviews(user);

await Promise.all([
AchievementModel.deleteMany({ userId: user._id }),
CollectionModel.deleteMany({ userId: user._id }),
// delete all comments posted on this user's profile, and all their replies
CommentModel.aggregate([
{
$match: { $or: [
{ author: user._id, deletedAt: null },
{ target: user._id, deletedAt: null },
] },
},
{
$set: {
deletedAt: deletedAt,
},
// delete all comments posted on this user's profile, and all their replies
const commentAgg = await CommentModel.aggregate([
{
$match: {
deletedAt: null,
targetModel: 'User',
$or: [
{ author: user._id },
{ target: user._id },
],
},
{
$lookup: {
from: CommentModel.collection.name,
localField: '_id',
foreignField: 'target',
as: 'children',
pipeline: [
{
$match: {
deletedAt: null,
},
},
{
$set: {
deletedAt: deletedAt,
},
},
{
$lookup: {
from: CommentModel.collection.name,
localField: '_id',
foreignField: 'target',
as: 'children',
pipeline: [
{
$project: {
_id: 1,
},
],
},
},
],
},
},
{
$project: {
_id: 1,
children: 1,
},
]),
},
]);

const commentIdsToDelete = [];

for (const comment of commentAgg) {
commentIdsToDelete.push(comment._id);

for (const child of comment.children) {
commentIdsToDelete.push(child._id);
}
}

await Promise.all([
AchievementModel.deleteMany({ userId: user._id }),
CollectionModel.deleteMany({ userId: user._id }),
CommentModel.updateMany({ _id: { $in: commentIdsToDelete } }, { $set: { deletedAt: deletedAt } }),
GraphModel.deleteMany({ $or: [{ source: user._id }, { target: user._id }] }),
// delete in keyvaluemodel where key contains userId
KeyValueModel.deleteMany({ key: { $regex: `.*${user._id}.*` } }),
Expand Down
79 changes: 73 additions & 6 deletions tests/pages/api/user/user-delete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import TestId from '@root/constants/testId';
import dbConnect, { dbDisconnect } from '@root/lib/dbConnect';
import { getTokenCookieValue } from '@root/lib/getTokenCookie';
import { NextApiRequestWithAuth } from '@root/lib/withAuth';
import { UserModel } from '@root/models/mongoose';
import { CommentModel, UserModel } from '@root/models/mongoose';
import { cancelSubscription, stripe } from '@root/pages/api/subscription';
import { enableFetchMocks } from 'jest-fetch-mock';
import mongoose from 'mongoose';
import mongoose, { Types } from 'mongoose';
import { testApiHandler } from 'next-test-api-route-handler';
import modifyUserHandler from '../../../../pages/api/user/index';
import mockSubscription from '../subscription/mockSubscription';
Expand All @@ -31,10 +31,10 @@ jest.mock('stripe', () => {
};
});
});
beforeAll(async () => {
beforeEach(async () => {
await dbConnect();
});
afterAll(async() => {
afterEach(async() => {
await dbDisconnect();
});
enableFetchMocks();
Expand Down Expand Up @@ -146,8 +146,7 @@ describe('pages/api/collection/index.ts', () => {
expect(res.status).toBe(200);
},
});
});
test('Even though we have a valid token, user should not be found if they have been deleted', async () => {

await testApiHandler({
pagesHandler: async (_, res) => {
const req: NextApiRequestWithAuth = {
Expand All @@ -173,4 +172,72 @@ describe('pages/api/collection/index.ts', () => {
},
});
});
test('Deleting comments and replies', async () => {
const comment1 = await CommentModel.create({
author: new Types.ObjectId(TestId.USER),
text: '1',
target: new Types.ObjectId(TestId.USER_B),
targetModel: 'User',
});

await CommentModel.create({
author: new Types.ObjectId(TestId.USER_B),
text: '1reply',
target: comment1._id,
targetModel: 'Comment',
});

await CommentModel.create({
author: new Types.ObjectId(TestId.USER),
text: '1replyb',
target: comment1._id,
targetModel: 'Comment',
});

const comment2 = await CommentModel.create({
author: new Types.ObjectId(TestId.USER_B),
text: '2',
target: new Types.ObjectId(TestId.USER),
targetModel: 'User',
});

await CommentModel.create({
author: new Types.ObjectId(TestId.USER),
text: '2reply',
target: comment2._id,
targetModel: 'Comment',
});

const commentCount = await CommentModel.countDocuments({ deletedAt: null });

expect(commentCount).toBe(5);

await testApiHandler({
pagesHandler: async (_, res) => {
const req: NextApiRequestWithAuth = {
method: 'DELETE',
cookies: {
token: cookie,
},
headers: {
'content-type': 'application/json',
},
} as unknown as NextApiRequestWithAuth;

await modifyUserHandler(req, res);
},
test: async ({ fetch }) => {
const res = await fetch();
const response = await res.json();

expect(response.error).toBeUndefined();
expect(response.updated).toBe(true);
expect(res.status).toBe(200);

const commentCount = await CommentModel.countDocuments({ deletedAt: null });

expect(commentCount).toBe(0);
},
});
});
});

0 comments on commit 6129ede

Please sign in to comment.