feat(api): add create user invitation endpoint

This commit is contained in:
Ali BARIN
2025-04-25 16:33:07 +00:00
parent c8adf19c45
commit bdca75590f
8 changed files with 145 additions and 1 deletions

View File

@@ -0,0 +1,23 @@
import { renderObject } from '../../../../helpers/renderer.js';
import User from '../../../../models/user.js';
export default async (request, response) => {
const user = await User.query().insertAndFetch(userParams(request));
await user.sendInvitationEmail();
renderObject(response, user, {
status: 201,
serializer: 'PublicUserInvitation',
});
};
const userParams = (request) => {
const { fullName, email, roleId } = request.body;
return {
fullName,
status: 'invited',
email: email?.toLowerCase(),
roleId,
};
};

View File

@@ -0,0 +1,92 @@
import request from 'supertest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createApiToken } from '../../../../../test/factories/api-token.js';
import { createRole } from '../../../../../test/factories/role.js';
import { createUser } from '../../../../../test/factories/user.js';
import createUserMock from '../../../../../test/mocks/rest/api/v1/user-invitations/create-user-invitation.js';
import app from '../../../../app.js';
import * as license from '../../../../helpers/license.ee.js';
import User from '../../../../models/user.js';
describe('POST /api/v1/user-invitations', () => {
let token, roleId;
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
roleId = (await createRole({ name: 'User' })).id;
token = (await createApiToken()).token;
});
it('should return created user with valid data', async () => {
const userData = {
email: 'created@sample.com',
fullName: 'Full Name',
password: 'samplePassword123',
roleId,
};
const response = await request(app)
.post('/api/v1/user-invitations')
.set('x-api-token', token)
.send(userData)
.expect(201);
const refetchedRegisteredUser = await User.query()
.findById(response.body.data.id)
.throwIfNotFound();
const expectedPayload = createUserMock(refetchedRegisteredUser);
expect(response.body).toStrictEqual(expectedPayload);
expect(refetchedRegisteredUser.roleId).toStrictEqual(roleId);
});
it('should return unprocessable entity response with already used email', async () => {
await createUser({
email: 'created@sample.com',
});
const userData = {
email: 'created@sample.com',
fullName: 'Full Name',
password: 'samplePassword123',
roleId,
};
const response = await request(app)
.post('/api/v1/user-invitations')
.set('x-api-token', token)
.send(userData)
.expect(422);
expect(response.body.errors).toStrictEqual({
email: ["'email' must be unique."],
});
expect(response.body.meta).toStrictEqual({
type: 'UniqueViolationError',
});
});
it('should return unprocessable entity response with invalid user data', async () => {
const userData = {
email: null,
fullName: null,
roleId: null,
};
const response = await request(app)
.post('/api/v1/user-invitations')
.set('x-api-token', token)
.send(userData)
.expect(422);
expect(response.body.meta.type).toStrictEqual('ModelValidation');
expect(response.body.errors).toStrictEqual({
email: ["must have required property 'email'"],
fullName: ['must be string'],
roleId: ['must be string'],
});
});
});

View File

@@ -1,10 +1,12 @@
import { Router } from 'express';
import getUserInvitationsAction from '../../../controllers/api/v1/user-invitations/get-user-invitations.ee.js';
import createUserInvitationAction from '../../../controllers/api/v1/user-invitations/create-user-invitation.ee.js';
import deleteUserInvitationAction from '../../../controllers/api/v1/user-invitations/delete-user-invitation.ee.js';
const router = Router();
router.get('/', getUserInvitationsAction);
router.post('/', createUserInvitationAction);
router.delete('/:userId', deleteUserInvitationAction);
export default router;

View File

@@ -8,6 +8,7 @@ const publicUserInvitationSerializer = (user) => {
updatedAt: user.updatedAt.getTime(),
status: user.status,
fullName: user.fullName,
acceptInvitationUrl: user.acceptInvitationUrl,
};
if (user.role) {

View File

@@ -22,6 +22,7 @@ describe('publicUserInvitation', () => {
id: user.id,
status: user.status,
updatedAt: user.updatedAt.getTime(),
acceptInvitationUrl: user.acceptInvitationUrl,
};
expect(publicUserInvitationSerializer(user)).toStrictEqual(expectedPayload);

View File

@@ -0,0 +1,24 @@
const createUserInvitationMock = (user) => {
const userData = {
createdAt: user.createdAt.getTime(),
email: user.email,
fullName: user.fullName,
id: user.id,
status: user.status,
updatedAt: user.updatedAt.getTime(),
acceptInvitationUrl: user.acceptInvitationUrl,
};
return {
data: userData,
meta: {
count: 1,
currentPage: null,
isArray: false,
totalPages: null,
type: 'User',
},
};
};
export default createUserInvitationMock;

View File

@@ -19,6 +19,7 @@ const getUserInvitationsAction = async (users, roles) => {
: null,
status: user.status,
updatedAt: user.updatedAt.getTime(),
acceptInvitationUrl: user.acceptInvitationUrl,
};
});

View File

@@ -30,7 +30,7 @@ export default defineConfig({
autoUpdate: true,
statements: 99.44,
branches: 98.41,
functions: 99.09,
functions: 99.1,
lines: 99.44,
},
},