Skip to content

Commit

Permalink
Merge pull request #3717 from 3drepo/ISSUE_3699
Browse files Browse the repository at this point in the history
ISSUE #3699 - Endpoint to get avatar of teamspace member
  • Loading branch information
carmenfan committed Nov 1, 2022
2 parents 9e4e977 + 35da842 commit f401057
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 6 deletions.
14 changes: 14 additions & 0 deletions backend/src/v5/middleware/dataConverter/inputs/teamspaces/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,18 @@ Teamspaces.canRemoveTeamspaceMember = async (req, res, next) => {
await next();
};

Teamspaces.memberExists = async (req, res, next) => {
const { params } = req;
const { teamspace, member } = params;
try {
if (await hasAccessToTeamspace(teamspace, member)) {
await next();
} else {
throw templates.userNotFound;
}
} catch (err) {
respond(req, res, err);
}
};

module.exports = Teamspaces;
48 changes: 47 additions & 1 deletion backend/src/v5/routes/teamspaces/teamspaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

const { canRemoveTeamspaceMember, memberExists } = require('../../middleware/dataConverter/inputs/teamspaces');
const { Router } = require('express');
const Teamspaces = require('../../processors/teamspaces/teamspaces');
const { canRemoveTeamspaceMember } = require('../../middleware/dataConverter/inputs/teamspaces');
const { fileExtensionFromBuffer } = require('../../utils/helper/typeCheck');
const { hasAccessToTeamspace } = require('../../middleware/permissions/permissions');
const { isTeamspaceAdmin } = require('../../middleware/permissions/permissions');
Expand Down Expand Up @@ -55,6 +55,19 @@ const getAvatar = async (req, res) => {
}
};

const getTeamspaceMemberAvatar = async (req, res) => {
try {
const { member } = req.params;
const buffer = await Teamspaces.getAvatar(member);
const fileExt = await fileExtensionFromBuffer(buffer);
req.params.format = fileExt || 'png';
respond(req, res, templates.ok, buffer);
} catch (err) {
/* istanbul ignore next */
respond(req, res, err);
}
};

const getQuotaInfo = async (req, res) => {
const { teamspace } = req.params;

Expand Down Expand Up @@ -283,6 +296,39 @@ const establishRoutes = () => {
*/
router.delete('/:teamspace/members/:username', hasAccessToTeamspace, canRemoveTeamspaceMember, removeTeamspaceMember);

/**
* @openapi
* /teamspaces/{teamspace}/members/{member}/avatar:
* get:
* description: Gets the avatar of a member of the teamspace
* tags: [Teamspaces]
* parameters:
* - name: teamspace
* description: name of teamspace
* in: path
* required: true
* schema:
* type: string
* - name: member
* description: username of teamspace member
* in: path
* required: true
* schema:
* type: string
* operationId: getTeamspaceMemberAvatar
* responses:
* 401:
* $ref: "#/components/responses/notLoggedIn"
* 200:
* description: Gets the avatar of a member of a teamspace
* content:
* image/png:
* schema:
* type: string
* format: binary
*/
router.get('/:teamspace/members/:member/avatar', hasAccessToTeamspace, memberExists, getTeamspaceMemberAvatar);

return router;
};

Expand Down
50 changes: 46 additions & 4 deletions backend/tests/v5/e2e/routes/teamspaces/teamspaces.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const usersInFirstTeamspace = [
ServiceHelper.generateUserCredentials(),
];
const userWithFsAvatar = ServiceHelper.generateUserCredentials();
const userWithNoAvatar = ServiceHelper.generateUserCredentials();
const userWithGridFsAvatar = ServiceHelper.generateUserCredentials();

// This is the list of teamspaces the user has access to
Expand Down Expand Up @@ -188,6 +189,10 @@ const setupData = async () => {
userWithFsAvatar,
[tsWithFsAvatar.name],
),
ServiceHelper.db.createUser(
userWithNoAvatar,
[tsWithGridFsAvatar.name],
),
ServiceHelper.db.createUser(
userWithGridFsAvatar,
[tsWithGridFsAvatar.name],
Expand Down Expand Up @@ -262,6 +267,7 @@ const testGetTeamspaceMembers = () => {

return data;
});
expectedData.push({ user: teamspaces[0].name });
expect(res.body.members.length).toBe(expectedData.length);
expect(res.body.members).toEqual(expect.arrayContaining(expectedData));
});
Expand Down Expand Up @@ -342,7 +348,7 @@ const testGetQuotaInfo = () => {
expiryDate: validExpiryDate,
freeTier: false,
data: { used: 0, available: spaceLimitInBytes },
seats: { used: 2, available: collaboratorLimit },
seats: { used: 3, available: collaboratorLimit },
},
);
});
Expand All @@ -358,7 +364,7 @@ const testGetQuotaInfo = () => {
expiryDate: validExpiryDate - 10,
freeTier: false,
data: { used: 0, available: spaceLimitInBytes },
seats: { used: 1, available: collaboratorLimit },
seats: { used: 2, available: collaboratorLimit },
},
);
});
Expand All @@ -372,7 +378,7 @@ const testGetQuotaInfo = () => {
expiryDate: validExpiryDate,
freeTier: false,
data: { used: 0, available: spaceLimitInBytes },
seats: { used: 1, available: 'unlimited' },
seats: { used: 2, available: 'unlimited' },
},
);
});
Expand All @@ -386,7 +392,7 @@ const testGetQuotaInfo = () => {
expiryDate: null,
freeTier: true,
data: { used: 0, available: spaceLimitInBytes },
seats: { used: 4, available: config.subscriptions.basic.collaborators },
seats: { used: 5, available: config.subscriptions.basic.collaborators },
},
);
});
Expand Down Expand Up @@ -465,6 +471,41 @@ const testRemoveTeamspaceMember = () => {
});
};

