diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 9fa93d4b18..6948d1bce2 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,7 +4,7 @@ # NOTE! # # Please read the README.md file in this directory that defines what should -# be placed in this file +# be placed in this file. # ############################################################################## ############################################################################## diff --git a/README.md b/README.md index 17c17f4626..327fa7941a 100644 --- a/README.md +++ b/README.md @@ -53,4 +53,4 @@ Core features include: ## Image Upload -To enable image upload functionalities create an images folder in the root of the project +To enable image upload functionalities create an images folder in the root of the project \ No newline at end of file diff --git a/codegen.ts b/codegen.ts index 47f6985cad..3f49de49f0 100644 --- a/codegen.ts +++ b/codegen.ts @@ -47,6 +47,8 @@ const config: CodegenConfig = { EventAttendee: "../models/EventAttendee#InterfaceEventAttendee", + UserFamily: "../models/userFamily#InterfaceUserFamily", + Feedback: "../models/Feedback#InterfaceFeedback", // File: '../models/File#InterfaceFile', diff --git a/sample_data/userFamilies.json b/sample_data/userFamilies.json new file mode 100644 index 0000000000..b936f13366 --- /dev/null +++ b/sample_data/userFamilies.json @@ -0,0 +1,20 @@ +[ + { + "_id": "60f18f31b7e5c4a2a4c3f905", + "title": "Smith Family", + "users": [ + "64378abd85008f171cf2990d", + "65378abd85008f171cf2990d", + "66378abd85008f171cf2990d" + ] + }, + { + "_id": "60f18f31b7e5c4a2a4c3f906", + "title": "Johnson Family", + "users": [ + "66378abd85008f171cf2990d", + "65378abd85008f171cf2990d", + "64378abd85008f171cf2990d" + ] + } +] diff --git a/schema.graphql b/schema.graphql index 8ea527723c..19d7ad4b4d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -521,6 +521,7 @@ type Mutation { addUserCustomData(dataName: String!, dataValue: Any!, organizationId: ID!): UserCustomData! addUserImage(file: String!): User! addUserToGroupChat(chatId: ID!, userId: ID!): GroupChat! + addUserToUserFamily(familyId: ID!, userId: ID!): UserFamily! adminRemoveEvent(eventId: ID!): Event! adminRemoveGroup(groupId: ID!): GroupChat! assignUserTag(input: ToggleUserTagAssignInput!): User @@ -543,6 +544,7 @@ type Mutation { createPlugin(pluginCreatedBy: String!, pluginDesc: String!, pluginName: String!, uninstalledOrgs: [ID!]): Plugin! createPost(data: PostInput!, file: String): Post createSampleOrganization: Boolean! + createUserFamily(data: createUserFamilyInput!): UserFamily! createUserTag(input: CreateUserTagInput!): UserTag deleteAdvertisementById(id: ID!): DeletePayload! deleteDonationById(id: ID!): DeletePayload! @@ -574,7 +576,9 @@ type Mutation { removePost(id: ID!): Post removeSampleOrganization: Boolean! removeUserCustomData(organizationId: ID!): UserCustomData! + removeUserFamily(familyId: ID!): UserFamily! removeUserFromGroupChat(chatId: ID!, userId: ID!): GroupChat! + removeUserFromUserFamily(familyId: ID!, userId: ID!): UserFamily! removeUserImage: User! removeUserTag(id: ID!): UserTag revokeRefreshTokenForUser: Boolean! @@ -1073,6 +1077,14 @@ type UserEdge { node: User! } +type UserFamily { + _id: ID! + admins: [User!]! + creator: User! + title: String + users: [User!]! +} + input UserInput { appLanguageCode: String email: EmailAddress! @@ -1204,4 +1216,9 @@ input createGroupChatInput { organizationId: ID! title: String! userIds: [ID!]! +} + +input createUserFamilyInput { + title: String! + userIds: [ID!]! } \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index e5945d7454..b454f7e676 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -177,12 +177,24 @@ export const LENGTH_VALIDATION_ERROR = { PARAM: "stringValidation", }; +export const USER_FAMILY_MIN_MEMBERS_ERROR_CODE = { + MESSAGE: "InputValidationError", + CODE: "membersInUserFamilyLessThanOne", + PARAM: "membersInUserFamilyLessThanOne", +}; + export const REGEX_VALIDATION_ERROR = { MESSAGE: "Error: Entered value must be a valid string", CODE: "string.notValid", PARAM: "stringValidation", }; +export const USER_FAMILY_NOT_FOUND_ERROR = { + MESSAGE: "Error: User Family Not Found", + CODE: "userfamilyNotFound", + PARAM: "userfamilyNotFound", +}; + export const USER_NOT_AUTHORIZED_SUPERADMIN = { MESSAGE: "Error: Current user must be a SUPERADMIN", CODE: "role.notValid.superadmin", diff --git a/src/models/User.ts b/src/models/User.ts index 8f8eb098be..befe7ee2f3 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -6,7 +6,6 @@ import type { InterfaceEvent } from "./Event"; import type { InterfaceMembershipRequest } from "./MembershipRequest"; import type { InterfaceOrganization } from "./Organization"; import { createLoggingMiddleware } from "../libraries/dbLogger"; -import { LOG } from "../constants"; /** * This is an interface that represents a database(MongoDB) document for User. diff --git a/src/models/userFamily.ts b/src/models/userFamily.ts new file mode 100644 index 0000000000..a9d8577ea4 --- /dev/null +++ b/src/models/userFamily.ts @@ -0,0 +1,56 @@ +import type { PopulatedDoc, Types, Document, Model } from "mongoose"; +import { Schema, model, models } from "mongoose"; +import type { InterfaceUser } from "./User"; +/** + * This is an interface that represents a database(MongoDB) document for Family. + */ + +export interface InterfaceUserFamily { + _id: Types.ObjectId; + title: string; + users: PopulatedDoc[]; + admins: PopulatedDoc[]; + creator: PopulatedDoc[]; +} + +/** + * @param title - Name of the user Family (type: String) + * Description: Name of the user Family. + */ + +/** + * @param users - Members associated with the user Family (type: String) + * Description: Members associated with the user Family. + */ +const userFamilySchema = new Schema({ + title: { + type: String, + required: true, + }, + users: [ + { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + ], + admins: [ + { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + ], + creator: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, +}); + +const userFamilyModel = (): Model => + model("UserFamily", userFamilySchema); + +// This syntax is needed to prevent Mongoose OverwriteModelError while running tests. +export const UserFamily = (models.UserFamily || + userFamilyModel()) as ReturnType; diff --git a/src/resolvers/Mutation/addUserToUserFamily.ts b/src/resolvers/Mutation/addUserToUserFamily.ts new file mode 100644 index 0000000000..1c1cd590e3 --- /dev/null +++ b/src/resolvers/Mutation/addUserToUserFamily.ts @@ -0,0 +1,82 @@ +import "dotenv/config"; +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { errors, requestContext } from "../../libraries"; +import { adminCheck } from "../../utilities/userFamilyAdminCheck"; +import { User } from "../../models"; +import { UserFamily } from "../../models/userFamily"; +import { + USER_FAMILY_NOT_FOUND_ERROR, + USER_ALREADY_MEMBER_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../constants"; +/** + * This function adds user to the family. + * @param _parent - parent of current request + * @param args - payload provided with the request + * @param context - context of the entire application + * @remarks The following checks are done: + * 1. If the family exists + * 2. If the user exists + * 3. If the user is already member of the family + * 4. If the user is admin of the user Family + * @returns Updated family + */ +export const addUserToUserFamily: MutationResolvers["addUserToUserFamily"] = + async (_parent, args, context) => { + const userFamily = await UserFamily.findOne({ + _id: args.familyId, + }).lean(); + + const currentUser = await User.findById({ + _id: context.userId, + }); + + // Checks whether user with _id === args.userId exists. + if (currentUser === null) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + //check wheather family exists + if (!userFamily) { + throw new errors.NotFoundError( + requestContext.translate(USER_FAMILY_NOT_FOUND_ERROR.MESSAGE), + USER_FAMILY_NOT_FOUND_ERROR.CODE, + USER_FAMILY_NOT_FOUND_ERROR.PARAM + ); + } + + //check whether user is admin of the family + await adminCheck(currentUser?._id, userFamily); + + const isUserMemberOfUserFamily = userFamily.users.some((user) => { + user.equals(args.userId); + }); + + // Checks whether user with _id === args.userId is already a member of Family. + if (isUserMemberOfUserFamily) { + throw new errors.ConflictError( + requestContext.translate(USER_ALREADY_MEMBER_ERROR.MESSAGE), + USER_ALREADY_MEMBER_ERROR.CODE, + USER_ALREADY_MEMBER_ERROR.PARAM + ); + } + + // Adds args.userId to users lists on family group and return the updated family. + return await UserFamily.findOneAndUpdate( + { + _id: args.familyId, + }, + { + $push: { + users: args.userId, + }, + }, + { + new: true, + } + ).lean(); + }; diff --git a/src/resolvers/Mutation/createUserFamily.ts b/src/resolvers/Mutation/createUserFamily.ts new file mode 100644 index 0000000000..9a7f911625 --- /dev/null +++ b/src/resolvers/Mutation/createUserFamily.ts @@ -0,0 +1,81 @@ +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { User } from "../../models"; +import { errors, requestContext } from "../../libraries"; +import { + LENGTH_VALIDATION_ERROR, + USER_FAMILY_MIN_MEMBERS_ERROR_CODE, + USER_NOT_FOUND_ERROR, +} from "../../constants"; +import { isValidString } from "../../libraries/validators/validateString"; +import { UserFamily } from "../../models/userFamily"; +import { superAdminCheck } from "../../utilities"; +/** + * This Function enables to create a user Family + * @param _parent - parent of current request + * @param args - payload provided with the request + * @param context - context of entire application + * @remarks - The following checks are done: + * 1. If the user exists + * 2. If the user is super admin + * 3. If there are atleast two members in the family. + * @returns Created user Family + */ +export const createUserFamily: MutationResolvers["createUserFamily"] = async ( + _parent, + args, + context +) => { + const currentUser = await User.findById({ + _id: context.userId, + }); + + // Checks whether user with _id === args.userId exists. + if (!currentUser) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + // Check whether the user is super admin. + superAdminCheck(currentUser); + + let validationResultName = { + isLessThanMaxLength: false, + }; + + if (args && args.data && typeof args.data.title === "string") { + validationResultName = isValidString(args.data.title, 256); + } + + if (!validationResultName.isLessThanMaxLength) { + throw new errors.InputValidationError( + requestContext.translate( + `${LENGTH_VALIDATION_ERROR.MESSAGE} 256 characters in name` + ), + LENGTH_VALIDATION_ERROR.CODE + ); + } + + // Check if there are at least 2 members + if (args.data?.userIds.length < 2) { + throw new errors.InputValidationError( + requestContext.translate(USER_FAMILY_MIN_MEMBERS_ERROR_CODE.MESSAGE), + USER_FAMILY_MIN_MEMBERS_ERROR_CODE.CODE, + USER_FAMILY_MIN_MEMBERS_ERROR_CODE.PARAM + ); + } + + const userfamilyTitle = args.data?.title; + + const createdUserFamily = await UserFamily.create({ + ...args.data, + title: userfamilyTitle, + users: [context.userId, ...args.data.userIds], + admins: [context.userId], + creator: context.userId, + }); + + return createdUserFamily.toObject(); +}; diff --git a/src/resolvers/Mutation/index.ts b/src/resolvers/Mutation/index.ts index 68e6c4c916..2d3332bcf8 100644 --- a/src/resolvers/Mutation/index.ts +++ b/src/resolvers/Mutation/index.ts @@ -53,6 +53,10 @@ import { removeComment } from "./removeComment"; import { removeDirectChat } from "./removeDirectChat"; import { removeEvent } from "./removeEvent"; import { removeEventAttendee } from "./removeEventAttendee"; +import { addUserToUserFamily } from "./addUserToUserFamily"; +import { removeUserFromUserFamily } from "./removeUserFromUserFamily"; +import { removeUserFamily } from "./removeUserFamily"; +import { createUserFamily } from "./createUserFamily"; import { removeGroupChat } from "./removeGroupChat"; import { removeAdvertisement } from "./removeAdvertisement"; import { removeMember } from "./removeMember"; @@ -104,6 +108,10 @@ export const Mutation: MutationResolvers = { addUserToGroupChat, adminRemoveEvent, adminRemoveGroup, + addUserToUserFamily, + removeUserFamily, + removeUserFromUserFamily, + createUserFamily, assignUserTag, blockPluginCreationBySuperadmin, blockUser, diff --git a/src/resolvers/Mutation/removeUserFamily.ts b/src/resolvers/Mutation/removeUserFamily.ts new file mode 100644 index 0000000000..38181ad1a9 --- /dev/null +++ b/src/resolvers/Mutation/removeUserFamily.ts @@ -0,0 +1,60 @@ +import { + USER_FAMILY_NOT_FOUND_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../constants"; +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { errors, requestContext } from "../../libraries"; +import { UserFamily } from "../../models/userFamily"; +import { User } from "../../models"; +import { superAdminCheck } from "../../utilities"; +/** + * This function enables to remove a user family. + * @param _parent - parent of current request + * @param args - payload provided with the request + * @param context - context of entire application. + * @remarks - The following checks are done: + * 1. If the user family exists. + * 2. If the user is super admin. + * @returns Deleted user family. + */ +export const removeUserFamily: MutationResolvers["removeUserFamily"] = async ( + _parent, + args, + context +) => { + const userFamily = await UserFamily.findOne({ + _id: args.familyId, + }).lean(); + + const currentUser = await User.findOne({ + _id: context.userId, + }); + + // Checks whether currentUser exists. + if (!currentUser) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + // Checks whether currentUser is a SUPERADMIN + superAdminCheck(currentUser); + + // Checks if a family with _id === args.familyId exists + if (!userFamily) { + throw new errors.NotFoundError( + requestContext.translate(USER_FAMILY_NOT_FOUND_ERROR.MESSAGE), + USER_FAMILY_NOT_FOUND_ERROR.CODE, + USER_FAMILY_NOT_FOUND_ERROR.PARAM + ); + } + + // Deletes the UserFamily. + await UserFamily.deleteOne({ + _id: userFamily._id, + }); + + return userFamily; +}; diff --git a/src/resolvers/Mutation/removeUserFromUserFamily.ts b/src/resolvers/Mutation/removeUserFromUserFamily.ts new file mode 100644 index 0000000000..17d411f672 --- /dev/null +++ b/src/resolvers/Mutation/removeUserFromUserFamily.ts @@ -0,0 +1,129 @@ +import { + ADMIN_REMOVING_ADMIN, + ADMIN_REMOVING_CREATOR, + USER_FAMILY_NOT_FOUND_ERROR, + USER_NOT_FOUND_ERROR, + USER_REMOVING_SELF, +} from "../../constants"; +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { errors, requestContext } from "../../libraries"; +import { User } from "../../models"; +import { UserFamily } from "../../models/userFamily"; +import { adminCheck } from "../../utilities/userFamilyAdminCheck"; +import { Types } from "mongoose"; +/** + * This function enables to remove a user from group chat. + * @param _parent - parent of current request + * @param args - payload provided with the request + * @param context - context of entire publication + * @remarks The following checks are done: + * 1. If the family exists. + * 2. If the user to be removed is member of the organisation. + * 3. If the user is admin of the family + * @returns Updated group chat. + */ +export const removeUserFromUserFamily: MutationResolvers["removeUserFromUserFamily"] = + async (_parent, args, context) => { + const userFamily = await UserFamily.findById({ + _id: args.familyId, + }).lean(); + + const currentUser = await User.findById({ + _id: context.userId, + }); + + const user = await User.findById({ + _id: args.userId, + }); + + // Check whether the user exists. + if (!user) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + const userIsMemberOfUserFamily = userFamily?.users.some((member) => { + return Types.ObjectId(member).equals(user?._id); + }); + + const userIdUserFamilyAdmin = userFamily?.admins.some((admin) => { + Types.ObjectId(admin).equals(user?._id); + }); + + //Check whether user family exists. + if (!userFamily) { + throw new errors.NotFoundError( + requestContext.translate(USER_FAMILY_NOT_FOUND_ERROR.MESSAGE), + USER_FAMILY_NOT_FOUND_ERROR.CODE, + USER_FAMILY_NOT_FOUND_ERROR.PARAM + ); + } + + //check whether user is admin of the family. + await adminCheck(currentUser?._id, userFamily); + + //Check whether user is member of the family. + if (!userIsMemberOfUserFamily) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + // Check if the current user is removing self + if (user._id.equals(currentUser?._id)) { + throw new errors.ConflictError( + requestContext.translate(USER_REMOVING_SELF.MESSAGE), + USER_REMOVING_SELF.CODE, + USER_REMOVING_SELF.PARAM + ); + } + + /* + userIsUserFamilyAdmin being true implies that the current user is an admin of userFamily. + If userIsUserFamilyAdmin is true pushes error message to errors list and breaks out of loop. + */ + if (userIdUserFamilyAdmin) { + throw new errors.ConflictError( + requestContext.translate(ADMIN_REMOVING_ADMIN.MESSAGE), + ADMIN_REMOVING_ADMIN.CODE, + ADMIN_REMOVING_ADMIN.PARAM + ); + } + + /* + Administrators cannot remove creator of userFamily from the members list. + Following if block matches userFamily's creator's id to + user's id. Match being true implies that current user is the creator + of userFamily. If match is true assigns error message to errors list + and breaks out of loop. + */ + if (Types.ObjectId(userFamily.creator.toString()).equals(user._id)) { + throw new errors.UnauthorizedError( + requestContext.translate(ADMIN_REMOVING_CREATOR.MESSAGE), + ADMIN_REMOVING_CREATOR.CODE, + ADMIN_REMOVING_CREATOR.PARAM + ); + } + + //Removes args.userId from users list of user family ans return the updated family. + return await UserFamily.findOneAndUpdate( + { + _id: args.familyId, + }, + { + $set: { + users: userFamily.users.filter( + (user) => user.toString() !== args.userId.toString() + ), + }, + }, + { + new: true, + } + ).lean(); + }; diff --git a/src/resolvers/UserFamily/admins.ts b/src/resolvers/UserFamily/admins.ts new file mode 100644 index 0000000000..9571bd0c02 --- /dev/null +++ b/src/resolvers/UserFamily/admins.ts @@ -0,0 +1,14 @@ +import { User } from "../../models"; +import type { UserFamilyResolvers } from "../../types/generatedGraphQLTypes"; +/** + * This resolver function will fetch and return the admins of the Organization from database. + * @param parent - An object that is the return value of the resolver for this field's parent. + * @returns An object that contains the list of all admins of the organization. + */ +export const admins: UserFamilyResolvers["admins"] = async (parent) => { + return await User.find({ + _id: { + $in: parent.admins, + }, + }).lean(); +}; diff --git a/src/resolvers/UserFamily/creator.ts b/src/resolvers/UserFamily/creator.ts new file mode 100644 index 0000000000..9e10abf8f4 --- /dev/null +++ b/src/resolvers/UserFamily/creator.ts @@ -0,0 +1,24 @@ +import { User } from "../../models"; +import { errors, requestContext } from "../../libraries"; +import type { UserFamilyResolvers } from "../../types/generatedGraphQLTypes"; +import { USER_NOT_FOUND_ERROR } from "../../constants"; +/** + * This resolver function will fetch and return the creator of the Organization from database. + * @param parent - An object that is the return value of the resolver for this field's parent. + * @returns An object that contains the creator data. If the creator not exists then throws an `NotFoundError` error. + */ +export const creator: UserFamilyResolvers["creator"] = async (parent) => { + const user = await User.findOne({ + _id: parent.creator.toString(), + }).lean(); + + if (!user) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + return user; +}; diff --git a/src/resolvers/UserFamily/index.ts b/src/resolvers/UserFamily/index.ts new file mode 100644 index 0000000000..a89939e37d --- /dev/null +++ b/src/resolvers/UserFamily/index.ts @@ -0,0 +1,10 @@ +import type { UserFamilyResolvers } from "../../types/generatedGraphQLTypes"; +import { users } from "./users"; +import { admins } from "./admins"; +import { creator } from "./creator"; + +export const UserFamily: UserFamilyResolvers = { + users, + admins, + creator, +}; diff --git a/src/resolvers/UserFamily/users.ts b/src/resolvers/UserFamily/users.ts new file mode 100644 index 0000000000..c02ba909f4 --- /dev/null +++ b/src/resolvers/UserFamily/users.ts @@ -0,0 +1,14 @@ +import type { UserFamilyResolvers } from "../../types/generatedGraphQLTypes"; +import { User } from "../../models"; +/** + * This resolver function will fetch and return the list of all Members of the user family from database. + * @param parent - An object that is the return value of the resolver for this field's parent. + * @returns An `object` that contains the Member data. + */ +export const users: UserFamilyResolvers["users"] = async (parent) => { + return await User.find({ + _id: { + $in: parent.users, + }, + }).lean(); +}; diff --git a/src/resolvers/index.ts b/src/resolvers/index.ts index 7ded81a68f..fbfe904d28 100644 --- a/src/resolvers/index.ts +++ b/src/resolvers/index.ts @@ -11,6 +11,7 @@ import { GroupChat } from "./GroupChat"; import { GroupChatMessage } from "./GroupChatMessage"; import { MembershipRequest } from "./MembershipRequest"; import { Mutation } from "./Mutation"; +import { UserFamily } from "./UserFamily"; import { Organization } from "./Organization"; import { Post } from "./Post"; import { Query } from "./Query"; @@ -42,6 +43,7 @@ const resolvers: Resolvers = { Event, Feedback, GroupChat, + UserFamily, GroupChatMessage, MembershipRequest, Mutation, diff --git a/src/typeDefs/inputs.ts b/src/typeDefs/inputs.ts index e4aa13d73e..d0bdd94af4 100644 --- a/src/typeDefs/inputs.ts +++ b/src/typeDefs/inputs.ts @@ -29,6 +29,11 @@ export const inputs = gql` title: String! } + input createUserFamilyInput { + title: String! + userIds: [ID!]! + } + input CreateUserTagInput { name: String! parentTagId: ID diff --git a/src/typeDefs/mutations.ts b/src/typeDefs/mutations.ts index 1843e92141..a2439dccef 100644 --- a/src/typeDefs/mutations.ts +++ b/src/typeDefs/mutations.ts @@ -34,6 +34,14 @@ export const mutations = gql` addUserToGroupChat(userId: ID!, chatId: ID!): GroupChat! @auth + addUserToUserFamily(userId: ID!, familyId: ID!): UserFamily! @auth + + removeUserFromUserFamily(userId: ID!, familyId: ID!): UserFamily! @auth + + removeUserFamily(familyId: ID!): UserFamily! @auth + + createUserFamily(data: createUserFamilyInput!): UserFamily! @auth + adminRemoveEvent(eventId: ID!): Event! @auth adminRemoveGroup(groupId: ID!): GroupChat! @auth diff --git a/src/typeDefs/types.ts b/src/typeDefs/types.ts index c58764ccbe..1923a9a37a 100644 --- a/src/typeDefs/types.ts +++ b/src/typeDefs/types.ts @@ -75,6 +75,14 @@ export const types = gql` updatedAt: DateTime! } + type UserFamily { + _id: ID! + title: String + users: [User!]! + admins: [User!]! + creator: User! + } + # A page info type adhering to Relay Specification for both cursor based pagination type ConnectionPageInfo { hasNextPage: Boolean! diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index fd68a701a1..161abec318 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -9,6 +9,7 @@ import type { InterfaceDirectChatMessage as InterfaceDirectChatMessageModel } fr import type { InterfaceDonation as InterfaceDonationModel } from '../models/Donation'; import type { InterfaceEvent as InterfaceEventModel } from '../models/Event'; import type { InterfaceEventAttendee as InterfaceEventAttendeeModel } from '../models/EventAttendee'; +import type { InterfaceUserFamily as InterfaceUserFamilyModel } from '../models/userFamily'; import type { InterfaceFeedback as InterfaceFeedbackModel } from '../models/Feedback'; import type { InterfaceGroup as InterfaceGroupModel } from '../models/Group'; import type { InterfaceGroupChat as InterfaceGroupChatModel } from '../models/GroupChat'; @@ -587,6 +588,7 @@ export type Mutation = { addUserCustomData: UserCustomData; addUserImage: User; addUserToGroupChat: GroupChat; + addUserToUserFamily: UserFamily; adminRemoveEvent: Event; adminRemoveGroup: GroupChat; assignUserTag?: Maybe; @@ -609,6 +611,7 @@ export type Mutation = { createPlugin: Plugin; createPost?: Maybe; createSampleOrganization: Scalars['Boolean']['output']; + createUserFamily: UserFamily; createUserTag?: Maybe; deleteAdvertisementById: DeletePayload; deleteDonationById: DeletePayload; @@ -640,7 +643,9 @@ export type Mutation = { removePost?: Maybe; removeSampleOrganization: Scalars['Boolean']['output']; removeUserCustomData: UserCustomData; + removeUserFamily: UserFamily; removeUserFromGroupChat: GroupChat; + removeUserFromUserFamily: UserFamily; removeUserImage: User; removeUserTag?: Maybe; revokeRefreshTokenForUser: Scalars['Boolean']['output']; @@ -727,6 +732,12 @@ export type MutationAddUserToGroupChatArgs = { }; +export type MutationAddUserToUserFamilyArgs = { + familyId: Scalars['ID']['input']; + userId: Scalars['ID']['input']; +}; + + export type MutationAdminRemoveEventArgs = { eventId: Scalars['ID']['input']; }; @@ -852,6 +863,11 @@ export type MutationCreatePostArgs = { }; +export type MutationCreateUserFamilyArgs = { + data: CreateUserFamilyInput; +}; + + export type MutationCreateUserTagArgs = { input: CreateUserTagInput; }; @@ -999,12 +1015,23 @@ export type MutationRemoveUserCustomDataArgs = { }; +export type MutationRemoveUserFamilyArgs = { + familyId: Scalars['ID']['input']; +}; + + export type MutationRemoveUserFromGroupChatArgs = { chatId: Scalars['ID']['input']; userId: Scalars['ID']['input']; }; +export type MutationRemoveUserFromUserFamilyArgs = { + familyId: Scalars['ID']['input']; + userId: Scalars['ID']['input']; +}; + + export type MutationRemoveUserTagArgs = { id: Scalars['ID']['input']; }; @@ -1829,6 +1856,15 @@ export type UserEdge = { node: User; }; +export type UserFamily = { + __typename?: 'UserFamily'; + _id: Scalars['ID']['output']; + admins: Array; + creator: User; + title?: Maybe; + users: Array; +}; + export type UserInput = { appLanguageCode?: InputMaybe; email: Scalars['EmailAddress']['input']; @@ -1977,6 +2013,11 @@ export type CreateGroupChatInput = { userIds: Array; }; +export type CreateUserFamilyInput = { + title: Scalars['String']['input']; + userIds: Array; +}; + export type ResolverTypeWrapper = Promise | T; @@ -2172,6 +2213,7 @@ export type ResolversTypes = { UserConnection: ResolverTypeWrapper & { edges: Array> }>; UserCustomData: ResolverTypeWrapper; UserEdge: ResolverTypeWrapper & { node: ResolversTypes['User'] }>; + UserFamily: ResolverTypeWrapper; UserInput: UserInput; UserOrderByInput: UserOrderByInput; UserPhone: ResolverTypeWrapper; @@ -2188,6 +2230,7 @@ export type ResolversTypes = { UsersConnectionResult: ResolverTypeWrapper & { data?: Maybe, errors: Array }>; createChatInput: CreateChatInput; createGroupChatInput: CreateGroupChatInput; + createUserFamilyInput: CreateUserFamilyInput; }; /** Mapping between all available schema types and the resolvers parents */ @@ -2299,6 +2342,7 @@ export type ResolversParentTypes = { UserConnection: Omit & { edges: Array> }; UserCustomData: UserCustomData; UserEdge: Omit & { node: ResolversParentTypes['User'] }; + UserFamily: InterfaceUserFamilyModel; UserInput: UserInput; UserPhone: UserPhone; UserPhoneInput: UserPhoneInput; @@ -2313,6 +2357,7 @@ export type ResolversParentTypes = { UsersConnectionResult: Omit & { data?: Maybe, errors: Array }; createChatInput: CreateChatInput; createGroupChatInput: CreateGroupChatInput; + createUserFamilyInput: CreateUserFamilyInput; }; export type AuthDirectiveArgs = { }; @@ -2691,6 +2736,7 @@ export type MutationResolvers>; addUserImage?: Resolver>; addUserToGroupChat?: Resolver>; + addUserToUserFamily?: Resolver>; adminRemoveEvent?: Resolver>; adminRemoveGroup?: Resolver>; assignUserTag?: Resolver, ParentType, ContextType, RequireFields>; @@ -2713,6 +2759,7 @@ export type MutationResolvers>; createPost?: Resolver, ParentType, ContextType, RequireFields>; createSampleOrganization?: Resolver; + createUserFamily?: Resolver>; createUserTag?: Resolver, ParentType, ContextType, RequireFields>; deleteAdvertisementById?: Resolver>; deleteDonationById?: Resolver>; @@ -2744,7 +2791,9 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; removeSampleOrganization?: Resolver; removeUserCustomData?: Resolver>; + removeUserFamily?: Resolver>; removeUserFromGroupChat?: Resolver>; + removeUserFromUserFamily?: Resolver>; removeUserImage?: Resolver; removeUserTag?: Resolver, ParentType, ContextType, RequireFields>; revokeRefreshTokenForUser?: Resolver; @@ -3018,6 +3067,15 @@ export type UserEdgeResolvers; }; +export type UserFamilyResolvers = { + _id?: Resolver; + admins?: Resolver, ParentType, ContextType>; + creator?: Resolver; + title?: Resolver, ParentType, ContextType>; + users?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type UserPhoneResolvers = { home?: Resolver, ParentType, ContextType>; mobile?: Resolver, ParentType, ContextType>; @@ -3133,6 +3191,7 @@ export type Resolvers = { UserConnection?: UserConnectionResolvers; UserCustomData?: UserCustomDataResolvers; UserEdge?: UserEdgeResolvers; + UserFamily?: UserFamilyResolvers; UserPhone?: UserPhoneResolvers; UserTag?: UserTagResolvers; UserTagEdge?: UserTagEdgeResolvers; diff --git a/src/utilities/userFamilyAdminCheck.ts b/src/utilities/userFamilyAdminCheck.ts new file mode 100644 index 0000000000..bf482733bf --- /dev/null +++ b/src/utilities/userFamilyAdminCheck.ts @@ -0,0 +1,36 @@ +import { Types } from "mongoose"; +import { errors, requestContext } from "../libraries"; +import { USER_NOT_AUTHORIZED_ADMIN } from "../constants"; +import type { InterfaceUserFamily } from "../models/userFamily"; +import { User } from "../models"; +/** + * If the current user is an admin of the organisation, this function returns `true` otherwise it returns `false`. + * @remarks + * This is a utility method. + * @param userId - Current user id. + * @param userFamily - userFamily data of `InterfaceuserFamily` type. + * @returns `True` or `False`. + */ +export const adminCheck = async ( + userId: string | Types.ObjectId, + userFamily: InterfaceUserFamily +): Promise => { + const userIsUserFamilyAdmin = userFamily.admins.some( + (admin) => admin === userId || Types.ObjectId(admin).equals(userId) + ); + + const user = await User.findOne({ + _id: userId, + }); + const isUserSuperAdmin: boolean = user + ? user.userType === "SUPERADMIN" + : false; + + if (!userIsUserFamilyAdmin && !isUserSuperAdmin) { + throw new errors.UnauthorizedError( + requestContext.translate(`${USER_NOT_AUTHORIZED_ADMIN.MESSAGE}`), + USER_NOT_AUTHORIZED_ADMIN.CODE, + USER_NOT_AUTHORIZED_ADMIN.PARAM + ); + } +}; diff --git a/tests/helpers/userAndUserFamily.ts b/tests/helpers/userAndUserFamily.ts new file mode 100644 index 0000000000..561f38cf9d --- /dev/null +++ b/tests/helpers/userAndUserFamily.ts @@ -0,0 +1,75 @@ +import { nanoid } from "nanoid"; +import type { InterfaceUserFamily } from "../../src/models/userFamily"; +import { User } from "../../src/models"; +import { UserFamily } from "../../src/models/userFamily"; +import type { InterfaceUser } from "../../src/models"; + +import type { Document } from "mongoose"; +/* eslint-disable */ +export type TestUserFamilyType = + | (InterfaceUserFamily & Document) + | null; + +export type TestUserType = + | (InterfaceUser & Document) + | null; +/* eslint-enable */ +export const createTestUserFunc = async (): Promise => { + const testUser = await User.create({ + email: `email${nanoid().toLowerCase()}@gmail.com`, + password: `pass${nanoid().toLowerCase()}`, + firstName: `firstName${nanoid().toLowerCase()}`, + lastName: `lastName${nanoid().toLowerCase()}`, + appLanguageCode: "en", + userType: "SUPERADMIN", + }); + + return testUser; +}; + +export const createTestUserFamilyWithAdmin = async ( + userID: string, + isMember = true, + isAdmin = true +): Promise => { + const testUser = await createTestUserFunc(); + if (testUser) { + const testUserFamily = await UserFamily.create({ + title: `name${nanoid().toLocaleLowerCase()}`, + users: isMember ? [testUser._id] : [], + admins: isAdmin ? [testUser._id] : [], + creator: [testUser._id], + }); + + await User.updateOne( + { + _id: userID, + }, + { + $push: { + createdUserFamily: testUserFamily._id, + joinedUserFamily: testUserFamily._id, + adminForUserFamily: testUserFamily._id, + }, + } + ); + + return testUserFamily; + } else { + return null; + } +}; + +export const createTestUserAndUserFamily = async ( + isMember = true, + isAdmin = true +): Promise<[TestUserType, TestUserFamilyType]> => { + const testUser = await createTestUserFunc(); + const testUserFamily = await createTestUserFamilyWithAdmin( + testUser?._id, + isMember, + isAdmin + ); + + return [testUser, testUserFamily]; +}; diff --git a/tests/resolvers/Mutation/addUserToUserFamily.spec.ts b/tests/resolvers/Mutation/addUserToUserFamily.spec.ts new file mode 100644 index 0000000000..663f979ac6 --- /dev/null +++ b/tests/resolvers/Mutation/addUserToUserFamily.spec.ts @@ -0,0 +1,154 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import { UserFamily } from "../../../src/models/userFamily"; +import type { MutationAddUserToUserFamilyArgs } from "../../../src/types/generatedGraphQLTypes"; +import { connect, disconnect } from "../../helpers/db"; + +import { + USER_ALREADY_MEMBER_ERROR, + USER_NOT_FOUND_ERROR, + USER_FAMILY_NOT_FOUND_ERROR, +} from "../../../src/constants"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import type { + TestUserType, + TestUserFamilyType, +} from "../../helpers/userAndUserFamily"; + +import { createTestUserAndUserFamily } from "../../helpers/userAndUserFamily"; + +let testUser: TestUserType; +let testUserFamily: TestUserFamilyType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const resultsArray = await createTestUserAndUserFamily(); + testUser = resultsArray[0]; + testUserFamily = resultsArray[1]; +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolver -> mutation -> addUserToUserFamily", () => { + afterAll(() => { + vi.doUnmock("../../../src/constants"); + vi.resetModules(); + }); + + it(`throws NotFoundError if no user Family exists with _id === args.familyId`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => message); + try { + const args: MutationAddUserToUserFamilyArgs = { + familyId: Types.ObjectId().toString(), + userId: testUser?.id, + }; + + const context = { + userId: testUser?.id, + }; + + const { addUserToUserFamily } = await import( + "../../../src/resolvers/Mutation/addUserToUserFamily" + ); + await addUserToUserFamily?.({}, args, context); + } catch (error) { + expect(spy).toBeCalledWith(USER_FAMILY_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `${USER_FAMILY_NOT_FOUND_ERROR.MESSAGE}` + ); + } + }); + + it(`throws NotFoundError if no user exists with _id === args.userId`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => message); + try { + const args: MutationAddUserToUserFamilyArgs = { + familyId: testUserFamily?._id, + userId: Types.ObjectId().toString(), + }; + + const context = { + userId: testUser?._id, + }; + + const { addUserToUserFamily } = await import( + "../../../src/resolvers/Mutation/addUserToUserFamily" + ); + await addUserToUserFamily?.({}, args, context); + } catch (error) { + expect(spy).toBeCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws ConflictError if user with _id === args.userId is already a member + of user family group with _id === args.familyId`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => message); + try { + const args: MutationAddUserToUserFamilyArgs = { + familyId: testUserFamily?._id, + userId: testUser?.id, + }; + + const context = { + userId: testUser?._id, + }; + + const { addUserToUserFamily } = await import( + "../../../src/resolvers/Mutation/addUserToUserFamily" + ); + await addUserToUserFamily?.({}, args, context); + } catch (error) { + expect(spy).toBeCalledWith(USER_ALREADY_MEMBER_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + USER_ALREADY_MEMBER_ERROR.MESSAGE + ); + } + }); + + it(`add the user family with _id === args.familyId and returns it`, async () => { + await UserFamily.updateOne( + { + _id: testUserFamily?._id, + }, + { + $set: { + users: [], + }, + } + ); + + const args: MutationAddUserToUserFamilyArgs = { + familyId: testUserFamily?.id, + userId: testUser?.id, + }; + + const context = { + userId: testUser?.id, + }; + + const { addUserToUserFamily } = await import( + "../../../src/resolvers/Mutation/addUserToUserFamily" + ); + const addUserToUserFamilyPayload = await addUserToUserFamily?.( + {}, + args, + context + ); + expect(addUserToUserFamilyPayload?._id).toEqual(testUserFamily?._id); + expect(addUserToUserFamilyPayload?.users).toEqual([testUser?._id]); + }); +}); diff --git a/tests/resolvers/Mutation/createUserFamily.spec.ts b/tests/resolvers/Mutation/createUserFamily.spec.ts new file mode 100644 index 0000000000..dde3a66dde --- /dev/null +++ b/tests/resolvers/Mutation/createUserFamily.spec.ts @@ -0,0 +1,189 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import type { MutationCreateUserFamilyArgs } from "../../../src/types/generatedGraphQLTypes"; +import { connect, disconnect } from "../../helpers/db"; + +import { + LENGTH_VALIDATION_ERROR, + USER_FAMILY_MIN_MEMBERS_ERROR_CODE, + USER_NOT_AUTHORIZED_SUPERADMIN, + USER_NOT_FOUND_ERROR, +} from "../../../src/constants"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import type { TestUserType } from "../../helpers/userAndUserFamily"; +import { createTestUserFunc } from "../../helpers/userAndUserFamily"; +import { createTestUserFunc as createTestUser } from "../../helpers/user"; + +let testUser: TestUserType; +let testUser2: TestUserType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const resultsArray = await createTestUserFunc(); + const secondUser = await createTestUser(); + + testUser = resultsArray; + testUser2 = secondUser; +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Mutation -> createUserFamily", () => { + it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => `Translated ${message}`); + + try { + const args: MutationCreateUserFamilyArgs = { + data: { + title: "title", + userIds: [testUser?._id, testUser2?._id], + }, + }; + + const context = { + userId: Types.ObjectId().toString(), + }; + + const { createUserFamily: createUserFamilyResolver } = await import( + "../../../src/resolvers/Mutation/createUserFamily" + ); + + await createUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toHaveBeenCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}` + ); + } + }); + + it(`throws Not Authorized error if user is not a super admin`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => message); + try { + const args: MutationCreateUserFamilyArgs = { + data: { + title: "title", + userIds: [testUser?._id, testUser2?._id], + }, + }; + + const context = { + userId: testUser2?._id, + }; + + const { createUserFamily: createUserFamilyResolver } = await import( + "../../../src/resolvers/Mutation/createUserFamily" + ); + + await createUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toHaveBeenCalledWith(USER_NOT_AUTHORIZED_SUPERADMIN.MESSAGE); + expect((error as Error).message).toEqual( + `${USER_NOT_AUTHORIZED_SUPERADMIN.MESSAGE}` + ); + } + }); + + it(`throws String Length Validation error if name is greater than 256 characters`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => message); + try { + const args: MutationCreateUserFamilyArgs = { + data: { + title: + "JWQPfpdkGGGKyryb86K4YN85nDj4m4F7gEAMBbMXLax73pn2okV6kpWY0EYO0XSlUc0fAlp45UCgg3s6mqsRYF9FOlzNIDFLZ1rd03Z17cdJRuvBcAmbC0imyqGdXHGDUQmVyOjDkaOLAvjhB5uDeuEqajcAPTcKpZ6LMpigXuqRAd0xGdPNXyITC03FEeKZAjjJL35cSIUeMv5eWmiFlmmm70FU1Bp6575zzBtEdyWPLflcA2GpGmmf4zvT7nfgN3NIkwQIhk9OwP8dn75YYczcYuUzLpxBu1Lyog77YlAj5DNdTIveXu9zHeC6V4EEUcPQtf1622mhdU3jZNMIAyxcAG4ErtztYYRqFs0ApUxXiQI38rmiaLcicYQgcOxpmFvqRGiSduiCprCYm90CHWbQFq4w2uhr8HhR3r9HYMIYtrRyO6C3rPXaQ7otpjuNgE0AKI57AZ4nGG1lvNwptFCY60JEndSLX9Za6XP1zkVRLaMZArQNl", + userIds: [testUser?._id, testUser2?._id], + }, + }; + const context = { + userId: testUser?._id, + }; + + const { createUserFamily: createUserFamilyResolver } = await import( + "../../../src/resolvers/Mutation/createUserFamily" + ); + + await createUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toHaveBeenCalledWith( + `${LENGTH_VALIDATION_ERROR.MESSAGE} 256 characters in name` + ); + expect((error as Error).message).toEqual( + `${LENGTH_VALIDATION_ERROR.MESSAGE} 256 characters in name` + ); + } + }); + + it(`throws InputValidationError if userIds array has fewer than 2 members`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => message); + try { + const args: MutationCreateUserFamilyArgs = { + data: { + title: "title", + userIds: [testUser?._id], + }, + }; + + const context = { + userId: testUser?.id, + }; + + const { createUserFamily: createUserFamilyResolver } = await import( + "../../../src/resolvers/Mutation/createUserFamily" + ); + + await createUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toHaveBeenCalledWith( + USER_FAMILY_MIN_MEMBERS_ERROR_CODE.MESSAGE + ); + expect((error as Error).message).toEqual( + `${USER_FAMILY_MIN_MEMBERS_ERROR_CODE.MESSAGE}` + ); + } + }); + + it(`creates the user Family and returns it`, async () => { + const args: MutationCreateUserFamilyArgs = { + data: { + title: "title", + userIds: [testUser2?._id, testUser?._id], + }, + }; + + const context = { + userId: testUser?.id, + }; + + const { createUserFamily: createUserFamilyResolver } = await import( + "../../../src/resolvers/Mutation/createUserFamily" + ); + + const createUserFamilyPayload = await createUserFamilyResolver?.( + {}, + args, + context + ); + + expect(createUserFamilyPayload).toEqual( + expect.objectContaining({ + title: "title", + }) + ); + }); +}); diff --git a/tests/resolvers/Mutation/removeUserFamily.spec.ts b/tests/resolvers/Mutation/removeUserFamily.spec.ts new file mode 100644 index 0000000000..c2fbf8cfcd --- /dev/null +++ b/tests/resolvers/Mutation/removeUserFamily.spec.ts @@ -0,0 +1,139 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import { UserFamily } from "../../../src/models/userFamily"; +import type { MutationRemoveUserFamilyArgs } from "../../../src/types/generatedGraphQLTypes"; +import { connect, disconnect } from "../../helpers/db"; + +import { + USER_FAMILY_NOT_FOUND_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../../src/constants"; +import { + beforeAll, + afterAll, + describe, + it, + expect, + afterEach, + vi, +} from "vitest"; +import { createTestUserFunc } from "../../helpers/userAndUserFamily"; +import type { + TestUserFamilyType, + TestUserType, +} from "../../helpers/userAndUserFamily"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testUsers: TestUserType[]; +let testUserFamily: TestUserFamilyType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const tempUser1 = await createTestUserFunc(); + const tempUser2 = await createTestUserFunc(); + testUsers = [tempUser1, tempUser2]; + + testUserFamily = await UserFamily.create({ + title: "Family", + admins: [tempUser1, tempUser2], + creator: tempUser1, + users: [tempUser1, tempUser2], + }); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Mutation -> removeUserFamily", () => { + afterEach(() => { + vi.resetAllMocks(); + vi.doMock("../../src/constants"); + vi.resetModules(); + }); + + it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => `Translated ${message}`); + + try { + const args: MutationRemoveUserFamilyArgs = { + familyId: testUserFamily?._id, + }; + + const context = { + userId: Types.ObjectId().toString(), + }; + + const { removeUserFamily: removeUserFamilyResolver } = await import( + "../../../src/resolvers/Mutation/removeUserFamily" + ); + + await removeUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toHaveBeenCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}` + ); + } + }); + + it(`throws NotFoundError if no user family exists with _id === args.familyId`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => `Translated ${message}`); + + try { + const args: MutationRemoveUserFamilyArgs = { + familyId: testUserFamily?._id, + }; + + const context = { + userId: testUsers[1]?._id, + }; + + const { removeUserFamily: removeUserFamilyResolver } = await import( + "../../../src/resolvers/Mutation/removeUserFamily" + ); + + await removeUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toHaveBeenCalledWith(USER_FAMILY_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${USER_FAMILY_NOT_FOUND_ERROR.MESSAGE}` + ); + } + }); + + it(`throws User is not SUPERADMIN error if current user is with _id === context.userId is not a SUPERADMIN`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => message); + + try { + const args: MutationRemoveUserFamilyArgs = { + familyId: testUserFamily?.id, + }; + + const context = { + userId: testUsers[0]?.id, + }; + + const { removeUserFamily: removeUserFamilyResolver } = await import( + "../../../src/resolvers/Mutation/removeUserFamily" + ); + + await removeUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toHaveBeenCalledWith(USER_FAMILY_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `${USER_FAMILY_NOT_FOUND_ERROR.MESSAGE}` + ); + } + }); +}); diff --git a/tests/resolvers/Mutation/removeUserFromUserFamily.spec.ts b/tests/resolvers/Mutation/removeUserFromUserFamily.spec.ts new file mode 100644 index 0000000000..6baecbe871 --- /dev/null +++ b/tests/resolvers/Mutation/removeUserFromUserFamily.spec.ts @@ -0,0 +1,289 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import { UserFamily } from "../../../src/models/userFamily"; +import type { MutationRemoveUserFromUserFamilyArgs } from "../../../src/types/generatedGraphQLTypes"; +import { connect, disconnect } from "../../helpers/db"; + +import { + ADMIN_REMOVING_CREATOR, + USER_FAMILY_NOT_FOUND_ERROR, + USER_NOT_FOUND_ERROR, + USER_REMOVING_SELF, +} from "../../../src/constants"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import { createTestUserFunc } from "../../helpers/userAndUserFamily"; +import type { + TestUserFamilyType, + TestUserType, +} from "../../helpers/userAndUserFamily"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testUsers: TestUserType[]; +let testUserFamily: TestUserFamilyType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const tempUser1 = await createTestUserFunc(); + const tempUser2 = await createTestUserFunc(); + const tempUser3 = await createTestUserFunc(); + const tempUser4 = await createTestUserFunc(); + const tempUser5 = await createTestUserFunc(); + testUsers = [tempUser1, tempUser2, tempUser3, tempUser4, tempUser5]; + testUserFamily = await UserFamily.create({ + title: "title", + users: [ + testUsers[0]?._id, + testUsers[1]?._id, + testUsers[2]?._id, + testUsers[4]?._id, + ], + admins: [testUsers[2]?._id, testUsers[1]?._id], + creator: testUsers[2]?._id, + }); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolver -> Mutation -> removerUserFromUserFamily", () => { + it("should throw user not found error when user with _id === args.userId does not exist", async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => `Translated ${message}`); + try { + const args: MutationRemoveUserFromUserFamilyArgs = { + familyId: testUserFamily?.id, + userId: testUsers[4]?._id, + }; + + const context = { + userId: testUsers[1]?.id, + }; + + const { removeUserFromUserFamily: removeUserFromUserFamilyResolver } = + await import( + "../../../src/resolvers/Mutation/removeUserFromUserFamily" + ); + + await removeUserFromUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toHaveBeenCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}` + ); + } + }); + + it(`throws NotFoundError if no user family exists with _id === args.familyId`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => message); + try { + const args: MutationRemoveUserFromUserFamilyArgs = { + familyId: Types.ObjectId().toString(), + userId: testUsers[4]?._id, + }; + + const context = { + userId: testUsers[0]?._id, + }; + + const { removeUserFromUserFamily: removeUserFromUserFamilyResolver } = + await import( + "../../../src/resolvers/Mutation/removeUserFromUserFamily" + ); + + await removeUserFromUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toBeCalledWith(USER_FAMILY_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `${USER_FAMILY_NOT_FOUND_ERROR.MESSAGE}` + ); + } + }); + + it(`throws UnauthorizedError if users field of user family with _id === args.familyId + does not contain user with _id === args.userId`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => message); + try { + const args: MutationRemoveUserFromUserFamilyArgs = { + familyId: testUserFamily?.id, + userId: testUsers[3]?._id, + }; + + const context = { + userId: testUsers[2]?.id, + }; + + const { removeUserFromUserFamily: removeUserFromUserFamilyResolver } = + await import( + "../../../src/resolvers/Mutation/removeUserFromUserFamily" + ); + + await removeUserFromUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toBeCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `${USER_NOT_FOUND_ERROR.MESSAGE}` + ); + } + }); + + it("should throw member not found error when user with _id === args.data.userId does not exist in the user Family", async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => `Translated ${message}`); + try { + const args: MutationRemoveUserFromUserFamilyArgs = { + familyId: testUserFamily?.id, + userId: testUsers[3]?._id, + }; + + const context = { + userId: testUsers[2]?.id, + }; + + const { removeUserFromUserFamily: removeUserFromUserFamilyResolver } = + await import( + "../../../src/resolvers/Mutation/removeUserFromUserFamily" + ); + + await removeUserFromUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toHaveBeenCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}` + ); + } + }); + + it("should throw admin cannot remove self error when user with _id === args.data.userId === context.userId", async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => `Translated ${message}`); + try { + const args: MutationRemoveUserFromUserFamilyArgs = { + familyId: testUserFamily?.id, + userId: testUsers[2]?._id, + }; + + const context = { + userId: testUsers[2]?.id, + }; + + const { removeUserFromUserFamily: removeUserFromUserFamilyResolver } = + await import( + "../../../src/resolvers/Mutation/removeUserFromUserFamily" + ); + + await removeUserFromUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toHaveBeenCalledWith(USER_REMOVING_SELF.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${USER_REMOVING_SELF.MESSAGE}` + ); + } + }); + + it("should throw admin cannot remove another admin error when user with _id === args.data.userId is also an admin in the user Family", async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => `Translated ${message}`); + try { + const args: MutationRemoveUserFromUserFamilyArgs = { + familyId: testUserFamily?.id, + userId: testUsers[1]?._id, + }; + + const context = { + userId: testUsers[2]?.id, + }; + + const { removeUserFromUserFamily: removeUserFromUserFamilyResolver } = + await import( + "../../../src/resolvers/Mutation/removeUserFromUserFamily" + ); + + await removeUserFromUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toHaveBeenCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}` + ); + } + }); + + it("should throw admin cannot remove creator error when user with _id === args.data.userId is the user Family creator in the user Family", async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => `Translated ${message}`); + try { + const args: MutationRemoveUserFromUserFamilyArgs = { + familyId: testUserFamily?.id, + userId: testUsers[2]?._id, + }; + + const context = { + userId: testUsers[1]?.id, + }; + + const { removeUserFromUserFamily: removeUserFromUserFamilyResolver } = + await import( + "../../../src/resolvers/Mutation/removeUserFromUserFamily" + ); + + await removeUserFromUserFamilyResolver?.({}, args, context); + } catch (error) { + expect(spy).toHaveBeenCalledWith(ADMIN_REMOVING_CREATOR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${ADMIN_REMOVING_CREATOR.MESSAGE}` + ); + } + }); + + it("remove that user with _id === args.data.userId from that user Family", async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => `Translated ${message}`); + try { + const args: MutationRemoveUserFromUserFamilyArgs = { + familyId: testUserFamily?.id, + userId: testUsers[4]?._id, + }; + + const context = { + userId: testUsers[2]?.id, + }; + + const { removeUserFromUserFamily: removeUserFromUserFamilyResolver } = + await import( + "../../../src/resolvers/Mutation/removeUserFromUserFamily" + ); + + const updatedUserFamily = await removeUserFromUserFamilyResolver?.( + {}, + args, + context + ); + + expect(updatedUserFamily?.users).not.toContain(testUsers[4]?._id); + } catch (error) { + expect(spy).toHaveBeenCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}` + ); + } + }); +}); diff --git a/tests/resolvers/UserFamily/admins.spec.ts b/tests/resolvers/UserFamily/admins.spec.ts new file mode 100644 index 0000000000..9e4e92372d --- /dev/null +++ b/tests/resolvers/UserFamily/admins.spec.ts @@ -0,0 +1,39 @@ +import "dotenv/config"; +import { admins as usersResolver } from "../../../src/resolvers/UserFamily/admins"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { User } from "../../../src/models"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { TestUserFamilyType } from "../../helpers/userAndUserFamily"; +import { createTestUserAndUserFamily } from "../../helpers/userAndUserFamily"; + +let testUserFamily: TestUserFamilyType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const resultArray = await createTestUserAndUserFamily(); + testUserFamily = resultArray[1]; +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> UserFamily -> admins", () => { + it(`returns user objects for parent.admins`, async () => { + if (testUserFamily) { + const parent = testUserFamily.toObject(); + + const usersPayload = await usersResolver?.(parent, {}, {}); + + const users = await User.find({ + _id: { + $in: testUserFamily?.admins, + }, + }).lean(); + + expect(usersPayload).toEqual(users); + } + }); +}); diff --git a/tests/resolvers/UserFamily/creator.spec.ts b/tests/resolvers/UserFamily/creator.spec.ts new file mode 100644 index 0000000000..c9cb6f349d --- /dev/null +++ b/tests/resolvers/UserFamily/creator.spec.ts @@ -0,0 +1,111 @@ +import "dotenv/config"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import { User } from "../../../src/models"; +import { UserFamily } from "../../../src/models/userFamily"; + +import { USER_NOT_FOUND_ERROR } from "../../../src/constants"; +import { + beforeAll, + afterAll, + describe, + it, + expect, + afterEach, + vi, +} from "vitest"; +import type { + TestUserType, + TestUserFamilyType, +} from "../../helpers/userAndUserFamily"; +import { createTestUserAndUserFamily } from "../../helpers/userAndUserFamily"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testUser: TestUserType; +let testUserFamily: TestUserFamilyType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const userAndUserFamily = await createTestUserAndUserFamily(); + testUser = userAndUserFamily[0]; + testUserFamily = userAndUserFamily[1]; +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> UserFamily -> creator", () => { + afterEach(() => { + vi.doUnmock("../../../src/constants"); + vi.resetModules(); + }); + + it(`throws NotFoundError if no user exists with _id === parent.creator`, async () => { + const { requestContext } = await import("../../../src/libraries"); + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementation((message) => `Translated ${message}`); + + try { + testUserFamily = await UserFamily.findOneAndUpdate( + { + _id: testUserFamily?._id, + }, + { + $set: { + creator: Types.ObjectId().toString(), + }, + }, + { + new: true, + } + ); + + const parent = testUserFamily?.toObject(); + + const { creator: creatorResolver } = await import( + "../../../src/resolvers/UserFamily/creator" + ); + if (parent) { + await creatorResolver?.(parent, {}, {}); + } + } catch (error) { + expect(spy).toHaveBeenCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}` + ); + } + }); + + it(`returns user object for parent.creator`, async () => { + testUserFamily = await UserFamily.findOneAndUpdate( + { + _id: testUserFamily?._id, + }, + { + $set: { + creator: testUser?._id, + }, + }, + { + new: true, + } + ); + + const parent = testUserFamily?.toObject(); + + const { creator: creatorResolver } = await import( + "../../../src/resolvers/UserFamily/creator" + ); + if (parent) { + const creatorPayload = await creatorResolver?.(parent, {}, {}); + const creator = await User.findOne({ + _id: testUserFamily?.creator.toString(), + }).lean(); + + expect(creatorPayload).toEqual(creator); + } + }); +}); diff --git a/tests/resolvers/UserFamily/users.spec.ts b/tests/resolvers/UserFamily/users.spec.ts new file mode 100644 index 0000000000..2654812cf8 --- /dev/null +++ b/tests/resolvers/UserFamily/users.spec.ts @@ -0,0 +1,39 @@ +import "dotenv/config"; +import { users as usersResolver } from "../../../src/resolvers/UserFamily/users"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { User } from "../../../src/models"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { TestUserFamilyType } from "../../helpers/userAndUserFamily"; +import { createTestUserAndUserFamily } from "../../helpers/userAndUserFamily"; + +let testUserFamily: TestUserFamilyType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const resultArray = await createTestUserAndUserFamily(); + testUserFamily = resultArray[1]; +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> UserFamily -> users", () => { + it(`returns user objects for parent.users`, async () => { + if (testUserFamily) { + const parent = testUserFamily.toObject(); + + const usersPayload = await usersResolver?.(parent, {}, {}); + + const users = await User.find({ + _id: { + $in: testUserFamily?.users, + }, + }).lean(); + + expect(usersPayload).toEqual(users); + } + }); +}); diff --git a/tests/utilities/userFamilyAdminCheck.spec.ts b/tests/utilities/userFamilyAdminCheck.spec.ts new file mode 100644 index 0000000000..fa7d3c45a8 --- /dev/null +++ b/tests/utilities/userFamilyAdminCheck.spec.ts @@ -0,0 +1,155 @@ +import "dotenv/config"; +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from "vitest"; +import { connect, disconnect } from "../helpers/db"; +import { USER_NOT_AUTHORIZED_ADMIN } from "../../src/constants"; +import type { + TestUserFamilyType, + TestUserType, +} from "../helpers/userAndUserFamily"; +import { createTestUserAndUserFamily } from "../helpers/userAndUserFamily"; +import { createTestUserFunc } from "../helpers/user"; +import mongoose from "mongoose"; +import type { InterfaceUserFamily } from "../../src/models/userFamily"; +import { User } from "../../src/models"; +import { UserFamily } from "../../src/models/userFamily"; + +let testUser: TestUserType; +let testUserFamily: TestUserFamilyType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const userAndUserFamily = await createTestUserAndUserFamily(false, false); + testUser = await createTestUserFunc(); + testUserFamily = userAndUserFamily[1]; +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("utilities -> userFamilyAdminCheck", () => { + afterEach(() => { + vi.resetModules(); + }); + + it("throws error if userIsUserFamilyAdmin === false and isUserSuperAdmin === false", async () => { + const { requestContext } = await import("../../src/libraries"); + + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + + try { + const { adminCheck } = await import( + "../../src/utilities/userFamilyAdminCheck" + ); + await adminCheck( + testUser?._id, + testUserFamily ?? ({} as InterfaceUserFamily) + ); + } catch (error) { + expect((error as Error).message).toEqual( + `Translated ${USER_NOT_AUTHORIZED_ADMIN.MESSAGE}` + ); + } + expect(spy).toBeCalledWith(USER_NOT_AUTHORIZED_ADMIN.MESSAGE); + }); + + it("throws no error if userIsUserFamilyAdmin === false and isUserSuperAdmin === true", async () => { + const updatedUser = await User.findOneAndUpdate( + { + _id: testUser?._id, + }, + { + userType: "SUPERADMIN", + }, + { + new: true, + upsert: true, + } + ); + + const { adminCheck } = await import( + "../../src/utilities/userFamilyAdminCheck" + ); + + await expect( + adminCheck( + updatedUser?._id, + testUserFamily ?? ({} as InterfaceUserFamily) + ) + ).resolves.not.toThrowError(); + }); + + it("throws no error if user is an admin in that user family but not super admin", async () => { + const updatedUser = await User.findOneAndUpdate( + { + _id: testUser?._id, + }, + { + userType: "USER", + }, + { + new: true, + upsert: true, + } + ); + + const updatedUserFamily = await UserFamily.findOneAndUpdate( + { + _id: testUserFamily?._id, + }, + { + $push: { + admins: testUser?._id, + }, + }, + { + new: true, + upsert: true, + } + ); + + const { adminCheck } = await import( + "../../src/utilities/userFamilyAdminCheck" + ); + + await expect( + adminCheck( + updatedUser?._id, + updatedUserFamily ?? ({} as InterfaceUserFamily) + ) + ).resolves.not.toThrowError(); + }); + it("throws error if user is not found with the specific Id", async () => { + const { requestContext } = await import("../../src/libraries"); + + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + + try { + const { adminCheck } = await import( + "../../src/utilities/userFamilyAdminCheck" + ); + await adminCheck( + new mongoose.Types.ObjectId(), + testUserFamily ?? ({} as InterfaceUserFamily) + ); + } catch (error) { + expect((error as Error).message).toEqual( + `Translated ${USER_NOT_AUTHORIZED_ADMIN.MESSAGE}` + ); + } + expect(spy).toBeCalledWith(USER_NOT_AUTHORIZED_ADMIN.MESSAGE); + }); +});