diff --git a/app/api/server/v1/teams.ts b/app/api/server/v1/teams.ts index ca1d12ed4bdb..83a108da25b3 100644 --- a/app/api/server/v1/teams.ts +++ b/app/api/server/v1/teams.ts @@ -2,7 +2,7 @@ import { Promise } from 'meteor/promise'; import { API } from '../api'; import { Team } from '../../../../server/sdk'; -import { hasPermission } from '../../../authorization/server'; +import { hasAtLeastOnePermission, hasPermission } from '../../../authorization/server'; API.v1.addRoute('teams.list', { authRequired: true }, { get() { @@ -40,6 +40,9 @@ API.v1.addRoute('teams.listAll', { authRequired: true }, { API.v1.addRoute('teams.create', { authRequired: true }, { post() { + if (!hasPermission(this.userId, 'create-team')) { + return API.v1.unauthorized(); + } const { name, type, members, room, owner } = this.bodyParams; if (!name) { @@ -62,15 +65,71 @@ API.v1.addRoute('teams.create', { authRequired: true }, { API.v1.addRoute('teams.members', { authRequired: true }, { get() { - const { teamId } = this.queryParams; + const { offset, count } = this.getPaginationItems(); + const { teamId, teamName } = this.queryParams; + + const { records, total } = Promise.await(Team.members(teamId, teamName, { offset, count })); + + return API.v1.success({ + members: records, + total, + count: records.length, + offset, + }); + }, +}); + +API.v1.addRoute('teams.addMembers', { authRequired: true }, { + post() { + if (!hasAtLeastOnePermission(this.userId, ['add-team-member', 'edit-team-member'])) { + return API.v1.unauthorized(); + } + + const { teamId, teamName, members } = this.bodyParams; + + Promise.await(Team.addMembers(this.userId, teamId, teamName, members)); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('teams.updateMember', { authRequired: true }, { + post() { + if (!hasAtLeastOnePermission(this.userId, ['edit-team-member'])) { + return API.v1.unauthorized(); + } + + const { teamId, teamName, member } = this.bodyParams; + + Promise.await(Team.updateMember(teamId, teamName, member)); + + return API.v1.success(); + }, +}); - if (!teamId) { - return API.v1.failure('Team ID is required'); +API.v1.addRoute('teams.removeMembers', { authRequired: true }, { + post() { + if (!hasAtLeastOnePermission(this.userId, ['edit-team-member'])) { + return API.v1.unauthorized(); } - const members = Promise.await(Team.members(this.userId, teamId)); + const { teamId, teamName, members } = this.bodyParams; + + Promise.await(Team.removeMembers(teamId, teamName, members)); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('teams.leave', { authRequired: true }, { + post() { + const { teamId, teamName } = this.bodyParams; + + Promise.await(Team.removeMembers(teamId, teamName, [{ + userId: this.userId, + }])); - return API.v1.success({ members }); + return API.v1.success(); }, }); diff --git a/app/lib/server/functions/addUserToRoom.js b/app/lib/server/functions/addUserToRoom.js index 69b7bd21f0ad..1d82e7b1b54d 100644 --- a/app/lib/server/functions/addUserToRoom.js +++ b/app/lib/server/functions/addUserToRoom.js @@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { AppEvents, Apps } from '../../../apps/server'; import { callbacks } from '../../../callbacks'; import { Messages, Rooms, Subscriptions } from '../../../models'; +import { Team } from '../../../../server/sdk'; import { RoomMemberActions, roomTypes } from '../../../utils/server'; export const addUserToRoom = function(rid, user, inviter, silenced) { @@ -84,5 +85,10 @@ export const addUserToRoom = function(rid, user, inviter, silenced) { }); } + if (room.teamMain && room.teamId) { + // if user is joining to main team channel, create a membership + Promise.await(Team.addMember(inviter, user._id, room.teamId)); + } + return true; }; diff --git a/app/models/server/raw/Team.ts b/app/models/server/raw/Team.ts index 226e745ed81c..a754a63e435f 100644 --- a/app/models/server/raw/Team.ts +++ b/app/models/server/raw/Team.ts @@ -1,4 +1,4 @@ -import { Collection, FindOneOptions, Cursor } from 'mongodb'; +import { Collection, FindOneOptions, Cursor, UpdateWriteOpResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; import { ITeam } from '../../../../definition/ITeam'; @@ -25,4 +25,18 @@ export class TeamRaw extends BaseRaw { findOneByName(name: string, options?: FindOneOptions): Promise { return this.col.findOne({ name }, options); } + + findOneByMainRoomId(roomId: string, options?: FindOneOptions): Promise { + return this.col.findOne({ roomId }, options); + } + + updateMainRoomForTeam(id: string, roomId: string): Promise { + return this.col.updateOne({ + _id: id, + }, { + $set: { + roomId, + }, + }); + } } diff --git a/app/models/server/raw/TeamMember.ts b/app/models/server/raw/TeamMember.ts index 42cfcf4555fd..16ffe848d3ec 100644 --- a/app/models/server/raw/TeamMember.ts +++ b/app/models/server/raw/TeamMember.ts @@ -1,7 +1,8 @@ -import { Collection, FindOneOptions, Cursor } from 'mongodb'; +import { Collection, FindOneOptions, Cursor, InsertOneWriteOpResult, UpdateWriteOpResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; import { ITeamMember } from '../../../../definition/ITeam'; +import { IUser } from '../../../../definition/IUser'; type T = ITeamMember; export class TeamMemberRaw extends BaseRaw { @@ -30,4 +31,40 @@ export class TeamMemberRaw extends BaseRaw { findByTeamId(teamId: string, options?: FindOneOptions): Cursor { return this.col.find({ teamId }, options); } + + updateOneByUserIdAndTeamId(userId: string, teamId: string, update: Partial): Promise { + return this.col.updateOne({ userId, teamId }, { $set: update }); + } + + createOneByTeamIdAndUserId(teamId: string, userId: string, createdBy: Pick): Promise> { + return this.insertOne({ + teamId, + userId, + createdAt: new Date(), + _updatedAt: new Date(), + createdBy, + }); + } + + updateRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array): Promise { + return this.col.updateOne({ + teamId, + userId, + }, { + $addToSet: { + roles: { $each: roles }, + }, + }); + } + + removeRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array): Promise { + return this.col.updateOne({ + teamId, + userId, + }, { + $pull: { + roles: { $in: roles }, + }, + }); + } } diff --git a/definition/ITeam.ts b/definition/ITeam.ts index 34574c212a2f..2ea42cf79692 100644 --- a/definition/ITeam.ts +++ b/definition/ITeam.ts @@ -9,6 +9,7 @@ export enum TEAM_TYPE { export interface ITeam extends IRocketChatRecord { name: string; type: TEAM_TYPE; + roomId: string; createdBy: Pick; createdAt: Date; } diff --git a/server/methods/addRoomLeader.js b/server/methods/addRoomLeader.js index b602c75f984e..9265815e2620 100644 --- a/server/methods/addRoomLeader.js +++ b/server/methods/addRoomLeader.js @@ -5,6 +5,7 @@ import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; import { api } from '../sdk/api'; +import { Team } from '../sdk'; Meteor.methods({ addRoomLeader(rid, userId) { @@ -57,6 +58,11 @@ Meteor.methods({ role: 'leader', }); + const team = Promise.await(Team.getOneByRoomId(rid)); + if (team) { + Promise.await(Team.addRolesToMember(team._id, userId, ['leader'])); + } + if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'added', diff --git a/server/methods/addRoomModerator.js b/server/methods/addRoomModerator.js index 8b430cb667a4..6d0d3879a996 100644 --- a/server/methods/addRoomModerator.js +++ b/server/methods/addRoomModerator.js @@ -5,6 +5,7 @@ import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; import { api } from '../sdk/api'; +import { Team } from '../sdk'; Meteor.methods({ addRoomModerator(rid, userId) { @@ -57,6 +58,11 @@ Meteor.methods({ role: 'moderator', }); + const team = Promise.await(Team.getOneByRoomId(rid)); + if (team) { + Promise.await(Team.addRolesToMember(team._id, userId, ['moderator'])); + } + if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'added', diff --git a/server/methods/addRoomOwner.js b/server/methods/addRoomOwner.js index 91473caef901..a98405e8e276 100644 --- a/server/methods/addRoomOwner.js +++ b/server/methods/addRoomOwner.js @@ -3,6 +3,7 @@ import { check } from 'meteor/check'; import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; +import { Team } from '../sdk'; import { settings } from '../../app/settings'; import { api } from '../sdk/api'; @@ -57,6 +58,11 @@ Meteor.methods({ role: 'owner', }); + const team = Promise.await(Team.getOneByRoomId(rid)); + if (team) { + Promise.await(Team.addRolesToMember(team._id, userId, ['owner'])); + } + if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'added', diff --git a/server/methods/removeRoomLeader.js b/server/methods/removeRoomLeader.js index b88a6a7729dc..77ce4340c444 100644 --- a/server/methods/removeRoomLeader.js +++ b/server/methods/removeRoomLeader.js @@ -5,6 +5,7 @@ import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; import { api } from '../sdk/api'; +import { Team } from '../sdk'; Meteor.methods({ removeRoomLeader(rid, userId) { @@ -57,6 +58,11 @@ Meteor.methods({ role: 'leader', }); + const team = Promise.await(Team.getOneByRoomId(rid)); + if (team) { + Promise.await(Team.removeRolesFromMember(team._id, userId, ['leader'])); + } + if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'removed', diff --git a/server/methods/removeRoomModerator.js b/server/methods/removeRoomModerator.js index ef86a9c176be..c4237dbda231 100644 --- a/server/methods/removeRoomModerator.js +++ b/server/methods/removeRoomModerator.js @@ -5,6 +5,7 @@ import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; import { api } from '../sdk/api'; +import { Team } from '../sdk'; Meteor.methods({ removeRoomModerator(rid, userId) { @@ -57,6 +58,11 @@ Meteor.methods({ role: 'moderator', }); + const team = Promise.await(Team.getOneByRoomId(rid)); + if (team) { + Promise.await(Team.removeRolesFromMember(team._id, userId, ['moderator'])); + } + if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'removed', diff --git a/server/methods/removeRoomOwner.js b/server/methods/removeRoomOwner.js index b4dcf08ca26c..d05a9559369b 100644 --- a/server/methods/removeRoomOwner.js +++ b/server/methods/removeRoomOwner.js @@ -5,6 +5,7 @@ import { hasPermission, getUsersInRole } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; import { api } from '../sdk/api'; +import { Team } from '../sdk'; Meteor.methods({ removeRoomOwner(rid, userId) { @@ -64,6 +65,11 @@ Meteor.methods({ role: 'owner', }); + const team = Promise.await(Team.getOneByRoomId(rid)); + if (team) { + Promise.await(Team.removeRolesFromMember(team._id, userId, ['owner'])); + } + if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'removed', diff --git a/server/sdk/types/IRoomService.ts b/server/sdk/types/IRoomService.ts index eb3862eccf26..ab5a470006be 100644 --- a/server/sdk/types/IRoomService.ts +++ b/server/sdk/types/IRoomService.ts @@ -12,8 +12,9 @@ interface ICreateRoomOptions extends Partial { +interface ICreateRoomExtraData extends Record { teamId: string; + teamMain: boolean; } export interface ICreateRoomParams { diff --git a/server/sdk/types/ITeamService.ts b/server/sdk/types/ITeamService.ts index a9427ddfa7d3..8c3532fafa1a 100644 --- a/server/sdk/types/ITeamService.ts +++ b/server/sdk/types/ITeamService.ts @@ -8,11 +8,20 @@ export interface ITeamCreateParams { owner?: string; // the team owner. If not present, owner = requester } +export interface ITeamMemberParams { + userId?: string; + userName?: string; + roles?: Array; +} + export interface ITeamService { create(uid: string, params: ITeamCreateParams): Promise; list(uid: string, options?: IPaginationOptions): Promise>; listAll(options?: IPaginationOptions): Promise>; - members(uid: string, teamId: string): Promise>; + members(teamId: string, teamName: string, options?: IPaginationOptions): Promise>; + addMembers(uid: string, teamId: string, teamName: string, members: Array): Promise; + updateMember(teamId: string, teamName: string, members: ITeamMemberParams): Promise; + removeMembers(teamId: string, teamName: string, members: Array): Promise; getInfoByName(teamName: string): Promise | undefined>; getInfoById(teamId: string): Promise | undefined>; } diff --git a/server/services/team/service.ts b/server/services/team/service.ts index 96ce741c440a..d58b24748b8b 100644 --- a/server/services/team/service.ts +++ b/server/services/team/service.ts @@ -2,8 +2,9 @@ import { Db } from 'mongodb'; import { TeamRaw } from '../../../app/models/server/raw/Team'; import { ITeam, ITeamMember, TEAM_TYPE, IRecordsWithTotal, IPaginationOptions } from '../../../definition/ITeam'; -import { Authorization, Room } from '../../sdk'; -import { ITeamCreateParams, ITeamService } from '../../sdk/types/ITeamService'; +import { Room } from '../../sdk'; +import { ITeamCreateParams, ITeamMemberParams, ITeamService } from '../../sdk/types/ITeamService'; +import { IUser } from '../../../definition/IUser'; import { ServiceClass } from '../../sdk/types/ServiceClass'; import { UsersRaw } from '../../../app/models/server/raw/Users'; import { RoomsRaw } from '../../../app/models/server/raw/Rooms'; @@ -32,11 +33,6 @@ export class TeamService extends ServiceClass implements ITeamService { } async create(uid: string, { team, room = { name: team.name, extraData: {} }, members, owner }: ITeamCreateParams): Promise { - const hasPermission = await Authorization.hasPermission(uid, 'create-team'); - if (!hasPermission) { - throw new Error('no-permission'); - } - const existingTeam = await this.TeamModel.findOneByName(team.name, { projection: { _id: 1 } }); if (existingTeam) { throw new Error('team-name-already-exists'); @@ -62,13 +58,18 @@ export class TeamService extends ServiceClass implements ITeamService { createdAt: new Date(), createdBy, _updatedAt: new Date(), // TODO how to avoid having to do this? + roomId: '', // this will be populated at the end }; try { const result = await this.TeamModel.insertOne(teamData); const teamId = result.insertedId; + // the same uid can be passed at 3 positions: owner, member list or via caller + // if the owner is present, remove it from the members list + // if the owner is not present, remove the caller from the members list + const excludeFromMembers = owner ? [owner] : [uid]; - const membersList: Array> = members?.filter((memberId) => ![uid, owner].includes(memberId)) + const membersList: Array> = members?.filter((memberId) => !excludeFromMembers.includes(memberId)) .map((memberId) => ({ teamId, userId: memberId, @@ -98,10 +99,13 @@ export class TeamService extends ServiceClass implements ITeamService { extraData: { ...room.extraData, teamId, + teamMain: true, }, }; - await Room.create(owner || uid, newRoom); + const createdRoom = await Room.create(owner || uid, newRoom); + + await this.TeamModel.updateMainRoomForTeam(teamId, createdRoom._id); return { _id: teamId, @@ -146,18 +150,140 @@ export class TeamService extends ServiceClass implements ITeamService { }; } - async members(userId: string, teamId: string): Promise> { - const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId); - const hasPermission = await Authorization.hasAtLeastOnePermission(userId, ['add-team-member', 'edit-team-member', 'view-all-teams']); - if (!hasPermission) { - throw new Error('no-permission'); + async members(teamId: string, teamName: string, { offset, count }: IPaginationOptions = { offset: 0, count: 50 }): Promise> { + if (!teamId) { + const teamIdName = await this.TeamModel.findOneByName(teamName, { projection: { _id: 1 } }); + if (!teamIdName) { + throw new Error('team-does-not-exist'); + } + + teamId = teamIdName._id; + } + + const cursor = this.TeamMembersModel.findByTeamId(teamId, { + limit: count, + skip: offset, + }); + + return { + total: await cursor.count(), + records: await cursor.toArray(), + }; + } + + async addMembers(uid: string, teamId: string, teamName: string, members: Array): Promise { + const createdBy = await this.Users.findOneById(uid, { projection: { username: 1 } }); + if (!createdBy) { + throw new Error('invalid-user'); + } + + if (!teamId) { + const teamIdName = await this.TeamModel.findOneByName(teamName, { projection: { _id: 1 } }); + if (!teamIdName) { + throw new Error('team-does-not-exist'); + } + + teamId = teamIdName._id; + } + + const membersList: Array> = members?.map((member) => ({ + teamId, + userId: member.userId ? member.userId : '', + roles: member.roles ? member.roles : [], + createdAt: new Date(), + createdBy, + _updatedAt: new Date(), // TODO how to avoid having to do this? + })) || []; + + await this.TeamMembersModel.insertMany(membersList); + } + + async updateMember(teamId: string, teamName: string, member: ITeamMemberParams): Promise { + if (!teamId) { + const teamIdName = await this.TeamModel.findOneByName(teamName, { projection: { _id: 1 } }); + if (!teamIdName) { + throw new Error('team-does-not-exist'); + } + + teamId = teamIdName._id; + } + + if (!member.userId) { + member.userId = await this.Users.findOneByUsername(member.userName); + if (!member.userId) { + throw new Error('invalid-user'); + } + } + + const memberUpdate: Partial = { + roles: member.roles ? member.roles : [], + _updatedAt: new Date(), + }; + + await this.TeamMembersModel.updateOneByUserIdAndTeamId(member.userId, teamId, memberUpdate); + } + + async removeMembers(teamId: string, teamName: string, members: Array): Promise { + if (!teamId) { + const teamIdName = await this.TeamModel.findOneByName(teamName, { projection: { _id: 1 } }); + if (!teamIdName) { + throw new Error('team-does-not-exist'); + } + + teamId = teamIdName._id; + } + + for await (const member of members) { + if (!member.userId) { + member.userId = await this.Users.findOneByUsername(member.userName); + if (!member.userId) { + throw new Error('invalid-user'); + } + } + + const existingMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(member.userId, teamId); + if (!existingMember) { + throw new Error('member-does-not-exist'); + } + + this.TeamMembersModel.removeById(existingMember._id); } + } + + async addMember({ _id, username }: IUser, userId: string, teamId: string): Promise { + const isAlreadyAMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } }); + + if (isAlreadyAMember) { + return false; + } + + return (await this.TeamMembersModel.createOneByTeamIdAndUserId(teamId, userId, { _id, username })).ops[0]; + } + + async getOneByRoomId(roomId: string): Promise { + return this.TeamModel.findOneByMainRoomId(roomId, { projection: { _id: 1 } }); + } + + async addRolesToMember(teamId: string, userId: string, roles: Array): Promise { + const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } }); + + if (!isMember) { + // TODO should this throw an error instead? + return false; + } + + return !!await this.TeamMembersModel.updateRolesByTeamIdAndUserId(teamId, userId, roles); + } + + async removeRolesFromMember(teamId: string, userId: string, roles: Array): Promise { + const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } }); - if (!isMember && !hasPermission) { - return []; + if (!isMember) { + // TODO should this throw an error instead? + return false; } - return this.TeamMembersModel.findByTeamId(teamId).toArray(); + return !!await this.TeamMembersModel.removeRolesByTeamIdAndUserId(teamId, userId, roles); } async getInfoByName(teamName: string): Promise | undefined> { diff --git a/tests/end-to-end/api/25-teams.js b/tests/end-to-end/api/25-teams.js index 9855cfea7c03..9fd66e2e2709 100644 --- a/tests/end-to-end/api/25-teams.js +++ b/tests/end-to-end/api/25-teams.js @@ -77,4 +77,183 @@ describe('[Teams]', () => { .end(done); }); }); + + describe('/teams.addMembers', () => { + it('should add members to a public team', (done) => { + request.post(api('teams.addMembers')) + .set(credentials) + .send({ + teamName: community, + members: [ + { + userId: 'test-123', + roles: ['member'], + }, + { + userId: 'test-456', + roles: ['member'], + }, + ], + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .then(() => + request.get(api('teams.members')) + .set(credentials) + .query({ + teamName: community, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((response) => { + expect(response.body).to.have.property('success', true); + expect(response.body).to.have.property('members'); + expect(response.body.members).to.have.lengthOf(3); + expect(response.body.members[1].userId).to.eql('test-123'); + expect(response.body.members[1].roles).to.have.lengthOf(1); + expect(response.body.members[1].roles).to.eql(['member']); + expect(response.body.members[2].userId).to.eql('test-456'); + expect(response.body.members[2].roles).to.have.lengthOf(1); + expect(response.body.members[2].roles).to.eql(['member']); + }), + ) + .then(() => done()) + .catch(done); + }); + }); + + describe('/teams.members', () => { + it('should list all the members from a public team', (done) => { + request.get(api('teams.members')) + .set(credentials) + .query({ + teamName: community, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('count', 3); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('total', 3); + expect(res.body).to.have.property('members'); + expect(res.body.members).to.have.length(3); + expect(res.body.members[0]).to.have.property('_id'); + expect(res.body.members[0]).to.have.property('roles'); + expect(res.body.members[0]).to.have.property('createdAt'); + }) + .end(done); + }); + }); + + describe('/teams.updateMember', () => { + it('should update member\'s data in a public team', (done) => { + request.post(api('teams.updateMember')) + .set(credentials) + .send({ + teamName: community, + member: + { + userId: 'test-123', + roles: ['member', 'owner'], + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .then(() => + request.get(api('teams.members')) + .set(credentials) + .query({ + teamName: community, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((response) => { + expect(response.body).to.have.property('success', true); + expect(response.body).to.have.property('members'); + expect(response.body.members).to.have.length(3); + expect(response.body.members[1].userId).to.eql('test-123'); + expect(response.body.members[1].roles).to.have.lengthOf(2); + expect(response.body.members[1].roles).to.eql(['member', 'owner']); + }), + ) + .then(() => done()) + .catch(done); + }); + }); + + describe('/teams.removeMembers', () => { + it('should remove one member from a public team', (done) => { + request.post(api('teams.removeMembers')) + .set(credentials) + .send({ + teamName: community, + members: [ + { + userId: 'test-456', + }, + ], + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .then(() => + request.get(api('teams.members')) + .set(credentials) + .query({ + teamName: community, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((response) => { + expect(response.body).to.have.property('success', true); + expect(response.body).to.have.property('members'); + expect(response.body.members).to.have.lengthOf(2); + expect(response.body.members[1].userId).to.eql('test-123'); + }), + ) + .then(() => done()) + .catch(done); + }); + }); + + describe('/teams.leave', () => { + it('should remove the calling user from the team', (done) => { + request.post(api('teams.leave')) + .set(credentials) + .send({ + teamName: community, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .then(() => + request.get(api('teams.members')) + .set(credentials) + .query({ + teamName: community, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((response) => { + expect(response.body).to.have.property('success', true); + expect(response.body).to.have.property('members'); + expect(response.body.members).to.have.lengthOf(1); + expect(response.body.members[0].userId).to.eql('test-123'); + }), + ) + .then(() => done()) + .catch(done); + }); + }); });