src/app/core/teams/teams.service.spec.ts
import _ from 'lodash';
import mongoose from 'mongoose';
import should from 'should';
import { assert, createSandbox } from 'sinon';
import { TeamRoles } from './team-role.model';
import { ITeam, Team, TeamDocument } from './team.model';
import teamsService from './teams.service';
import { auditService, config, emailService } from '../../../dependencies';
import { logger } from '../../../lib/logger';
import {
BadRequestError,
ForbiddenError,
InternalServerError
} from '../../common/errors';
import { IUser, User, UserDocument } from '../user/user.model';
import userService from '../user/user.service';
/**
* Helpers
*/
function clearDatabase() {
return Promise.all([Team.deleteMany({}).exec(), User.deleteMany({}).exec()]);
}
function userSpec(key): Partial<IUser> {
return {
name: `${key} Name`,
email: `${key}@email.domain`,
username: `${key}_username`,
organization: `${key} Organization`
};
}
function proxyPkiUserSpec(key) {
const spec = userSpec(key);
spec.provider = 'proxy-pki';
spec.providerData = {
dn: key,
dnLower: key.toLowerCase()
};
return spec;
}
function localUserSpec(key) {
const spec = userSpec(key);
spec.provider = 'local';
spec.password = 'password';
return spec;
}
function teamSpec(key): Partial<ITeam> {
return {
name: key,
description: `${key} Team Description`
};
}
/**
* Unit tests
*/
describe('Team Service:', () => {
// Specs for tests
const spec: {
team: Record<string, Partial<ITeam>>;
nestedTeam: Record<string, Partial<ITeam>>;
user: Record<string, Partial<IUser>>;
userTeams: Record<string, { team: string; role: string }[]>;
} = {
team: {},
nestedTeam: {},
user: {},
userTeams: {}
};
// Teams for tests
spec.team.teamWithExternalTeam = teamSpec('external-team');
spec.team.teamWithExternalTeam.implicitMembers = true;
spec.team.teamWithExternalTeam.requiresExternalTeams = ['external-group'];
spec.team.teamWithExternalRoles = teamSpec('external-roles');
spec.team.teamWithExternalRoles.implicitMembers = true;
spec.team.teamWithExternalRoles.requiresExternalRoles = ['external-role'];
spec.team.teamWithExternalRoles2 = teamSpec('external-roles-2');
spec.team.teamWithExternalRoles2.implicitMembers = true;
spec.team.teamWithExternalRoles2.requiresExternalRoles = ['external-role-2'];
spec.team.teamWithNoExternalTeam = teamSpec('no-external');
spec.team.teamWithNoExternalTeam.requiresExternalTeams = [];
spec.team.teamWithNoExternalTeam2 = teamSpec('no-external-2');
spec.team.teamWithNoExternalTeam2.requiresExternalTeams = [];
spec.team.teamWithNullRequiredExternalRoles = teamSpec('req-roles-null');
spec.team.teamWithNullRequiredExternalRoles.requiresExternalRoles = null;
// User implicit added to team by having an external group
spec.user.implicit1 = proxyPkiUserSpec('implicit1');
spec.user.implicit1.externalGroups = ['external-group', 'external-group-2'];
// User implicit added to team by having an external role
spec.user.implicit2 = proxyPkiUserSpec('implicit2');
spec.user.implicit2.externalRoles = ['external-role', 'external-role-2'];
// User explicitly added to a group. Group is added in before() block below
spec.user.explicit = proxyPkiUserSpec('explicit');
spec.userTeams.explicit = [
{ team: 'teamWithNoExternalTeam', role: TeamRoles.Member }
];
// Generic test users
spec.user.user1 = localUserSpec('user1');
spec.user.user1.roles = { user: true };
spec.userTeams.user1 = [
{ team: 'teamWithNoExternalTeam', role: TeamRoles.Member },
{ team: 'teamWithNoExternalTeam2', role: TeamRoles.Member }
];
spec.user.user2 = localUserSpec('user2');
spec.user.user3 = localUserSpec('user3');
spec.user.user3.roles = { user: true };
spec.userTeams.user3 = [
{ team: 'teamWithNoExternalTeam', role: TeamRoles.Admin }
];
spec.user.admin = localUserSpec(TeamRoles.Admin);
spec.user.admin.roles = { user: true, admin: true };
spec.user.blocked = localUserSpec('blocked');
spec.user.blocked.roles = { user: true };
spec.user.blocked.externalRoles = ['external-role-2'];
spec.userTeams.blocked = [
{ team: 'teamWithExternalRoles2', role: 'blocked' }
];
let user: Record<string, UserDocument> = {};
let team: Record<string, TeamDocument> = {};
let sandbox;
beforeEach(async () => {
sandbox = createSandbox();
sandbox.stub(auditService, 'audit').resolves();
await clearDatabase();
user = {};
team = {};
// Create the teams
await Promise.all(
_.keys(spec.team).map(async (key) => {
team[key] = await new Team(spec.team[key]).save();
})
);
// Create the users
await Promise.all(
_.keys(spec.user).map(async (key) => {
user[key] = await new User(spec.user[key]).save();
})
);
// Add users to teams
await Promise.all(
_.keys(spec.userTeams).map(async (k) => {
user[k] = await User.findOneAndUpdate(
{ _id: user[k]._id },
{
$set: {
teams: spec.userTeams[k].map((ut) => ({
_id: team[ut.team],
role: ut.role
}))
}
},
{ new: true }
).exec();
})
);
});
afterEach(() => {
sandbox.restore();
return clearDatabase();
});
describe('getActiveTeamRole', () => {
it('return existing explicit team role for user (model object)', () => {
const role = teamsService.getActiveTeamRole(
user.explicit,
team.teamWithNoExternalTeam
);
should.exist(role);
role.should.equal(TeamRoles.Member);
});
it('return existing explicit team role for user (plain object)', () => {
const role = teamsService.getActiveTeamRole(
user.explicit,
team.teamWithNoExternalTeam
);
should.exist(role);
role.should.equal(TeamRoles.Member);
});
it('return null role for user not in team', () => {
const role = teamsService.getActiveTeamRole(
user.implicit1,
team.teamWithExternalTeam
);
should.not.exist(role);
});
describe('implicitMembers.strategy = "teams"', () => {
beforeEach(() => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.implicitMembers.enabled').returns(true);
configGetStub
.withArgs('teams.implicitMembers.strategy')
.returns('teams');
configGetStub.callThrough();
});
it('return implicit team role for user', () => {
const role = teamsService.getActiveTeamRole(
user.implicit1,
team.teamWithExternalTeam
);
should.exist(role);
role.should.equal(TeamRoles.Member);
});
});
describe('implicitMembers.strategy = "roles"', () => {
beforeEach(() => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.implicitMembers.enabled').returns(true);
configGetStub
.withArgs('teams.implicitMembers.strategy')
.returns('roles');
configGetStub.callThrough();
});
it('return null role for user not implicitly in team', () => {
const role = teamsService.getActiveTeamRole(
user.implicit1,
team.teamWithExternalTeam
);
should.not.exist(role);
});
it('return implicit team role for user', () => {
const role = teamsService.getActiveTeamRole(
user.implicit2,
team.teamWithExternalRoles
);
should.exist(role);
role.should.equal(TeamRoles.Member);
});
it('return blocked role for user blocked from team that they would otherwise be an implicit member', () => {
const role = teamsService.getActiveTeamRole(
user.blocked,
team.teamWithExternalRoles2
);
should.exist(role);
role.should.equal('blocked');
});
});
});
describe('meetsRoleRequirement', () => {
it('should not reject for user with specified role in team', async () => {
await teamsService
.meetsRoleRequirement(
user.explicit,
team.teamWithNoExternalTeam,
TeamRoles.Member
)
.should.be.fulfilled();
});
it('should reject for user without specified role in team', async () => {
await teamsService
.meetsRoleRequirement(
user.explicit,
team.teamWithNoExternalTeam,
TeamRoles.Admin
)
.should.be.rejectedWith(
new ForbiddenError(
'The user does not have the required roles for the team'
)
);
});
});
describe('read', () => {
it('read finds team', async () => {
const t = await teamsService.read(team.teamWithNoExternalTeam._id);
should.exist(t);
t.name.should.equal('no-external');
});
it('read returns null when no team found', async () => {
const t = await teamsService.read('123412341234123412341234');
should.not.exist(t);
});
});
describe('readTeamMember', () => {
it('read finds team', async () => {
const t = await userService.read(user.admin._id);
should.exist(t);
t.name.should.equal('admin Name');
});
it('read returns null when no team found', async () => {
const t = await userService.read(
new mongoose.Types.ObjectId('123412341234123412341234')
);
should.not.exist(t);
});
});
describe('create', () => {
it('explicit admin should be used', async () => {
const queryParams = { dir: 'ASC', page: '0', size: '5', sort: 'name' };
const creator = await User.findOne({ name: 'user1 Name' }).exec();
const admin = await User.findOne({ name: 'user2 Name' }).exec();
await teamsService.create(teamSpec('test-create-2'), creator, admin._id);
const _team = await Team.findOne({ name: 'test-create-2' }).exec();
const members = await userService.searchUsers(
queryParams,
{ 'teams._id': _team._id },
null
);
members.elements.should.have.length(1);
members.elements[0].name.should.equal(admin.name);
});
it('null admin should default admin to creator', async () => {
const queryParams = { dir: 'ASC', page: '0', size: '5', sort: 'name' };
// const creator = await User.findOne({name: 'user1 Name'}).exec();
const creator = user.user1;
// null admin should default to creator
await teamsService.create(teamSpec('test-create'), creator, null);
const _team = await Team.findOne({ name: 'test-create' }).exec();
const members = await userService.searchUsers(
queryParams,
{ 'teams._id': _team._id },
null
);
members.elements.should.have.length(1);
members.elements[0].name.should.equal(creator.name);
});
it('nested team created', async () => {
const creator = user.user1;
let _team = teamSpec('nested-team');
_team.parent = team.teamWithNoExternalTeam._id;
await teamsService.create(_team, creator, null);
_team = await Team.findOne({ name: 'nested-team' }).exec();
should.exist(_team);
_team.should.be.Object();
_team.parent
.toString()
.should.equal(team.teamWithNoExternalTeam._id.toString());
_team.ancestors.length.should.equal(1);
});
});
describe('update', () => {
it('should update team', async () => {
const updates = {
name: `${team.teamWithNoExternalTeam.name}_updated`
};
const updatedTeam = await teamsService
.update(team.teamWithNoExternalTeam, updates)
.should.be.fulfilled();
// Verify updates were applied on returned object
updatedTeam.name.should.equal(updates.name);
// Verify updates were applied by requerying
const t = await Team.findById(team.teamWithNoExternalTeam._id);
t.name.should.equal(updates.name);
});
});
describe('delete', () => {
it('should delete team, if team has no resources', async () => {
const beforeTeamCount = await Team.countDocuments({});
await teamsService
.delete(team.teamWithNoExternalTeam)
.should.be.fulfilled();
// Verify Team no longer exists
const tResult = await Team.findById(team.teamWithNoExternalTeam._id);
should.not.exist(tResult);
// Verify only one team was deleted
const afterTeamCount = await Team.countDocuments({});
afterTeamCount.should.equal(beforeTeamCount - 1);
// Verify team entry is removed from user
const count = await User.countDocuments({
'teams._id': team.teamWithNoExternalTeam._id
});
count.should.equal(0);
});
it('should reject if team has resources', async () => {
sandbox.stub(teamsService, 'getResourceCount').resolves(1);
await teamsService
.delete(team.teamWithNoExternalTeam)
.should.be.rejectedWith(
new BadRequestError('There are still resources in this team.')
);
// Verify team still exists
const result = await Team.findById(team.teamWithNoExternalTeam._id);
should.exist(result);
// Verify team entry is not removed from user
const uResult = await User.findById(user.explicit._id);
const userTeam = uResult.teams.find((t) =>
t._id.equals(team.teamWithNoExternalTeam._id)
);
should.exist(userTeam);
});
});
describe('search', () => {
beforeEach(async () => {
const teams = [...Array(94).keys()].map((index) => {
return new Team({
name: `Name-${index}`,
description: `Description-${index}`
});
});
await Promise.all(teams.map((team) => team.save()));
});
it('empty search results page returned', async () => {
const queryParams = { size: 10 };
const query = {};
const search = '';
const result = await teamsService.search(
queryParams,
query,
search,
user.user2
);
should.exist(result);
result.totalSize.should.equal(0);
result.pageSize.should.equal(queryParams.size);
result.pageNumber.should.equal(0);
result.totalPages.should.equal(0);
result.elements.should.be.an.Array();
result.elements.length.should.be.equal(0);
});
it('search results page returned', async () => {
const queryParams = { size: 10 };
const query = {};
const search = '';
const result = await teamsService.search(
queryParams,
query,
search,
user.admin
);
should.exist(result);
result.totalSize.should.equal(100);
result.pageSize.should.equal(queryParams.size);
result.pageNumber.should.equal(0);
result.totalPages.should.equal(100 / queryParams.size);
result.elements.should.be.an.Array();
result.elements.length.should.be.equal(queryParams.size);
});
it('filtered search results page returned', async () => {
const queryParams = { size: 10 };
const query = { _id: { $in: [team.teamWithNoExternalTeam._id] } };
const search = '';
const result = await teamsService.search(
queryParams,
query,
search,
user.user1
);
should.exist(result);
result.totalSize.should.equal(1);
result.pageSize.should.equal(queryParams.size);
result.pageNumber.should.equal(0);
result.totalPages.should.equal(1);
result.elements.should.be.an.Array();
result.elements.length.should.be.equal(1);
});
it('check isMember field', async () => {
const queryParams = { size: 100 };
const query = {};
const search = '';
const result = await teamsService.search(
queryParams,
query,
search,
user.user1
);
should.exist(result);
result.totalSize.should.equal(2);
result.pageSize.should.equal(queryParams.size);
result.pageNumber.should.equal(0);
result.totalPages.should.equal(1);
result.elements.should.be.an.Array();
result.elements.length.should.be.equal(2);
const isMemberResults = (
result.elements as unknown as Array<
TeamDocument & {
isMember: boolean;
}
>
)
.filter((element) => element.isMember)
.map((team) => team.name);
// user 1 is only members of these two teams (defined by user setup above)
isMemberResults.length.should.equal(2);
isMemberResults[0].should.equal(team.teamWithNoExternalTeam2.name);
isMemberResults[1].should.equal(team.teamWithNoExternalTeam.name);
const result2 = await teamsService.search(
queryParams,
query,
search,
user.user3
);
should.exist(result2);
result2.totalSize.should.equal(1);
result2.pageSize.should.equal(queryParams.size);
result2.pageNumber.should.equal(0);
result2.totalPages.should.equal(1);
result2.elements.should.be.an.Array();
result2.elements.length.should.be.equal(1);
const isMemberResults2 = (
result2.elements as unknown as Array<
TeamDocument & {
isMember: boolean;
}
>
)
.filter((element) => element.isMember)
.map((team) => team.name);
// user 3 is only members of one of these teams (defined by user setup above)
isMemberResults2.length.should.equal(1);
isMemberResults2[0].should.equal(team.teamWithNoExternalTeam.name);
});
});
describe('addMemberToTeam', () => {
it('adds user to team', async () => {
await teamsService.addMemberToTeam(
user.user1,
team.teamWithNoExternalTeam,
TeamRoles.Member
);
const uResult = await User.findById(user.user1);
const userTeam = uResult.teams.find((t) =>
t._id.equals(team.teamWithNoExternalTeam._id)
);
should.exist(userTeam);
userTeam.role.should.equal(TeamRoles.Member);
});
});
describe('updateMemberRole', () => {
it('update role', async () => {
await teamsService.updateMemberRole(
user.explicit,
team.teamWithNoExternalTeam,
TeamRoles.Admin
);
const uResult = await User.findById(user.explicit._id);
const userTeam = uResult.teams.find((t) =>
t._id.equals(team.teamWithNoExternalTeam._id)
);
should.exist(userTeam);
userTeam.role.should.equal(TeamRoles.Admin);
});
it('downgrade admin role; succeed if team has other admins', async () => {
await teamsService.addMemberToTeam(
user.user2,
team.teamWithNoExternalTeam,
TeamRoles.Admin
);
await teamsService
.updateMemberRole(
user.user3,
team.teamWithNoExternalTeam,
TeamRoles.Member
)
.should.be.fulfilled();
});
it('downgrade admin role; reject if team has no other admins', async () => {
await teamsService
.updateMemberRole(
user.user3,
team.teamWithNoExternalTeam,
TeamRoles.Member
)
.should.be.rejectedWith(
new BadRequestError('Team must have at least one admin')
);
});
});
describe('removeMemberFromTeam', () => {
it('remove admin user; succeed if team has other admins', async () => {
await teamsService.addMemberToTeam(
user.user2,
team.teamWithNoExternalTeam,
TeamRoles.Admin
);
await teamsService
.removeMemberFromTeam(user.user3, team.teamWithNoExternalTeam)
.should.be.fulfilled();
});
it('remove admin user; reject if team has no other admins', async () => {
await teamsService
.removeMemberFromTeam(user.user3, team.teamWithNoExternalTeam)
.should.be.rejectedWith(
new BadRequestError('Team must have at least one admin')
);
});
});
describe('meetsRequiredExternalTeams', () => {
it('meetsRequiredExternalTeams', () => {
let _user = new User({ bypassAccessCheck: true });
let _team = new Team({});
let match = teamsService.meetsRequiredExternalTeams(_user, _team);
match.should.equal(true);
_user = new User({ bypassAccessCheck: false });
_team = new Team({});
match = teamsService.meetsRequiredExternalTeams(_user, _team);
match.should.equal(false);
_user = new User({ bypassAccessCheck: false });
_team = new Team({ requiresExternalTeams: ['one'] });
match = teamsService.meetsRequiredExternalTeams(_user, _team);
match.should.equal(false);
_user = new User({ bypassAccessCheck: false, externalGroups: ['two'] });
_team = new Team({ requiresExternalTeams: ['one'] });
match = teamsService.meetsRequiredExternalTeams(_user, _team);
match.should.equal(false);
_user = new User({ bypassAccessCheck: false, externalGroups: ['one'] });
_team = new Team({ requiresExternalTeams: ['one'] });
match = teamsService.meetsRequiredExternalTeams(_user, _team);
match.should.equal(true);
_user = new User({ bypassAccessCheck: false, externalGroups: ['two'] });
_team = new Team({ requiresExternalTeams: ['one', 'two'] });
match = teamsService.meetsRequiredExternalTeams(_user, _team);
match.should.equal(true);
_user = new User({
bypassAccessCheck: false,
externalGroups: ['two', 'four']
});
_team = new Team({ requiresExternalTeams: ['one', 'two'] });
match = teamsService.meetsRequiredExternalTeams(_user, _team);
match.should.equal(true);
_user = new User({
bypassAccessCheck: false,
externalGroups: ['two', 'four']
});
_team = new Team({ requiresExternalTeams: ['four', 'one', 'two'] });
match = teamsService.meetsRequiredExternalTeams(_user, _team);
match.should.equal(true);
_user = new User({ bypassAccessCheck: false, externalGroups: ['two'] });
_team = new Team({ requiresExternalTeams: [] });
match = teamsService.meetsRequiredExternalTeams(_user, _team);
match.should.equal(false);
});
});
describe('meetsRequiredExternalRoles', () => {
it('meetsRequiredExternalRoles', () => {
let _user = new User({});
let _team = new Team({});
let match = teamsService.meetsRequiredExternalRoles(_user, _team);
match.should.equal(false);
_user = new User({});
_team = new Team({ requiresExternalRoles: ['one'] });
match = teamsService.meetsRequiredExternalRoles(_user, _team);
match.should.equal(false);
_user = new User({ externalRoles: ['two'] });
_team = new Team({ requiresExternalRoles: ['one'] });
match = teamsService.meetsRequiredExternalRoles(_user, _team);
match.should.equal(false);
_user = new User({ externalRoles: ['one'] });
_team = new Team({ requiresExternalRoles: ['one'] });
match = teamsService.meetsRequiredExternalRoles(_user, _team);
match.should.equal(true);
_user = new User({ externalRoles: ['two'] });
_team = new Team({ requiresExternalRoles: ['one', 'two'] });
match = teamsService.meetsRequiredExternalRoles(_user, _team);
match.should.equal(false);
_user = new User({ externalRoles: ['one', 'two', 'three'] });
_team = new Team({ requiresExternalRoles: ['one', 'two'] });
match = teamsService.meetsRequiredExternalRoles(_user, _team);
match.should.equal(true);
_user = new User({ externalRoles: ['two', 'four'] });
_team = new Team({ requiresExternalRoles: ['four', 'one', 'two'] });
match = teamsService.meetsRequiredExternalRoles(_user, _team);
match.should.equal(false);
_user = new User({ externalRoles: ['two'] });
_team = new Team({ requiresExternalRoles: [] });
match = teamsService.meetsRequiredExternalRoles(_user, _team);
match.should.equal(false);
});
});
describe('isImplicitMember', () => {
describe('strategy = roles', () => {
beforeEach(() => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.implicitMembers.enabled').returns(true);
configGetStub
.withArgs('teams.implicitMembers.strategy')
.returns('roles');
configGetStub.callThrough();
});
it('should not match when user.externalRoles and team.requiresExternalRoles are undefined', () => {
const _user = new User({});
const _team = new Team({});
const match = teamsService.isImplicitMember(_user, _team);
match.should.equal(false);
});
it('should not match when team does not have requiresExternalRoles', () => {
const _user = new User({ externalRoles: ['one', 'two', 'three'] });
let _team = new Team({});
let match = teamsService.isImplicitMember(_user, _team);
match.should.equal(false);
_team = new Team({ requiresExternalRoles: [] });
match = teamsService.isImplicitMember(_user, _team);
match.should.equal(false);
});
it('should match when user has required external roles', () => {
const _user = new User({ externalRoles: ['one', 'two', 'three'] });
let _team = new Team({ requiresExternalRoles: ['one'] });
let match = teamsService.isImplicitMember(_user, _team);
match.should.equal(true);
_team = new Team({ requiresExternalRoles: ['one', 'two'] });
match = teamsService.isImplicitMember(_user, _team);
match.should.equal(true);
_team = new Team({ requiresExternalRoles: ['one', 'three'] });
match = teamsService.isImplicitMember(_user, _team);
match.should.equal(true);
});
});
describe('strategy = teams', () => {
beforeEach(() => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.implicitMembers.enabled').returns(true);
configGetStub
.withArgs('teams.implicitMembers.strategy')
.returns('teams');
configGetStub.callThrough();
});
it('should not match when user.externalRoles and team.requiresExternalTeams are undefined', () => {
const _user = new User({});
const _team = new Team({});
const match = teamsService.isImplicitMember(_user, _team);
match.should.equal(false);
});
it('should not match when team does not have requiresExternalTeams', () => {
const _user = new User({ externalGroups: ['one', 'two', 'three'] });
let _team = new Team({});
let match = teamsService.isImplicitMember(_user, _team);
match.should.equal(false);
_team = new Team({ requiresExternalTeams: [] });
match = teamsService.isImplicitMember(_user, _team);
match.should.equal(false);
});
it('should match when user has required external teams', () => {
let _user = new User({ externalGroups: ['one'] });
const _team = new Team({
requiresExternalTeams: ['one', 'two', 'three']
});
let match = teamsService.isImplicitMember(_user, _team);
match.should.equal(true);
_user = new User({ externalGroups: ['two'] });
match = teamsService.isImplicitMember(_user, _team);
match.should.equal(true);
_user = new User({ externalGroups: ['three'] });
match = teamsService.isImplicitMember(_user, _team);
match.should.equal(true);
});
});
describe('enabled = false', () => {
beforeEach(() => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.implicitMembers.enabled').returns(false);
configGetStub.callThrough();
});
it('should not match any since disabled', () => {
let _user = new User({});
let _team = new Team({});
let match = teamsService.isImplicitMember(_user, _team);
match.should.equal(false);
_user = new User({
externalRoles: ['one', 'two', 'three'],
externalGroups: ['one', 'two', 'three']
});
match = teamsService.isImplicitMember(_user, _team);
match.should.equal(false);
_team = new Team({
requiresExternalRoles: [],
requiresExternalGroups: []
});
match = teamsService.isImplicitMember(_user, _team);
match.should.equal(false);
_team = new Team({ requiresExternalRoles: ['one'] });
match = teamsService.isImplicitMember(_user, _team);
match.should.equal(false);
_team = new Team({ requiresExternalRoles: ['one', 'two'] });
match = teamsService.isImplicitMember(_user, _team);
match.should.equal(false);
_team = new Team({ requiresExternalRoles: ['one', 'three'] });
match = teamsService.isImplicitMember(_user, _team);
match.should.equal(false);
});
});
});
describe('sendRequestEmail', () => {
const toEmails = ['email1@server.com', 'email2@server.com'];
it('should create mailOptions properly', async () => {
const sendMailStub = sandbox.stub(emailService, 'sendMail');
const _user = new User({
name: 'test',
username: 'test',
email: 'test@test.test'
});
const _team = new Team({
_id: '12345',
name: 'test team'
});
const expectedEmailContent = `HEADER
<p>Hey there <strong>${_team.name}</strong> Admin,</p>
<p>A user named <strong>${_user.name}</strong> with username <strong>${
_user.username
}</strong> has requested access to the team.</p>
<p>Click <a href="${config.get('app.clientUrl')}/team/${
_team._id
}">here</a> to give them access!</p>
FOOTER
`;
await teamsService.sendRequestEmail(toEmails, _user, _team, {});
assert.called(sendMailStub);
const [mailOptions] = sendMailStub.getCall(0).args;
should.exist(mailOptions, 'expected mailOptions to exist');
for (const key of ['bcc', 'from', 'replyTo', 'subject', 'html']) {
should.exist(mailOptions[key], `expected mailOptions.${key} to exist`);
}
mailOptions.bcc.should.be.Array();
mailOptions.bcc.length.should.equal(2);
mailOptions.bcc[0].should.equal(toEmails[0]);
mailOptions.bcc[1].should.equal(toEmails[1]);
mailOptions.from.should.equal(config.get('coreEmails.default.from'));
mailOptions.replyTo.should.equal(
config.get('coreEmails.default.replyTo')
);
mailOptions.subject.should.equal(
`${config.get('app.title')}: A user has requested access to Team ${
_team.name
}`
);
mailOptions.html.should.equal(expectedEmailContent);
});
it('should fail silently and log error', async () => {
sandbox.stub(emailService, 'sendMail').throws('error');
const logStub = sandbox.stub(logger, 'error');
await teamsService.sendRequestEmail(
toEmails,
user.user1,
team.teamWithNoExternalTeam,
{}
);
assert.calledOnce(logStub);
const [message] = logStub.getCall(0).args;
should(message).equal('Failure sending email.');
});
});
describe('requestAccessToTeam', () => {
it('should reject if no team admins are found', async () => {
await teamsService
.requestAccessToTeam(user.admin, team.teamWithNoExternalTeam2, {})
.should.be.rejectedWith(
new InternalServerError('Error retrieving team admins')
);
const requesterCount = await User.countDocuments({
teams: {
$elemMatch: {
_id: new mongoose.Types.ObjectId(team.teamWithNoExternalTeam2._id),
role: 'requester'
}
}
}).exec();
requesterCount.should.equal(0);
});
it('should work', async () => {
await teamsService
.requestAccessToTeam(user.admin, team.teamWithNoExternalTeam, {})
.should.be.fulfilled();
const requesterCount = await User.countDocuments({
teams: {
$elemMatch: {
_id: new mongoose.Types.ObjectId(team.teamWithNoExternalTeam._id),
role: 'requester'
}
}
}).exec();
requesterCount.should.equal(1);
});
});
describe('requestNewTeam', () => {
const _user = new User({
name: 'test',
username: 'test',
email: 'test@test.test'
});
it('should properly reject invalid parameters', async () => {
await teamsService
.requestNewTeam(null, null, null, null, null)
.should.be.rejectedWith(
new BadRequestError('Organization cannot be empty')
);
await teamsService
.requestNewTeam('org', null, null, null, null)
.should.be.rejectedWith(new BadRequestError('AOI cannot be empty'));
await teamsService
.requestNewTeam('org', 'aoi', null, null, null)
.should.be.rejectedWith(
new BadRequestError('Description cannot be empty')
);
await teamsService
.requestNewTeam('org', 'aoi', 'description', null, null)
.should.be.rejectedWith(new BadRequestError('Invalid requester'));
});
it('should create mailOptions properly', async () => {
const sendMailStub = sandbox.stub(emailService, 'sendMail');
const expectedEmailContent = `HEADER
<p>Hey there ${config.get('app.title')} Admins,</p>
<p>A user named <strong>${_user.name}</strong> with username <strong>${
_user.username
}</strong> has requested a new team:</p>
<p>
\t<strong>Organization:</strong> org<br/>
\t<strong>AOI:</strong> aoi<br/>
\t<strong>Description:</strong> description<br/>
</p>
<p>Click <a href="${config.get(
'app.clientUrl'
)}/team/create">here</a> to create this team!</p>
FOOTER
`;
await teamsService.requestNewTeam('org', 'aoi', 'description', _user, {
headers: {}
});
assert.called(sendMailStub);
const [mailOptions] = sendMailStub.getCall(0).args;
should.exist(mailOptions, 'expected mailOptions to exist');
for (const key of ['bcc', 'from', 'replyTo', 'subject', 'html']) {
should.exist(mailOptions[key], `expected mailOptions.${key} to exist`);
}
mailOptions.bcc.should.equal(config.get('coreEmails.newTeamRequest.bcc'));
mailOptions.from.should.equal(config.get('coreEmails.default.from'));
mailOptions.replyTo.should.equal(
config.get('coreEmails.default.replyTo')
);
mailOptions.subject.should.equal('New Team Requested');
mailOptions.html.should.equal(expectedEmailContent);
});
it('should fail silently and log error', async () => {
sandbox.stub(emailService, 'sendMail').throws('error');
const logStub = sandbox.stub(logger, 'error');
await teamsService.requestNewTeam(
'org',
'aoi',
'description',
user.user1,
{ headers: {} }
);
assert.calledOnce(logStub);
const [message] = logStub.getCall(0).args;
should(message).equal('Failure sending email.');
});
});
describe('getImplicitTeamIds', () => {
describe('strategy = roles', () => {
beforeEach(() => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.implicitMembers.enabled').returns(true);
configGetStub
.withArgs('teams.implicitMembers.strategy')
.returns('roles');
configGetStub.callThrough();
});
it('reject for non-existent user', async () => {
await teamsService
.getImplicitTeamIds(null)
.should.be.rejectedWith(
new InternalServerError('User does not exist')
);
});
it('should find implicit teams for user with matching external roles (model object)', async () => {
const teamIds = await teamsService.getImplicitTeamIds(user.implicit2);
should.exist(teamIds);
teamIds.should.be.Array();
teamIds.length.should.equal(2);
});
it('should find implicit teams for user with matching external roles (plain object)', async () => {
const teamIds = await teamsService.getImplicitTeamIds(user.implicit2);
should.exist(teamIds);
teamIds.should.be.Array();
teamIds.length.should.equal(2);
});
it('should not find implicit teams for user without matching external roles', async () => {
const _user = await User.findOne({
username: 'implicit1_username'
}).exec();
should.exist(_user, 'expected implicit1 to exist');
_user.username.should.equal('implicit1_username');
const teamIds = await teamsService.getImplicitTeamIds(_user);
should.exist(teamIds);
teamIds.should.be.Array();
teamIds.should.be.empty();
});
it('should not find implicit teams for user with matching external roles, but is explicitly blocked', async () => {
const _user = await User.findOne({
username: 'blocked_username'
}).exec();
should.exist(_user, 'expected blocked to exist');
_user.username.should.equal('blocked_username');
const teamIds = await teamsService.getImplicitTeamIds(_user);
should.exist(teamIds);
teamIds.should.be.Array();
teamIds.should.be.empty();
});
});
describe('strategy = teams;', () => {
beforeEach(() => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.implicitMembers.enabled').returns(true);
configGetStub
.withArgs('teams.implicitMembers.strategy')
.returns('teams');
configGetStub.callThrough();
});
it('should find implicit teams for user with matching external teams', async () => {
const _user = await User.findOne({
username: 'implicit1_username'
}).exec();
should.exist(_user, 'expected implicit1 to exist');
_user.username.should.equal('implicit1_username');
const teamIds = await teamsService.getImplicitTeamIds(_user);
should.exist(teamIds);
teamIds.should.be.Array();
teamIds.length.should.equal(1);
});
it('should not find implicit teams for user without matching external teams', async () => {
const _user = await User.findOne({
username: 'implicit2_username'
}).exec();
should.exist(_user, 'expected user2 to exist');
_user.username.should.equal('implicit2_username');
const teamIds = await teamsService.getImplicitTeamIds(_user);
should.exist(teamIds);
teamIds.should.be.Array();
teamIds.should.be.empty();
});
});
describe('enabled = false;', () => {
beforeEach(() => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.implicitMembers.enabled').returns(false);
configGetStub.callThrough();
});
it('should not find implicit teams for users with matching external roles/teams if disabled', async () => {
const user1 = await User.findOne({ username: 'user1_username' }).exec();
should.exist(user1, 'expected user1 to exist');
user1.username.should.equal('user1_username');
const user2 = await User.findOne({ username: 'user2_username' }).exec();
should.exist(user2, 'expected user2 to exist');
user2.username.should.equal('user2_username');
let teamIds = await teamsService.getImplicitTeamIds(user1);
should.exist(teamIds);
teamIds.should.be.Array();
teamIds.should.be.empty();
teamIds = await teamsService.getImplicitTeamIds(user2);
should.exist(teamIds);
teamIds.should.be.Array();
teamIds.should.be.empty();
});
});
});
describe('Nested Teams', () => {
beforeEach(async () => {
spec.nestedTeam.nestedTeam1 = teamSpec('nested-team-1');
spec.nestedTeam.nestedTeam2 = teamSpec('nested-team-2');
spec.nestedTeam.nestedTeam3 = teamSpec('nested-team-3');
spec.nestedTeam.nestedTeam1_1 = teamSpec('nested-team-1-1');
spec.nestedTeam.nestedTeam1_2 = teamSpec('nested-team-1-2');
spec.nestedTeam.nestedTeam2_1 = teamSpec('nested-team-2-1');
// Create nested teams
let t = new Team({
...spec.nestedTeam.nestedTeam1,
parent: team.teamWithNoExternalTeam._id,
ancestors: [team.teamWithNoExternalTeam._id]
});
team.nestedTeam1 = await t.save();
t = new Team({
...spec.nestedTeam.nestedTeam2,
parent: team.teamWithNoExternalTeam._id,
ancestors: [team.teamWithNoExternalTeam._id]
});
team.nestedTeam2 = await t.save();
t = new Team({
...spec.nestedTeam.nestedTeam3,
parent: team.teamWithNoExternalTeam._id,
ancestors: [team.teamWithNoExternalTeam._id]
});
team.nestedTeam3 = await t.save();
t = new Team({
...spec.nestedTeam.nestedTeam1_1,
parent: team.nestedTeam1._id,
ancestors: [team.teamWithNoExternalTeam._id, team.nestedTeam1._id]
});
team.nestedTeam1_1 = await t.save();
t = new Team({
...spec.nestedTeam.nestedTeam1_2,
parent: team.nestedTeam1._id,
ancestors: [team.teamWithNoExternalTeam._id, team.nestedTeam1._id]
});
team.nestedTeam1_2 = await t.save();
t = new Team({
...spec.nestedTeam.nestedTeam2_1,
parent: team.nestedTeam2._id,
ancestors: [team.teamWithNoExternalTeam._id, team.nestedTeam2._id]
});
team['nestedTeam2_1'] = await t.save();
});
describe('getTeamRole', () => {
it('Should get team role for root team', () => {
const role = teamsService.getActiveTeamRole(
user.user1,
team.teamWithNoExternalTeam
);
should.exist(role);
role.should.equal(TeamRoles.Member);
});
it('Should get team role for nested team when nested teams are enabled', () => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.nestedTeams').returns(true);
configGetStub.callThrough();
const role = teamsService.getActiveTeamRole(
user.user1,
team.nestedTeam2_1
);
should.exist(role);
role.should.equal(TeamRoles.Member);
});
it('Should not get team role for nested team when nested teams are disabled', () => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.nestedTeams').returns(false);
configGetStub.callThrough();
const role = teamsService.getActiveTeamRole(
user.user1,
team.nestedTeam2_1
);
should.not.exist(role);
});
it('Should not get team role for nested team', () => {
const role = teamsService.getActiveTeamRole(
user.user2,
team.nestedTeam1
);
should.not.exist(role);
});
});
describe('getNestedTeamIds', () => {
it('return empty array if nestedTeams is disabled in config', async () => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.nestedTeams').returns(false);
configGetStub.callThrough();
const nestedTeamIds = await teamsService.getNestedTeamIds([
team.teamWithNoExternalTeam._id
]);
should.exist(nestedTeamIds);
nestedTeamIds.should.be.Array();
nestedTeamIds.length.should.equal(0);
});
it('undefined parent teams', async () => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.nestedTeams').returns(true);
configGetStub.callThrough();
const nestedTeamIds = await teamsService.getNestedTeamIds();
should.exist(nestedTeamIds);
nestedTeamIds.should.be.Array();
nestedTeamIds.length.should.equal(0);
});
it('empty parent teams array', async () => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.nestedTeams').returns(true);
configGetStub.callThrough();
const nestedTeamIds = await teamsService.getNestedTeamIds([]);
should.exist(nestedTeamIds);
nestedTeamIds.should.be.Array();
nestedTeamIds.length.should.equal(0);
});
it('default/all roles', async () => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.nestedTeams').returns(true);
configGetStub.callThrough();
const nestedTeamIds = await teamsService.getNestedTeamIds([
team.teamWithNoExternalTeam._id
]);
should.exist(nestedTeamIds);
nestedTeamIds.should.be.Array();
nestedTeamIds.length.should.equal(6);
});
it('explicitly pass "member" role', async () => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.nestedTeams').returns(true);
configGetStub.callThrough();
const nestedTeamIds = await teamsService.getNestedTeamIds([
team.nestedTeam1._id
]);
should.exist(nestedTeamIds);
nestedTeamIds.should.be.Array();
nestedTeamIds.length.should.equal(2);
});
});
describe('getAncestorTeamIds', () => {
it('should return team ancestors', async () => {
const ancestors = await teamsService.getAncestorTeamIds([
team.nestedTeam2_1._id
]);
ancestors.should.deepEqual([
team.teamWithNoExternalTeam._id,
team.nestedTeam2._id
]);
});
it('should return empty array for team without ancestors', async () => {
const ancestors = await teamsService.getAncestorTeamIds([
team.teamWithNoExternalTeam._id
]);
ancestors.should.deepEqual([]);
});
it('should return empty array when no teams are passed in', async () => {
const ancestors = await teamsService.getAncestorTeamIds();
ancestors.should.deepEqual([]);
});
});
describe('updateTeams', () => {
it('implicit members disabled; nested teams disabled', async () => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.implicitMembers.enabled').returns(false);
configGetStub.withArgs('teams.nestedTeams').returns(false);
configGetStub.callThrough();
const user = new User({
teams: [
{ role: TeamRoles.Member, _id: team.teamWithNoExternalTeam._id },
{ role: 'editor', _id: team.nestedTeam1._id },
{ role: TeamRoles.Admin, _id: team.nestedTeam2._id }
],
externalRoles: ['external-role']
});
await teamsService.updateTeams(user);
user.teams.length.should.equal(3);
});
it('implicit members enabled; nested teams disabled', async () => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.implicitMembers.enabled').returns(true);
configGetStub
.withArgs('teams.implicitMembers.strategy')
.returns('roles');
configGetStub.withArgs('teams.nestedTeams').returns(false);
configGetStub.callThrough();
const user = new User({
teams: [
{ role: TeamRoles.Member, _id: team.teamWithNoExternalTeam._id },
{ role: 'editor', _id: team.nestedTeam1._id },
{ role: TeamRoles.Admin, _id: team.nestedTeam2._id }
],
externalRoles: ['external-role']
});
await teamsService.updateTeams(user);
user.teams.length.should.equal(4);
});
it('implicit members disabled; nested teams enabled', async () => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.implicitMembers.enabled').returns(false);
configGetStub.withArgs('teams.nestedTeams').returns(true);
configGetStub.callThrough();
const user = new User({
teams: [
{ role: TeamRoles.Member, _id: team.teamWithNoExternalTeam._id },
{ role: 'editor', _id: team.nestedTeam1._id },
{ role: TeamRoles.Admin, _id: team.nestedTeam2._id }
],
externalRoles: ['external-role']
});
await teamsService.updateTeams(user);
user.teams.length.should.equal(7);
});
it('implicit members enabled; nested teams enabled', async () => {
const configGetStub = sandbox.stub(config, 'get');
configGetStub.withArgs('teams.implicitMembers.enabled').returns(true);
configGetStub
.withArgs('teams.implicitMembers.strategy')
.returns('roles');
configGetStub.withArgs('teams.nestedTeams').returns(true);
configGetStub.callThrough();
const user = new User({
teams: [
{ role: TeamRoles.Member, _id: team.teamWithNoExternalTeam._id },
{ role: 'editor', _id: team.nestedTeam1._id },
{ role: TeamRoles.Admin, _id: team.nestedTeam2._id }
],
externalRoles: ['external-role']
});
await teamsService.updateTeams(user);
user.teams.length.should.equal(8);
});
});
});
describe('getExplicitTeamIds', () => {
const _user = new User({
teams: [
{
_id: '000000000000000000000001',
role: TeamRoles.Member
},
{
_id: '000000000000000000000002',
role: TeamRoles.Member
},
{
_id: '000000000000000000000003',
role: 'editor'
},
{
_id: '000000000000000000000004',
role: TeamRoles.Admin
},
{
_id: '000000000000000000000005',
role: 'editor'
}
]
});
it('should reject for non-existent user', async () => {
await teamsService
.getExplicitTeamIds(null)
.should.be.rejectedWith(new InternalServerError('User does not exist'));
});
it('should return no teams for user with empty teams array', async () => {
const teamIds = await teamsService.getExplicitTeamIds(
new User({ teams: [] })
);
should.exist(teamIds, 'expected teamIds to exist');
teamIds.length.should.equal(0);
});
it('should return no teams for user with no teams array', async () => {
const teamIds = await teamsService.getExplicitTeamIds(new User());
should.exist(teamIds, 'expected teamIds to exist');
teamIds.length.should.equal(0);
});
it('should return all team ids when roles is not specified', async () => {
const teamIds = await teamsService.getExplicitTeamIds(_user);
should.exist(teamIds, 'expected teamIds to exist');
teamIds.length.should.equal(5);
for (let i = 0; i < teamIds.length; i++) {
teamIds[i].toString().should.equal(_user.teams[i]._id.toString());
}
});
it('should return only team ids where user is a member', async () => {
const teamIds = await teamsService.getExplicitTeamIds(
_user,
TeamRoles.Member
);
should.exist(teamIds, 'expected teamIds to exist');
teamIds.length.should.equal(2);
teamIds[0].toString().should.equal('000000000000000000000001');
teamIds[1].toString().should.equal('000000000000000000000002');
});
it('should return only team ids where user is an editor', async () => {
const teamIds = await teamsService.getExplicitTeamIds(
_user,
TeamRoles.Editor
);
should.exist(teamIds, 'expected teamIds to exist');
teamIds.length.should.equal(2);
teamIds[0].toString().should.equal('000000000000000000000003');
teamIds[1].toString().should.equal('000000000000000000000005');
});
it('should return only team ids where user is an admin', async () => {
const teamIds = await teamsService.getExplicitTeamIds(
_user,
TeamRoles.Admin
);
should.exist(teamIds, 'expected teamIds to exist');
teamIds.length.should.equal(1);
teamIds[0].toString().should.equal('000000000000000000000004');
});
});
describe('getTeamIds', () => {
const _user = new User({
teams: [
{
_id: '000000000000000000000001',
role: TeamRoles.Member
},
{
_id: '000000000000000000000002',
role: TeamRoles.Member
},
{
_id: '000000000000000000000003',
role: 'editor'
},
{
_id: '000000000000000000000004',
role: TeamRoles.Admin
},
{
_id: '000000000000000000000005',
role: 'editor'
}
]
});
it('should find all team members', async () => {
const teamIds = await teamsService.getTeamIds(
_user,
...teamsService.getRoles(TeamRoles.Member)
);
should.exist(teamIds);
teamIds.should.have.length(5);
});
it('should find all team editors', async () => {
const teamIds = await teamsService.getTeamIds(
_user,
...teamsService.getRoles(TeamRoles.Editor)
);
should.exist(teamIds);
teamIds.should.have.length(3);
});
it('should find all team admins', async () => {
const teamIds = await teamsService.getTeamIds(
_user,
...teamsService.getRoles(TeamRoles.Admin)
);
should.exist(teamIds);
teamIds.should.have.length(1);
});
it('should not', async () => {
const teamIds = await teamsService.getTeamIds(
user.blocked,
...teamsService.getRoles(TeamRoles.Member)
);
should.exist(teamIds);
teamIds.should.have.length(0);
});
});
describe('filterTeamIds', () => {
const _user = new User({
teams: [
{
_id: new mongoose.Types.ObjectId('000000000000000000000001'),
role: TeamRoles.Member
},
{
_id: new mongoose.Types.ObjectId('000000000000000000000002'),
role: TeamRoles.Member
},
{
_id: new mongoose.Types.ObjectId('000000000000000000000003'),
role: 'editor'
},
{
_id: new mongoose.Types.ObjectId('000000000000000000000004'),
role: TeamRoles.Admin
},
{
_id: new mongoose.Types.ObjectId('000000000000000000000005'),
role: 'editor'
}
]
});
it('should filter teamIds for membership (basic)', async () => {
const teamIds = await teamsService.filterTeamIds(_user, [
new mongoose.Types.ObjectId('000000000000000000000001')
]);
should.exist(teamIds);
teamIds.should.have.length(1);
should(teamIds[0].toString()).equal('000000000000000000000001');
});
it('should filter teamIds for membership (advanced)', async () => {
const teamIds = await teamsService.filterTeamIds(_user, [
new mongoose.Types.ObjectId('000000000000000000000001'),
new mongoose.Types.ObjectId('000000000000000000000002')
]);
should.exist(teamIds);
teamIds.should.have.length(2);
should(teamIds[0].toString()).equal('000000000000000000000001');
should(teamIds[1].toString()).equal('000000000000000000000002');
});
it('should filter teamIds for membership when no access', async () => {
const teamIds = await teamsService.filterTeamIds(_user, [
new mongoose.Types.ObjectId('000000000000000000000006')
]);
should.exist(teamIds);
teamIds.should.have.length(0);
});
it('should filter teamIds for membership when no filter', async () => {
const teamIds = await teamsService.filterTeamIds(_user);
should.exist(teamIds);
teamIds.should.have.length(5);
should(teamIds[0].toString()).equal('000000000000000000000001');
should(teamIds[1].toString()).equal('000000000000000000000002');
should(teamIds[2].toString()).equal('000000000000000000000003');
should(teamIds[3].toString()).equal('000000000000000000000004');
should(teamIds[4].toString()).equal('000000000000000000000005');
});
});
});