const testGetMemberAvatar = () => {
describe('Get teamspace member avatar', () => {
const route = (ts = tsWithGridFsAvatar.name, member = tsWithGridFsAvatar.name) => `/v5/teamspaces/${ts}/members/${member}/avatar`;
test('should fail without a valid session', async () => {
const res = await agent.get(route()).expect(templates.notLoggedIn.status);
expect(res.body.code).toEqual(templates.notLoggedIn.code);
});

test('should fail if the user does not have access to the teamspace', async () => {
const res = await agent.get(`${route()}/?key=${testUser.apiKey}`).expect(templates.teamspaceNotFound.status);
expect(res.body.code).toEqual(templates.teamspaceNotFound.code);
});

test('should fail the requested user is not a member of the teamspace', async () => {
const res = await agent.get(`${route(tsWithGridFsAvatar.name, testUser.user)}/?key=${userWithGridFsAvatar.apiKey}`).expect(templates.userNotFound.status);
expect(res.body.code).toEqual(templates.userNotFound.code);
});

test('should fail if the teamspace does not exist', async () => {
const res = await agent.get(`${route('sldkfjdl')}/?key=${userWithGridFsAvatar.apiKey}`).expect(templates.teamspaceNotFound.status);
expect(res.body.code).toEqual(templates.teamspaceNotFound.code);
});

test(`should return ${templates.fileNotFound.code} if the member does not have an avatar`, async () => {
const res = await agent.get(`${route(tsWithGridFsAvatar.name, userWithNoAvatar.user)}/?key=${userWithGridFsAvatar.apiKey}`).expect(templates.fileNotFound.status);
expect(res.body.code).toEqual(templates.fileNotFound.code);
});

test('should return member avatar', async () => {
const res = await agent.get(`${route()}/?key=${userWithGridFsAvatar.apiKey}`).expect(templates.ok.status);
expect(res.body).toEqual(Buffer.from(gridFsAvatarData));
});
});
};

describe('E2E routes/teamspaces', () => {
beforeAll(async () => {
server = await ServiceHelper.app();
Expand All @@ -477,4 +518,5 @@ describe('E2E routes/teamspaces', () => {
testGetAvatar();
testGetQuotaInfo();
testRemoveTeamspaceMember();
testGetMemberAvatar();
});
2 changes: 1 addition & 1 deletion backend/tests/v5/helper/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ db.createTeamspaceRole = (ts) => createTeamspaceRole(ts);
db.createTeamspace = (teamspace, admins = [], breaking = false, customData) => {
const permissions = admins.map((adminUser) => ({ user: adminUser, permissions: TEAMSPACE_ADMIN }));
return Promise.all([
ServiceHelper.db.createUser({ user: teamspace, password: teamspace }, [],
ServiceHelper.db.createUser({ user: teamspace, password: teamspace }, [teamspace],
{ permissions: breaking ? undefined : permissions, ...customData }),
ServiceHelper.db.createTeamspaceRole(teamspace),
createTeamspaceSettings(teamspace),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,51 @@ const testCanRemoveTeamspaceMember = () => {
});
};

const testMemberExists = () => {
describe('memberExists', () => {
test('next() should be called if the provided username is member of the teamspace', async () => {
const mockCB = jest.fn(() => {});

await Teamspaces.memberExists(
{ params: { teamspace, member: adminUser } },
{},
mockCB,
);
expect(mockCB).toHaveBeenCalledTimes(1);
expect(Responder.respond).not.toHaveBeenCalled();
expect(TeamspacesModel.hasAccessToTeamspace).toHaveBeenCalledTimes(1);
expect(TeamspacesModel.hasAccessToTeamspace).toHaveBeenCalledWith(teamspace, adminUser);
});

test('should respond with error if hasAccess throws an error', async () => {
const mockCB = jest.fn(() => {});
const req = { params: { teamspace, member: adminUser } };
const err = new Error(generateRandomString());
TeamspacesModel.hasAccessToTeamspace.mockRejectedValueOnce(err);

await Teamspaces.memberExists(req, {}, mockCB);
expect(mockCB).not.toHaveBeenCalled();
expect(TeamspacesModel.hasAccessToTeamspace).toHaveBeenCalledTimes(1);
expect(TeamspacesModel.hasAccessToTeamspace).toHaveBeenCalledWith(teamspace, adminUser);
expect(Responder.respond).toHaveBeenCalledTimes(1);
expect(Responder.respond).toHaveBeenCalledWith(req, {}, err);
});

test(`should respond with ${templates.notAuthorized.code} if the member has no access`, async () => {
const mockCB = jest.fn(() => {});
const req = { params: { teamspace, member: nonTsMemberUser } };

await Teamspaces.memberExists(req, {}, mockCB);
expect(mockCB).not.toHaveBeenCalled();
expect(TeamspacesModel.hasAccessToTeamspace).toHaveBeenCalledTimes(1);
expect(TeamspacesModel.hasAccessToTeamspace).toHaveBeenCalledWith(teamspace, nonTsMemberUser);
expect(Responder.respond).toHaveBeenCalledTimes(1);
expect(Responder.respond).toHaveBeenCalledWith(req, {}, templates.userNotFound);
});
});
};

describe('middleware/dataConverter/inputs/teamspaces', () => {
testCanRemoveTeamspaceMember();
testMemberExists();
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const { templates } = require(`${src}/utils/responseCodes`);

jest.mock('../../../../../../src/v5/utils/sessions');
const Sessions = require(`${src}/utils/sessions`);

const TSMiddlewares = require(`${src}/middleware/permissions/components/teamspaces`);

// Mock respond function to just return the resCode
Expand Down

0 comments on commit f401057

Please sign in to comment.