ToolJet/server/test/controllers/app.e2e-spec.ts
Kiran Ashok b66d38cf45
Feature :: onboarding self hosted users (#4933)
* fix :: keystroke not taken initially

* fix :: trim company name

* fix :: setting autocomplete to off

* removing redirectsso and confirmation page as its unused now

* few ui corrections , laoding states

* Fixed organization login sso issue

* Add db transaction wrap

* refactoring the code

* flow changes

* Added db tranxn wrap to auth services

* adding accept invite screen

* added verify organization token route

* fixes

* adding disable_multiworkspace to default config

* cleanup

* intermediate commit :: revamping organization page , api integration

* fixes

* feat :: organization token send in invite from org account setup

* fix :: making orgtoken conditional and ui changes / functional updates organization invte

* removed unwanted code

* fix :: login/account setup for sso user in single workspace flow

* fixes

* fix :: CTA loader misaligned

* fix :: sso login single workspace

* fix

* fix :: role check db

* fix :: role check DB

* fix :: setting fallback screen for organizational invite

* feat :: org signup

* fix :: loading states bug

* fix :: loading bug

* fixes

* added password length validation

* fixes

* fix: sub path for static asset serve (#4665)

* fix: sub path for static asset serve

* fix: sub path for static asset serve

* fix: sub path for static asset serve (#4668)

* Bugfix :: Sort event fired on sort removal (#4542)

* onsort applied on sort removal

* bugcheck

* removing unused toast

* Feature :: Table image column type addition (#4547)

* feat :: adding column type image

* feat :: adding image fit property

* Fix :: Closing of textarea ontype Table widget (#4549)

* fix :: closing of textarea ontype

* fix :: bug not able to clear value

* bug fixed : generate file in text is not supported (#4346)

* add eslintignore for frontend (#4669)

* [cypress] Fix failing manage SSO spec in single workspace #4390 (#4509)

* added  [data-cy=login-page-logo] also removed one dropdown of same name

* Revert command.js

* call loginpagelogo

* Fixed toast test case

* updated text

* remove timer

* revert linting

* Github text fixed

* Replaced the woocommerce connection screenshot from dark to light one (#4654)

Signed-off-by: Pakeetharan Balasubramaniam <bpakee@gmail.com>

Signed-off-by: Pakeetharan Balasubramaniam <bpakee@gmail.com>

* gifs for Keyboard Shortcuts Added (#4643)

* Fix. Image to button group docs (#4630) (#4631)

* Add. Image to button group docs (#4630)

* Update docs/docs/widgets/button-group.md

Co-authored-by: Shubhendra Singh Chauhan <withshubh@gmail.com>

* [added] Translated global strings in french #4169 (#4232)

* [added] Translated global strings in french 

Added global strings translated to French language , Issue: #4169

* [localization ] Fixed the Proposed Changes in French Translation

* [localization ] Fixed the Remaining Proposed Changes in French Translation v2

* Update selectHost

Co-authored-by: roiLeo <medina.leo42@gmail.com>

Co-authored-by: Shubhendra Singh Chauhan <withshubh@gmail.com>
Co-authored-by: roiLeo <medina.leo42@gmail.com>

* [localization] issue-4188 added spanish keys for card app (#4229)

* issue-4188 added spanish keys for card app

* IT-4188 fixed PR

* Improve toast message shortcut on component deletion (#4513)

* Improve toast message shortcut

* Improved user agent check

Co-authored-by: Kavin Venkatachalam <50441969+kavinvenkatachalam@users.noreply.github.com>

* Improved user agent check

Co-authored-by: Kavin Venkatachalam <50441969+kavinvenkatachalam@users.noreply.github.com>

* feat: Add DTO for organization create (#4651)

* feat: Add DTO for organization create

* chore: fix eslint error

* chore: eslint ...

* Fixed failing manage SSO spec in multi workspace (#4658)

* Fixed failing manage SSO spec in multi workspace

* github to gitHib

* persist data without saving the query (#4649)

* Fixed failing dashboard specs (#4667)

* Add and modify data-cy

* Fix failing manage users spec in multiworkspace

* Fix failing user permission spec in multi workspace

* Fix failing dashboard spec

* Fix failing manage group spec

* Fix failing manage users spec in single workspace

* Fix failing user permission spec in single workspace

* indentation changes

* Remove wait

* [Marketplace] AWS S3 (#4670)

* fix: sub path for static asset serve (#4665)

* fix: sub path for static asset serve

* fix: sub path for static asset serve

* marketplace: s3

* rename operations file to query_operations

* fix: crash on click of existing plugin

* remove unused console

* adds missing async await

* add isoptional for repo

* plugin card ui style fixes

* update plugin name

Co-authored-by: Gandharv <gandharvkumargarg@gmail.com>

* fix: Added a modal with warning message on disabling password (#4552)

* Copy to clipboard (#4588)

* Update EventManager.jsx

* Update EventManager.jsx

* Update EventManager.jsx

* fix :: adding sso configs based on org id

* loader and password check

* multiworkspace invite flow updated

* restrict only active users

* fix

* added error message for password length check

* fix :: password trim

* fix :: all happy flows tested , single and multi exept sso

* fix: message on invalid password

* revert: package-lock.json file

* feat :: changing ui for user present for org invite in  multi workspace

* fix :: remove password check for existing user multiworkspace

* fix :: added fallback

* fix :: typo

* fix :: adding checks

* fix

* feat :: for single workspace user logged in directly

* fix :: code meaning fix

* fix

* fixes

* fix

* fix

* fix

* fix :: sso fix and bug updates

* fix :: json resolution for only single workspace

* token :: app level

* fix

* fix

* fixes

* fix

* ui fixes , removed loading and added checks

* showing sso options

* fix :: back to

* fix single workspace

* adding sso check ui

* sso single workspace fix

* code refactoring

* fix :: bugfix on click enter submit in signup and signin

* qa fix :: typo

* fix for sending welcome email on invite

* bug fixes

* fix

* qa bug fixes and translations

* switch workspace fixes

* fix :: company name taking empty spaces

* adding some more translations

* making all screens center aligned

* fix :: login page not loading

* fix :: singup conditional

* fixes

* typo fix

* fix :: for diabled cases of sso , password login , disabled signup

* fix

* fix :: added max input length for workspace name

* fix

* fix :: missing validation on edit email

* fix :: all screens vertically aligned

* fix :: alignment link expiry screen

* fix

* fix :: styling terms and condition

* fix :: for redirect url loginto workspace fixed according to new design

* typo

* feat :: removed onboarding modal , redundant

* typo fix

* fix

* name is now mandatory for sign up

* fix for password retry count not updating

* showing onboarding questions for sign up users

* fix :: spaces in password in diff screens

* fix :: darkmode initial

* fix for accept organization invite success message

* fix :: dark mode

* fix :: dark mode

* feat :: updating all dark mode images

* bugfix img

* bugfix ::img

* dark mode :: improvements

* single workspace signup fix

* updating images

* stylefix

* self review :: bugfixes

* sign up page fix

* fix for asking password for single ws activation

* or separator fix for signup page

* fix

* feat :: updating cta images

* fix :: loader bugs

* hiding sso options in org invite, sign in via sso

* fix :: dark mode img

* bugfixes :: cta changed

* Fix :: onboarding styles fixes (#4773)

* fix: styling in authWrappers

* cleanup

Co-authored-by: Vijaykant Yadav <vjy239@gmail.com>

* fix :: conflicting styles

* fix :: loaders , and added header to link expired screen , style fixes

* fix :: mobile onboarding btn

* fix :: loading onboarding completion

* fix :: subpath not taking img path

* fix :: path

* fix :: removing trailing img / for subpath

* fix :: cta img

* fix :: remove scrollbar

* last name as undefined in comments

* fixed loading bug and cursor of company input onboard form

* comments fixes

* fix :: removing verify email screen on org invite

* comments fix

* fix :: no login methods

* fix :: bug in expired  org invite verification link

* fix :: edge case name being empty when user types multiple spaces

* fix :: comments breaking

* fix :: notification breaking

* fix :: user groups table lastname

* fix for sso redirection

* fix :: empty first and last name during org invite

* bug fixed :On updating any permissions under permission tab, currentTab switches to app tab (#4734)

* fix: popout editor closing for tables (#4674)

Co-authored-by: Kavin Venkatachalam <kavin.saratha@gmail.com>

* Added default value for backgroundColor In NumberInput (#4378)

* fix: added background styles for number input

* fix: removed additional f from the background color property

* fix: added backgroundColor property from styles

* fixes: default bg color for dark mode

Co-authored-by: arpitnath <arpitnath42@gmail.com>
Co-authored-by: Kavin Venkatachalam <kavin.saratha@gmail.com>

* Added the hover effects on datasources (#4303)

Co-authored-by: Kavin Venkatachalam <kavin.saratha@gmail.com>

* Added Background Color to Text Input (#4194)

* Added Background Color to Text Input

* Added Default Value for Background Color

* added default value and make background of text input consistent with the dark theme

Co-authored-by: manishkushare <kushare.manish9@gmail.com>
Co-authored-by: Kavin Venkatachalam <kavin.saratha@gmail.com>

* feat: Add update version button on installable plugin (#4766)

* feat: Add update version button on installable plugin

* use id from update method

* removes redundant check

* use plugin id for fetching files from s3

* fetch latest plugins once updated

* disable update and remove buttons while update in progress

* replace href with link-span cx + send body in patch request

* fix: Dark mode on table's `Striped` and `Striped & Bordered` table type mode (#4611)

* [ Hotfix ] :: Hard to resize table columns  (#4438)

* fix :: hard to resize , scrollbar should not be visible

* fix :: resizer not working in pewview

* Enhancement : [RangeSlider widget] Bind onChange event (#4192)

* add onChange event and its handler

* add handler for slider change as well

* fix lint

* added onChange fire event when slider value is changed

Co-authored-by: manishkushare <kushare.manish9@gmail.com>

* Added data-cy for table widget elements (#4792)

* Add data-cy to tooltip label

* Add data-cy to table filter pop-over

* Add data-cy to table elements

* Add review changes

* [docs] Widget updates (#4793)

* widget updates

* minor update

* Update README.md (#4784)

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Fixed grammatical errors in localization doc (#4800)

* [docs] widget property updates (#4806)

* fixes

* added resend invite API

* Onboarding revamp test cases (#4770)

* fix: test cases for oauth

* fix: app controller test cases

* fix: test cases for oauth

* fix: app controller test cases

* remove: unused vars

* fix: test cases for oauth

* fix: app controller test cases

* remove: unused vars

* chore: PR fixes and cleanup

* fix: single workspace test cases

* fix: test cases for oauth

* fix: app controller test cases

* remove: unused vars

* chore: PR fixes and cleanup

* fix: test cases for oauth

* fix: single workspace test cases

* add: test cases for invitation links

* add: test cases for sso

* fix: test cases for oauth

* fix: app controller test cases

* remove: unused vars

* chore: PR fixes and cleanup

* fix: test cases for oauth

* fix: single workspace test cases

* add: test cases for invitation links

* fix: test cases for oauth

* fix: app controller test cases

* chore: PR fixes and cleanup

* add: test cases for sso

* fix: app test cases

* fix: linting in files

* fix: test cases for oauth

* fix: app controller test cases

* remove: unused vars

* chore: PR fixes and cleanup

* fix: test cases for oauth

* fix: single workspace test cases

* add: test cases for invitation links

* fix: test cases for oauth

* fix: app controller test cases

* chore: PR fixes and cleanup

* add: test cases for sso

* fix: app test cases

* fix: test cases for oauth

* fix: linting in files

* rebase: from feature/onboarding-revamp

Co-authored-by: Vijaykant Yadav <vjy239@gmail.com>

* feat :: api update resend invite

* loading state resend mail

* adding loading state

* checkpoints shown only till tablet

* fix: failing test cases for sso

* pr:: changes changed all imports and removed unwanted fragments

* pr changes

* fix :: naming images

* fix :: style organized for onboarding

* fix for user seed issue

* removing unwanted styles

* Setting darkmode from props :: onboarding (#4885)

* passing darkmode as props to components

* error toast

* Review comment :: rename env variable

* Pr :: changes , code cleanup onborading form

* develop merge

* init :: CE setup page

* pr changes ::form splitted to components

* admin , worspace ui :: init save

* password warning added

* chore :: lint folder renaming

* chore :: lint fix

* review comments

* fixes

* fix for test cases failure

* changes :: removed empty divs

* cleanup

* feat :: onboarding setup completed

* updates

* first-user setup account

* fixes

* add guard for first user sign up api

* validation changes

* feat :: api integration

* workspace is mandatory for first user

* fix :: code cleanup , darkmode

* feat :: signup not enable info card

* fix code cleanup

* Fixed all e2e test cases

* Fixed an issue

* added style changes , signup status

* deisgn review style changes

* self review :: code improvements

* pr review changes

* removed unwanted state

* typo fixes

* feat :: improved ux on form , autofocus and enter key support, removing warnings

* pr review changes :: common constants and helpers moved

Signed-off-by: Pakeetharan Balasubramaniam <bpakee@gmail.com>
Co-authored-by: Muhsin Shah <muhsinshah21@gmail.com>
Co-authored-by: gsmithun4 <gsmithun4@gmail.com>
Co-authored-by: Gandharv <gandharvkumargarg@gmail.com>
Co-authored-by: Manish Kushare <kushare.manish9@gmail.com>
Co-authored-by: Akshay <akshaysasidharan93@gmail.com>
Co-authored-by: alammoiz <moixalam@gmail.com>
Co-authored-by: Pakeetharan Balasubramaniam <bpakee@gmail.com>
Co-authored-by: Akhilesh Kumar Mishra <79476272+iamakhileshmishra@users.noreply.github.com>
Co-authored-by: Hemanth Kumar <49117799+Hemanthhari2000@users.noreply.github.com>
Co-authored-by: Shubhendra Singh Chauhan <withshubh@gmail.com>
Co-authored-by: akk312000 <akk312000@gmail.com>
Co-authored-by: roiLeo <medina.leo42@gmail.com>
Co-authored-by: Jose Morales <jmoralesmnz@gmail.com>
Co-authored-by: 3t8 <62209650+3t8@users.noreply.github.com>
Co-authored-by: Kavin Venkatachalam <50441969+kavinvenkatachalam@users.noreply.github.com>
Co-authored-by: Santosh Bhandari <bsantosh909@gmail.com>
Co-authored-by: Arpit <arpitnath42@gmail.com>
Co-authored-by: Ajith KV <ajith.jaban@gmail.com>
Co-authored-by: Akarsh Jain <72064462+akarsh-jain-790@users.noreply.github.com>
Co-authored-by: Utsav Paul <91927689+Smartmind12@users.noreply.github.com>
Co-authored-by: Vijaykant Yadav <vjaris42@Vijaykants-MacBook-Pro.local>
Co-authored-by: Vijaykant Yadav <vjy239@gmail.com>
Co-authored-by: geisterfurz007 <geisterfurz007@users.noreply.github.com>
Co-authored-by: Kavin Venkatachalam <kavin.saratha@gmail.com>
Co-authored-by: Abhushan Gautam <carefreeav09@gmail.com>
Co-authored-by: Syed Ansar <82027712+Syed-Ansar@users.noreply.github.com>
Co-authored-by: Rahul Sunil <rahulsunil2@gmail.com>
Co-authored-by: Srisuma Atluri <40341173+Srisuma13@users.noreply.github.com>
Co-authored-by: Midhun Kumar E <midhun752@gmail.com>
Co-authored-by: Navaneeth Pk <navaneeth@tooljet.io>
Co-authored-by: Akasshhg <91525166+gogoiakash2311@users.noreply.github.com>
2022-12-21 00:13:18 +05:30

930 lines
35 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-unused-vars */
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { getManager, Repository, Not } from 'typeorm';
import { User } from 'src/entities/user.entity';
import { clearDB, createUser, authHeaderForUser, createNestAppInstanceWithEnvMock } from '../test.helper';
import { OrganizationUser } from 'src/entities/organization_user.entity';
import { Organization } from 'src/entities/organization.entity';
import { SSOConfigs } from 'src/entities/sso_config.entity';
import { EmailService } from '@services/email.service';
import { v4 as uuidv4 } from 'uuid';
describe('Authentication', () => {
let app: INestApplication;
let userRepository: Repository<User>;
let orgRepository: Repository<Organization>;
let orgUserRepository: Repository<OrganizationUser>;
let ssoConfigsRepository: Repository<SSOConfigs>;
let mockConfig;
let current_organization: Organization;
let current_user: User;
beforeEach(async () => {
await clearDB();
});
beforeAll(async () => {
({ app, mockConfig } = await createNestAppInstanceWithEnvMock());
userRepository = app.get('UserRepository');
orgRepository = app.get('OrganizationRepository');
orgUserRepository = app.get('OrganizationUserRepository');
ssoConfigsRepository = app.get('SSOConfigsRepository');
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
describe('Single organization', () => {
beforeEach(async () => {
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
switch (key) {
case 'DISABLE_SIGNUPS':
return 'false';
case 'DISABLE_MULTI_WORKSPACE':
return 'true';
default:
return process.env[key];
}
});
});
it('should create new users and organization', async () => {
const response = await request(app.getHttpServer())
.post('/api/setup-admin')
.send({ email: 'test@tooljet.io', name: 'test', password: 'password', workspace: 'tooljet' });
expect(response.statusCode).toBe(201);
const user = await userRepository.findOneOrFail({
where: { email: 'test@tooljet.io' },
relations: ['organizationUsers'],
});
const organization = await orgRepository.findOneOrFail({
where: { id: user?.organizationUsers?.[0]?.organizationId },
});
expect(user.defaultOrganizationId).toBe(user?.organizationUsers?.[0]?.organizationId);
expect(organization.name).toBe('tooljet');
const groupPermissions = await user.groupPermissions;
const groupNames = groupPermissions.map((x) => x.group);
expect(new Set(['all_users', 'admin'])).toEqual(new Set(groupNames));
const adminGroup = groupPermissions.find((x) => x.group == 'admin');
expect(adminGroup.appCreate).toBeTruthy();
expect(adminGroup.appDelete).toBeTruthy();
expect(adminGroup.folderCreate).toBeTruthy();
expect(adminGroup.orgEnvironmentVariableCreate).toBeTruthy();
expect(adminGroup.orgEnvironmentVariableUpdate).toBeTruthy();
expect(adminGroup.orgEnvironmentVariableDelete).toBeTruthy();
expect(adminGroup.folderUpdate).toBeTruthy();
expect(adminGroup.folderDelete).toBeTruthy();
const allUserGroup = groupPermissions.find((x) => x.group == 'all_users');
expect(allUserGroup.appCreate).toBeFalsy();
expect(allUserGroup.appDelete).toBeFalsy();
expect(allUserGroup.folderCreate).toBeFalsy();
expect(allUserGroup.orgEnvironmentVariableCreate).toBeFalsy();
expect(allUserGroup.orgEnvironmentVariableUpdate).toBeFalsy();
expect(allUserGroup.orgEnvironmentVariableDelete).toBeFalsy();
expect(allUserGroup.folderUpdate).toBeFalsy();
expect(allUserGroup.folderDelete).toBeFalsy();
});
describe('Single organization operations', () => {
beforeEach(async () => {
current_organization = (await createUser(app, { email: 'admin@tooljet.io' })).organization;
});
it('should not create new users since organization already exist', async () => {
const response = await request(app.getHttpServer())
.post('/api/signup')
.send({ email: 'test@tooljet.io', name: 'test', password: 'password' });
expect(response.statusCode).toBe(406);
});
it('authenticate if valid credentials', async () => {
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'admin@tooljet.io', password: 'password' })
.expect(201);
});
it('authenticate to organization if valid credentials', async () => {
await request(app.getHttpServer())
.post('/api/authenticate/' + current_organization.id)
.send({ email: 'admin@tooljet.io', password: 'password' })
.expect(201);
});
it('throw unauthorized error if user does not exist in given organization if valid credentials', async () => {
await request(app.getHttpServer())
.post('/api/authenticate/82249621-efc1-4cd2-9986-5c22182fa8a7')
.send({ email: 'admin@tooljet.io', password: 'password' })
.expect(401);
});
it('throw 401 if user is archived', async () => {
await createUser(app, { email: 'user@tooljet.io', status: 'archived' });
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'password' })
.expect(401);
const adminUser = await userRepository.findOneOrFail({
email: 'admin@tooljet.io',
});
await orgUserRepository.update({ userId: adminUser.id }, { status: 'archived' });
await request(app.getHttpServer())
.get('/api/organizations/users')
.set('Authorization', authHeaderForUser(adminUser))
.expect(401);
});
it('throw 401 if user is invited', async () => {
await createUser(app, { email: 'user@tooljet.io', status: 'invited' });
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'password' })
.expect(401);
const adminUser = await userRepository.findOneOrFail({
email: 'admin@tooljet.io',
});
await orgUserRepository.update({ userId: adminUser.id }, { status: 'invited' });
await request(app.getHttpServer())
.get('/api/organizations/users')
.set('Authorization', authHeaderForUser(adminUser))
.expect(401);
});
it('throw 400 if invalid credentials', async () => {
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'amdin@tooljet.i0', password: 'password' })
.expect(400);
});
it('should throw 401 if form login is disabled', async () => {
await ssoConfigsRepository.update({ organizationId: current_organization.id }, { enabled: false });
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'admin@tooljet.io', password: 'password' })
.expect(401);
});
});
});
describe('Multi organization', () => {
beforeEach(async () => {
const { organization, user } = await createUser(app, {
email: 'admin@tooljet.io',
firstName: 'user',
lastName: 'name',
});
current_organization = organization;
current_user = user;
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
switch (key) {
case 'DISABLE_SIGNUPS':
return 'false';
default:
return process.env[key];
}
});
});
describe('sign up disabled', () => {
beforeEach(async () => {
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
switch (key) {
case 'DISABLE_SIGNUPS':
return 'true';
default:
return process.env[key];
}
});
});
it('should not create new users', async () => {
const response = await request(app.getHttpServer()).post('/api/signup').send({ email: 'test@tooljet.io' });
expect(response.statusCode).toBe(403);
});
});
describe('sign up enabled and authorization', () => {
it('should create new users', async () => {
const response = await request(app.getHttpServer())
.post('/api/signup')
.send({ email: 'test@tooljet.io', name: 'test', password: 'password' });
expect(response.statusCode).toBe(201);
const user = await userRepository.findOneOrFail({
where: { email: 'test@tooljet.io' },
relations: ['organizationUsers'],
});
const organization = await orgRepository.findOneOrFail({
where: { id: user?.organizationUsers?.[0]?.organizationId },
});
expect(user.defaultOrganizationId).toBe(user?.organizationUsers?.[0]?.organizationId);
expect(organization?.name).toBe('Untitled workspace');
const groupPermissions = await user.groupPermissions;
const groupNames = groupPermissions.map((x) => x.group);
expect(new Set(['all_users', 'admin'])).toEqual(new Set(groupNames));
const adminGroup = groupPermissions.find((x) => x.group == 'admin');
expect(adminGroup.appCreate).toBeTruthy();
expect(adminGroup.appDelete).toBeTruthy();
expect(adminGroup.folderCreate).toBeTruthy();
expect(adminGroup.orgEnvironmentVariableCreate).toBeTruthy();
expect(adminGroup.orgEnvironmentVariableUpdate).toBeTruthy();
expect(adminGroup.orgEnvironmentVariableDelete).toBeTruthy();
expect(adminGroup.folderUpdate).toBeTruthy();
expect(adminGroup.folderDelete).toBeTruthy();
const allUserGroup = groupPermissions.find((x) => x.group == 'all_users');
expect(allUserGroup.appCreate).toBeFalsy();
expect(allUserGroup.appDelete).toBeFalsy();
expect(allUserGroup.folderCreate).toBeFalsy();
expect(allUserGroup.orgEnvironmentVariableCreate).toBeFalsy();
expect(allUserGroup.orgEnvironmentVariableUpdate).toBeFalsy();
expect(allUserGroup.orgEnvironmentVariableDelete).toBeFalsy();
expect(allUserGroup.folderUpdate).toBeFalsy();
expect(allUserGroup.folderDelete).toBeFalsy();
});
it('authenticate if valid credentials', async () => {
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'admin@tooljet.io', password: 'password' })
.expect(201);
});
it('authenticate to organization if valid credentials', async () => {
await request(app.getHttpServer())
.post('/api/authenticate/' + current_organization.id)
.send({ email: 'admin@tooljet.io', password: 'password' })
.expect(201);
});
it('throw unauthorized error if user does not exist in given organization if valid credentials', async () => {
await request(app.getHttpServer())
.post('/api/authenticate/82249621-efc1-4cd2-9986-5c22182fa8a7')
.send({ email: 'admin@tooljet.io', password: 'password' })
.expect(401);
});
it('throw 401 if user is archived', async () => {
const { orgUser } = await createUser(app, { email: 'user@tooljet.io', status: 'archived' });
await request(app.getHttpServer())
.post(`/api/authenticate/${orgUser.organizationId}`)
.send({ email: 'user@tooljet.io', password: 'password' })
.expect(401);
const adminUser = await userRepository.findOneOrFail({
email: 'admin@tooljet.io',
});
await orgUserRepository.update({ userId: adminUser.id }, { status: 'archived' });
await request(app.getHttpServer())
.get('/api/organizations/users')
.set('Authorization', authHeaderForUser(adminUser))
.expect(401);
});
it('throw 401 if user is invited', async () => {
const { orgUser } = await createUser(app, { email: 'user@tooljet.io', status: 'invited' });
const response = await request(app.getHttpServer())
.post(`/api/authenticate/${orgUser.organizationId}`)
.send({ email: 'user@tooljet.io', password: 'password' })
.expect(401);
const adminUser = await userRepository.findOneOrFail({
email: 'admin@tooljet.io',
});
await orgUserRepository.update({ userId: adminUser.id }, { status: 'invited' });
await request(app.getHttpServer())
.get('/api/organizations/users')
.set('Authorization', authHeaderForUser(adminUser))
.expect(401);
});
it('login to new organization if user is archived', async () => {
const { orgUser } = await createUser(app, { email: 'user@tooljet.io', status: 'archived' });
const response = await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'password' });
expect(response.statusCode).toBe(201);
expect(response.body.organization_id).not.toBe(orgUser.organizationId);
expect(response.body.organization).toBe('Untitled workspace');
});
it('login to new organization if user is invited', async () => {
const { orgUser } = await createUser(app, { email: 'user@tooljet.io', status: 'invited' });
const response = await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'password' });
expect(response.statusCode).toBe(201);
expect(response.body.organization_id).not.toBe(orgUser.organizationId);
expect(response.body.organization).toBe('Untitled workspace');
});
it('throw 401 if invalid credentials', async () => {
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'amdin@tooljet.io', password: 'password' })
.expect(401);
});
it('throw 401 if invalid credentials, maximum retry limit reached error after 5 retries', async () => {
await createUser(app, { email: 'user@tooljet.io', status: 'active' });
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'psswrd' })
.expect(401);
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'psswrd' })
.expect(401);
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'psswrd' })
.expect(401);
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'psswrd' })
.expect(401);
const invalidCredentialResp = await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'psswrd' });
expect(invalidCredentialResp.statusCode).toBe(401);
expect(invalidCredentialResp.body.message).toBe('Invalid credentials');
const response = await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'psswrd' });
expect(response.statusCode).toBe(401);
expect(response.body.message).toBe(
'Maximum password retry limit reached, please reset your password using forgot password option'
);
});
it('throw 401 if invalid credentials, maximum retry limit reached error will not throw if DISABLE_PASSWORD_RETRY_LIMIT is set to true', async () => {
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
switch (key) {
case 'DISABLE_PASSWORD_RETRY_LIMIT':
return 'true';
default:
return process.env[key];
}
});
await createUser(app, { email: 'user@tooljet.io', status: 'active' });
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'pssword' })
.expect(401);
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'psswrd' })
.expect(401);
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'psswrd' })
.expect(401);
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'psswrd' })
.expect(401);
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'psswrd' })
.expect(401);
const response = await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'user@tooljet.io', password: 'psswrd' });
expect(response.statusCode).toBe(401);
expect(response.body.message).toBe('Invalid credentials');
});
it('throw 401 if invalid credentials, maximum retry limit reached error will not throw after the count configured in PASSWORD_RETRY_LIMIT', async () => {
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
switch (key) {
case 'PASSWORD_RETRY_LIMIT':
return '3';
default:
return process.env[key];
}
});
await createUser(app, { email: 'user@tooljet.io', status: 'active' });
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'admin@tooljet.io', password: 'psswrd' })
.expect(401);
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'admin@tooljet.io', password: 'psswrd' })
.expect(401);
const invalidCredentialResp = await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'admin@tooljet.io', password: 'psswrd' });
expect(invalidCredentialResp.statusCode).toBe(401);
expect(invalidCredentialResp.body.message).toBe('Invalid credentials');
const response = await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'admin@tooljet.io', password: 'psswrd' });
expect(response.statusCode).toBe(401);
expect(response.body.message).toBe(
'Maximum password retry limit reached, please reset your password using forgot password option'
);
});
it('should throw 401 if form login is disabled', async () => {
await ssoConfigsRepository.update({ organizationId: current_organization.id }, { enabled: false });
await request(app.getHttpServer())
.post('/api/authenticate/' + current_organization.id)
.send({ email: 'admin@tooljet.io', password: 'password' })
.expect(401);
});
it('should create new organization if login is disabled for default organization', async () => {
await ssoConfigsRepository.update({ organizationId: current_organization.id }, { enabled: false });
const response = await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'admin@tooljet.io', password: 'password' });
expect(response.statusCode).toBe(201);
expect(response.body.organization_id).not.toBe(current_organization.id);
expect(response.body.organization).toBe('Untitled workspace');
});
it('should be able to switch between organizations with admin privilege', async () => {
const { organization: invited_organization } = await createUser(
app,
{ organizationName: 'New Organization' },
current_user
);
const response = await request(app.getHttpServer())
.get('/api/switch/' + invited_organization.id)
.set('Authorization', authHeaderForUser(current_user));
expect(response.statusCode).toBe(200);
expect(Object.keys(response.body).sort()).toEqual(
[
'id',
'email',
'first_name',
'last_name',
'auth_token',
'admin',
'organization_id',
'organization',
'group_permissions',
'app_group_permissions',
].sort()
);
const {
email,
first_name,
last_name,
admin,
group_permissions,
app_group_permissions,
organization_id,
organization,
} = response.body;
expect(email).toEqual(current_user.email);
expect(first_name).toEqual(current_user.firstName);
expect(last_name).toEqual(current_user.lastName);
expect(admin).toBeTruthy();
expect(organization_id).toBe(invited_organization.id);
expect(organization).toBe(invited_organization.name);
expect(group_permissions).toHaveLength(2);
expect(group_permissions.some((gp) => gp.group === 'all_users')).toBeTruthy();
expect(group_permissions.some((gp) => gp.group === 'admin')).toBeTruthy();
expect(Object.keys(group_permissions[0]).sort()).toEqual(
[
'id',
'organization_id',
'group',
'app_create',
'app_delete',
'updated_at',
'created_at',
'folder_create',
'org_environment_variable_create',
'org_environment_variable_update',
'org_environment_variable_delete',
'folder_delete',
'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
await current_user.reload();
expect(current_user.defaultOrganizationId).toBe(invited_organization.id);
});
it('should be able to switch between organizations with user privilege', async () => {
const { organization: invited_organization } = await createUser(
app,
{ groups: ['all_users'], organizationName: 'New Organization' },
current_user
);
const response = await request(app.getHttpServer())
.get('/api/switch/' + invited_organization.id)
.set('Authorization', authHeaderForUser(current_user));
expect(response.statusCode).toBe(200);
expect(Object.keys(response.body).sort()).toEqual(
[
'id',
'email',
'first_name',
'last_name',
'auth_token',
'admin',
'organization_id',
'organization',
'group_permissions',
'app_group_permissions',
].sort()
);
const {
email,
first_name,
last_name,
admin,
group_permissions,
app_group_permissions,
organization_id,
organization,
} = response.body;
expect(email).toEqual(current_user.email);
expect(first_name).toEqual(current_user.firstName);
expect(last_name).toEqual(current_user.lastName);
expect(admin).toBeFalsy();
expect(organization_id).toBe(invited_organization.id);
expect(organization).toBe(invited_organization.name);
expect(group_permissions).toHaveLength(1);
expect(group_permissions[0].group).toEqual('all_users');
expect(Object.keys(group_permissions[0]).sort()).toEqual(
[
'id',
'organization_id',
'group',
'app_create',
'app_delete',
'updated_at',
'created_at',
'folder_create',
'org_environment_variable_create',
'org_environment_variable_update',
'org_environment_variable_delete',
'folder_delete',
'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
await current_user.reload();
expect(current_user.defaultOrganizationId).toBe(invited_organization.id);
});
});
});
describe('POST /api/forgot-password', () => {
beforeEach(async () => {
await createUser(app, {
email: 'admin@tooljet.io',
firstName: 'user',
lastName: 'name',
});
});
it('should return error if required params are not present', async () => {
const response = await request(app.getHttpServer()).post('/api/forgot-password');
expect(response.statusCode).toBe(400);
expect(response.body.message).toStrictEqual(['email should not be empty', 'email must be an email']);
});
it('should set token and send email', async () => {
const emailServiceMock = jest.spyOn(EmailService.prototype, 'sendPasswordResetEmail');
emailServiceMock.mockImplementation();
const response = await request(app.getHttpServer())
.post('/api/forgot-password')
.send({ email: 'admin@tooljet.io' });
expect(response.statusCode).toBe(201);
const user = await getManager().findOne(User, {
where: { email: 'admin@tooljet.io' },
});
expect(emailServiceMock).toHaveBeenCalledWith(user.email, user.forgotPasswordToken);
});
});
describe('POST /api/reset-password', () => {
beforeEach(async () => {
await createUser(app, {
email: 'admin@tooljet.io',
firstName: 'user',
lastName: 'name',
});
});
it('should return error if required params are not present', async () => {
const response = await request(app.getHttpServer()).post('/api/reset-password');
expect(response.statusCode).toBe(400);
expect(response.body.message).toStrictEqual([
'Password should contain more than 5 letters',
'password should not be empty',
'password must be a string',
'token should not be empty',
'token must be a string',
]);
});
it('should reset password', async () => {
const user = await getManager().findOne(User, {
where: { email: 'admin@tooljet.io' },
});
user.forgotPasswordToken = 'token';
await user.save();
const response = await request(app.getHttpServer()).post('/api/reset-password').send({
password: 'new_password',
token: 'token',
});
expect(response.statusCode).toBe(201);
await request(app.getHttpServer())
.post('/api/authenticate')
.send({ email: 'admin@tooljet.io', password: 'new_password' })
.expect(201);
});
});
describe('POST /api/accept-invite', () => {
describe('Multi-Workspace Enabled', () => {
beforeEach(() => {
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
switch (key) {
case 'DISABLE_MULTI_WORKSPACE':
return 'false';
default:
return process.env[key];
}
});
});
it('should allow users to accept invitation when Multi-Workspace is enabled', async () => {
const userData = await createUser(app, {
email: 'organizationUser@tooljet.io',
status: 'invited',
});
const { user, orgUser } = userData;
const response = await request(app.getHttpServer()).post('/api/accept-invite').send({
token: orgUser.invitationToken,
});
expect(response.statusCode).toBe(201);
const organizationUser = await getManager().findOneOrFail(OrganizationUser, { where: { userId: user.id } });
expect(organizationUser.status).toEqual('active');
});
it('should not allow users to accept invitation when user sign up is not completed', async () => {
const userData = await createUser(app, {
email: 'organizationUser@tooljet.io',
invitationToken: uuidv4(),
status: 'invited',
});
const { user, orgUser } = userData;
const response = await request(app.getHttpServer()).post('/api/accept-invite').send({
token: orgUser.invitationToken,
});
expect(response.statusCode).toBe(401);
expect(response.body.message).toBe(
'Please setup your account using account setup link shared via email before accepting the invite'
);
});
});
describe('Multi-Workspace Disabled', () => {
beforeEach(() => {
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
switch (key) {
case 'DISABLE_MULTI_WORKSPACE':
return 'true';
default:
return process.env[key];
}
});
});
it('should allow users to accept invitation when Multi-Workspace is disabled', async () => {
const userData = await createUser(app, {
email: 'organizationUser@tooljet.io',
status: 'invited',
});
const { user, orgUser } = userData;
const response = await request(app.getHttpServer()).post('/api/accept-invite').send({
token: orgUser.invitationToken,
password: uuidv4(),
});
expect(response.statusCode).toBe(201);
const organizationUser = await getManager().findOneOrFail(OrganizationUser, { where: { userId: user.id } });
expect(organizationUser.status).toEqual('active');
});
it('should not allow users to accept invitation when user not entered password for single workspace', async () => {
const userData = await createUser(app, {
email: 'organizationUser@tooljet.io',
invitationToken: uuidv4(),
status: 'invited',
});
const { orgUser } = userData;
const response = await request(app.getHttpServer()).post('/api/accept-invite').send({
token: orgUser.invitationToken,
});
expect(response.statusCode).toBe(400);
expect(response.body.message).toBe('Please enter password');
});
});
});
describe('GET /api/verify-invite-token', () => {
describe('Multi-Workspace Enabled', () => {
beforeEach(async () => {
const { organization, user, orgUser } = await createUser(app, {
email: 'admin@tooljet.io',
firstName: 'user',
lastName: 'name',
});
current_organization = organization;
current_user = user;
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
switch (key) {
case 'DISABLE_MULTI_WORKSPACE':
return 'false';
default:
return process.env[key];
}
});
});
it('should return 400 while verifying invalid invitation token', async () => {
await request(app.getHttpServer()).get(`/api/verify-invite-token?token=${uuidv4()}`).expect(400);
});
it('should return user info while verifying invitation token', async () => {
const userData = await createUser(app, {
email: 'organizationUser@tooljet.io',
invitationToken: uuidv4(),
status: 'invited',
});
const {
user: { invitationToken },
} = userData;
const response = await request(app.getHttpServer()).get(`/api/verify-invite-token?token=${invitationToken}`);
const {
body: { email, name, onboarding_details },
status,
} = response;
expect(status).toBe(200);
expect(email).toEqual('organizationUser@tooljet.io');
expect(name).toEqual('test test');
expect(Object.keys(onboarding_details)).toEqual(['password', 'questions']);
await userData.user.reload();
expect(userData.user.status).toBe('verified');
});
it('should return redirect url while verifying invitation token, organization token is available and user does not exist', async () => {
const { orgUser } = await createUser(app, {
email: 'organizationUser@tooljet.io',
invitationToken: uuidv4(),
status: 'invited',
});
const { invitationToken } = orgUser;
const response = await request(app.getHttpServer())
.get(`/api/verify-invite-token?token=${uuidv4()}&organizationToken=${invitationToken}`)
.expect(200);
const {
body: { redirect_url },
status,
} = response;
expect(status).toBe(200);
expect(redirect_url).toBe(`${process.env['TOOLJET_HOST']}/organization-invitations/${invitationToken}`);
});
it('should return redirect url while verifying invitation token, organization token is not available and user exist', async () => {
const { user } = await createUser(app, {
email: 'organizationUser@tooljet.io',
invitationToken: uuidv4(),
status: 'invited',
});
const { invitationToken } = user;
const response = await request(app.getHttpServer())
.get(`/api/verify-invite-token?token=${invitationToken}&organizationToken=${uuidv4()}`)
.expect(200);
const {
body: { redirect_url },
status,
} = response;
expect(status).toBe(200);
expect(redirect_url).toBe(`${process.env['TOOLJET_HOST']}/invitations/${invitationToken}`);
});
});
describe('Multi-Workspace Disabled', () => {
beforeEach(async () => {
const { organization, user } = await createUser(app, {
email: 'admin@tooljet.io',
firstName: 'user',
lastName: 'name',
});
current_organization = organization;
current_user = user;
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
switch (key) {
case 'DISABLE_MULTI_WORKSPACE':
return 'true';
default:
return process.env[key];
}
});
});
it('should return 400 while verifying invalid organization token', async () => {
await request(app.getHttpServer()).get(`/api/verify-organization-token?token=${uuidv4()}`).expect(400);
});
it('should return 400 if the user is archived', async () => {
const {
orgUser: { invitationToken },
} = await createUser(app, {
email: 'organizationUser@tooljet.io',
invitationToken: uuidv4(),
status: 'archived',
});
await request(app.getHttpServer()).get(`/api/verify-organization-token?token=${invitationToken}`).expect(400);
});
it('should return user info while verifying organization token', async () => {
const userData = await createUser(app, {
email: 'organizationUser@tooljet.io',
invitationToken: uuidv4(),
status: 'invited',
});
const {
orgUser: { invitationToken },
user,
} = userData;
const response = await request(app.getHttpServer()).get(
`/api/verify-organization-token?token=${invitationToken}`
);
const {
body: { email, name, onboarding_details },
status,
} = response;
expect(status).toBe(200);
expect(email).toEqual('organizationUser@tooljet.io');
expect(name).toEqual('test test');
expect(Object.keys(onboarding_details)).toEqual(['password']);
await user.reload();
expect(user.status).toBe('verified');
});
});
});
afterAll(async () => {
await app.close();
});
});